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",
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
"name": "vben admin antd dev",
|
||||
|
||||
@@ -41,12 +41,15 @@ stages:
|
||||
- pnpm build:ele
|
||||
- '# 执行编译命令naive'
|
||||
- pnpm build:naive
|
||||
- '# 执行编译命令tdesign'
|
||||
- pnpm build:tdesign
|
||||
artifacts:
|
||||
- name: BUILD_ARTIFACT
|
||||
path:
|
||||
- ./apps/web-antd/dist/
|
||||
- ./apps/web-ele/dist/
|
||||
- ./apps/web-naive/dist/
|
||||
- ./apps/web-tdesign/dist/
|
||||
caches:
|
||||
- ~/.npm
|
||||
- ~/.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,
|
||||
erpNumberFormatter,
|
||||
fenToYuan,
|
||||
formatFileSize,
|
||||
formatPast2,
|
||||
isFunction,
|
||||
isString,
|
||||
@@ -354,12 +355,7 @@ setupVbenVxeTable({
|
||||
// add by 星语:文件大小格式化
|
||||
vxeUI.formats.add('formatFileSize', {
|
||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||
if (!cellValue) return '0 B';
|
||||
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]}`;
|
||||
return formatFileSize(cellValue, digits);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -10,11 +10,10 @@ import { computed, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 { checkFileType } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
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 { $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 { checkImgType, defaultImageAccepts } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
@@ -159,7 +164,7 @@ async function beforeUpload(file: File) {
|
||||
}
|
||||
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkImgType(file, accept);
|
||||
const isAct = isImage(file.name, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
|
||||
@@ -71,12 +71,15 @@ function toggleExpanded() {
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
@apply rounded-sm bg-gray-400/40;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-gray-400/60;
|
||||
}
|
||||
|
||||
@@ -191,11 +191,17 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'category',
|
||||
label: '角色类别',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色类别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'publicStatus',
|
||||
|
||||
@@ -40,7 +40,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const [SystemDescription] = useDescription({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<BaseDescription :data="business" />
|
||||
<Divider />
|
||||
<SystemDescription :data="business" />
|
||||
|
||||
@@ -46,7 +46,7 @@ async function handleDelete(row: CrmBusinessStatusApi.BusinessStatus) {
|
||||
await deleteBusinessStatus(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} catch {
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
/** 添加状态 */
|
||||
async function handleAddStatus() {
|
||||
formData.value!.statuses!.unshift({
|
||||
formData.value!.statuses!.splice(-3, 0, {
|
||||
name: '',
|
||||
percent: undefined,
|
||||
} as any);
|
||||
|
||||
@@ -38,7 +38,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const [SystemDescriptions] = useDescription({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<BaseDescriptions :data="contact" />
|
||||
<Divider />
|
||||
<SystemDescriptions :data="contact" />
|
||||
|
||||
@@ -110,6 +110,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下单日期',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -120,6 +121,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择合同开始时间',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -130,6 +132,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择合同结束时间',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -197,6 +200,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
placeholder: '请输入产品总金额',
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
},
|
||||
@@ -207,6 +211,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
placeholder: '请输入整单折扣',
|
||||
},
|
||||
rules: z.number().min(0).max(100).optional().default(0),
|
||||
},
|
||||
|
||||
@@ -42,7 +42,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const [SystemDescriptions] = useDescription({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<BaseDescriptions :data="contract" />
|
||||
<Divider />
|
||||
<SystemDescriptions :data="contract" />
|
||||
|
||||
@@ -49,7 +49,6 @@ const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // 团队
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
column: 4,
|
||||
class: 'mx-4',
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const [SystemDescriptions] = useDescription({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<BaseDescriptions :data="customer" />
|
||||
<Divider />
|
||||
<SystemDescriptions :data="customer" />
|
||||
|
||||
@@ -50,6 +50,7 @@ export function useFormSchema(
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择下次联系时间',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
||||
@@ -66,7 +66,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmPermissionApi.BusinessTransferReqVO;
|
||||
const data =
|
||||
(await formApi.getValues()) as CrmPermissionApi.BusinessTransferReqVO;
|
||||
try {
|
||||
switch (bizType.value) {
|
||||
case BizTypeEnum.CRM_BUSINESS: {
|
||||
|
||||
@@ -126,10 +126,8 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 产品下拉选项 */
|
||||
const productOptions = ref<CrmProductApi.Product[]>([]);
|
||||
|
||||
/** 初始化 */
|
||||
const productOptions = ref<CrmProductApi.Product[]>([]); // 产品下拉选项
|
||||
onMounted(async () => {
|
||||
productOptions.value = await getProductSimpleList();
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'interval',
|
||||
@@ -74,6 +74,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择时间间隔',
|
||||
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
||||
},
|
||||
defaultValue: 2,
|
||||
@@ -91,6 +92,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
placeholder: '请选择归属部门',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
|
||||
@@ -25,13 +25,11 @@ const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const [QueryForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: useGridFormSchema(),
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: $t('common.query'),
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'interval',
|
||||
@@ -48,6 +48,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择时间间隔',
|
||||
options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
|
||||
},
|
||||
defaultValue: 2,
|
||||
@@ -65,6 +66,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
placeholder: '请选择归属部门',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
@@ -77,6 +79,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择员工',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -42,13 +42,11 @@ const gridEvents: VxeGridListeners = {
|
||||
};
|
||||
const [QueryForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: useGridFormSchema(),
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: $t('common.query'),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
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 { getSimpleUserList } from '#/api/system/user';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
@@ -28,20 +27,16 @@ export const customerSummaryTabs = [
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'times',
|
||||
label: '时间范围',
|
||||
component: 'RangePicker',
|
||||
fieldName: 'time',
|
||||
label: '选择年份',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
picker: 'year',
|
||||
showTime: false,
|
||||
format: 'YYYY',
|
||||
ranges: {},
|
||||
valueFormat: 'YYYY',
|
||||
placeholder: '请选择年份',
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
|
||||
formatDateTime(endOfDay(new Date(new Date().getFullYear(), 11, 31))),
|
||||
] as [Date, Date],
|
||||
defaultValue: new Date().getFullYear().toString(),
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
@@ -56,6 +51,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
placeholder: '请选择归属部门',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { ContentWrap, Page } from '@vben/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { beginOfDay, endOfDay, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
|
||||
@@ -29,13 +30,11 @@ const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const [QueryForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: useGridFormSchema(),
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: $t('common.query'),
|
||||
@@ -70,6 +69,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
async function handleTabChange(key: any) {
|
||||
activeTabName.value = key;
|
||||
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[] = [];
|
||||
const columnsData: any[] = [];
|
||||
let tableData: any[] = [];
|
||||
|
||||
@@ -39,13 +39,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
format: 'YYYY-MM-DD',
|
||||
picker: 'year',
|
||||
},
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
@@ -60,6 +58,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
placeholder: '请选择归属部门',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
|
||||
@@ -27,13 +27,11 @@ const { renderEcharts: renderRightEcharts } = useEcharts(rightChartRef);
|
||||
|
||||
const [QueryForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: useGridFormSchema(),
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: $t('common.query'),
|
||||
|
||||
@@ -57,7 +57,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
defaultValue: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
|
||||
] as [Date, Date],
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'deptId',
|
||||
@@ -72,6 +72,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
treeDefaultExpandAll: true,
|
||||
placeholder: '请选择归属部门',
|
||||
},
|
||||
defaultValue: userStore.userInfo?.deptId,
|
||||
},
|
||||
|
||||
@@ -25,13 +25,11 @@ const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const [QueryForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: useGridFormSchema(),
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: $t('common.query'),
|
||||
|
||||
@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择出生年',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择出生日期',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
placeholder: '请选择出生日期',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -189,7 +189,7 @@ function handleSliderChange(prop: string) {
|
||||
:max="100"
|
||||
:min="0"
|
||||
@change="handleSliderChange(dataRef.prop)"
|
||||
class="mr-[16px]"
|
||||
class="mr-4"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
|
||||
@@ -385,7 +385,7 @@ onMounted(() => {
|
||||
}"
|
||||
>
|
||||
<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
|
||||
v-model="pageComponents"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useRoute } from 'vue-router';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { isEmpty, isNumber } from '@vben/utils';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message, Radio, RadioGroup } from 'ant-design-vue';
|
||||
|
||||
@@ -26,29 +26,35 @@ defineOptions({ name: 'DiyTemplateDecorate' });
|
||||
const route = useRoute();
|
||||
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 templateItems = ref([
|
||||
{ name: '基础设置', icon: 'lucide:settings' },
|
||||
{ name: '首页', icon: 'lucide:home' },
|
||||
{ name: '我的', icon: 'lucide:user' },
|
||||
]); // 左上角工具栏操作按钮
|
||||
{ key: 0, name: '基础设置', icon: 'lucide:settings' },
|
||||
{ key: 1, name: '首页', icon: 'lucide:home' },
|
||||
{ key: 2, name: '我的', icon: 'lucide:user' },
|
||||
]);
|
||||
|
||||
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
||||
// 当前编辑的属性
|
||||
const currentFormData = ref<
|
||||
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
||||
>({
|
||||
property: '',
|
||||
} as MallDiyPageApi.DiyPage); // 当前编辑的属性
|
||||
} as MallDiyPageApi.DiyPage);
|
||||
// templateItem 对应的缓存
|
||||
const currentFormDataMap = ref<
|
||||
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
||||
>(new Map()); // templateItem 对应的缓存
|
||||
|
||||
const previewUrl = ref(''); // 商城 H5 预览地址
|
||||
|
||||
const templateLibs = [] as DiyComponentLibrary[]; // 模板组件库
|
||||
const libs = ref<DiyComponentLibrary[]>(templateLibs); // 当前组件库
|
||||
>(new Map());
|
||||
// 商城 H5 预览地址
|
||||
const previewUrl = ref('');
|
||||
// 模板组件库
|
||||
const templateLibs = [] as DiyComponentLibrary[];
|
||||
// 当前组件库
|
||||
const libs = ref<DiyComponentLibrary[]>(templateLibs);
|
||||
|
||||
/** 获取详情 */
|
||||
async function getPageDetail(id: any) {
|
||||
@@ -58,38 +64,32 @@ async function getPageDetail(id: any) {
|
||||
});
|
||||
try {
|
||||
formData.value = await getDiyTemplateProperty(id);
|
||||
|
||||
// 拼接手机预览链接
|
||||
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
|
||||
const accessStore = useAccessStore();
|
||||
previewUrl.value = `${domain}?templateId=${formData.value.id}&${accessStore.tenantId}`;
|
||||
previewUrl.value = `${domain}?templateId=${formData.value.id}&tenantId=${accessStore.tenantId}`;
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 模板选项切换 */
|
||||
// TODO @xingyu:貌似切换不对;“个人中心”切换不过去;
|
||||
function handleTemplateItemChange(event: any) {
|
||||
// 从事件对象中获取值
|
||||
const val = event.target?.value ?? event;
|
||||
// 切换模版
|
||||
selectedTemplateItem.value = isNumber(val)
|
||||
? val
|
||||
: templateItems.value.findIndex((item) => item.name === val.name);
|
||||
|
||||
function handleTemplateItemChange(val: any) {
|
||||
const changeValue = val.target.value;
|
||||
// 缓存模版编辑数据
|
||||
currentFormDataMap.value.set(
|
||||
templateItems.value[selectedTemplateItem.value]?.name || '',
|
||||
templateItems.value[changeValue]!.name,
|
||||
currentFormData.value!,
|
||||
);
|
||||
// 切换模版
|
||||
selectedTemplateItem.value = changeValue;
|
||||
|
||||
// 读取模版缓存
|
||||
const data = currentFormDataMap.value.get(
|
||||
templateItems.value[selectedTemplateItem.value]?.name || '',
|
||||
templateItems.value[changeValue]!.name,
|
||||
);
|
||||
|
||||
// 情况一:编辑模板
|
||||
if (val === 0) {
|
||||
if (changeValue === 0) {
|
||||
libs.value = templateLibs;
|
||||
currentFormData.value = (isEmpty(data) ? formData.value : data) as
|
||||
| MallDiyPageApi.DiyPage
|
||||
@@ -99,22 +99,14 @@ function handleTemplateItemChange(event: any) {
|
||||
|
||||
// 情况二:编辑页面
|
||||
libs.value = PAGE_LIBS;
|
||||
const pageData = isEmpty(data)
|
||||
? formData.value!.pages.find(
|
||||
(page: MallDiyPageApi.DiyPage) =>
|
||||
page.name === templateItems.value[val]?.name,
|
||||
)
|
||||
: data;
|
||||
|
||||
// 如果找不到页面数据,使用默认值
|
||||
currentFormData.value = pageData
|
||||
? (pageData as
|
||||
| MallDiyPageApi.DiyPage
|
||||
| MallDiyTemplateApi.DiyTemplateProperty)
|
||||
: ({
|
||||
property: '',
|
||||
name: templateItems.value[val]?.name || '',
|
||||
} as MallDiyPageApi.DiyPage);
|
||||
currentFormData.value = (
|
||||
isEmpty(data)
|
||||
? formData.value!.pages.find(
|
||||
(page: MallDiyPageApi.DiyPage) =>
|
||||
page.name === templateItems.value[changeValue]!.name,
|
||||
)
|
||||
: data
|
||||
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
@@ -210,15 +202,8 @@ onMounted(async () => {
|
||||
size="large"
|
||||
@change="handleTemplateItemChange"
|
||||
>
|
||||
<template v-for="(item, index) in templateItems" :key="index">
|
||||
<Radio.Button
|
||||
:value="item"
|
||||
:class="
|
||||
index === selectedTemplateItem
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<template v-for="item in templateItems" :key="item.key">
|
||||
<Radio.Button :value="item.key">
|
||||
<IconifyIcon
|
||||
:icon="item.icon"
|
||||
class="mt-2 flex size-5 items-center"
|
||||
|
||||
@@ -20,6 +20,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '秒杀时段名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入秒杀时段名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -160,12 +160,18 @@ export function useCreateFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'userId',
|
||||
label: '分销员编号',
|
||||
component: 'InputSearch',
|
||||
componentProps: {
|
||||
placeholder: '请输入分销员编号',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'bindUserId',
|
||||
label: '上级推广员编号',
|
||||
component: 'InputSearch',
|
||||
componentProps: {
|
||||
placeholder: '请输入上级推广员编号',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
@@ -178,6 +184,9 @@ export function useUpdateFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'bindUserId',
|
||||
label: '上级推广员编号',
|
||||
component: 'InputSearch',
|
||||
componentProps: {
|
||||
placeholder: '请输入上级推广员编号',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -21,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '分组名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分组名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'title',
|
||||
label: '公告标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入公告标题',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,12 +21,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '岗位名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入岗位名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'code',
|
||||
label: '岗位编码',
|
||||
componentProps: {
|
||||
placeholder: '请输入岗位编码',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -35,6 +41,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
placeholder: '请输入显示顺序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -53,6 +60,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'remark',
|
||||
label: '岗位备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入岗位备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,12 +26,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '角色标识',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色标识',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -59,6 +65,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'remark',
|
||||
label: '角色备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
|
||||
placeholder: '请选择社交平台',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
||||
@@ -28,6 +28,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '租户名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入租户名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -46,18 +49,27 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'contactName',
|
||||
label: '联系人',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入联系人',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'contactMobile',
|
||||
label: '联系手机',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入联系手机',
|
||||
},
|
||||
rules: 'mobile',
|
||||
},
|
||||
{
|
||||
label: '用户名称',
|
||||
fieldName: 'username',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名称',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
@@ -78,6 +90,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '账号额度',
|
||||
fieldName: 'accountCount',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入账号额度',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,6 +22,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '套餐名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入套餐名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -45,6 +48,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'username',
|
||||
label: '用户名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -44,6 +47,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'nickname',
|
||||
label: '用户昵称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户昵称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -117,6 +123,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
erpCountInputFormatter,
|
||||
erpNumberFormatter,
|
||||
fenToYuan,
|
||||
formatFileSize,
|
||||
formatPast2,
|
||||
isFunction,
|
||||
isString,
|
||||
@@ -344,12 +345,7 @@ setupVbenVxeTable({
|
||||
// add by 星语:文件大小格式化
|
||||
vxeUI.formats.add('formatFileSize', {
|
||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||
if (!cellValue) return '0 B';
|
||||
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]}`;
|
||||
return formatFileSize(cellValue, digits);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 { $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 { checkImgType, defaultImageAccepts } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
@@ -173,7 +178,7 @@ const handleRemove = async (file: UploadFile) => {
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkImgType(file, accept);
|
||||
const isAct = isImage(file.name, accept);
|
||||
if (!isAct) {
|
||||
ElMessage.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
|
||||
@@ -20,33 +20,33 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
component: () => import('#/views/crm/clue/detail/index.vue'),
|
||||
},
|
||||
// {
|
||||
// path: 'customer/detail/:id',
|
||||
// name: 'CrmCustomerDetail',
|
||||
// meta: {
|
||||
// title: '客户详情',
|
||||
// activePath: '/crm/customer',
|
||||
// },
|
||||
// component: () => import('#/views/crm/customer/detail/index.vue'),
|
||||
// },
|
||||
// {
|
||||
// path: 'business/detail/:id',
|
||||
// name: 'CrmBusinessDetail',
|
||||
// meta: {
|
||||
// title: '商机详情',
|
||||
// activePath: '/crm/business',
|
||||
// },
|
||||
// component: () => import('#/views/crm/business/detail/index.vue'),
|
||||
// },
|
||||
// {
|
||||
// path: 'contract/detail/:id',
|
||||
// name: 'CrmContractDetail',
|
||||
// meta: {
|
||||
// title: '合同详情',
|
||||
// activePath: '/crm/contract',
|
||||
// },
|
||||
// component: () => import('#/views/crm/contract/detail/index.vue'),
|
||||
// },
|
||||
{
|
||||
path: 'customer/detail/:id',
|
||||
name: 'CrmCustomerDetail',
|
||||
meta: {
|
||||
title: '客户详情',
|
||||
activePath: '/crm/customer',
|
||||
},
|
||||
component: () => import('#/views/crm/customer/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'business/detail/:id',
|
||||
name: 'CrmBusinessDetail',
|
||||
meta: {
|
||||
title: '商机详情',
|
||||
activePath: '/crm/business',
|
||||
},
|
||||
component: () => import('#/views/crm/business/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'contract/detail/:id',
|
||||
name: 'CrmContractDetail',
|
||||
meta: {
|
||||
title: '合同详情',
|
||||
activePath: '/crm/contract',
|
||||
},
|
||||
component: () => import('#/views/crm/contract/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'receivable-plan/detail/:id',
|
||||
name: 'CrmReceivablePlanDetail',
|
||||
@@ -65,15 +65,15 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
component: () => import('#/views/crm/receivable/detail/index.vue'),
|
||||
},
|
||||
// {
|
||||
// path: 'contact/detail/:id',
|
||||
// name: 'CrmContactDetail',
|
||||
// meta: {
|
||||
// title: '联系人详情',
|
||||
// activePath: '/crm/contact',
|
||||
// },
|
||||
// component: () => import('#/views/crm/contact/detail/index.vue'),
|
||||
// },
|
||||
{
|
||||
path: 'contact/detail/:id',
|
||||
name: 'CrmContactDetail',
|
||||
meta: {
|
||||
title: '联系人详情',
|
||||
activePath: '/crm/contact',
|
||||
},
|
||||
component: () => import('#/views/crm/contact/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'product/detail/:id',
|
||||
name: 'CrmProductDetail',
|
||||
@@ -83,6 +83,54 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
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',
|
||||
componentProps: {
|
||||
placeholder: '请输入温度参数',
|
||||
class: 'w-full',
|
||||
precision: 2,
|
||||
min: 0,
|
||||
max: 2,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -55,9 +56,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入回复数 Token 数',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
max: 8192,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -67,9 +69,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入上下文数量',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
max: 20,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
||||
@@ -71,12 +71,15 @@ function toggleExpanded() {
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
@apply rounded-sm bg-gray-400/40;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-gray-400/60;
|
||||
}
|
||||
|
||||
@@ -189,11 +189,17 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'category',
|
||||
label: '角色类别',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入角色类别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'publicStatus',
|
||||
|
||||
@@ -105,10 +105,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入温度参数',
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
min: 0,
|
||||
max: 2,
|
||||
controlsPosition: 'right',
|
||||
class: '!w-full',
|
||||
},
|
||||
dependencies: {
|
||||
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