Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vben into reform-mp
This commit is contained in:
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
@@ -2,15 +2,6 @@
|
|||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"name": "vben admin playground dev",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:5555",
|
|
||||||
"env": { "NODE_ENV": "development" },
|
|
||||||
"sourceMaps": true,
|
|
||||||
"webRoot": "${workspaceFolder}/playground"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"name": "vben admin antd dev",
|
"name": "vben admin antd dev",
|
||||||
|
|||||||
@@ -41,12 +41,15 @@ stages:
|
|||||||
- pnpm build:ele
|
- pnpm build:ele
|
||||||
- '# 执行编译命令naive'
|
- '# 执行编译命令naive'
|
||||||
- pnpm build:naive
|
- pnpm build:naive
|
||||||
|
- '# 执行编译命令tdesign'
|
||||||
|
- pnpm build:tdesign
|
||||||
artifacts:
|
artifacts:
|
||||||
- name: BUILD_ARTIFACT
|
- name: BUILD_ARTIFACT
|
||||||
path:
|
path:
|
||||||
- ./apps/web-antd/dist/
|
- ./apps/web-antd/dist/
|
||||||
- ./apps/web-ele/dist/
|
- ./apps/web-ele/dist/
|
||||||
- ./apps/web-naive/dist/
|
- ./apps/web-naive/dist/
|
||||||
|
- ./apps/web-tdesign/dist/
|
||||||
caches:
|
caches:
|
||||||
- ~/.npm
|
- ~/.npm
|
||||||
- ~/.yarn
|
- ~/.yarn
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
- **组件**:二次封装了多个常用的组件
|
- **组件**:二次封装了多个常用的组件
|
||||||
- **示例**:内置丰富的示例
|
- **示例**:内置丰富的示例
|
||||||
|
|
||||||
## 外包项目请联系【非项目需求请勿扫码,非客服,不解答项目问题】
|
## [外包项目请联系【非项目需求请勿扫码,非客服,不解答项目问题】](https://www.shuduokeji.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
PORT=5320
|
|
||||||
ACCESS_TOKEN_SECRET=access_token_secret
|
|
||||||
REFRESH_TOKEN_SECRET=refresh_token_secret
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# @vben/backend-mock
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
|
|
||||||
|
|
||||||
## Running the app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# development
|
|
||||||
$ pnpm run start
|
|
||||||
|
|
||||||
# production mode
|
|
||||||
$ pnpm run build
|
|
||||||
```
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_CODES } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const codes =
|
|
||||||
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
|
|
||||||
|
|
||||||
return useResponseSuccess(codes);
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
|
||||||
import {
|
|
||||||
clearRefreshTokenCookie,
|
|
||||||
setRefreshTokenCookie,
|
|
||||||
} from '~/utils/cookie-utils';
|
|
||||||
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_USERS } from '~/utils/mock-data';
|
|
||||||
import {
|
|
||||||
forbiddenResponse,
|
|
||||||
useResponseError,
|
|
||||||
useResponseSuccess,
|
|
||||||
} from '~/utils/response';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { password, username } = await readBody(event);
|
|
||||||
if (!password || !username) {
|
|
||||||
setResponseStatus(event, 400);
|
|
||||||
return useResponseError(
|
|
||||||
'BadRequestException',
|
|
||||||
'Username and password are required',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const findUser = MOCK_USERS.find(
|
|
||||||
(item) => item.username === username && item.password === password,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!findUser) {
|
|
||||||
clearRefreshTokenCookie(event);
|
|
||||||
return forbiddenResponse(event, 'Username or password is incorrect.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = generateAccessToken(findUser);
|
|
||||||
const refreshToken = generateRefreshToken(findUser);
|
|
||||||
|
|
||||||
setRefreshTokenCookie(event, refreshToken);
|
|
||||||
|
|
||||||
return useResponseSuccess({
|
|
||||||
...findUser,
|
|
||||||
accessToken,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { defineEventHandler } from 'h3';
|
|
||||||
import {
|
|
||||||
clearRefreshTokenCookie,
|
|
||||||
getRefreshTokenFromCookie,
|
|
||||||
} from '~/utils/cookie-utils';
|
|
||||||
import { useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const refreshToken = getRefreshTokenFromCookie(event);
|
|
||||||
if (!refreshToken) {
|
|
||||||
return useResponseSuccess('');
|
|
||||||
}
|
|
||||||
|
|
||||||
clearRefreshTokenCookie(event);
|
|
||||||
|
|
||||||
return useResponseSuccess('');
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { defineEventHandler } from 'h3';
|
|
||||||
import {
|
|
||||||
clearRefreshTokenCookie,
|
|
||||||
getRefreshTokenFromCookie,
|
|
||||||
setRefreshTokenCookie,
|
|
||||||
} from '~/utils/cookie-utils';
|
|
||||||
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_USERS } from '~/utils/mock-data';
|
|
||||||
import { forbiddenResponse } from '~/utils/response';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const refreshToken = getRefreshTokenFromCookie(event);
|
|
||||||
if (!refreshToken) {
|
|
||||||
return forbiddenResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearRefreshTokenCookie(event);
|
|
||||||
|
|
||||||
const userinfo = verifyRefreshToken(refreshToken);
|
|
||||||
if (!userinfo) {
|
|
||||||
return forbiddenResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const findUser = MOCK_USERS.find(
|
|
||||||
(item) => item.username === userinfo.username,
|
|
||||||
);
|
|
||||||
if (!findUser) {
|
|
||||||
return forbiddenResponse(event);
|
|
||||||
}
|
|
||||||
const accessToken = generateAccessToken(findUser);
|
|
||||||
|
|
||||||
setRefreshTokenCookie(event, refreshToken);
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { eventHandler, setHeader } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { unAuthorizedResponse } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
const data = `
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": 123456789012345678901234567890123456789012345678901234567890,
|
|
||||||
"name": "John Doe",
|
|
||||||
"age": 30,
|
|
||||||
"email": "john-doe@demo.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 987654321098765432109876543210987654321098765432109876543210,
|
|
||||||
"name": "Jane Smith",
|
|
||||||
"age": 25,
|
|
||||||
"email": "jane@demo.com"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
setHeader(event, 'Content-Type', 'application/json');
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_MENUS } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const menus =
|
|
||||||
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
|
|
||||||
return useResponseSuccess(menus);
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { eventHandler, getQuery, setResponseStatus } from 'h3';
|
|
||||||
import { useResponseError } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
|
||||||
const { status } = getQuery(event);
|
|
||||||
setResponseStatus(event, Number(status));
|
|
||||||
return useResponseError(`${status}`);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import {
|
|
||||||
sleep,
|
|
||||||
unAuthorizedResponse,
|
|
||||||
useResponseSuccess,
|
|
||||||
} from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
await sleep(600);
|
|
||||||
return useResponseSuccess(null);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import {
|
|
||||||
sleep,
|
|
||||||
unAuthorizedResponse,
|
|
||||||
useResponseSuccess,
|
|
||||||
} from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
await sleep(1000);
|
|
||||||
return useResponseSuccess(null);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import {
|
|
||||||
sleep,
|
|
||||||
unAuthorizedResponse,
|
|
||||||
useResponseSuccess,
|
|
||||||
} from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
await sleep(2000);
|
|
||||||
return useResponseSuccess(null);
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
|
||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
timeZone: 'Asia/Shanghai',
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
});
|
|
||||||
|
|
||||||
function generateMockDataList(count: number) {
|
|
||||||
const dataList = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const dataItem: Record<string, any> = {
|
|
||||||
id: faker.string.uuid(),
|
|
||||||
pid: 0,
|
|
||||||
name: faker.commerce.department(),
|
|
||||||
status: faker.helpers.arrayElement([0, 1]),
|
|
||||||
createTime: formatterCN.format(
|
|
||||||
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
|
|
||||||
),
|
|
||||||
remark: faker.lorem.sentence(),
|
|
||||||
};
|
|
||||||
if (faker.datatype.boolean()) {
|
|
||||||
dataItem.children = Array.from(
|
|
||||||
{ length: faker.number.int({ min: 1, max: 5 }) },
|
|
||||||
() => ({
|
|
||||||
id: faker.string.uuid(),
|
|
||||||
pid: dataItem.id,
|
|
||||||
name: faker.commerce.department(),
|
|
||||||
status: faker.helpers.arrayElement([0, 1]),
|
|
||||||
createTime: formatterCN.format(
|
|
||||||
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
|
|
||||||
),
|
|
||||||
remark: faker.lorem.sentence(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dataList.push(dataItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataList;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData = generateMockDataList(10);
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const listData = structuredClone(mockData);
|
|
||||||
|
|
||||||
return useResponseSuccess(listData);
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return useResponseSuccess(MOCK_MENU_LIST);
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { eventHandler, getQuery } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
const namesMap: Record<string, any> = {};
|
|
||||||
|
|
||||||
function getNames(menus: any[]) {
|
|
||||||
menus.forEach((menu) => {
|
|
||||||
namesMap[menu.name] = String(menu.id);
|
|
||||||
if (menu.children) {
|
|
||||||
getNames(menu.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
getNames(MOCK_MENU_LIST);
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
const { id, name } = getQuery(event);
|
|
||||||
|
|
||||||
return (name as string) in namesMap &&
|
|
||||||
(!id || namesMap[name as string] !== String(id))
|
|
||||||
? useResponseSuccess(true)
|
|
||||||
: useResponseSuccess(false);
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { eventHandler, getQuery } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
const pathMap: Record<string, any> = { '/': 0 };
|
|
||||||
|
|
||||||
function getPaths(menus: any[]) {
|
|
||||||
menus.forEach((menu) => {
|
|
||||||
pathMap[menu.path] = String(menu.id);
|
|
||||||
if (menu.children) {
|
|
||||||
getPaths(menu.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
getPaths(MOCK_MENU_LIST);
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
const { id, path } = getQuery(event);
|
|
||||||
|
|
||||||
return (path as string) in pathMap &&
|
|
||||||
(!id || pathMap[path as string] !== String(id))
|
|
||||||
? useResponseSuccess(true)
|
|
||||||
: useResponseSuccess(false);
|
|
||||||
});
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
|
||||||
import { eventHandler, getQuery } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
timeZone: 'Asia/Shanghai',
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuIds = getMenuIds(MOCK_MENU_LIST);
|
|
||||||
|
|
||||||
function generateMockDataList(count: number) {
|
|
||||||
const dataList = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const dataItem: Record<string, any> = {
|
|
||||||
id: faker.string.uuid(),
|
|
||||||
name: faker.commerce.product(),
|
|
||||||
status: faker.helpers.arrayElement([0, 1]),
|
|
||||||
createTime: formatterCN.format(
|
|
||||||
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
|
|
||||||
),
|
|
||||||
permissions: faker.helpers.arrayElements(menuIds),
|
|
||||||
remark: faker.lorem.sentence(),
|
|
||||||
};
|
|
||||||
|
|
||||||
dataList.push(dataItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataList;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData = generateMockDataList(100);
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
page = 1,
|
|
||||||
pageSize = 20,
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
remark,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
status,
|
|
||||||
} = getQuery(event);
|
|
||||||
let listData = structuredClone(mockData);
|
|
||||||
if (name) {
|
|
||||||
listData = listData.filter((item) =>
|
|
||||||
item.name.toLowerCase().includes(String(name).toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
listData = listData.filter((item) =>
|
|
||||||
item.id.toLowerCase().includes(String(id).toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (remark) {
|
|
||||||
listData = listData.filter((item) =>
|
|
||||||
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (startTime) {
|
|
||||||
listData = listData.filter((item) => item.createTime >= startTime);
|
|
||||||
}
|
|
||||||
if (endTime) {
|
|
||||||
listData = listData.filter((item) => item.createTime <= endTime);
|
|
||||||
}
|
|
||||||
if (['0', '1'].includes(status as string)) {
|
|
||||||
listData = listData.filter((item) => item.status === Number(status));
|
|
||||||
}
|
|
||||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
|
||||||
});
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
|
||||||
import { eventHandler, getQuery } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import {
|
|
||||||
sleep,
|
|
||||||
unAuthorizedResponse,
|
|
||||||
usePageResponseSuccess,
|
|
||||||
} from '~/utils/response';
|
|
||||||
|
|
||||||
function generateMockDataList(count: number) {
|
|
||||||
const dataList = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const dataItem = {
|
|
||||||
id: faker.string.uuid(),
|
|
||||||
imageUrl: faker.image.avatar(),
|
|
||||||
imageUrl2: faker.image.avatar(),
|
|
||||||
open: faker.datatype.boolean(),
|
|
||||||
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
|
|
||||||
productName: faker.commerce.productName(),
|
|
||||||
price: faker.commerce.price(),
|
|
||||||
currency: faker.finance.currencyCode(),
|
|
||||||
quantity: faker.number.int({ min: 1, max: 100 }),
|
|
||||||
available: faker.datatype.boolean(),
|
|
||||||
category: faker.commerce.department(),
|
|
||||||
releaseDate: faker.date.past(),
|
|
||||||
rating: faker.number.float({ min: 1, max: 5 }),
|
|
||||||
description: faker.commerce.productDescription(),
|
|
||||||
weight: faker.number.float({ min: 0.1, max: 10 }),
|
|
||||||
color: faker.color.human(),
|
|
||||||
inProduction: faker.datatype.boolean(),
|
|
||||||
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
|
|
||||||
};
|
|
||||||
|
|
||||||
dataList.push(dataItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataList;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData = generateMockDataList(100);
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(600);
|
|
||||||
|
|
||||||
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
|
||||||
// 规范化分页参数,处理 string[]
|
|
||||||
const pageRaw = Array.isArray(page) ? page[0] : page;
|
|
||||||
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
|
|
||||||
const pageNumber = Math.max(
|
|
||||||
1,
|
|
||||||
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
|
|
||||||
);
|
|
||||||
const pageSizeNumber = Math.min(
|
|
||||||
100,
|
|
||||||
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
|
|
||||||
);
|
|
||||||
const listData = structuredClone(mockData);
|
|
||||||
|
|
||||||
// 规范化 query 入参,兼容 string[]
|
|
||||||
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
|
|
||||||
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
|
|
||||||
// 检查 sortBy 是否是 listData 元素的合法属性键
|
|
||||||
if (
|
|
||||||
typeof sortKeyRaw === 'string' &&
|
|
||||||
listData[0] &&
|
|
||||||
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
|
|
||||||
) {
|
|
||||||
// 定义数组元素的类型
|
|
||||||
type ItemType = (typeof listData)[0];
|
|
||||||
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
|
|
||||||
const isDesc = sortOrderRaw === 'desc';
|
|
||||||
listData.sort((a, b) => {
|
|
||||||
const aValue = a[sortKey] as unknown;
|
|
||||||
const bValue = b[sortKey] as unknown;
|
|
||||||
|
|
||||||
let result = 0;
|
|
||||||
|
|
||||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
||||||
result = aValue - bValue;
|
|
||||||
} else if (aValue instanceof Date && bValue instanceof Date) {
|
|
||||||
result = aValue.getTime() - bValue.getTime();
|
|
||||||
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
|
|
||||||
if (aValue === bValue) {
|
|
||||||
result = 0;
|
|
||||||
} else {
|
|
||||||
result = aValue ? 1 : -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const aStr = String(aValue);
|
|
||||||
const bStr = String(bValue);
|
|
||||||
const aNum = Number(aStr);
|
|
||||||
const bNum = Number(bStr);
|
|
||||||
result =
|
|
||||||
Number.isFinite(aNum) && Number.isFinite(bNum)
|
|
||||||
? aNum - bNum
|
|
||||||
: aStr.localeCompare(bStr, undefined, {
|
|
||||||
numeric: true,
|
|
||||||
sensitivity: 'base',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDesc ? -result : result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return usePageResponseSuccess(
|
|
||||||
String(pageNumber),
|
|
||||||
String(pageSizeNumber),
|
|
||||||
listData,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { defineEventHandler } from 'h3';
|
|
||||||
|
|
||||||
export default defineEventHandler(() => 'Test get handler');
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { defineEventHandler } from 'h3';
|
|
||||||
|
|
||||||
export default defineEventHandler(() => 'Test post handler');
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
import { getTimezone } from '~/utils/timezone-utils';
|
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
return useResponseSuccess(getTimezone());
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
|
|
||||||
import { useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler(() => {
|
|
||||||
const data = TIME_ZONE_OPTIONS.map((o) => ({
|
|
||||||
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
|
|
||||||
value: o.timezone,
|
|
||||||
}));
|
|
||||||
return useResponseSuccess(data);
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { eventHandler, readBody } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
import { setTimezone } from '~/utils/timezone-utils';
|
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
const body = await readBody<{ timezone?: unknown }>(event);
|
|
||||||
const timezone =
|
|
||||||
typeof body?.timezone === 'string' ? body.timezone : undefined;
|
|
||||||
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
|
|
||||||
if (!timezone || !allowed) {
|
|
||||||
setResponseStatus(event, 400);
|
|
||||||
return useResponseError('Bad Request', 'Invalid timezone');
|
|
||||||
}
|
|
||||||
setTimezone(timezone);
|
|
||||||
return useResponseSuccess({});
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
return useResponseSuccess({
|
|
||||||
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
|
||||||
});
|
|
||||||
// return useResponseError("test")
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { eventHandler } from 'h3';
|
|
||||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
|
||||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
|
||||||
|
|
||||||
export default eventHandler((event) => {
|
|
||||||
const userinfo = verifyAccessToken(event);
|
|
||||||
if (!userinfo) {
|
|
||||||
return unAuthorizedResponse(event);
|
|
||||||
}
|
|
||||||
return useResponseSuccess(userinfo);
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { NitroErrorHandler } from 'nitropack';
|
|
||||||
|
|
||||||
const errorHandler: NitroErrorHandler = function (error, event) {
|
|
||||||
event.node.res.end(`[Error Handler] ${error.stack}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default errorHandler;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { defineEventHandler } from 'h3';
|
|
||||||
import { forbiddenResponse, sleep } from '~/utils/response';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
event.node.res.setHeader(
|
|
||||||
'Access-Control-Allow-Origin',
|
|
||||||
event.headers.get('Origin') ?? '*',
|
|
||||||
);
|
|
||||||
if (event.method === 'OPTIONS') {
|
|
||||||
event.node.res.statusCode = 204;
|
|
||||||
event.node.res.statusMessage = 'No Content.';
|
|
||||||
return 'OK';
|
|
||||||
} else if (
|
|
||||||
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
|
|
||||||
event.path.startsWith('/api/system/')
|
|
||||||
) {
|
|
||||||
await sleep(Math.floor(Math.random() * 2000));
|
|
||||||
return forbiddenResponse(event, '演示环境,禁止修改');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import errorHandler from './error';
|
|
||||||
|
|
||||||
process.env.COMPATIBILITY_DATE = new Date().toISOString();
|
|
||||||
export default defineNitroConfig({
|
|
||||||
devErrorHandler: errorHandler,
|
|
||||||
errorHandler: '~/error',
|
|
||||||
routeRules: {
|
|
||||||
'/api/**': {
|
|
||||||
cors: true,
|
|
||||||
headers: {
|
|
||||||
'Access-Control-Allow-Credentials': 'true',
|
|
||||||
'Access-Control-Allow-Headers':
|
|
||||||
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
|
|
||||||
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Expose-Headers': '*',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@vben/backend-mock",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"private": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "",
|
|
||||||
"scripts": {
|
|
||||||
"build": "nitro build",
|
|
||||||
"start": "nitro dev"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@faker-js/faker": "catalog:",
|
|
||||||
"jsonwebtoken": "catalog:",
|
|
||||||
"nitropack": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jsonwebtoken": "catalog:",
|
|
||||||
"h3": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineEventHandler } from 'h3';
|
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
|
||||||
return `
|
|
||||||
<h1>Hello Vben Admin</h1>
|
|
||||||
<h2>Mock service is starting</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/api/user">/api/user/info</a></li>
|
|
||||||
<li><a href="/api/menu">/api/menu/all</a></li>
|
|
||||||
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
|
||||||
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
|
||||||
<li><a href="/api/upload">/api/upload</a></li>
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./.nitro/types/tsconfig.json"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
|
||||||
|
|
||||||
import { deleteCookie, getCookie, setCookie } from 'h3';
|
|
||||||
|
|
||||||
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
|
|
||||||
deleteCookie(event, 'jwt', {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'none',
|
|
||||||
secure: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRefreshTokenCookie(
|
|
||||||
event: H3Event<EventHandlerRequest>,
|
|
||||||
refreshToken: string,
|
|
||||||
) {
|
|
||||||
setCookie(event, 'jwt', refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 24 * 60 * 60, // unit: seconds
|
|
||||||
sameSite: 'none',
|
|
||||||
secure: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
|
|
||||||
const refreshToken = getCookie(event, 'jwt');
|
|
||||||
return refreshToken;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
|
||||||
|
|
||||||
import type { UserInfo } from './mock-data';
|
|
||||||
|
|
||||||
import { getHeader } from 'h3';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
import { MOCK_USERS } from './mock-data';
|
|
||||||
|
|
||||||
// TODO: Replace with your own secret key
|
|
||||||
const ACCESS_TOKEN_SECRET = 'access_token_secret';
|
|
||||||
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
|
|
||||||
|
|
||||||
export interface UserPayload extends UserInfo {
|
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateAccessToken(user: UserInfo) {
|
|
||||||
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateRefreshToken(user: UserInfo) {
|
|
||||||
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
|
|
||||||
expiresIn: '30d',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyAccessToken(
|
|
||||||
event: H3Event<EventHandlerRequest>,
|
|
||||||
): null | Omit<UserInfo, 'password'> {
|
|
||||||
const authHeader = getHeader(event, 'Authorization');
|
|
||||||
if (!authHeader?.startsWith('Bearer')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenParts = authHeader.split(' ');
|
|
||||||
if (tokenParts.length !== 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const token = tokenParts[1] as string;
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(
|
|
||||||
token,
|
|
||||||
ACCESS_TOKEN_SECRET,
|
|
||||||
) as unknown as UserPayload;
|
|
||||||
|
|
||||||
const username = decoded.username;
|
|
||||||
const user = MOCK_USERS.find((item) => item.username === username);
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { password: _pwd, ...userinfo } = user;
|
|
||||||
return userinfo;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyRefreshToken(
|
|
||||||
token: string,
|
|
||||||
): null | Omit<UserInfo, 'password'> {
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
|
|
||||||
const username = decoded.username;
|
|
||||||
const user = MOCK_USERS.find(
|
|
||||||
(item) => item.username === username,
|
|
||||||
) as UserInfo;
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { password: _pwd, ...userinfo } = user;
|
|
||||||
return userinfo;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
export interface UserInfo {
|
|
||||||
id: number;
|
|
||||||
password: string;
|
|
||||||
realName: string;
|
|
||||||
roles: string[];
|
|
||||||
username: string;
|
|
||||||
homePath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimezoneOption {
|
|
||||||
offset: number;
|
|
||||||
timezone: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MOCK_USERS: UserInfo[] = [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
password: '123456',
|
|
||||||
realName: 'Vben',
|
|
||||||
roles: ['super'],
|
|
||||||
username: 'vben',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
password: '123456',
|
|
||||||
realName: 'Admin',
|
|
||||||
roles: ['admin'],
|
|
||||||
username: 'admin',
|
|
||||||
homePath: '/workspace',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
password: '123456',
|
|
||||||
realName: 'Jack',
|
|
||||||
roles: ['user'],
|
|
||||||
username: 'jack',
|
|
||||||
homePath: '/analytics',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MOCK_CODES = [
|
|
||||||
// super
|
|
||||||
{
|
|
||||||
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
|
|
||||||
username: 'vben',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// admin
|
|
||||||
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
|
|
||||||
username: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// user
|
|
||||||
codes: ['AC_1000001', 'AC_1000002'],
|
|
||||||
username: 'jack',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const dashboardMenus = [
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
order: -1,
|
|
||||||
title: 'page.dashboard.title',
|
|
||||||
},
|
|
||||||
name: 'Dashboard',
|
|
||||||
path: '/dashboard',
|
|
||||||
redirect: '/analytics',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'Analytics',
|
|
||||||
path: '/analytics',
|
|
||||||
component: '/dashboard/analytics/index',
|
|
||||||
meta: {
|
|
||||||
affixTab: true,
|
|
||||||
title: 'page.dashboard.analytics',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Workspace',
|
|
||||||
path: '/workspace',
|
|
||||||
component: '/dashboard/workspace/index',
|
|
||||||
meta: {
|
|
||||||
title: 'page.dashboard.workspace',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
|
||||||
const roleWithMenus = {
|
|
||||||
admin: {
|
|
||||||
component: '/demos/access/admin-visible',
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:button-cursor',
|
|
||||||
title: 'demos.access.adminVisible',
|
|
||||||
},
|
|
||||||
name: 'AccessAdminVisibleDemo',
|
|
||||||
path: '/demos/access/admin-visible',
|
|
||||||
},
|
|
||||||
super: {
|
|
||||||
component: '/demos/access/super-visible',
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:button-cursor',
|
|
||||||
title: 'demos.access.superVisible',
|
|
||||||
},
|
|
||||||
name: 'AccessSuperVisibleDemo',
|
|
||||||
path: '/demos/access/super-visible',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
component: '/demos/access/user-visible',
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:button-cursor',
|
|
||||||
title: 'demos.access.userVisible',
|
|
||||||
},
|
|
||||||
name: 'AccessUserVisibleDemo',
|
|
||||||
path: '/demos/access/user-visible',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
icon: 'ic:baseline-view-in-ar',
|
|
||||||
keepAlive: true,
|
|
||||||
order: 1000,
|
|
||||||
title: 'demos.title',
|
|
||||||
},
|
|
||||||
name: 'Demos',
|
|
||||||
path: '/demos',
|
|
||||||
redirect: '/demos/access',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'AccessDemos',
|
|
||||||
path: '/demosaccess',
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:cloud-key-outline',
|
|
||||||
title: 'demos.access.backendPermissions',
|
|
||||||
},
|
|
||||||
redirect: '/demos/access/page-control',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'AccessPageControlDemo',
|
|
||||||
path: '/demos/access/page-control',
|
|
||||||
component: '/demos/access/index',
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:page-previous-outline',
|
|
||||||
title: 'demos.access.pageAccess',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AccessButtonControlDemo',
|
|
||||||
path: '/demos/access/button-control',
|
|
||||||
component: '/demos/access/button-control',
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:button-cursor',
|
|
||||||
title: 'demos.access.buttonControl',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AccessMenuVisible403Demo',
|
|
||||||
path: '/demos/access/menu-visible-403',
|
|
||||||
component: '/demos/access/menu-visible-403',
|
|
||||||
meta: {
|
|
||||||
authority: ['no-body'],
|
|
||||||
icon: 'mdi:button-cursor',
|
|
||||||
menuVisibleWithForbidden: true,
|
|
||||||
title: 'demos.access.menuVisible403',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roleWithMenus[role],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MOCK_MENUS = [
|
|
||||||
{
|
|
||||||
menus: [...dashboardMenus, ...createDemosMenus('super')],
|
|
||||||
username: 'vben',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
menus: [...dashboardMenus, ...createDemosMenus('admin')],
|
|
||||||
username: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
menus: [...dashboardMenus, ...createDemosMenus('user')],
|
|
||||||
username: 'jack',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MOCK_MENU_LIST = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Workspace',
|
|
||||||
status: 1,
|
|
||||||
type: 'menu',
|
|
||||||
icon: 'mdi:dashboard',
|
|
||||||
path: '/workspace',
|
|
||||||
component: '/dashboard/workspace/index',
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:workspace',
|
|
||||||
title: 'page.dashboard.workspace',
|
|
||||||
affixTab: true,
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:settings',
|
|
||||||
order: 9997,
|
|
||||||
title: 'system.title',
|
|
||||||
badge: 'new',
|
|
||||||
badgeType: 'normal',
|
|
||||||
badgeVariants: 'primary',
|
|
||||||
},
|
|
||||||
status: 1,
|
|
||||||
type: 'catalog',
|
|
||||||
name: 'System',
|
|
||||||
path: '/system',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 201,
|
|
||||||
pid: 2,
|
|
||||||
path: '/system/menu',
|
|
||||||
name: 'SystemMenu',
|
|
||||||
authCode: 'System:Menu:List',
|
|
||||||
status: 1,
|
|
||||||
type: 'menu',
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:menu',
|
|
||||||
title: 'system.menu.title',
|
|
||||||
},
|
|
||||||
component: '/system/menu/list',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 20_101,
|
|
||||||
pid: 201,
|
|
||||||
name: 'SystemMenuCreate',
|
|
||||||
status: 1,
|
|
||||||
type: 'button',
|
|
||||||
authCode: 'System:Menu:Create',
|
|
||||||
meta: { title: 'common.create' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20_102,
|
|
||||||
pid: 201,
|
|
||||||
name: 'SystemMenuEdit',
|
|
||||||
status: 1,
|
|
||||||
type: 'button',
|
|
||||||
authCode: 'System:Menu:Edit',
|
|
||||||
meta: { title: 'common.edit' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20_103,
|
|
||||||
pid: 201,
|
|
||||||
name: 'SystemMenuDelete',
|
|
||||||
status: 1,
|
|
||||||
type: 'button',
|
|
||||||
authCode: 'System:Menu:Delete',
|
|
||||||
meta: { title: 'common.delete' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 202,
|
|
||||||
pid: 2,
|
|
||||||
path: '/system/dept',
|
|
||||||
name: 'SystemDept',
|
|
||||||
status: 1,
|
|
||||||
type: 'menu',
|
|
||||||
authCode: 'System:Dept:List',
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:container-services',
|
|
||||||
title: 'system.dept.title',
|
|
||||||
},
|
|
||||||
component: '/system/dept/list',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 20_401,
|
|
||||||
pid: 202,
|
|
||||||
name: 'SystemDeptCreate',
|
|
||||||
status: 1,
|
|
||||||
type: 'button',
|
|
||||||
authCode: 'System:Dept:Create',
|
|
||||||
meta: { title: 'common.create' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20_402,
|
|
||||||
pid: 202,
|
|
||||||
name: 'SystemDeptEdit',
|
|
||||||
status: 1,
|
|
||||||
type: 'button',
|
|
||||||
authCode: 'System:Dept:Edit',
|
|
||||||
meta: { title: 'common.edit' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20_403,
|
|
||||||
pid: 202,
|
|
||||||
name: 'SystemDeptDelete',
|
|
||||||
status: 1,
|
|
||||||
type: 'button',
|
|
||||||
authCode: 'System:Dept:Delete',
|
|
||||||
meta: { title: 'common.delete' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
meta: {
|
|
||||||
badgeType: 'dot',
|
|
||||||
order: 9998,
|
|
||||||
title: 'demos.vben.title',
|
|
||||||
icon: 'carbon:data-center',
|
|
||||||
},
|
|
||||||
name: 'Project',
|
|
||||||
path: '/vben-admin',
|
|
||||||
type: 'catalog',
|
|
||||||
status: 1,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 901,
|
|
||||||
pid: 9,
|
|
||||||
name: 'VbenDocument',
|
|
||||||
path: '/vben-admin/document',
|
|
||||||
component: 'IFrameView',
|
|
||||||
type: 'embedded',
|
|
||||||
status: 1,
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:book',
|
|
||||||
iframeSrc: 'https://doc.vben.pro',
|
|
||||||
title: 'demos.vben.document',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 902,
|
|
||||||
pid: 9,
|
|
||||||
name: 'VbenGithub',
|
|
||||||
path: '/vben-admin/github',
|
|
||||||
component: 'IFrameView',
|
|
||||||
type: 'link',
|
|
||||||
status: 1,
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:logo-github',
|
|
||||||
link: 'https://github.com/vbenjs/vue-vben-admin',
|
|
||||||
title: 'Github',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 903,
|
|
||||||
pid: 9,
|
|
||||||
name: 'VbenAntdv',
|
|
||||||
path: '/vben-admin/antdv',
|
|
||||||
component: 'IFrameView',
|
|
||||||
type: 'link',
|
|
||||||
status: 0,
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:hexagon-vertical-solid',
|
|
||||||
badgeType: 'dot',
|
|
||||||
link: 'https://ant.vben.pro',
|
|
||||||
title: 'demos.vben.antdv',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
component: '_core/about/index',
|
|
||||||
type: 'menu',
|
|
||||||
status: 1,
|
|
||||||
meta: {
|
|
||||||
icon: 'lucide:copyright',
|
|
||||||
order: 9999,
|
|
||||||
title: 'demos.vben.about',
|
|
||||||
},
|
|
||||||
name: 'About',
|
|
||||||
path: '/about',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function getMenuIds(menus: any[]) {
|
|
||||||
const ids: number[] = [];
|
|
||||||
menus.forEach((item) => {
|
|
||||||
ids.push(item.id);
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
ids.push(...getMenuIds(item.children));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 时区选项
|
|
||||||
*/
|
|
||||||
export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
|
|
||||||
{
|
|
||||||
offset: -5,
|
|
||||||
timezone: 'America/New_York',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
timezone: 'Europe/London',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 8,
|
|
||||||
timezone: 'Asia/Shanghai',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 9,
|
|
||||||
timezone: 'Asia/Tokyo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 9,
|
|
||||||
timezone: 'Asia/Seoul',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
|
||||||
|
|
||||||
import { setResponseStatus } from 'h3';
|
|
||||||
|
|
||||||
export function useResponseSuccess<T = any>(data: T) {
|
|
||||||
return {
|
|
||||||
code: 0,
|
|
||||||
data,
|
|
||||||
error: null,
|
|
||||||
message: 'ok',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePageResponseSuccess<T = any>(
|
|
||||||
page: number | string,
|
|
||||||
pageSize: number | string,
|
|
||||||
list: T[],
|
|
||||||
{ message = 'ok' } = {},
|
|
||||||
) {
|
|
||||||
const pageData = pagination(
|
|
||||||
Number.parseInt(`${page}`),
|
|
||||||
Number.parseInt(`${pageSize}`),
|
|
||||||
list,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...useResponseSuccess({
|
|
||||||
items: pageData,
|
|
||||||
total: list.length,
|
|
||||||
}),
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useResponseError(message: string, error: any = null) {
|
|
||||||
return {
|
|
||||||
code: -1,
|
|
||||||
data: null,
|
|
||||||
error,
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forbiddenResponse(
|
|
||||||
event: H3Event<EventHandlerRequest>,
|
|
||||||
message = 'Forbidden Exception',
|
|
||||||
) {
|
|
||||||
setResponseStatus(event, 403);
|
|
||||||
return useResponseError(message, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
|
|
||||||
setResponseStatus(event, 401);
|
|
||||||
return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sleep(ms: number) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pagination<T = any>(
|
|
||||||
pageNo: number,
|
|
||||||
pageSize: number,
|
|
||||||
array: T[],
|
|
||||||
): T[] {
|
|
||||||
const offset = (pageNo - 1) * Number(pageSize);
|
|
||||||
return offset + Number(pageSize) >= array.length
|
|
||||||
? array.slice(offset)
|
|
||||||
: array.slice(offset, offset + Number(pageSize));
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
let mockTimeZone: null | string = null;
|
|
||||||
|
|
||||||
export const setTimezone = (timeZone: string) => {
|
|
||||||
mockTimeZone = timeZone;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTimezone = () => {
|
|
||||||
return mockTimeZone;
|
|
||||||
};
|
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
erpCountInputFormatter,
|
erpCountInputFormatter,
|
||||||
erpNumberFormatter,
|
erpNumberFormatter,
|
||||||
fenToYuan,
|
fenToYuan,
|
||||||
|
formatFileSize,
|
||||||
formatPast2,
|
formatPast2,
|
||||||
isFunction,
|
isFunction,
|
||||||
isString,
|
isString,
|
||||||
@@ -354,12 +355,7 @@ setupVbenVxeTable({
|
|||||||
// add by 星语:文件大小格式化
|
// add by 星语:文件大小格式化
|
||||||
vxeUI.formats.add('formatFileSize', {
|
vxeUI.formats.add('formatFileSize', {
|
||||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||||
if (!cellValue) return '0 B';
|
return formatFileSize(cellValue, digits);
|
||||||
const unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
const index = Math.floor(Math.log(cellValue) / Math.log(1024));
|
|
||||||
const size = cellValue / 1024 ** index;
|
|
||||||
const formattedSize = size.toFixed(digits);
|
|
||||||
return `${formattedSize} ${unitArr[index]}`;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ import { computed, ref, toRefs, watch } from 'vue';
|
|||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { isFunction, isObject, isString } from '@vben/utils';
|
import { checkFileType, isFunction, isObject, isString } from '@vben/utils';
|
||||||
|
|
||||||
import { Button, message, Upload } from 'ant-design-vue';
|
import { Button, message, Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
import { checkFileType } from './helper';
|
|
||||||
import { UploadResultStatus } from './typing';
|
import { UploadResultStatus } from './typing';
|
||||||
import { useUpload, useUploadType } from './use-upload';
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* 默认图片类型
|
|
||||||
*/
|
|
||||||
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
||||||
|
|
||||||
export function checkFileType(file: File, accepts: string[]) {
|
|
||||||
if (!accepts || accepts.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const newTypes = accepts.join('|');
|
|
||||||
const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i');
|
|
||||||
return reg.test(file.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkImgType(
|
|
||||||
file: File,
|
|
||||||
accepts: string[] = defaultImageAccepts,
|
|
||||||
) {
|
|
||||||
return checkFileType(file, accepts);
|
|
||||||
}
|
|
||||||
@@ -10,11 +10,16 @@ import { computed, ref, toRefs, watch } from 'vue';
|
|||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { isFunction, isObject, isString } from '@vben/utils';
|
import {
|
||||||
|
defaultImageAccepts,
|
||||||
|
isFunction,
|
||||||
|
isImage,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
} from '@vben/utils';
|
||||||
|
|
||||||
import { message, Modal, Upload } from 'ant-design-vue';
|
import { message, Modal, Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
import { checkImgType, defaultImageAccepts } from './helper';
|
|
||||||
import { UploadResultStatus } from './typing';
|
import { UploadResultStatus } from './typing';
|
||||||
import { useUpload, useUploadType } from './use-upload';
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
|
|
||||||
@@ -159,7 +164,7 @@ async function beforeUpload(file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { maxSize, accept } = props;
|
const { maxSize, accept } = props;
|
||||||
const isAct = checkImgType(file, accept);
|
const isAct = isImage(file.name, accept);
|
||||||
if (!isAct) {
|
if (!isAct) {
|
||||||
message.error($t('ui.upload.acceptUpload', [accept]));
|
message.error($t('ui.upload.acceptUpload', [accept]));
|
||||||
isActMsg.value = false;
|
isActMsg.value = false;
|
||||||
|
|||||||
@@ -71,12 +71,15 @@ function toggleExpanded() {
|
|||||||
.scrollbar-thin::-webkit-scrollbar {
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-track {
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
@apply rounded-sm bg-gray-400/40;
|
@apply rounded-sm bg-gray-400/40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
@apply bg-gray-400/60;
|
@apply bg-gray-400/60;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,11 +191,17 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '角色名称',
|
label: '角色名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色名称',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'category',
|
fieldName: 'category',
|
||||||
label: '角色类别',
|
label: '角色类别',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色类别',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'publicStatus',
|
fieldName: 'publicStatus',
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
|||||||
const [Descriptions] = useDescription({
|
const [Descriptions] = useDescription({
|
||||||
bordered: false,
|
bordered: false,
|
||||||
column: 4,
|
column: 4,
|
||||||
class: 'mx-4',
|
|
||||||
schema: useDetailSchema(),
|
schema: useDetailSchema(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const [SystemDescription] = useDescription({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div>
|
||||||
<BaseDescription :data="business" />
|
<BaseDescription :data="business" />
|
||||||
<Divider />
|
<Divider />
|
||||||
<SystemDescription :data="business" />
|
<SystemDescription :data="business" />
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) {
|
|||||||
await deleteBusinessStatus(row.id!);
|
await deleteBusinessStatus(row.id!);
|
||||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
handleRefresh();
|
handleRefresh();
|
||||||
} catch {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
|
|
||||||
/** 添加状态 */
|
/** 添加状态 */
|
||||||
async function handleAddStatus() {
|
async function handleAddStatus() {
|
||||||
formData.value!.statuses!.unshift({
|
formData.value!.statuses!.splice(-3, 0, {
|
||||||
name: '',
|
name: '',
|
||||||
percent: undefined,
|
percent: undefined,
|
||||||
} as any);
|
} as any);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
|||||||
const [Descriptions] = useDescription({
|
const [Descriptions] = useDescription({
|
||||||
bordered: false,
|
bordered: false,
|
||||||
column: 4,
|
column: 4,
|
||||||
class: 'mx-4',
|
|
||||||
schema: useDetailSchema(),
|
schema: useDetailSchema(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const [SystemDescriptions] = useDescription({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div>
|
||||||
<BaseDescriptions :data="contact" />
|
<BaseDescriptions :data="contact" />
|
||||||
<Divider />
|
<Divider />
|
||||||
<SystemDescriptions :data="contact" />
|
<SystemDescriptions :data="contact" />
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
showTime: false,
|
showTime: false,
|
||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择下单日期',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,6 +121,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
showTime: false,
|
showTime: false,
|
||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择合同开始时间',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -130,6 +132,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
showTime: false,
|
showTime: false,
|
||||||
format: 'YYYY-MM-DD',
|
format: 'YYYY-MM-DD',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择合同结束时间',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -197,6 +200,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
min: 0,
|
min: 0,
|
||||||
precision: 2,
|
precision: 2,
|
||||||
|
placeholder: '请输入产品总金额',
|
||||||
},
|
},
|
||||||
rules: z.number().min(0).optional().default(0),
|
rules: z.number().min(0).optional().default(0),
|
||||||
},
|
},
|
||||||
@@ -207,6 +211,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
min: 0,
|
min: 0,
|
||||||
precision: 2,
|
precision: 2,
|
||||||
|
placeholder: '请输入整单折扣',
|
||||||
},
|
},
|
||||||
rules: z.number().min(0).max(100).optional().default(0),
|
rules: z.number().min(0).max(100).optional().default(0),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
|||||||
const [Descriptions] = useDescription({
|
const [Descriptions] = useDescription({
|
||||||
bordered: false,
|
bordered: false,
|
||||||
column: 4,
|
column: 4,
|
||||||
class: 'mx-4',
|
|
||||||
schema: useDetailSchema(),
|
schema: useDetailSchema(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const [SystemDescriptions] = useDescription({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div>
|
||||||
<BaseDescriptions :data="contract" />
|
<BaseDescriptions :data="contract" />
|
||||||
<Divider />
|
<Divider />
|
||||||
<SystemDescriptions :data="contract" />
|
<SystemDescriptions :data="contract" />
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
|||||||
const [Descriptions] = useDescription({
|
const [Descriptions] = useDescription({
|
||||||
bordered: false,
|
bordered: false,
|
||||||
column: 4,
|
column: 4,
|
||||||
class: 'mx-4',
|
|
||||||
schema: useDetailSchema(),
|
schema: useDetailSchema(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const [SystemDescriptions] = useDescription({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div>
|
||||||
<BaseDescriptions :data="customer" />
|
<BaseDescriptions :data="customer" />
|
||||||
<Divider />
|
<Divider />
|
||||||
<SystemDescriptions :data="customer" />
|
<SystemDescriptions :data="customer" />
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function useFormSchema(
|
|||||||
showTime: true,
|
showTime: true,
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择下次联系时间',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const data = (await formApi.getValues()) as CrmPermissionApi.BusinessTransferReqVO;
|
const data =
|
||||||
|
(await formApi.getValues()) as CrmPermissionApi.BusinessTransferReqVO;
|
||||||
try {
|
try {
|
||||||
switch (bizType.value) {
|
switch (bizType.value) {
|
||||||
case BizTypeEnum.CRM_BUSINESS: {
|
case BizTypeEnum.CRM_BUSINESS: {
|
||||||
|
|||||||
@@ -126,10 +126,8 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 产品下拉选项 */
|
|
||||||
const productOptions = ref<CrmProductApi.Product[]>([]);
|
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
|
const productOptions = ref<CrmProductApi.Product[]>([]); // 产品下拉选项
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
productOptions.value = await getProductSimpleList();
|
productOptions.value = await getProductSimpleList();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
defaultValue: [
|
defaultValue: [
|
||||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||||
] as [Date, Date],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'interval',
|
fieldName: 'interval',
|
||||||
@@ -74,6 +74,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
|
placeholder: '请选择时间间隔',
|
||||||
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
||||||
},
|
},
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
@@ -91,6 +92,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
childrenField: 'children',
|
childrenField: 'children',
|
||||||
treeDefaultExpandAll: true,
|
treeDefaultExpandAll: true,
|
||||||
|
placeholder: '请选择归属部门',
|
||||||
},
|
},
|
||||||
defaultValue: userStore.userInfo?.deptId,
|
defaultValue: userStore.userInfo?.deptId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,13 +25,11 @@ const { renderEcharts } = useEcharts(chartRef);
|
|||||||
|
|
||||||
const [QueryForm, formApi] = useVbenForm({
|
const [QueryForm, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
// 是否可展开
|
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
submitButtonOptions: {
|
submitButtonOptions: {
|
||||||
content: $t('common.query'),
|
content: $t('common.query'),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
defaultValue: [
|
defaultValue: [
|
||||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||||
] as [Date, Date],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'interval',
|
fieldName: 'interval',
|
||||||
@@ -48,6 +48,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
|
placeholder: '请选择时间间隔',
|
||||||
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
||||||
},
|
},
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
@@ -65,6 +66,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
childrenField: 'children',
|
childrenField: 'children',
|
||||||
treeDefaultExpandAll: true,
|
treeDefaultExpandAll: true,
|
||||||
|
placeholder: '请选择归属部门',
|
||||||
},
|
},
|
||||||
defaultValue: userStore.userInfo?.deptId,
|
defaultValue: userStore.userInfo?.deptId,
|
||||||
},
|
},
|
||||||
@@ -77,6 +79,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
allowClear: true,
|
allowClear: true,
|
||||||
labelField: 'nickname',
|
labelField: 'nickname',
|
||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
|
placeholder: '请选择员工',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -42,13 +42,11 @@ const gridEvents: VxeGridListeners = {
|
|||||||
};
|
};
|
||||||
const [QueryForm, formApi] = useVbenForm({
|
const [QueryForm, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
// 是否可展开
|
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
submitButtonOptions: {
|
submitButtonOptions: {
|
||||||
content: $t('common.query'),
|
content: $t('common.query'),
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
import { useUserStore } from '@vben/stores';
|
import { useUserStore } from '@vben/stores';
|
||||||
import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
import { getSimpleDeptList } from '#/api/system/dept';
|
import { getSimpleDeptList } from '#/api/system/dept';
|
||||||
import { getSimpleUserList } from '#/api/system/user';
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@@ -28,20 +27,16 @@ export const customerSummaryTabs = [
|
|||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
fieldName: 'times',
|
fieldName: 'time',
|
||||||
label: '时间范围',
|
label: '选择年份',
|
||||||
component: 'RangePicker',
|
component: 'DatePicker',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
...getRangePickerDefaultProps(),
|
|
||||||
picker: 'year',
|
picker: 'year',
|
||||||
showTime: false,
|
|
||||||
format: 'YYYY',
|
format: 'YYYY',
|
||||||
ranges: {},
|
valueFormat: 'YYYY',
|
||||||
|
placeholder: '请选择年份',
|
||||||
},
|
},
|
||||||
defaultValue: [
|
defaultValue: new Date().getFullYear().toString(),
|
||||||
formatDateTime(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
|
|
||||||
formatDateTime(endOfDay(new Date(new Date().getFullYear(), 11, 31))),
|
|
||||||
] as [Date, Date],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'deptId',
|
fieldName: 'deptId',
|
||||||
@@ -56,6 +51,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
childrenField: 'children',
|
childrenField: 'children',
|
||||||
treeDefaultExpandAll: true,
|
treeDefaultExpandAll: true,
|
||||||
|
placeholder: '请选择归属部门',
|
||||||
},
|
},
|
||||||
defaultValue: userStore.userInfo?.deptId,
|
defaultValue: userStore.userInfo?.deptId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { onMounted, ref } from 'vue';
|
|||||||
|
|
||||||
import { ContentWrap, Page } from '@vben/common-ui';
|
import { ContentWrap, Page } from '@vben/common-ui';
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import { beginOfDay, endOfDay, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
import { Tabs } from 'ant-design-vue';
|
import { Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
@@ -29,13 +30,11 @@ const { renderEcharts } = useEcharts(chartRef);
|
|||||||
|
|
||||||
const [QueryForm, formApi] = useVbenForm({
|
const [QueryForm, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
// 是否可展开
|
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
submitButtonOptions: {
|
submitButtonOptions: {
|
||||||
content: $t('common.query'),
|
content: $t('common.query'),
|
||||||
@@ -70,6 +69,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
async function handleTabChange(key: any) {
|
async function handleTabChange(key: any) {
|
||||||
activeTabName.value = key;
|
activeTabName.value = key;
|
||||||
const queryParams = (await formApi.getValues()) as any;
|
const queryParams = (await formApi.getValues()) as any;
|
||||||
|
// 将年份转换为年初和年末的日期时间
|
||||||
|
const selectYear = Number.parseInt(queryParams.time);
|
||||||
|
queryParams.times = [];
|
||||||
|
queryParams.times[0] = formatDateTime(beginOfDay(new Date(selectYear, 0, 1)));
|
||||||
|
queryParams.times[1] = formatDateTime(endOfDay(new Date(selectYear, 11, 31)));
|
||||||
let data: any[] = [];
|
let data: any[] = [];
|
||||||
const columnsData: any[] = [];
|
const columnsData: any[] = [];
|
||||||
let tableData: any[] = [];
|
let tableData: any[] = [];
|
||||||
|
|||||||
@@ -39,13 +39,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'RangePicker',
|
component: 'RangePicker',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
...getRangePickerDefaultProps(),
|
...getRangePickerDefaultProps(),
|
||||||
format: 'YYYY-MM-DD',
|
|
||||||
picker: 'year',
|
|
||||||
},
|
},
|
||||||
defaultValue: [
|
defaultValue: [
|
||||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||||
] as [Date, Date],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'deptId',
|
fieldName: 'deptId',
|
||||||
@@ -60,6 +58,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
childrenField: 'children',
|
childrenField: 'children',
|
||||||
treeDefaultExpandAll: true,
|
treeDefaultExpandAll: true,
|
||||||
|
placeholder: '请选择归属部门',
|
||||||
},
|
},
|
||||||
defaultValue: userStore.userInfo?.deptId,
|
defaultValue: userStore.userInfo?.deptId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,13 +27,11 @@ const { renderEcharts: renderRightEcharts } = useEcharts(rightChartRef);
|
|||||||
|
|
||||||
const [QueryForm, formApi] = useVbenForm({
|
const [QueryForm, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
// 是否可展开
|
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
submitButtonOptions: {
|
submitButtonOptions: {
|
||||||
content: $t('common.query'),
|
content: $t('common.query'),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
defaultValue: [
|
defaultValue: [
|
||||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||||
] as [Date, Date],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'deptId',
|
fieldName: 'deptId',
|
||||||
@@ -72,6 +72,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
childrenField: 'children',
|
childrenField: 'children',
|
||||||
treeDefaultExpandAll: true,
|
treeDefaultExpandAll: true,
|
||||||
|
placeholder: '请选择归属部门',
|
||||||
},
|
},
|
||||||
defaultValue: userStore.userInfo?.deptId,
|
defaultValue: userStore.userInfo?.deptId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,13 +25,11 @@ const { renderEcharts } = useEcharts(chartRef);
|
|||||||
|
|
||||||
const [QueryForm, formApi] = useVbenForm({
|
const [QueryForm, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
// 是否可展开
|
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
submitButtonOptions: {
|
submitButtonOptions: {
|
||||||
content: $t('common.query'),
|
content: $t('common.query'),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
showTime: true,
|
showTime: true,
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择出生年',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
showTime: true,
|
showTime: true,
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择出生日期',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
showTime: true,
|
showTime: true,
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
valueFormat: 'x',
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择出生日期',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ function handleSliderChange(prop: string) {
|
|||||||
:max="100"
|
:max="100"
|
||||||
:min="0"
|
:min="0"
|
||||||
@change="handleSliderChange(dataRef.prop)"
|
@change="handleSliderChange(dataRef.prop)"
|
||||||
class="mr-[16px]"
|
class="mr-4"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="2">
|
<Col :span="2">
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ onMounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-size-[auto_auto] relative mx-auto my-0 min-h-full w-96 items-center justify-center bg-no-repeat"
|
class="relative mx-auto my-0 min-h-full w-96 items-center justify-center bg-auto bg-no-repeat"
|
||||||
>
|
>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="pageComponents"
|
v-model="pageComponents"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useRoute } from 'vue-router';
|
|||||||
import { useTabs } from '@vben/hooks';
|
import { useTabs } from '@vben/hooks';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
import { isEmpty, isNumber } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { message, Radio, RadioGroup } from 'ant-design-vue';
|
import { message, Radio, RadioGroup } from 'ant-design-vue';
|
||||||
|
|
||||||
@@ -26,29 +26,35 @@ defineOptions({ name: 'DiyTemplateDecorate' });
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { refreshTab } = useTabs();
|
const { refreshTab } = useTabs();
|
||||||
|
|
||||||
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
|
||||||
|
// 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
|
||||||
|
const DIY_PAGE_INDEX_KEY = 'diy_page_index';
|
||||||
|
|
||||||
const selectedTemplateItem = ref(0);
|
const selectedTemplateItem = ref(0);
|
||||||
|
// 左上角工具栏操作按钮
|
||||||
const templateItems = ref([
|
const templateItems = ref([
|
||||||
{ name: '基础设置', icon: 'lucide:settings' },
|
{ key: 0, name: '基础设置', icon: 'lucide:settings' },
|
||||||
{ name: '首页', icon: 'lucide:home' },
|
{ key: 1, name: '首页', icon: 'lucide:home' },
|
||||||
{ name: '我的', icon: 'lucide:user' },
|
{ key: 2, name: '我的', icon: 'lucide:user' },
|
||||||
]); // 左上角工具栏操作按钮
|
]);
|
||||||
|
|
||||||
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
||||||
|
// 当前编辑的属性
|
||||||
const currentFormData = ref<
|
const currentFormData = ref<
|
||||||
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
||||||
>({
|
>({
|
||||||
property: '',
|
property: '',
|
||||||
} as MallDiyPageApi.DiyPage); // 当前编辑的属性
|
} as MallDiyPageApi.DiyPage);
|
||||||
|
// templateItem 对应的缓存
|
||||||
const currentFormDataMap = ref<
|
const currentFormDataMap = ref<
|
||||||
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
||||||
>(new Map()); // templateItem 对应的缓存
|
>(new Map());
|
||||||
|
// 商城 H5 预览地址
|
||||||
const previewUrl = ref(''); // 商城 H5 预览地址
|
const previewUrl = ref('');
|
||||||
|
// 模板组件库
|
||||||
const templateLibs = [] as DiyComponentLibrary[]; // 模板组件库
|
const templateLibs = [] as DiyComponentLibrary[];
|
||||||
const libs = ref<DiyComponentLibrary[]>(templateLibs); // 当前组件库
|
// 当前组件库
|
||||||
|
const libs = ref<DiyComponentLibrary[]>(templateLibs);
|
||||||
|
|
||||||
/** 获取详情 */
|
/** 获取详情 */
|
||||||
async function getPageDetail(id: any) {
|
async function getPageDetail(id: any) {
|
||||||
@@ -58,38 +64,32 @@ async function getPageDetail(id: any) {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
formData.value = await getDiyTemplateProperty(id);
|
formData.value = await getDiyTemplateProperty(id);
|
||||||
|
|
||||||
// 拼接手机预览链接
|
// 拼接手机预览链接
|
||||||
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
previewUrl.value = `${domain}?templateId=${formData.value.id}&${accessStore.tenantId}`;
|
previewUrl.value = `${domain}?templateId=${formData.value.id}&tenantId=${accessStore.tenantId}`;
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模板选项切换 */
|
/** 模板选项切换 */
|
||||||
// TODO @xingyu:貌似切换不对;“个人中心”切换不过去;
|
function handleTemplateItemChange(val: any) {
|
||||||
function handleTemplateItemChange(event: any) {
|
const changeValue = val.target.value;
|
||||||
// 从事件对象中获取值
|
|
||||||
const val = event.target?.value ?? event;
|
|
||||||
// 切换模版
|
|
||||||
selectedTemplateItem.value = isNumber(val)
|
|
||||||
? val
|
|
||||||
: templateItems.value.findIndex((item) => item.name === val.name);
|
|
||||||
|
|
||||||
// 缓存模版编辑数据
|
// 缓存模版编辑数据
|
||||||
currentFormDataMap.value.set(
|
currentFormDataMap.value.set(
|
||||||
templateItems.value[selectedTemplateItem.value]?.name || '',
|
templateItems.value[changeValue]!.name,
|
||||||
currentFormData.value!,
|
currentFormData.value!,
|
||||||
);
|
);
|
||||||
|
// 切换模版
|
||||||
|
selectedTemplateItem.value = changeValue;
|
||||||
|
|
||||||
// 读取模版缓存
|
// 读取模版缓存
|
||||||
const data = currentFormDataMap.value.get(
|
const data = currentFormDataMap.value.get(
|
||||||
templateItems.value[selectedTemplateItem.value]?.name || '',
|
templateItems.value[changeValue]!.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 情况一:编辑模板
|
// 情况一:编辑模板
|
||||||
if (val === 0) {
|
if (changeValue === 0) {
|
||||||
libs.value = templateLibs;
|
libs.value = templateLibs;
|
||||||
currentFormData.value = (isEmpty(data) ? formData.value : data) as
|
currentFormData.value = (isEmpty(data) ? formData.value : data) as
|
||||||
| MallDiyPageApi.DiyPage
|
| MallDiyPageApi.DiyPage
|
||||||
@@ -99,22 +99,14 @@ function handleTemplateItemChange(event: any) {
|
|||||||
|
|
||||||
// 情况二:编辑页面
|
// 情况二:编辑页面
|
||||||
libs.value = PAGE_LIBS;
|
libs.value = PAGE_LIBS;
|
||||||
const pageData = isEmpty(data)
|
currentFormData.value = (
|
||||||
|
isEmpty(data)
|
||||||
? formData.value!.pages.find(
|
? formData.value!.pages.find(
|
||||||
(page: MallDiyPageApi.DiyPage) =>
|
(page: MallDiyPageApi.DiyPage) =>
|
||||||
page.name === templateItems.value[val]?.name,
|
page.name === templateItems.value[changeValue]!.name,
|
||||||
)
|
)
|
||||||
: data;
|
: data
|
||||||
|
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||||
// 如果找不到页面数据,使用默认值
|
|
||||||
currentFormData.value = pageData
|
|
||||||
? (pageData as
|
|
||||||
| MallDiyPageApi.DiyPage
|
|
||||||
| MallDiyTemplateApi.DiyTemplateProperty)
|
|
||||||
: ({
|
|
||||||
property: '',
|
|
||||||
name: templateItems.value[val]?.name || '',
|
|
||||||
} as MallDiyPageApi.DiyPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提交表单 */
|
/** 提交表单 */
|
||||||
@@ -210,15 +202,8 @@ onMounted(async () => {
|
|||||||
size="large"
|
size="large"
|
||||||
@change="handleTemplateItemChange"
|
@change="handleTemplateItemChange"
|
||||||
>
|
>
|
||||||
<template v-for="(item, index) in templateItems" :key="index">
|
<template v-for="item in templateItems" :key="item.key">
|
||||||
<Radio.Button
|
<Radio.Button :value="item.key">
|
||||||
:value="item"
|
|
||||||
:class="
|
|
||||||
index === selectedTemplateItem
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
class="mt-2 flex size-5 items-center"
|
class="mt-2 flex size-5 items-center"
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '秒杀时段名称',
|
label: '秒杀时段名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入秒杀时段名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -160,12 +160,18 @@ export function useCreateFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'userId',
|
fieldName: 'userId',
|
||||||
label: '分销员编号',
|
label: '分销员编号',
|
||||||
component: 'InputSearch',
|
component: 'InputSearch',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入分销员编号',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'bindUserId',
|
fieldName: 'bindUserId',
|
||||||
label: '上级推广员编号',
|
label: '上级推广员编号',
|
||||||
component: 'InputSearch',
|
component: 'InputSearch',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入上级推广员编号',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -178,6 +184,9 @@ export function useUpdateFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'bindUserId',
|
fieldName: 'bindUserId',
|
||||||
label: '上级推广员编号',
|
label: '上级推广员编号',
|
||||||
component: 'InputSearch',
|
component: 'InputSearch',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入上级推广员编号',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '分组名称',
|
label: '分组名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入分组名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'mobile',
|
fieldName: 'mobile',
|
||||||
label: '手机号',
|
label: '手机号',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入手机号',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'title',
|
fieldName: 'title',
|
||||||
label: '公告标题',
|
label: '公告标题',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入公告标题',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '岗位名称',
|
label: '岗位名称',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入岗位名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'code',
|
fieldName: 'code',
|
||||||
label: '岗位编码',
|
label: '岗位编码',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入岗位编码',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -35,6 +41,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
min: 0,
|
min: 0,
|
||||||
|
placeholder: '请输入显示顺序',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
@@ -53,6 +60,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'remark',
|
fieldName: 'remark',
|
||||||
label: '岗位备注',
|
label: '岗位备注',
|
||||||
component: 'Textarea',
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入岗位备注',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '角色名称',
|
label: '角色名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'code',
|
fieldName: 'code',
|
||||||
label: '角色标识',
|
label: '角色标识',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色标识',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,6 +65,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'remark',
|
fieldName: 'remark',
|
||||||
label: '角色备注',
|
label: '角色备注',
|
||||||
component: 'Textarea',
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色备注',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
|
options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
|
||||||
|
placeholder: '请选择社交平台',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '租户名称',
|
label: '租户名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入租户名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,18 +49,27 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'contactName',
|
fieldName: 'contactName',
|
||||||
label: '联系人',
|
label: '联系人',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系人',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'contactMobile',
|
fieldName: 'contactMobile',
|
||||||
label: '联系手机',
|
label: '联系手机',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系手机',
|
||||||
|
},
|
||||||
rules: 'mobile',
|
rules: 'mobile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '用户名称',
|
label: '用户名称',
|
||||||
fieldName: 'username',
|
fieldName: 'username',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['id'],
|
triggerFields: ['id'],
|
||||||
@@ -78,6 +90,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
label: '账号额度',
|
label: '账号额度',
|
||||||
fieldName: 'accountCount',
|
fieldName: 'accountCount',
|
||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入账号额度',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '套餐名称',
|
label: '套餐名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入套餐名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,6 +48,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'remark',
|
fieldName: 'remark',
|
||||||
label: '备注',
|
label: '备注',
|
||||||
component: 'Textarea',
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'username',
|
fieldName: 'username',
|
||||||
label: '用户名称',
|
label: '用户名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -44,6 +47,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'nickname',
|
fieldName: 'nickname',
|
||||||
label: '用户昵称',
|
label: '用户昵称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户昵称',
|
||||||
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -117,6 +123,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'remark',
|
fieldName: 'remark',
|
||||||
label: '备注',
|
label: '备注',
|
||||||
component: 'Textarea',
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
erpCountInputFormatter,
|
erpCountInputFormatter,
|
||||||
erpNumberFormatter,
|
erpNumberFormatter,
|
||||||
fenToYuan,
|
fenToYuan,
|
||||||
|
formatFileSize,
|
||||||
formatPast2,
|
formatPast2,
|
||||||
isFunction,
|
isFunction,
|
||||||
isString,
|
isString,
|
||||||
@@ -344,12 +345,7 @@ setupVbenVxeTable({
|
|||||||
// add by 星语:文件大小格式化
|
// add by 星语:文件大小格式化
|
||||||
vxeUI.formats.add('formatFileSize', {
|
vxeUI.formats.add('formatFileSize', {
|
||||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||||
if (!cellValue) return '0 B';
|
return formatFileSize(cellValue, digits);
|
||||||
const unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
const index = Math.floor(Math.log(cellValue) / Math.log(1024));
|
|
||||||
const size = cellValue / 1024 ** index;
|
|
||||||
const formattedSize = size.toFixed(digits);
|
|
||||||
return `${formattedSize} ${unitArr[index]}`;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
export function checkFileType(file: File, accepts: string[]) {
|
|
||||||
if (!accepts || accepts.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const newTypes = accepts.join('|');
|
|
||||||
const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i');
|
|
||||||
return reg.test(file.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认图片类型
|
|
||||||
*/
|
|
||||||
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
||||||
|
|
||||||
export function checkImgType(
|
|
||||||
file: File,
|
|
||||||
accepts: string[] = defaultImageAccepts,
|
|
||||||
) {
|
|
||||||
return checkFileType(file, accepts);
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,16 @@ import { ref, toRefs, watch } from 'vue';
|
|||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { isFunction, isObject, isString } from '@vben/utils';
|
import {
|
||||||
|
defaultImageAccepts,
|
||||||
|
isFunction,
|
||||||
|
isImage,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
} from '@vben/utils';
|
||||||
|
|
||||||
import { ElMessage, ElUpload } from 'element-plus';
|
import { ElMessage, ElUpload } from 'element-plus';
|
||||||
|
|
||||||
import { checkImgType, defaultImageAccepts } from './helper';
|
|
||||||
import { UploadResultStatus } from './typing';
|
import { UploadResultStatus } from './typing';
|
||||||
import { useUpload, useUploadType } from './use-upload';
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
|
|
||||||
@@ -173,7 +178,7 @@ const handleRemove = async (file: UploadFile) => {
|
|||||||
|
|
||||||
const beforeUpload = async (file: File) => {
|
const beforeUpload = async (file: File) => {
|
||||||
const { maxSize, accept } = props;
|
const { maxSize, accept } = props;
|
||||||
const isAct = checkImgType(file, accept);
|
const isAct = isImage(file.name, accept);
|
||||||
if (!isAct) {
|
if (!isAct) {
|
||||||
ElMessage.error($t('ui.upload.acceptUpload', [accept]));
|
ElMessage.error($t('ui.upload.acceptUpload', [accept]));
|
||||||
isActMsg.value = false;
|
isActMsg.value = false;
|
||||||
|
|||||||
@@ -20,33 +20,33 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('#/views/crm/clue/detail/index.vue'),
|
component: () => import('#/views/crm/clue/detail/index.vue'),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: 'customer/detail/:id',
|
path: 'customer/detail/:id',
|
||||||
// name: 'CrmCustomerDetail',
|
name: 'CrmCustomerDetail',
|
||||||
// meta: {
|
meta: {
|
||||||
// title: '客户详情',
|
title: '客户详情',
|
||||||
// activePath: '/crm/customer',
|
activePath: '/crm/customer',
|
||||||
// },
|
},
|
||||||
// component: () => import('#/views/crm/customer/detail/index.vue'),
|
component: () => import('#/views/crm/customer/detail/index.vue'),
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// path: 'business/detail/:id',
|
path: 'business/detail/:id',
|
||||||
// name: 'CrmBusinessDetail',
|
name: 'CrmBusinessDetail',
|
||||||
// meta: {
|
meta: {
|
||||||
// title: '商机详情',
|
title: '商机详情',
|
||||||
// activePath: '/crm/business',
|
activePath: '/crm/business',
|
||||||
// },
|
},
|
||||||
// component: () => import('#/views/crm/business/detail/index.vue'),
|
component: () => import('#/views/crm/business/detail/index.vue'),
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// path: 'contract/detail/:id',
|
path: 'contract/detail/:id',
|
||||||
// name: 'CrmContractDetail',
|
name: 'CrmContractDetail',
|
||||||
// meta: {
|
meta: {
|
||||||
// title: '合同详情',
|
title: '合同详情',
|
||||||
// activePath: '/crm/contract',
|
activePath: '/crm/contract',
|
||||||
// },
|
},
|
||||||
// component: () => import('#/views/crm/contract/detail/index.vue'),
|
component: () => import('#/views/crm/contract/detail/index.vue'),
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
path: 'receivable-plan/detail/:id',
|
path: 'receivable-plan/detail/:id',
|
||||||
name: 'CrmReceivablePlanDetail',
|
name: 'CrmReceivablePlanDetail',
|
||||||
@@ -65,15 +65,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('#/views/crm/receivable/detail/index.vue'),
|
component: () => import('#/views/crm/receivable/detail/index.vue'),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: 'contact/detail/:id',
|
path: 'contact/detail/:id',
|
||||||
// name: 'CrmContactDetail',
|
name: 'CrmContactDetail',
|
||||||
// meta: {
|
meta: {
|
||||||
// title: '联系人详情',
|
title: '联系人详情',
|
||||||
// activePath: '/crm/contact',
|
activePath: '/crm/contact',
|
||||||
// },
|
},
|
||||||
// component: () => import('#/views/crm/contact/detail/index.vue'),
|
component: () => import('#/views/crm/contact/detail/index.vue'),
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
path: 'product/detail/:id',
|
path: 'product/detail/:id',
|
||||||
name: 'CrmProductDetail',
|
name: 'CrmProductDetail',
|
||||||
@@ -83,6 +83,54 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('#/views/crm/product/detail/index.vue'),
|
component: () => import('#/views/crm/product/detail/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'statistics/customer',
|
||||||
|
name: 'CrmStatisticsCustomer',
|
||||||
|
meta: {
|
||||||
|
title: '客户统计',
|
||||||
|
activePath: '/crm/statistics/customer',
|
||||||
|
},
|
||||||
|
component: () =>
|
||||||
|
import('#/views/crm/statistics/customer/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'statistics/funnel',
|
||||||
|
name: 'CrmStatisticsFunnel',
|
||||||
|
meta: {
|
||||||
|
title: '销售漏斗',
|
||||||
|
activePath: '/crm/statistics/funnel',
|
||||||
|
},
|
||||||
|
component: () => import('#/views/crm/statistics/funnel/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'statistics/performance',
|
||||||
|
name: 'CrmStatisticsPerformance',
|
||||||
|
meta: {
|
||||||
|
title: '员工业绩',
|
||||||
|
activePath: '/crm/statistics/performance',
|
||||||
|
},
|
||||||
|
component: () =>
|
||||||
|
import('#/views/crm/statistics/performance/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'statistics/portrait',
|
||||||
|
name: 'CrmStatisticsPortrait',
|
||||||
|
meta: {
|
||||||
|
title: '客户画像',
|
||||||
|
activePath: '/crm/statistics/portrait',
|
||||||
|
},
|
||||||
|
component: () =>
|
||||||
|
import('#/views/crm/statistics/portrait/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'statistics/rank',
|
||||||
|
name: 'CrmStatisticsRank',
|
||||||
|
meta: {
|
||||||
|
title: '排行榜',
|
||||||
|
activePath: '/crm/statistics/rank',
|
||||||
|
},
|
||||||
|
component: () => import('#/views/crm/statistics/rank/index.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -42,10 +42,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入温度参数',
|
placeholder: '请输入温度参数',
|
||||||
class: 'w-full',
|
|
||||||
precision: 2,
|
precision: 2,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 2,
|
max: 2,
|
||||||
|
controlsPosition: 'right',
|
||||||
|
class: '!w-full',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
@@ -55,9 +56,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入回复数 Token 数',
|
placeholder: '请输入回复数 Token 数',
|
||||||
class: 'w-full',
|
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 8192,
|
max: 8192,
|
||||||
|
controlsPosition: 'right',
|
||||||
|
class: '!w-full',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
@@ -67,9 +69,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入上下文数量',
|
placeholder: '请输入上下文数量',
|
||||||
class: 'w-full',
|
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 20,
|
max: 20,
|
||||||
|
controlsPosition: 'right',
|
||||||
|
class: '!w-full',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,12 +71,15 @@ function toggleExpanded() {
|
|||||||
.scrollbar-thin::-webkit-scrollbar {
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-track {
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
@apply rounded-sm bg-gray-400/40;
|
@apply rounded-sm bg-gray-400/40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
@apply bg-gray-400/60;
|
@apply bg-gray-400/60;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,11 +189,17 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
label: '角色名称',
|
label: '角色名称',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色名称',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'category',
|
fieldName: 'category',
|
||||||
label: '角色类别',
|
label: '角色类别',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色类别',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'publicStatus',
|
fieldName: 'publicStatus',
|
||||||
|
|||||||
@@ -105,10 +105,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入温度参数',
|
placeholder: '请输入温度参数',
|
||||||
controlsPosition: 'right',
|
|
||||||
class: '!w-full',
|
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 2,
|
max: 2,
|
||||||
|
controlsPosition: 'right',
|
||||||
|
class: '!w-full',
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
|
|||||||
102
apps/web-ele/src/views/crm/backlog/data.ts
Normal file
102
apps/web-ele/src/views/crm/backlog/data.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
export interface LeftSideItem {
|
||||||
|
name: string;
|
||||||
|
menu: string;
|
||||||
|
count: Ref<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跟进状态 */
|
||||||
|
export const FOLLOWUP_STATUS = [
|
||||||
|
{ label: '待跟进', value: false },
|
||||||
|
{ label: '已跟进', value: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 归属范围 */
|
||||||
|
export const SCENE_TYPES = [
|
||||||
|
{ label: '我负责的', value: 1 },
|
||||||
|
{ label: '我参与的', value: 2 },
|
||||||
|
{ label: '下属负责的', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 联系状态 */
|
||||||
|
export const CONTACT_STATUS = [
|
||||||
|
{ label: '今日需联系', value: 1 },
|
||||||
|
{ label: '已逾期', value: 2 },
|
||||||
|
{ label: '已联系', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 审批状态 */
|
||||||
|
export const AUDIT_STATUS = [
|
||||||
|
{ label: '待审批', value: 10 },
|
||||||
|
{ label: '审核通过', value: 20 },
|
||||||
|
{ label: '审核不通过', value: 30 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 回款提醒类型 */
|
||||||
|
export const RECEIVABLE_REMIND_TYPE = [
|
||||||
|
{ label: '待回款', value: 1 },
|
||||||
|
{ label: '已逾期', value: 2 },
|
||||||
|
{ label: '已回款', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 合同过期状态 */
|
||||||
|
export const CONTRACT_EXPIRY_TYPE = [
|
||||||
|
{ label: '即将过期', value: 1 },
|
||||||
|
{ label: '已过期', value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 左侧菜单 */
|
||||||
|
export const useLeftSides = (
|
||||||
|
customerTodayContactCount: Ref<number>,
|
||||||
|
clueFollowCount: Ref<number>,
|
||||||
|
customerFollowCount: Ref<number>,
|
||||||
|
customerPutPoolRemindCount: Ref<number>,
|
||||||
|
contractAuditCount: Ref<number>,
|
||||||
|
contractRemindCount: Ref<number>,
|
||||||
|
receivableAuditCount: Ref<number>,
|
||||||
|
receivablePlanRemindCount: Ref<number>,
|
||||||
|
): LeftSideItem[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: '今日需联系客户',
|
||||||
|
menu: 'customerTodayContact',
|
||||||
|
count: customerTodayContactCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '分配给我的线索',
|
||||||
|
menu: 'clueFollow',
|
||||||
|
count: clueFollowCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '分配给我的客户',
|
||||||
|
menu: 'customerFollow',
|
||||||
|
count: customerFollowCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '待进入公海的客户',
|
||||||
|
menu: 'customerPutPoolRemind',
|
||||||
|
count: customerPutPoolRemindCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '待审核合同',
|
||||||
|
menu: 'contractAudit',
|
||||||
|
count: contractAuditCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '待审核回款',
|
||||||
|
menu: 'receivableAudit',
|
||||||
|
count: receivableAuditCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '待回款提醒',
|
||||||
|
menu: 'receivablePlanRemind',
|
||||||
|
count: receivablePlanRemindCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '即将到期的合同',
|
||||||
|
menu: 'contractRemind',
|
||||||
|
count: contractRemindCount,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
115
apps/web-ele/src/views/crm/backlog/index.vue
Normal file
115
apps/web-ele/src/views/crm/backlog/index.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ElBadge, ElCard } from 'element-plus';
|
||||||
|
|
||||||
|
import { getFollowClueCount } from '#/api/crm/clue';
|
||||||
|
import {
|
||||||
|
getAuditContractCount,
|
||||||
|
getRemindContractCount,
|
||||||
|
} from '#/api/crm/contract';
|
||||||
|
import {
|
||||||
|
getFollowCustomerCount,
|
||||||
|
getPutPoolRemindCustomerCount,
|
||||||
|
getTodayContactCustomerCount,
|
||||||
|
} from '#/api/crm/customer';
|
||||||
|
import { getAuditReceivableCount } from '#/api/crm/receivable';
|
||||||
|
import { getReceivablePlanRemindCount } from '#/api/crm/receivable/plan';
|
||||||
|
|
||||||
|
import { useLeftSides } from './data';
|
||||||
|
import ClueFollowList from './modules/clue-follow-list.vue';
|
||||||
|
import ContractAuditList from './modules/contract-audit-list.vue';
|
||||||
|
import ContractRemindList from './modules/contract-remind-list.vue';
|
||||||
|
import CustomerFollowList from './modules/customer-follow-list.vue';
|
||||||
|
import CustomerPutPoolRemindList from './modules/customer-put-pool-remind-list.vue';
|
||||||
|
import CustomerTodayContactList from './modules/customer-today-contact-list.vue';
|
||||||
|
import ReceivableAuditList from './modules/receivable-audit-list.vue';
|
||||||
|
import ReceivablePlanRemindList from './modules/receivable-plan-remind-list.vue';
|
||||||
|
|
||||||
|
const leftMenu = ref('customerTodayContact');
|
||||||
|
|
||||||
|
const clueFollowCount = ref(0);
|
||||||
|
const customerFollowCount = ref(0);
|
||||||
|
const customerPutPoolRemindCount = ref(0);
|
||||||
|
const customerTodayContactCount = ref(0);
|
||||||
|
const contractAuditCount = ref(0);
|
||||||
|
const contractRemindCount = ref(0);
|
||||||
|
const receivableAuditCount = ref(0);
|
||||||
|
const receivablePlanRemindCount = ref(0);
|
||||||
|
|
||||||
|
const leftSides = useLeftSides(
|
||||||
|
customerTodayContactCount,
|
||||||
|
clueFollowCount,
|
||||||
|
customerFollowCount,
|
||||||
|
customerPutPoolRemindCount,
|
||||||
|
contractAuditCount,
|
||||||
|
contractRemindCount,
|
||||||
|
receivableAuditCount,
|
||||||
|
receivablePlanRemindCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentComponent = computed(() => {
|
||||||
|
const components = {
|
||||||
|
customerTodayContact: CustomerTodayContactList,
|
||||||
|
clueFollow: ClueFollowList,
|
||||||
|
contractAudit: ContractAuditList,
|
||||||
|
receivableAudit: ReceivableAuditList,
|
||||||
|
contractRemind: ContractRemindList,
|
||||||
|
customerFollow: CustomerFollowList,
|
||||||
|
customerPutPoolRemind: CustomerPutPoolRemindList,
|
||||||
|
receivablePlanRemind: ReceivablePlanRemindList,
|
||||||
|
} as const;
|
||||||
|
return components[leftMenu.value as keyof typeof components];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 侧边点击 */
|
||||||
|
function sideClick(item: { menu: string }) {
|
||||||
|
leftMenu.value = item.menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取数量 */
|
||||||
|
async function getCount() {
|
||||||
|
customerTodayContactCount.value = await getTodayContactCustomerCount();
|
||||||
|
customerPutPoolRemindCount.value = await getPutPoolRemindCustomerCount();
|
||||||
|
customerFollowCount.value = await getFollowCustomerCount();
|
||||||
|
clueFollowCount.value = await getFollowClueCount();
|
||||||
|
contractAuditCount.value = await getAuditContractCount();
|
||||||
|
contractRemindCount.value = await getRemindContractCount();
|
||||||
|
receivableAuditCount.value = await getAuditReceivableCount();
|
||||||
|
receivablePlanRemindCount.value = await getReceivablePlanRemindCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 激活时 */
|
||||||
|
onActivated(() => {
|
||||||
|
getCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getCount();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<div class="flex h-full w-full">
|
||||||
|
<ElCard class="w-1/5">
|
||||||
|
<div v-for="item in leftSides" :key="item.menu">
|
||||||
|
<div
|
||||||
|
class="flex cursor-pointer items-center justify-between border-b px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
@click="sideClick(item)"
|
||||||
|
>
|
||||||
|
<div>{{ item.name }}</div>
|
||||||
|
<ElBadge
|
||||||
|
v-if="item.count.value > 0"
|
||||||
|
:value="item.count.value"
|
||||||
|
:type="item.menu === leftMenu ? 'primary' : 'danger'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
<component class="ml-4 w-4/5" :is="currentComponent" />
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<!-- 分配给我的线索 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { CrmClueApi } from '#/api/crm/clue';
|
||||||
|
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getCluePage } from '#/api/crm/clue';
|
||||||
|
import { useGridColumns } from '#/views/crm/clue/data';
|
||||||
|
|
||||||
|
import { FOLLOWUP_STATUS } from '../data';
|
||||||
|
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
/** 打开线索详情 */
|
||||||
|
function handleDetail(row: CrmClueApi.Clue) {
|
||||||
|
push({ name: 'CrmClueDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
fieldName: 'followUpStatus',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: FOLLOWUP_STATUS,
|
||||||
|
},
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getCluePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
transformStatus: false,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<CrmClueApi.Clue>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Grid>
|
||||||
|
<template #name="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleDetail(row)">
|
||||||
|
{{ row.name }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleDetail(row)">
|
||||||
|
查看详情
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<!-- 待审核合同 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { CrmContractApi } from '#/api/crm/contract';
|
||||||
|
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getContractPage } from '#/api/crm/contract';
|
||||||
|
import { useGridColumns } from '#/views/crm/contract/data';
|
||||||
|
|
||||||
|
import { AUDIT_STATUS } from '../data';
|
||||||
|
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
/** 查看审批 */
|
||||||
|
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({
|
||||||
|
name: 'BpmProcessInstanceDetail',
|
||||||
|
query: { id: row.processInstanceId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开合同详情 */
|
||||||
|
function handleContractDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
/** 打开客户详情 */
|
||||||
|
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开联系人详情 */
|
||||||
|
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开商机详情 */
|
||||||
|
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
fieldName: 'auditStatus',
|
||||||
|
label: '合同状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: AUDIT_STATUS,
|
||||||
|
},
|
||||||
|
defaultValue: AUDIT_STATUS[0]!.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getContractPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
sceneType: 1, // 我负责的
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Grid>
|
||||||
|
<template #name="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleContractDetail(row)">
|
||||||
|
{{ row.name }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #customerName="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||||
|
{{ row.customerName }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #businessName="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleBusinessDetail(row)">
|
||||||
|
{{ row.businessName }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #contactName="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleContactDetail(row)">
|
||||||
|
{{ row.contactName }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '查看审批',
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
onClick: handleProcessDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<!-- 即将到期的合同 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { CrmContractApi } from '#/api/crm/contract';
|
||||||
|
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getContractPage } from '#/api/crm/contract';
|
||||||
|
import { useGridColumns } from '#/views/crm/contract/data';
|
||||||
|
|
||||||
|
import { CONTRACT_EXPIRY_TYPE } from '../data';
|
||||||
|
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
/** 查看审批 */
|
||||||
|
function handleProcessDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({
|
||||||
|
name: 'BpmProcessInstanceDetail',
|
||||||
|
query: { id: row.processInstanceId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开合同详情 */
|
||||||
|
function handleContractDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmContractDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开客户详情 */
|
||||||
|
function handleCustomerDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开联系人详情 */
|
||||||
|
function handleContactDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmContactDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开商机详情 */
|
||||||
|
function handleBusinessDetail(row: CrmContractApi.Contract) {
|
||||||
|
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
fieldName: 'expiryType',
|
||||||
|
label: '到期状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: CONTRACT_EXPIRY_TYPE,
|
||||||
|
},
|
||||||
|
defaultValue: CONTRACT_EXPIRY_TYPE[0]!.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getContractPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
sceneType: 1, // 自己负责的
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<CrmContractApi.Contract>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Grid>
|
||||||
|
<template #name="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleContractDetail(row)">
|
||||||
|
{{ row.name }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #customerName="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleCustomerDetail(row)">
|
||||||
|
{{ row.customerName }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #businessName="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleBusinessDetail(row)">
|
||||||
|
{{ row.businessName }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #signContactName="{ row }">
|
||||||
|
<ElButton type="primary" link @click="handleContactDetail(row)">
|
||||||
|
{{ row.signContactName }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '查看审批',
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
auth: ['crm:contract:update'],
|
||||||
|
onClick: handleProcessDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user