优化问题

This commit is contained in:
z9130 2024-09-19 16:46:03 +08:00
parent 0730e89935
commit 06ae572098
116 changed files with 5041 additions and 681 deletions

View File

@ -220,5 +220,6 @@
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true, "vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"svn.ignoreMissingSvnWarning": true
} }

3
apps/backend-mock/.env Normal file
View File

@ -0,0 +1,3 @@
PORT=5320
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret

View File

@ -0,0 +1,15 @@
# @vben/backend-mock
## Description
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
## Running the app
```bash
# development
$ pnpm run start
# production mode
$ pnpm run build
```

View File

@ -0,0 +1,14 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } 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);
});

View File

@ -0,0 +1,36 @@
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } 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);
}
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
...findUser,
accessToken,
});
});

View File

@ -0,0 +1,15 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});

View File

@ -0,0 +1,33 @@
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
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;
});

View File

@ -0,0 +1,13 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(menus);
});

View File

@ -0,0 +1,5 @@
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));
return useResponseError(`${status}`);
});

View File

@ -0,0 +1 @@
export default defineEventHandler(() => 'Test get handler');

View File

@ -0,0 +1 @@
export default defineEventHandler(() => 'Test post handler');

View File

@ -0,0 +1,11 @@
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(userinfo);
});

View File

@ -0,0 +1,7 @@
import type { NitroErrorHandler } from 'nitropack';
const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`);
};
export default errorHandler;

View File

@ -0,0 +1,7 @@
export default defineEventHandler((event) => {
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';
return 'OK';
}
});

View File

@ -0,0 +1,18 @@
import errorHandler from './error';
export default defineNitroConfig({
devErrorHandler: errorHandler,
errorHandler: '~/error',
routeRules: {
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
},
},
},
});

View File

@ -0,0 +1,20 @@
{
"name": "@vben/backend-mock",
"version": "0.0.1",
"description": "",
"private": true,
"license": "MIT",
"author": "",
"scripts": {
"build": "nitro build",
"start": "nitro dev"
},
"dependencies": {
"jsonwebtoken": "^9.0.2",
"nitropack": "^2.9.7"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.7",
"h3": "^1.12.0"
}
}

View File

@ -0,0 +1,12 @@
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>
</ul>
`;
});

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -0,0 +1,3 @@
{
"extends": "./.nitro/types/tsconfig.json"
}

View File

@ -0,0 +1,26 @@
import type { EventHandlerRequest, H3Event } 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 * 1000,
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event<EventHandlerRequest>) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}

View File

@ -0,0 +1,59 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } 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 token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
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);
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}

View File

@ -0,0 +1,186 @@
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: 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',
},
{
id: 2,
password: '123456',
realName: 'Jack',
roles: ['user'],
username: 'jack',
},
];
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 = [
{
component: 'BasicLayout',
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/',
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: 'page.demos.access.adminVisible',
},
name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible',
},
super: {
component: '/demos/access/super-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'page.demos.access.superVisible',
},
name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible',
},
user: {
component: '/demos/access/user-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'page.demos.access.userVisible',
},
name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible',
},
};
return [
{
component: 'BasicLayout',
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: 'page.demos.title',
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
{
name: 'AccessDemos',
path: '/demosaccess',
meta: {
icon: 'mdi:cloud-key-outline',
title: 'page.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: 'page.demos.access.pageAccess',
},
},
{
name: 'AccessButtonControlDemo',
path: '/demos/access/button-control',
component: '/demos/access/button-control',
meta: {
icon: 'mdi:button-cursor',
title: 'page.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: 'page.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',
},
];

View File

@ -0,0 +1,29 @@
import type { EventHandlerRequest, H3Event } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,
data,
error: null,
message: 'ok',
};
}
export function useResponseError(message: string, error: any = null) {
return {
code: -1,
data: null,
error,
message,
};
}
export function forbiddenResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 403);
return useResponseError('ForbiddenException', 'Forbidden Exception');
}
export function unAuthorizedResponse(event: H3Event<EventHandlerRequest>) {
setResponseStatus(event, 401);
return useResponseError('UnauthorizedException', 'Unauthorized Exception');
}

View File

@ -24,442 +24,463 @@ const routes: RouteRecordRaw[] = [
}, },
], ],
}, },
{
name: 'ContractApproval',
path: '/contract/approval',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同立项',
},
children: [
{ {
name: 'ContractApproval', name: 'ContractApprovalEdit',
path: '/contract/approval', path: '/contract/approval/edit/:id?',
component: BasicLayout, beforeEnter: (e) => {
meta: { if (e.params.id && e.params.id === ':id') {
icon: 'lucide:area-chart', e.params.id = '';
title: '合同立项', e.fullPath = '/contract/approval/edit';
},
children: [
{
name: 'ContractApprovalEdit',
path: '/contract/approval/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = ''
e.fullPath = '/contract/approval/edit'
}
},
component: () => import('#/views/contract/approval/edit/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '立项申报',
activePath: '/contract/approval/edit/:id?'
},
},
{
name: 'ContractApprovalTodo',
path: '/contract/approval/todo',
component: () => import('#/views/contract/approval/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '立项提示',
},
},
{
name: 'ContractApprovalList',
path: '/contract/approval/list',
component: () => import('#/views/contract/approval/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '立项查询',
},
},
{
name: 'ContractApprovalSigningBasis',
path: '/contract/approval/signing-basis',
component: () => import('#/views/contract/approval/signing-basis/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签约依据维护',
},
} }
], },
}, component: () => import('#/views/contract/approval/edit/index.vue'),
{
name: 'ContractBusiness',
path: '/contract/business',
component: BasicLayout,
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '合同选商', title: '立项申报',
activePath: '/contract/approval/edit/:id?',
}, },
children: [
{
name: 'ContractBusinessEdit',
path: '/contract/business/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = ''
e.fullPath = '/contract/business/edit'
}
},
component: () => import('#/views/contract/business/edit/index.vue'),
meta: {
hideInMenu: true,
hideInTab: true,
icon: 'lucide:area-chart',
title: '选商填报',
activePath: '/contract/business/edit/:id?'
},
},
{
name: 'ContractBusinessTodo',
path: '/contract/business/todo',
component: () => import('#/views/contract/business/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '选商提示',
},
},
{
name: 'ContractBusinessList',
path: '/contract/business/list',
component: () => import('#/views/contract/business/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '选商查询',
},
},
],
}, },
{ {
name: 'ContractDeclaration', name: 'ContractApprovalTodo',
path: '/contract/declaration', path: '/contract/approval/todo',
component: BasicLayout, component: () => import('#/views/contract/approval/todo/index.vue'),
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '合同申报', title: '立项提示',
}, },
children: [
{
name: 'ContractDeclarationEdit',
path: '/contract/declaration/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = ''
e.fullPath = '/contract/declaration/edit'
}
},
component: () => import('#/views/contract/declaration/edit/index.vue'),
meta: {
hideInMenu: true,
hideInTab: true,
icon: 'lucide:area-chart',
title: '申报填报',
activePath: '/contract/declaration/edit/:id?'
},
},
{
name: 'ContractDeclarationTodo',
path: '/contract/declaration/todo',
component: () => import('#/views/contract/declaration/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '申报提示',
},
},
{
name: 'ContractDeclarationList',
path: '/contract/declaration/list',
component: () => import('#/views/contract/declaration/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '申报查询',
},
},
{
name: 'ContractDeclarationPrint',
path: '/contract/declaration/print',
component: () => import('#/views/contract/declaration/print/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '合同打印',
},
},
],
}, },
// {
// name: 'ContractAudit',
// path: '/contract/audit',
// component: BasicLayout,
// meta: {
// icon: 'lucide:area-chart',
// title: '合同审批',
// },
// children: [
// {
// name: 'ContractAuditTodo',
// path: '/contract/audit/todo',
// component: () => import('#/views/contract/audit/todo/index.vue'),
// meta: {
// icon: 'lucide:area-chart',
// title: '审批提示',
// },
// },
// {
// name: 'ContractAuditList',
// path: '/contract/audit/list',
// component: () => import('#/views/contract/audit/list/index.vue'),
// meta: {
// icon: 'lucide:area-chart',
// title: '审批查询',
// },
// },
// ],
// },
{ {
name: 'ContractSign', name: 'ContractApprovalList',
path: '/contract/sign', path: '/contract/approval/list',
component: BasicLayout, component: () => import('#/views/contract/approval/list/index.vue'),
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '合同签订', title: '立项查询',
}, },
children: [
{
name: 'ContractSignTodo',
path: '/contract/sign/todo',
component: () => import('#/views/contract/sign/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签订提示',
},
},
{
name: 'ContractSignList',
path: '/contract/sign/list',
component: () => import('#/views/contract/sign/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签订查询',
},
},
],
}, },
{ {
name: 'ContractPerform', name: 'ContractApprovalSigningBasis',
path: '/contract/perform', path: '/contract/approval/signing-basis',
component: BasicLayout, component: () =>
import('#/views/contract/approval/signing-basis/index.vue'),
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '合同履行', title: '签约依据维护',
},
},
],
},
{
name: 'ContractBusiness',
path: '/contract/business',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同选商',
},
children: [
{
name: 'ContractBusinessEdit',
path: '/contract/business/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = '';
e.fullPath = '/contract/business/edit';
}
},
component: () => import('#/views/contract/business/edit/index.vue'),
meta: {
hideInMenu: true,
hideInTab: true,
icon: 'lucide:area-chart',
title: '选商填报',
activePath: '/contract/business/edit/:id?',
}, },
children: [
{
name: 'ContractPerformTodo',
path: '/contract/perform/todo',
component: () => import('#/views/contract/perform/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '履行提示',
},
},
{
name: 'ContractPerformList',
path: '/contract/perform/list',
component: () => import('#/views/contract/perform/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '履行查询',
},
},
{
name: 'ContractPerformResult',
path: '/contract/perform/result',
component: () => import('#/views/contract/perform/result/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '履行结果填报',
},
},
{
name: 'ContractPerformTemporaryArchive',
path: '/contract/perform/temporary-archive',
component: () => import('#/views/contract/perform/temporary-archive/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '临时归档',
},
},
],
}, },
{ {
name: 'ContractArchive', name: 'ContractBusinessTodo',
path: '/contract/archive', path: '/contract/business/todo',
component: BasicLayout, component: () => import('#/views/contract/business/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '选商提示',
},
},
{
name: 'ContractBusinessList',
path: '/contract/business/list',
component: () => import('#/views/contract/business/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '选商查询',
},
},
],
},
{
name: 'ContractDeclaration',
path: '/contract/declaration',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同申报',
},
children: [
{
name: 'ContractDeclarationEdit',
path: '/contract/declaration/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = '';
e.fullPath = '/contract/declaration/edit';
}
},
component: () => import('#/views/contract/declaration/edit/index.vue'),
meta: {
hideInMenu: true,
hideInTab: true,
icon: 'lucide:area-chart',
title: '申报填报',
activePath: '/contract/declaration/edit/:id?',
},
},
{
name: 'ContractDeclarationTodo',
path: '/contract/declaration/todo',
component: () => import('#/views/contract/declaration/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '申报提示',
},
},
{
name: 'ContractDeclarationList',
path: '/contract/declaration/list',
component: () => import('#/views/contract/declaration/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '申报查询',
},
},
{
name: 'ContractDeclarationPrint',
path: '/contract/declaration/print',
component: () => import('#/views/contract/declaration/print/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '合同打印',
},
},
],
},
// {
// name: 'ContractAudit',
// path: '/contract/audit',
// component: BasicLayout,
// meta: {
// icon: 'lucide:area-chart',
// title: '合同审批',
// },
// children: [
// {
// name: 'ContractAuditTodo',
// path: '/contract/audit/todo',
// component: () => import('#/views/contract/audit/todo/index.vue'),
// meta: {
// icon: 'lucide:area-chart',
// title: '审批提示',
// },
// },
// {
// name: 'ContractAuditList',
// path: '/contract/audit/list',
// component: () => import('#/views/contract/audit/list/index.vue'),
// meta: {
// icon: 'lucide:area-chart',
// title: '审批查询',
// },
// },
// ],
// },
{
name: 'ContractSign',
path: '/contract/sign',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同签订',
},
children: [
{
name: 'ContractSignTodo',
path: '/contract/sign/todo',
component: () => import('#/views/contract/sign/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签订提示',
},
},
{
name: 'ContractSignList',
path: '/contract/sign/list',
component: () => import('#/views/contract/sign/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签订查询',
},
},
],
},
{
name: 'ContractPerform',
path: '/contract/perform',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同履行',
},
children: [
{
name: 'ContractPerformEdit',
path: '/contract/perform/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = '';
e.fullPath = '/contract/perform/edit';
}
},
component: () => import('#/views/contract/perform/edit/index.vue'),
meta: {
hideInMenu: true,
hideInTab: true,
icon: 'lucide:area-chart',
title: '选商填报',
activePath: '/contract/perform/edit/:id?',
},
},
{
name: 'ContractPerformTodo',
path: '/contract/perform/todo',
component: () => import('#/views/contract/perform/todo/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '履行提示',
},
},
{
name: 'ContractPerformList',
path: '/contract/perform/list',
component: () => import('#/views/contract/perform/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '履行查询',
},
},
{
name: 'ContractPerformResult',
path: '/contract/perform/result',
component: () => import('#/views/contract/perform/result/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '履行结果填报',
},
},
{
name: 'ContractPerformTemporaryArchive',
path: '/contract/perform/temporary-archive',
component: () =>
import('#/views/contract/perform/temporary-archive/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '临时归档',
},
},
],
},
{
name: 'ContractArchive',
path: '/contract/archive',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同归档',
},
children: [
{
name: 'ContractArchiveTodoArchive',
path: '/contract/archive/todo/archive',
component: () => import('#/views/contract/archive/todo/archive.vue'),
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '合同归档', title: '合同归档',
}, },
children: [
{
name: 'ContractArchiveTodoArchive',
path: '/contract/archive/todo/archive',
component: () => import('#/views/contract/archive/todo/archive.vue'),
meta: {
icon: 'lucide:area-chart',
title: '合同归档',
},
},
{
name: 'ContractArchiveTodoRetracement',
path: '/contract/archive/todo/retracement',
component: () => import('#/views/contract/archive/todo/retracement.vue'),
meta: {
icon: 'lucide:area-chart',
title: '合同回档',
},
},
{
name: 'ContractArchiveList',
path: '/contract/archive/list',
component: () => import('#/views/contract/archive/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '归档查询',
},
},
],
}, },
{ {
name: 'ContractSignAuthorization', name: 'ContractArchiveTodoRetracement',
path: '/contract/sign-authorization', path: '/contract/archive/todo/retracement',
component: BasicLayout, component: () =>
import('#/views/contract/archive/todo/retracement.vue'),
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '签约授权管理', title: '合同回档',
}, },
children: [
{
name: 'ContractSignAuthorizationEdit',
path: '/contract/sign-authorization/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = ''
e.fullPath = '/contract/sign-authorization/edit'
}
},
component: () => import('#/views/contract/sign-authorization/edit/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签约授权申报',
},
},
{
name: 'ContractSignAuthorizationList',
path: '/contract/sign-authorization/list',
component: () => import('#/views/contract/sign-authorization/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签约授权查询',
},
},
],
}, },
{ {
name: 'ContractCompany', name: 'ContractArchiveList',
path: '/contract/company', path: '/contract/archive/list',
component: BasicLayout, component: () => import('#/views/contract/archive/list/index.vue'),
meta: { meta: {
icon: 'lucide:area-chart', icon: 'lucide:area-chart',
title: '合同相对人', title: '归档查询',
}, },
children: [
{
name: 'ContractCompanyEdit',
path: '/contract/company/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = ''
e.fullPath = '/contract/company/edit'
}
},
component: () => import('#/views/contract/company/edit/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '相对人录入维护',
},
},
{
name: 'ContractCompanyList',
path: '/contract/company/list',
component: () => import('#/views/contract/company/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '相对人查询',
},
},
],
}, },
// { ],
// name: 'ContractStatistic', },
// path: '/contract/statistic', {
// meta: { name: 'ContractSignAuthorization',
// icon: 'lucide:area-chart', path: '/contract/sign-authorization',
// title: '统计分析', component: BasicLayout,
// }, meta: {
// children: [ icon: 'lucide:area-chart',
// { title: '签约授权管理',
// name: 'ContractStatisticAnalysis', },
// path: '/contract/statistic/analysis', children: [
// beforeEnter: (e) => { {
// if (e.params.id && e.params.id === ':id') { name: 'ContractSignAuthorizationEdit',
// e.params.id = '' path: '/contract/sign-authorization/edit/:id?',
// e.fullPath = '/contract/company/edit' beforeEnter: (e) => {
// } if (e.params.id && e.params.id === ':id') {
// }, e.params.id = '';
// component: () => import('#/views/contract/company/edit/index.vue'), e.fullPath = '/contract/sign-authorization/edit';
// meta: { }
// icon: 'lucide:area-chart', },
// title: '统计分析', component: () =>
// }, import('#/views/contract/sign-authorization/edit/index.vue'),
// }, meta: {
// { icon: 'lucide:area-chart',
// name: 'ContractStatisticAbrogateList', title: '签约授权申报',
// path: '/contract/statistic/abrogate-list', },
// component: () => import('#/views/contract/company/list/index.vue'), },
// meta: { {
// icon: 'lucide:area-chart', name: 'ContractSignAuthorizationList',
// title: '废除查询', path: '/contract/sign-authorization/list',
// }, component: () =>
// }, import('#/views/contract/sign-authorization/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '签约授权查询',
},
},
],
},
{
name: 'ContractCompany',
path: '/contract/company',
component: BasicLayout,
meta: {
icon: 'lucide:area-chart',
title: '合同相对人',
},
children: [
{
name: 'ContractCompanyEdit',
path: '/contract/company/edit/:id?',
beforeEnter: (e) => {
if (e.params.id && e.params.id === ':id') {
e.params.id = '';
e.fullPath = '/contract/company/edit';
}
},
component: () => import('#/views/contract/company/edit/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '相对人录入维护',
},
},
{
name: 'ContractCompanyList',
path: '/contract/company/list',
component: () => import('#/views/contract/company/list/index.vue'),
meta: {
icon: 'lucide:area-chart',
title: '相对人查询',
},
},
],
},
// {
// name: 'ContractStatistic',
// path: '/contract/statistic',
// meta: {
// icon: 'lucide:area-chart',
// title: '统计分析',
// },
// children: [
// {
// name: 'ContractStatisticAnalysis',
// path: '/contract/statistic/analysis',
// beforeEnter: (e) => {
// if (e.params.id && e.params.id === ':id') {
// e.params.id = ''
// e.fullPath = '/contract/company/edit'
// }
// },
// component: () => import('#/views/contract/company/edit/index.vue'),
// meta: {
// icon: 'lucide:area-chart',
// title: '统计分析',
// },
// },
// {
// name: 'ContractStatisticAbrogateList',
// path: '/contract/statistic/abrogate-list',
// component: () => import('#/views/contract/company/list/index.vue'),
// meta: {
// icon: 'lucide:area-chart',
// title: '废除查询',
// },
// },
// ], // ],
// }, // },
// { // {
// name: 'ContractPrint', // name: 'ContractPrint',
// path: '/contract/statistic', // path: '/contract/statistic',
// meta: { // meta: {
// icon: 'lucide:area-chart', // icon: 'lucide:area-chart',
// title: '统计分析', // title: '统计分析',
// }, // },
// children: [ // children: [
// { // {
// name: 'ContractPrintBusiness', // name: 'ContractPrintBusiness',
// path: '/contract/print/business', // path: '/contract/print/business',
// component: () => import('#/views/contract/company/edit/index.vue'), // component: () => import('#/views/contract/company/edit/index.vue'),
// meta: { // meta: {
// icon: 'lucide:area-chart', // icon: 'lucide:area-chart',
// title: '统计分析', // title: '统计分析',
// }, // },
// }, // },
// { // {
// name: 'ContractPrintAbrogateList', // name: 'ContractPrintAbrogateList',
// path: '/contract/print/abrogate-list', // path: '/contract/print/abrogate-list',
// component: () => import('#/views/contract/company/list/index.vue'), // component: () => import('#/views/contract/company/list/index.vue'),
// meta: { // meta: {
// icon: 'lucide:area-chart', // icon: 'lucide:area-chart',
// title: '废除查询', // title: '废除查询',
// }, // },
// }, // },
// ], // ],
// }, // },
]; ];
export default routes; export default routes;

View File

@ -30,6 +30,8 @@ export enum DICT_TYPE {
comprehensive_config = 'comprehensive_config', comprehensive_config = 'comprehensive_config',
/** 合同管理-签约依据类型 */ /** 合同管理-签约依据类型 */
contract_basis_type = 'contract_basis_type', contract_basis_type = 'contract_basis_type',
/** 合同管理-合同相对人性质 */
counterparty_nature = 'counterparty_nature',
/** 变更原因 */ /** 变更原因 */
cancel_reson = 'cancel_reson', cancel_reson = 'cancel_reson',
/** 划分标段 */ /** 划分标段 */
@ -45,5 +47,7 @@ export enum DICT_TYPE {
/** 付款性质 */ /** 付款性质 */
payment_nature = 'payment_nature', payment_nature = 'payment_nature',
/** 合同立项节点流程 */ /** 合同立项节点流程 */
contract_approval_flow_node = 'contract_approval_flow_node' contract_approval_flow_node = 'contract_approval_flow_node',
/** 合同立项节点流程 */
contract_abolish_flow_node = 'contract_abolish_flow_node'
} }

View File

@ -24,6 +24,8 @@ export default {
contract_basis_type: createEntry('合同管理-签约依据类型'), contract_basis_type: createEntry('合同管理-签约依据类型'),
counterparty_nature: createEntry('合同管理-合同相对人性质'),
cancel_reson: createEntry('变更原因', [ cancel_reson: createEntry('变更原因', [
{ label: '未填写原因', value: '0' }, { label: '未填写原因', value: '0' },
{ label: '用户取消', value: '1' }, { label: '用户取消', value: '1' },
@ -73,7 +75,7 @@ export default {
{ label: '待部门审核', value: 'departmentAudit' }, { label: '待部门审核', value: 'departmentAudit' },
{ label: '已结束', value: 'end' }, { label: '已结束', value: 'end' },
{ label: '废除发起', value: 'abolishEdit' }, { label: '废除发起', value: 'abolishEdit' },
{ label: '待废除审批', value: 'abolishEdit' }, { label: '待废除审批', value: 'abolishDepartmentAudit' },
]), ]),
contract_abolish_flow_node: createEntry('合同立项节点流程', [ contract_abolish_flow_node: createEntry('合同立项节点流程', [
@ -82,5 +84,4 @@ export default {
{ label: '已结束', value: 'end' }, { label: '已结束', value: 'end' },
{ label: '废除', value: 'abolishEdit' }, { label: '废除', value: 'abolishEdit' },
]), ]),
}; };

View File

@ -46,17 +46,21 @@ export function getFormSchema(params: any = {}) {
name: 'fs-dict-select', name: 'fs-dict-select',
vModel: 'value', vModel: 'value',
class: 'min-w-[200px]', class: 'min-w-[200px]',
prototype:true,
dict: dict({ dict: dict({
async getData({ form = {} }) { async getData({ form = {} }) {
return filterContractTypes(contractTypeData, '-1'); return filterContractTypes(contractTypeData, '-1');
}, },
}), }),
}, },
valueChange({ form, value, getComponentRef }) { valueChange: {
form.ctrTwoType = undefined; immediate: true, //是否立即执行一次
if (value) { handle({ form, value, getComponentRef }) {
form.ctrTwoType = undefined;
console.log(getComponentRef('ctrTwoType'));
getComponentRef('ctrTwoType').reloadDict(); // 执行city的select组件的reloadDict()方法触发“city”重新加载字典 getComponentRef('ctrTwoType').reloadDict(); // 执行city的select组件的reloadDict()方法触发“city”重新加载字典
} },
}, },
conditionalRender: { conditionalRender: {
match({ form }) { match({ form }) {
@ -82,6 +86,7 @@ export function getFormSchema(params: any = {}) {
name: 'fs-dict-select', name: 'fs-dict-select',
vModel: 'value', vModel: 'value',
class: 'min-w-[200px]', class: 'min-w-[200px]',
prototype:true,
dict: dict({ dict: dict({
async getData({ form = {} }) { async getData({ form = {} }) {
return filterContractTypes(contractTypeData, form.ctrType); return filterContractTypes(contractTypeData, form.ctrType);
@ -172,6 +177,8 @@ export function getFormSchema(params: any = {}) {
valueChange: { valueChange: {
immediate: true, //是否立即执行一次 immediate: true, //是否立即执行一次
handle({ form }) { handle({ form }) {
debugger;
console.log(form); console.log(form);
form.fundAllocationName = getDictObj( form.fundAllocationName = getDictObj(
DICT_TYPE.contract_fund_flow, DICT_TYPE.contract_fund_flow,
@ -267,7 +274,12 @@ export function getFormSchema(params: any = {}) {
render({ form }) { render({ form }) {
return ( return (
<span class="mr-2"> <span class="mr-2">
{getDictObj(DICT_TYPE.contract_currency_unit, form.priceType)} {
getDictObj(
DICT_TYPE.contract_currency_unit,
form.priceType || '',
)?.label
}
</span> </span>
); );
}, },
@ -293,7 +305,10 @@ export function getFormSchema(params: any = {}) {
render({ form }) { render({ form }) {
return ( return (
<span class=""> <span class="">
{getDictObj(DICT_TYPE.contract_organization_form, form.organiza)} {
getDictObj(DICT_TYPE.contract_organization_form, form.organiza)
?.label
}
</span> </span>
); );
}, },

View File

@ -1,9 +1,13 @@
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { dict } from '@fast-crud/fast-crud'; import { dict } from '@fast-crud/fast-crud';
import { DICT_TYPE, getDictObj, getDictOptions } from '#/utils/dict';
export function getFormSchema(_params: any = {}) { export function getFormSchema(_params: any = {}) {
return { return {
labelCol: { style: { width: '140px' } }, labelCol: { style: { width: '140px' } },
initialForm: {
currencyTypeId: 'CNY',
},
columns: { columns: {
providerName: { providerName: {
title: '单位全称', title: '单位全称',
@ -14,7 +18,7 @@ export function getFormSchema(_params: any = {}) {
vModel: 'value', vModel: 'value',
allowClear: false, allowClear: false,
}, },
rules: [{ required: true }], rules: [{ required: true, message: '请输入单位名称' }],
}, },
providerKind: { providerKind: {
title: '性质', title: '性质',
@ -25,10 +29,10 @@ export function getFormSchema(_params: any = {}) {
vModel: 'value', vModel: 'value',
class: 'min-w-[200px]', class: 'min-w-[200px]',
dict: dict({ dict: dict({
data: [], data: getDictOptions(DICT_TYPE.counterparty_nature),
}), }),
}, },
rules: [{ required: true }], rules: [{ required: true, message: '请选择单位性质' }],
}, },
providerAddr: { providerAddr: {
title: '住所', title: '住所',
@ -111,6 +115,15 @@ export function getFormSchema(_params: any = {}) {
data: getDictOptions(DICT_TYPE.contract_currency_unit), data: getDictOptions(DICT_TYPE.contract_currency_unit),
}), }),
}, },
valueChange: {
immediate: true, // 是否立即执行一次
handle({ form }) {
form.currencyTypeName = getDictObj(
DICT_TYPE.contract_currency_unit,
form.currencyTypeId,
)?.label;
},
},
}, },
marketScope: { marketScope: {
title: '营业执照经营范围', title: '营业执照经营范围',
@ -262,7 +275,7 @@ export function getFormSchemaByBank(_params: any = {}) {
}; };
} }
export function getFormSchemaByCertification(params: any = {}) { export function getFormSchemaByCertification(_params: any = {}) {
return { return {
labelCol: { style: { width: '140px' } }, labelCol: { style: { width: '140px' } },
initialForm: {}, initialForm: {},
@ -292,23 +305,23 @@ export function getFormSchemaByCertification(params: any = {}) {
key: 'time', key: 'time',
col: { span: 12 }, col: { span: 12 },
render({ form }) { render({ form }) {
//注意此处的v-model写法 // 注意此处的v-model写法
return ( return (
<div class="flex"> <div class="flex">
<a-form-item class="!mb-0 inline-block"> <a-form-item class="!mb-0 inline-block">
<a-date-picker <a-date-picker
v-model:value={form.aptitudeBeginDate}
placeholder=""
format="YYYY-MM-DD" format="YYYY-MM-DD"
placeholder=""
v-model:value={form.aptitudeBeginDate}
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
/> />
</a-form-item> </a-form-item>
<span class="mx-1"></span> <span class="mx-1"></span>
<a-form-item class="!mb-0 inline-block"> <a-form-item class="!mb-0 inline-block">
<a-date-picker <a-date-picker
v-model:value={form.aptitudeEndDate}
placeholder=""
format="YYYY-MM-DD" format="YYYY-MM-DD"
placeholder=""
v-model:value={form.aptitudeEndDate}
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
/> />
</a-form-item> </a-form-item>
@ -335,7 +348,7 @@ export function getFormSchemaByCertification(params: any = {}) {
}; };
} }
export function getFormSchemaByShareholder(params: any = {}) { export function getFormSchemaByShareholder(_params: any = {}) {
return { return {
labelCol: { style: { width: '140px' } }, labelCol: { style: { width: '140px' } },
columns: { columns: {
@ -367,7 +380,7 @@ export function getFormSchemaByShareholder(params: any = {}) {
name: 'fs-dict-select', name: 'fs-dict-select',
vModel: 'value', vModel: 'value',
dict: dict({ dict: dict({
data: [], data: getDictOptions(DICT_TYPE.counterparty_nature),
}), }),
}, },
}, },

View File

@ -1,30 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, onMounted, nextTick, onUnmounted } from 'vue'; import { computed, nextTick, onMounted, ref } from 'vue';
import { MdiAdd } from '@vben/icons'; import { useRoute, useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { dict, type FormScopeContext } from '@fast-crud/fast-crud';
import { MdiUpload } from '@vben/icons'; import { MdiUpload } from '@vben/icons';
import { useRouter } from 'vue-router'; import { useUserStore } from '@vben/stores';
import dayjs, { Dayjs } from 'dayjs';
import Apis from '#/api';
import { import {
message, message,
Modal, Modal,
type UploadChangeParam, type UploadChangeParam,
type UploadFile, type UploadFile,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { useUserStore } from '@vben/stores';
import { useRoute } from 'vue-router'; import Apis from '#/api';
import { FileUploader } from '#/utils/file';
import { import {
getFormSchema, getFormSchema,
getFormSchemaByBank, getFormSchemaByBank,
getFormSchemaByCertification, getFormSchemaByCertification,
getFormSchemaByShareholder, getFormSchemaByShareholder,
} from './curd'; } from './curd';
import { FileUploader } from '#/utils/file';
import { DICT_TYPE, getDictObj, getDictOptions } from '#/utils/dict';
import { useVxeTable } from '#/hooks/vxeTable';
const fileUploader = new FileUploader({}); const fileUploader = new FileUploader({});
@ -38,9 +35,7 @@ const formRefByBank = ref();
const formRefByCertification = ref(); const formRefByCertification = ref();
const formRefByShareholder = ref(); const formRefByShareholder = ref();
let isLoading = ref(false); const isLoading = ref(false);
let contractTypeData = ref([]);
const formBinding = ref({ const formBinding = ref({
...getFormSchema(), ...getFormSchema(),
@ -58,12 +53,12 @@ const formBindingByShareholder = ref({
...getFormSchemaByShareholder(), ...getFormSchemaByShareholder(),
}); });
let collapses = ['1', '2', '3', '4', '5']; const collapses = ['1', '2', '3', '4', '5'];
let collapseActiveKey = ref(collapses); const collapseActiveKey = ref(collapses);
function areArraysEqualUnordered(arr1, arr2) { function areArraysEqualUnordered(arr1, arr2) {
if (arr1.length !== arr2.length) return false; if (arr1.length !== arr2.length) return false;
const sortedArr1 = arr1.slice().sort(); const sortedArr1 = [...arr1].sort();
const sortedArr2 = arr2.slice().sort(); const sortedArr2 = [...arr2].sort();
return JSON.stringify(sortedArr1) === JSON.stringify(sortedArr2); return JSON.stringify(sortedArr1) === JSON.stringify(sortedArr2);
} }
const isFold = computed(() => { const isFold = computed(() => {
@ -90,29 +85,29 @@ function back() {
router.replace('/contract/company/list'); router.replace('/contract/company/list');
} }
let fileList = ref<UploadFile[]>([]); const fileList = ref<UploadFile[]>([]);
const handleChange = (info: UploadChangeParam) => { const handleChange = (info: UploadChangeParam) => {
fileList.value = info.fileList.length ? info.fileList : []; fileList.value = info.fileList.length > 0 ? info.fileList : [];
}; };
const handleChangeByCertification = (info: UploadChangeParam) => { const handleChangeByCertification = (info: UploadChangeParam) => {
formRefByCertification.value.setFormData({ formRefByCertification.value.setFormData({
fileList: info.fileList.length ? info.fileList : [], fileList: info.fileList.length > 0 ? info.fileList : [],
}); });
}; };
function handleDelete() { function handleDelete() {
Modal.confirm({ Modal.confirm({
title: '提示',
content: '是否确认删除该条记录?', content: '是否确认删除该条记录?',
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
await Apis.contractBaseInfo.post_deletes({ await Apis.contractBaseInfo.post_deletes({
params: { ids: currData.value['guid'] }, params: { ids: currData.value.guid },
}); });
message.success('删除成功'); message.success('删除成功');
back(); back();
}, },
title: '提示',
}); });
} }
@ -122,19 +117,24 @@ async function handleSave() {
isLoading.value = true; isLoading.value = true;
try { try {
let form = formRef.value.form; const form = formRef.value.form;
console.log(formRef.value); console.log(formRef.value);
await formRef.value.submit(); try {
await formRef.value.submit();
} catch {
message.error('请完成必填项的填写');
return;
}
const userStore = useUserStore(); const userStore = useUserStore();
let newForm = {}; let newForm = {};
// //
let fileList = formRef.value.form.fileList; const fileList = formRef.value.form.fileList;
let files: any = []; let files: any = [];
if (fileList && fileList.length) { if (fileList && fileList.length > 0) {
files = await fileUploader.upload(fileList, { source: 'ht' }); files = await fileUploader.upload(fileList, { source: 'ht' });
} }
console.log(files); console.log(files);
@ -146,44 +146,32 @@ async function handleSave() {
delete newForm.fileList; delete newForm.fileList;
// name // // name
for (const item of contractTypeData.value) { // for (const item of contractTypeData.value) {
if (item.contrLevelId == newForm.ctrType) { // if (item.contrLevelId == newForm.ctrType) {
newForm.ctrTypeName = item.contrLevelName; // newForm.ctrTypeName = item.contrLevelName;
} // }
if (item.contrLevelId == newForm.ctrTwoType) { // if (item.contrLevelId == newForm.ctrTwoType) {
newForm.ctrTwoTypeName = item.contrLevelName; // newForm.ctrTwoTypeName = item.contrLevelName;
} // }
} // }
newForm.fundDitchName = getDictObj( // newForm.fundDitchName = getDictObj(
DICT_TYPE.contract_funding_source, // DICT_TYPE.contract_funding_source,
newForm.fundDitch, // newForm.fundDitch,
)?.label; // )?.label;
newForm.priceTypeName = getDictObj( // newForm.priceTypeName = getDictObj(
DICT_TYPE.contract_currency_unit, // DICT_TYPE.contract_currency_unit,
newForm.priceType, // newForm.priceType,
)?.label; // )?.label;
newForm.contractMoney = newForm.budgetSum; // newForm.contractMoney = newForm.budgetSum;
newForm.currentContStepId = 0; // newForm.currentContStepId = 0;
console.log(newForm); console.log(newForm);
await Apis.contractBaseInfo.post_apply({ data: newForm }).then((data) => { await Apis.proProviderInfo.post_save({ data: newForm });
message.success('保存成功');
Modal.confirm({
title: '提示',
content: '保存成功!是否进行提交?',
onOk: () => {
handleSubmit();
},
onCancel: () => {
back();
},
});
});
} catch (error) { } catch (error) {
message.error('提交失败,请稍候再试'); message.error('提交失败,请稍候再试');
console.log(error); console.log(error);
@ -192,23 +180,6 @@ async function handleSave() {
} }
} }
async function handleSubmit() {
try {
//
await Apis.contractBaseInfo.post_apply({ data: {} }).then((data) => {
message.success('提交成功');
back();
});
} catch (error) {
message.error('提交失败,请稍候再试');
console.log(error);
} finally {
isLoading.value = false;
}
}
const currData = ref({});
onMounted(async () => { onMounted(async () => {
isLoading.value = true; isLoading.value = true;
console.log(id); console.log(id);
@ -219,7 +190,7 @@ onMounted(async () => {
try { try {
if (id) { if (id) {
let data = await Apis.contractBaseInfo.get_getOne({ const data = await Apis.proProviderInfo.get_Query({
params: { guid: id }, params: { guid: id },
}); });
console.log(data); console.log(data);
@ -230,7 +201,7 @@ onMounted(async () => {
}); });
if (data.fileUuid) { if (data.fileUuid) {
let files = await fileUploader.select(data.fileUuid); const files = await fileUploader.select(data.fileUuid);
console.log(files); console.log(files);
nextTick(() => { nextTick(() => {
formRef.value.setFormData({ formRef.value.setFormData({
@ -245,11 +216,11 @@ onMounted(async () => {
console.log(error); console.log(error);
Modal.error({ Modal.error({
title: '提示',
content: '当前合同信息不存在', content: '当前合同信息不存在',
onOk() { onOk() {
back(); back();
}, },
title: '提示',
}); });
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -258,49 +229,47 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Page ref="pageRef" contentClass="h-full flex flex-col"> <Page ref="pageRef" content-class="h-full flex flex-col">
<a-affix <a-affix
:target="() => pageRef.bodyRef"
:offset-top="0" :offset-top="0"
:style="{ zIndex: 50 }" :style="{ zIndex: 50 }"
:target="() => pageRef.bodyRef"
> >
<div class="flex w-full flex-row bg-white pl-1 pt-1"> <div class="flex w-full flex-row bg-white pl-1 pt-1">
<a-space class="flex-1"> <a-space class="flex-1">
<vben-button variant="primary" @click="handleSave()" <vben-button variant="primary" @click="handleSave()">
>保存</vben-button 保存
> </vben-button>
<vben-button v-if="id" variant="destructive" @click="handleDelete()"> <vben-button v-if="id" variant="destructive" @click="handleDelete()">
删除 删除
</vben-button> </vben-button>
<vben-button variant="secondary" @click="handleBack()" <vben-button variant="secondary" @click="handleBack()">
>返回</vben-button 返回
> </vben-button>
</a-space> </a-space>
<vben-button variant="secondary" @click="handleFold()" <vben-button variant="secondary" @click="handleFold()">
>一键{{ isFold ? '展开' : '收起' }}</vben-button 一键{{ isFold ? '展开' : '收起' }}
> </vben-button>
</div> </div>
</a-affix> </a-affix>
<a-spin :spinning="isLoading"> <a-spin :spinning="isLoading">
<div class="mx-auto overflow-auto py-2"> <div class="mx-auto overflow-auto py-2">
<a-collapse v-model:activeKey="collapseActiveKey" :bordered="false"> <a-collapse v-model:active-key="collapseActiveKey" :bordered="false">
<a-collapse-panel key="1" header="基本信息" class="w-full"> <a-collapse-panel key="1" class="w-full" header="基本信息">
<fs-form ref="formRef" class="w-full" v-bind="formBinding"> <fs-form ref="formRef" class="w-full" v-bind="formBinding" />
</fs-form>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="2" header="银行开户信息" class="w-full"> <a-collapse-panel key="2" class="w-full" header="银行开户信息">
<fs-form <fs-form
ref="formRefByBank" ref="formRefByBank"
class="w-full" class="w-full"
v-bind="formBindingByBank" v-bind="formBindingByBank"
> />
</fs-form>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="3" header="资质信息" class="w-full"> <a-collapse-panel key="3" class="w-full" header="资质信息">
<fs-form <fs-form
ref="formRefByCertification" ref="formRefByCertification"
class="w-full" class="w-full"
@ -308,11 +277,11 @@ onMounted(async () => {
> >
<template #form_fileList="scope"> <template #form_fileList="scope">
<a-upload <a-upload
v-model:fileList="scope.form.fileList" v-model:file-list="scope.form.fileList"
accept=".pdf"
:max-count="3"
name="file"
:before-upload="() => false" :before-upload="() => false"
:max-count="3"
accept=".pdf"
name="file"
@change="handleChangeByCertification" @change="handleChangeByCertification"
> >
<vben-button variant="secondary"> <vben-button variant="secondary">
@ -324,24 +293,23 @@ onMounted(async () => {
</fs-form> </fs-form>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="4" header="股东信息" class="w-full"> <a-collapse-panel key="4" class="w-full" header="股东信息">
<fs-form <fs-form
ref="formRefByShareholder" ref="formRefByShareholder"
class="w-full" class="w-full"
v-bind="formBindingByShareholder" v-bind="formBindingByShareholder"
> />
</fs-form>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="5" header="合同相对人附件" class="w-full"> <a-collapse-panel key="5" class="w-full" header="合同相对人附件">
<a-form :label-col="{ style: { width: '120px' } }"> <a-form :label-col="{ style: { width: '120px' } }">
<a-form-item name="fileList" label="附件上传"> <a-form-item label="附件上传" name="fileList">
<a-upload <a-upload
v-model:fileList="fileList" v-model:file-list="fileList"
accept=".pdf,.ppt,.pptx"
:max-count="3"
name="file"
:before-upload="() => false" :before-upload="() => false"
:max-count="3"
accept=".pdf,.ppt,.pptx"
name="file"
@change="handleChange" @change="handleChange"
> >
<a-button> <a-button>

View File

@ -96,15 +96,6 @@ export function getFormSchema(_params: any = {}) {
}, },
}, },
}, },
projectNum6: {
title: '公告发布方式',
key: 'projectNum6',
col: { span: 8 },
component: {
name: 'a-input',
vModel: 'value',
},
},
isBid: { isBid: {
title: '划分标段', title: '划分标段',
key: 'isBid', key: 'isBid',

View File

@ -41,6 +41,9 @@ let isLoading = ref(false);
let contractTypeData = ref([]); let contractTypeData = ref([]);
let collapses = ['1', '2'];
let collapseActiveKey = ref(collapses);
const formBinding = ref({ const formBinding = ref({
col: { span: 24 }, col: { span: 24 },
initialForm: { initialForm: {
@ -188,33 +191,31 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Page ref="pageRef" contentClass="h-full flex flex-col"> <div class="flex h-full w-full flex-col">
<div class="mx-auto overflow-auto py-2"> <a-collapse v-model:activeKey="collapseActiveKey" :bordered="false">
<a-collapse v-model:activeKey="collapseActiveKey" :bordered="false"> <a-collapse-panel key="1" header="基本信息" class="w-full">
<a-collapse-panel key="1" header="基本信息" class="w-full"> <fs-form ref="formRef" class="w-full" v-bind="formBinding">
<fs-form ref="formRef" class="w-full" v-bind="formBinding"> <template #form_fileList="scope">
<template #form_fileList="scope"> <a-upload
<a-upload v-model:fileList="scope.form.fileList"
v-model:fileList="scope.form.fileList" accept=".pdf,.ppt,.pptx"
accept=".pdf,.ppt,.pptx" :max-count="3"
:max-count="3" name="file"
name="file" >
> </a-upload>
</a-upload> </template>
</template> </fs-form>
</fs-form> </a-collapse-panel>
</a-collapse-panel>
<a-collapse-panel key="2" header="签约依据" class="w-full"> <a-collapse-panel key="2" header="签约依据" class="w-full">
<template #extra> </template> <template #extra> </template>
<VxeGrid ref="xGridRef" v-bind="gridOptions" class=""> <VxeGrid ref="xGridRef" v-bind="gridOptions" class="">
<template #toolbar_buttons></template> <template #toolbar_buttons></template>
</VxeGrid> </VxeGrid>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</div> </div>
</Page>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -0,0 +1,3 @@
<template>
<div>break</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>change</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>payment</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>relieve</div>
</template>

View File

@ -0,0 +1,68 @@
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { Page } from '@vben/common-ui';
const Payment = defineAsyncComponent(
() => import('./components/payment/index.vue'),
);
const Change = defineAsyncComponent(
() => import('./components/change/index.vue'),
);
const Relieve = defineAsyncComponent(
() => import('./components/relieve/index.vue'),
);
const Break = defineAsyncComponent(
() => import('./components/break/index.vue'),
);
const route = useRoute();
const type = route.query.type as string;
const id = route.query.id as string;
const tabKey = ref('payment');
const tabList = [
{
key: 'payment',
tab: '合同付款',
component: Payment,
},
{
key: 'change',
tab: '合同变更',
component: Change,
},
{
key: 'relieve',
tab: '合同终止(解除)申报',
component: Relieve,
},
{
key: 'break',
tab: '违约情况申报',
component: Break,
},
];
onMounted(() => {
// if (type) {
// tabKey.value = type;
// }
if (!type || !id) {
console.warn('缺少参数');
}
});
</script>
<template>
<Page content-class="h-full">
<a-tabs v-model:active-key="tabKey">
<a-tab-pane v-for="item in tabList" :key="item.key" :tab="item.tab">
<component :is="item.component" :id="id" />
</a-tab-pane>
</a-tabs>
</Page>
</template>

View File

@ -1,101 +1,119 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, onMounted } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { useRouter } from 'vue-router';
import { useVxeTable } from '#/hooks/vxeTable';
import { MdiAdd, MdiUpdate, MdiDelete, MdiImport, MdiExport, MdiRadioUnchecked, MdiRadioChecked } from '@vben/icons';
import { getFormSchema, getColumns, PrimaryKey } from './crud.tsx';
import { getMonthStartAndEnd } from '#/utils/time'
import Apis from '#/api'
import dayjs from 'dayjs';
import { message } from "ant-design-vue";
import { Modal } from 'ant-design-vue';
import { useRouter } from 'vue-router' import { Page, useVbenModal } from '@vben/common-ui';
import {
MdiAdd,
MdiDelete,
MdiExport,
MdiRadioChecked,
MdiRadioUnchecked,
MdiUpdate,
} from '@vben/icons';
import { message, Modal } from 'ant-design-vue';
import Apis from '#/api';
import { useVxeTable } from '#/hooks/vxeTable';
import { getMonthStartAndEnd } from '#/utils/time';
import { getColumns, getFormSchema, PrimaryKey } from './crud.tsx';
const router = useRouter(); const router = useRouter();
const checkedValue = ref('all') const checkedValue = ref('all');
const exportSearchParams = ref<any>({ const exportSearchParams = ref<any>({
daterange: getMonthStartAndEnd(), daterange: getMonthStartAndEnd(),
}); });
const searchRef = ref() const searchRef = ref();
let isConfirmLoading = ref(false) const isConfirmLoading = ref(false);
const [_Modal, modalApi] = useVbenModal({ const [_Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
isConfirmLoading.value = true isConfirmLoading.value = true;
try { try {
let params = {}; let params = {};
if (checkedValue.value == "daterange") { if (checkedValue.value == 'daterange') {
params = { params = {
startDate: exportSearchParams.value.daterange[0], startDate: exportSearchParams.value.daterange[0],
endDate: exportSearchParams.value.daterange[1], endDate: exportSearchParams.value.daterange[1],
}; };
} }
let res = await Apis.zbgl.post_export({ const res = await Apis.zbgl
params: params, config: { .post_export({
meta: { params,
responseType: 'blob' config: {
} meta: {
} responseType: 'blob',
}).send(); },
message.success("导出成功"); },
modalApi.close() })
.send();
message.success('导出成功');
modalApi.close();
showExportModal.value = false; showExportModal.value = false;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
isConfirmLoading.value = false isConfirmLoading.value = false;
} }
console.info('onConfirm');
console.info("onConfirm");
}, },
}); });
const { xGridRef, triggerProxy, gridProps } = useVxeTable({ ref: 'xGridRef' }); const { xGridRef, triggerProxy, gridProps } = useVxeTable({ ref: 'xGridRef' });
const treeData = ref([]); const treeData = ref([]);
/** Hooks - 表格 */ /** Hooks - 表格 */
const gridOptions = reactive(gridProps({ const gridOptions = reactive(
columns: getColumns(), gridProps({
proxyConfig: { columns: getColumns(),
autoLoad: false, proxyConfig: {
ajax: { autoLoad: false,
query: ({ page }) => { ajax: {
return Apis.contractBaseInfo.get_page({ params: { pageNum: page.currentPage, pageSize: page.pageSize, ...searchRef.value?.formData } }) query: ({ page }) => {
} return Apis.contractBaseInfo.get_page({
params: {
pageNum: page.currentPage,
pageSize: page.pageSize,
...searchRef.value?.formData,
},
});
},
},
}, },
}, pagerConfig: {
pagerConfig: { enabled: true,
enabled: true },
}, toolbarConfig: {
toolbarConfig: { enabled: true,
enabled: true },
}, }),
})); );
function handleEdit(record?: any) { function handleEdit(record?: any) {
if (record && record[PrimaryKey]) { if (record && record[PrimaryKey]) {
router.push("/contract/approval/edit/" + record[PrimaryKey]); router.push(`/contract/perform/edit/${record[PrimaryKey]}`);
} else { } else {
router.push("/contract/approval/edit"); router.push('/contract/perform/edit');
} }
} }
function handleDelete(row) { function handleDelete(row) {
Modal.confirm({ Modal.confirm({
title: '提示', title: '提示',
content: "是否确认删除该条记录?", content: '是否确认删除该条记录?',
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
await Apis.contractBaseInfo.post_deletes({ params: { ids: row[PrimaryKey] } }) await Apis.contractBaseInfo.post_deletes({
message.success("删除成功"); params: { ids: row[PrimaryKey] },
triggerProxy("reload"); });
message.success('删除成功');
triggerProxy('reload');
}, },
onCancel() { onCancel() {
console.log('Cancel'); console.log('Cancel');
@ -107,9 +125,9 @@ function handleExport() {
const $grid = xGridRef.value; const $grid = xGridRef.value;
if ($grid) { if ($grid) {
$grid.exportData({ $grid.exportData({
type: "xlsx", type: 'xlsx',
}); });
message.success("导出成功"); message.success('导出成功');
} }
} }
@ -133,66 +151,81 @@ function handleCellClick({ row }) {
} }
onMounted(() => { onMounted(() => {
triggerProxy('reload') triggerProxy('reload');
}) });
const searchForm = ref({ const searchForm = ref({
...getFormSchema(), ...getFormSchema(),
onSearch(context: any) { onSearch(context: any) {
triggerProxy('reload') triggerProxy('reload');
} },
}); });
function toPage() { function toPage() {
window.open("/iframe/meeting/standing-book", "_blank"); window.open('/iframe/meeting/standing-book', '_blank');
} }
function toDetail(row) { function toDetail(row) {
window.open("/iframe/meeting/start/" + row.guid, "_blank"); window.open(`/iframe/meeting/start/${row.guid}`, '_blank');
} }
// //
</script> </script>
<template> <template>
<Page contentClass="h-full flex flex-col"> <Page content-class="h-full flex flex-col">
<fs-search ref="searchRef" v-bind="searchForm"> <fs-search ref="searchRef" v-bind="searchForm">
<template #search_price="{ row }"> <template #search_price="{ row }">
<a-input-number v-model:value="row.budgetSum1" placeholder=""> <a-input-number v-model:value="row.budgetSum1" placeholder="" />
</a-input-number>
<span class="mx-1"></span> <span class="mx-1"></span>
<a-input-number v-model:value="row.budgetSum2" placeholder=""> <a-input-number v-model:value="row.budgetSum2" placeholder="" />
</a-input-number>
</template> </template>
<template #form_time="{ row }"> <template #form_time="{ row }">
<a-date-picker v-model:value="row.startTime" placeholder="" format="YYYY-MM-DD" value-format="YYYY-MM-DD"> <a-date-picker
</a-date-picker> v-model:value="row.startTime"
format="YYYY-MM-DD"
placeholder=""
value-format="YYYY-MM-DD"
/>
<span class="mx-1"></span> <span class="mx-1"></span>
<a-date-picker v-model:value="row.endTime" placeholder="" format="YYYY-MM-DD" value-format="YYYY-MM-DD"> <a-date-picker
</a-date-picker> v-model:value="row.endTime"
format="YYYY-MM-DD"
placeholder=""
value-format="YYYY-MM-DD"
/>
</template> </template>
</fs-search> </fs-search>
<div class="flex-1 min-h-300px"> <div class="min-h-300px flex-1">
<vxe-grid ref="xGridRef" v-bind="gridOptions" @cell-click="handleCellClick"> <vxe-grid
ref="xGridRef"
v-bind="gridOptions"
@cell-click="handleCellClick"
>
<template #toolbar_buttons> <template #toolbar_buttons>
<a-space> <a-space>
<vben-button variant="primary" @click="handleEdit()"> <vben-button variant="primary" @click="handleEdit()">
<MdiAdd class="text-lg mr-0.5" /> <MdiAdd class="mr-0.5 text-lg" />
新增 新增
</vben-button> </vben-button>
<vben-button variant="warning" :disabled="!selectRow || !selectRow[PrimaryKey]" <vben-button
@click="handleEdit(selectRow)"> :disabled="!selectRow || !selectRow[PrimaryKey]"
<MdiUpdate class="text-lg mr-0.5" /> variant="warning"
@click="handleEdit(selectRow)"
>
<MdiUpdate class="mr-0.5 text-lg" />
修改 修改
</vben-button> </vben-button>
<vben-button variant="primary" @click="handleExport()"> <vben-button variant="primary" @click="handleExport()">
<MdiExport class="text-lg mr-0.5" /> <MdiExport class="mr-0.5 text-lg" />
导出 导出
</vben-button> </vben-button>
<vben-button variant="destructive" :disabled="!selectRow || !selectRow[PrimaryKey]" <vben-button
@click="handleDelete(selectRow)"> :disabled="!selectRow || !selectRow[PrimaryKey]"
<MdiDelete class="text-lg mr-0.5" /> variant="destructive"
@click="handleDelete(selectRow)"
>
<MdiDelete class="mr-0.5 text-lg" />
删除 删除
</vben-button> </vben-button>
</a-space> </a-space>
@ -204,7 +237,6 @@ function toDetail(row) {
<MdiRadioUnchecked v-else /> <MdiRadioUnchecked v-else />
</span> </span>
</template> </template>
</vxe-grid> </vxe-grid>
</div> </div>
</Page> </Page>

View File

@ -1,8 +1,7 @@
import { defineConfig } from '@vben/vite-config'; import { defineConfig } from '@vben/vite-config';
import autoImport from 'unplugin-auto-import/vite';
import { autoBuildDict } from 'common-utils'; import { autoBuildDict } from 'common-utils';
import autoImport from 'unplugin-auto-import/vite';
export default defineConfig(async () => { export default defineConfig(async () => {
return { return {
@ -14,12 +13,12 @@ export default defineConfig(async () => {
sharedPath: 'src/utils/dict/shared.ts', sharedPath: 'src/utils/dict/shared.ts',
}), }),
autoImport({ autoImport({
dts: true, // (false) 配置文件生成位置,默认是根目录 /auto-imports.d.ts
// 自动导入 Vue 相关函数ref, reactive, toRef 等 // 自动导入 Vue 相关函数ref, reactive, toRef 等
imports: ['vue'], imports: ['vue'],
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/], include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/],
resolvers: [], resolvers: [],
vueTemplate: true, // 是否在 vue 模板中自动导入 vueTemplate: true, // 是否在 vue 模板中自动导入
dts: true, // (false) 配置文件生成位置,默认是根目录 /auto-imports.d.ts
}), }),
], ],
server: { server: {
@ -34,13 +33,6 @@ export default defineConfig(async () => {
target: `http://192.168.148.88:8083/rl`, target: `http://192.168.148.88:8083/rl`,
ws: true, ws: true,
}, },
'/api/uc': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/uc/, '/'),
// target: `http://10.71.220.24:8082`,
target: `http://192.168.148.88:8082`,
ws: true,
},
'/api/czg/app': { '/api/czg/app': {
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/czg\/app/, '/'), rewrite: (path) => path.replace(/^\/api\/czg\/app/, '/'),
@ -55,17 +47,10 @@ export default defineConfig(async () => {
// target: `http://192.168.148.88:8082`, // target: `http://192.168.148.88:8082`,
ws: true, ws: true,
}, },
'/api/zp/app': { '/api/uc': {
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/zp\/app/, '/'), rewrite: (path) => path.replace(/^\/api\/uc/, '/'),
// mock代理目标地址 // target: `http://10.71.220.24:8082`,
target: `http://192.168.153.236:8083/rl`,
ws: true,
},
'/api/zp/uc': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/zp\/uc/, '/'),
// mock代理目标地址
target: `http://192.168.148.88:8082`, target: `http://192.168.148.88:8082`,
ws: true, ws: true,
}, },
@ -83,6 +68,34 @@ export default defineConfig(async () => {
target: `http://192.168.148.88:8082`, target: `http://192.168.148.88:8082`,
ws: true, ws: true,
}, },
'/api/zp/app': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/zp\/app/, '/'),
// mock代理目标地址
target: `http://192.168.153.236:8083/rl`,
ws: true,
},
'/api/zp/uc': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/zp\/uc/, '/'),
// mock代理目标地址
target: `http://192.168.148.88:8082`,
ws: true,
},
'/api/zzz/app': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/zzz\/app/, '/'),
// target: `http://192.168.0.193:8083/rl`,
target: `http://192.168.147.164:8089/rl`,
ws: true,
},
'/api/zzz/uc': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/zzz\/uc/, '/'),
target: `http://192.168.148.88:8082`,
// target: `http://192.168.148.88:8082`,
ws: true,
},
}, },
}, },
}, },

5
apps/web-test/.env Normal file
View File

@ -0,0 +1,5 @@
# 应用标题
VITE_APP_TITLE=Vben Admin Antd
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-antd

View File

@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true

View File

@ -0,0 +1,16 @@
# 端口号
VITE_PORT=5666
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=/api
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=true
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true

View File

@ -0,0 +1,19 @@
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=none
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=hash
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true

35
apps/web-test/index.html Normal file
View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="description" content="A Modern Back-end Management System" />
<meta name="keywords" content="Vben Admin Vue3 Vite" />
<meta name="author" content="Vben" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" />
<script>
// 生产环境下注入百度统计
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
var _hmt = _hmt || [];
(function () {
var hm = document.createElement('script');
hm.src =
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,53 @@
{
"name": "web-test",
"version": "5.3.0-beta.2",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-antd"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^11.0.3",
"ant-design-vue": "^4.2.3",
"dayjs": "^1.11.13",
"pinia": "2.2.2",
"vue": "^3.5.4",
"vue-grid-layout": "3.0.0-beta1",
"vue-grid-layout-v3": "^3.0.3",
"vue-router": "^4.4.4",
"vue3-grid-layout-next": "^1.0.7"
}
}

View File

@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,114 @@
import type {
BaseFormComponentType,
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
// 业务表单组件适配
export type FormComponentType =
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
// 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({
components: {
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认的重置按钮
DefaultResetActionButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
// 自定义默认的提交按钮
DefaultSubmitActionButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
TimePicker,
TreeSelect,
Upload,
},
config: {
baseModelPropName: 'value',
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
required: (value, _params, ctx) => {
if ((!value && value !== 0) || value.length === 0) {
return $t('formRules.required', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<FormComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<FormComponentType>;
export type { VbenFormProps };

View File

@ -0,0 +1 @@
export * from './form';

View File

@ -0,0 +1,55 @@
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password: string;
username: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
desc: string;
realName: string;
userId: string;
username: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
*
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
*
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}

View File

@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

View File

@ -0,0 +1,10 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@ -0,0 +1,10 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}

View File

@ -0,0 +1 @@
export * from './core';

View File

@ -0,0 +1,104 @@
/**
*
*/
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) {
const client = new RequestClient({
baseURL,
});
/**
*
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// response数据解构
client.addResponseInterceptor({
fulfilled: (response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
},
});
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string) => message.error(msg)),
);
return client;
}
export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

39
apps/web-test/src/app.vue Normal file
View File

@ -0,0 +1,39 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAntdDesignTokens } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
const { tokens } = useAntdDesignTokens();
const tokenTheme = computed(() => {
const algorithm = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd
if (preferences.app.compact) {
algorithm.push(theme.compactAlgorithm);
}
return {
algorithm,
token: tokens,
};
});
</script>
<template>
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
<App>
<RouterView />
</App>
</ConfigProvider>
</template>

View File

@ -0,0 +1,35 @@
import { createApp } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import VueGridLayout from 'vue3-grid-layout-next';
import { setupI18n } from '#/locales';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
const app = createApp(App);
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 配置路由及路由守卫
app.use(router);
app.use(VueGridLayout);
app.mount('#app');
}
export { bootstrap };

View File

@ -0,0 +1,3 @@
<template>
<div class="text-white">123123</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="text-white">1231231231</div>
</template>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@ -0,0 +1,140 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([
{
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
message: '描述信息描述信息描述信息',
title: '收到了 14 份新周报',
},
{
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
message: '描述信息描述信息描述信息',
title: '朱偏右 回复了你',
},
{
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
message: '描述信息描述信息描述信息',
title: '曲丽丽 评论了你',
},
{
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
]);
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
{
handler: () => {
openWindow(VBEN_DOC_URL, {
target: '_blank',
});
},
icon: BookOpenText,
text: $t('widgets.document'),
},
{
handler: () => {
openWindow(VBEN_GITHUB_URL, {
target: '_blank',
});
},
icon: MdiGithub,
text: 'GitHub',
},
{
handler: () => {
openWindow(`${VBEN_GITHUB_URL}/issues`, {
target: '_blank',
});
},
icon: CircleHelp,
text: $t('widgets.qa'),
},
]);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
async function handleLogout() {
await authStore.logout(false);
}
function handleNoticeClear() {
notifications.value = [];
}
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown>
<UserDropdown
:avatar
:menus
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
/>
</template>
<template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@make-all="handleMakeAll"
/>
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
</template>

View File

@ -0,0 +1,6 @@
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };

View File

@ -0,0 +1,3 @@
# locale
每个app使用的国际化可能不同这里用于扩展国际化的功能例如扩展 dayjs、antd组件库的多语言切换以及app本身的国际化文件。

View File

@ -0,0 +1,94 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import { ref } from 'vue';
import { $t, setupI18n as coreSetup, loadLocalesMap } from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref<Locale>(antdDefaultLocale);
const modules = import.meta.glob('./langs/*.json');
const localesMap = loadLocalesMap(modules);
/**
*
*
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
*
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, setupI18n };

View File

@ -0,0 +1,8 @@
{
"page": {
"demos": {
"title": "Demos",
"antd": "Ant Design Vue"
}
}
}

View File

@ -0,0 +1,8 @@
{
"page": {
"demos": {
"title": "演示",
"antd": "Ant Design Vue"
}
}
}

31
apps/web-test/src/main.ts Normal file
View File

@ -0,0 +1,31 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
*
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();

View File

@ -0,0 +1,33 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description
* 使
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
accessMode: 'frontend',
authPageLayout: 'panel-center',
colorGrayMode: false,
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
defaultAvatar: '',
dynamicTitle: true,
// 是否开启检查更新
enableCheckUpdates: true,
// 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1,
// 开启布局设置按钮
enablePreferences: false,
enableRefreshToken: false,
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'page',
preferencesButtonPosition: 'auto',
watermark: false,
},
});

View File

@ -0,0 +1,42 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@ -0,0 +1,137 @@
import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { useTitle } from '@vueuse/core';
import { $t } from '#/locales';
import { coreRouteNames, dynamicRoutes } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
*
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
if (preferences.tabbar.enable) {
loadedPaths.add(to.path);
}
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
// 动态修改标题
if (preferences.app.dynamicTitle) {
const { title } = to.meta;
// useTitle(`${$t(title)} - ${preferences.app.name}`);
useTitle(`${$t(title)} - ${preferences.app.name}`);
}
});
}
/**
* 访
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: dynamicRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
*
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View File

@ -0,0 +1,37 @@
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };

View File

@ -0,0 +1,86 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH } from '@vben/constants';
import { AuthPageLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
{
meta: {
title: 'Root',
},
name: 'Root',
path: '/',
redirect: DEFAULT_HOME_PATH,
},
{
component: AuthPageLayout,
meta: {
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
children: [
{
name: 'Login',
path: 'login',
component: Login,
meta: {
title: $t('page.core.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.core.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.core.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.core.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.core.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@ -0,0 +1,31 @@
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统 */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由+静态路由组成 */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
export { coreRouteNames, dynamicRoutes, routes };

View File

@ -0,0 +1,49 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: '首页',
},
name: 'Dashboard',
path: '/home',
children: [
{
name: 'home',
path: '/home',
component: () => import('#/views/dashboard/home/index.vue'),
meta: {
affixTab: true,
hideInMenu: true,
icon: 'lucide:area-chart',
title: '首页',
},
},
// {
// name: 'Analytics',
// path: '/analytics',
// component: () => import('#/views/dashboard/analytics/index.vue'),
// meta: {
// affixTab: true,
// icon: 'lucide:area-chart',
// title: $t('page.dashboard.analytics'),
// },
// },
// {
// name: 'Workspace',
// path: '/workspace',
// component: () => import('#/views/dashboard/workspace/index.vue'),
// meta: {
// title: $t('page.dashboard.workspace'),
// },
// },
],
},
];
export default routes;

View File

@ -0,0 +1,30 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('page.demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
{
meta: {
title: $t('page.demos.antd'),
},
name: 'AntDesignDemos',
path: '/demos/ant-design',
component: () => import('#/views/demos/antd/index.vue'),
},
],
},
];
export default routes;

View File

@ -0,0 +1,81 @@
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
} from '@vben/constants';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
badgeType: 'dot',
icon: VBEN_LOGO_URL,
order: 9999,
title: $t('page.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
title: $t('page.vben.document'),
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: VBEN_GITHUB_URL,
title: 'Github',
},
},
{
name: 'VbenNaive',
path: '/vben-admin/naive',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:naiveui',
link: VBEN_NAIVE_PREVIEW_URL,
title: $t('page.vben.naive-ui'),
},
},
{
name: 'VbenElementPlus',
path: '/vben-admin/ele',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:element',
link: VBEN_ELE_PREVIEW_URL,
title: $t('page.vben.element-plus'),
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,112 @@
import type { LoginAndRegisterParams } from '@vben/common-ui';
import type { UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
*
* Asynchronously handle the login process
* @param params
*/
async function authLogin(
params: LoginAndRegisterParams,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
await logoutApi();
resetAllStores();
accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});

View File

@ -0,0 +1 @@
export * from './auth';

View File

@ -0,0 +1,3 @@
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { About } from '@vben/common-ui';
defineOptions({ name: 'About' });
</script>
<template>
<About />
</template>

View File

@ -0,0 +1,64 @@
<script lang="ts" setup>
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';
import { computed, ref } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
},
];
});
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: LoginCodeParams) {
// eslint-disable-next-line no-console
console.log(values);
}
</script>
<template>
<AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"
/>
</template>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import { computed, ref } from 'vue';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: string) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
}
</script>
<template>
<AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@ -0,0 +1,91 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: '超级管理员',
value: 'vben',
},
{
label: '管理员',
value: 'admin',
},
{
label: '用户',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
];
});
</script>
<template>
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
@submit="authStore.authLogin"
/>
</template>

View File

@ -0,0 +1,10 @@
<script lang="ts" setup>
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'QrCodeLogin' });
</script>
<template>
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
</template>

View File

@ -0,0 +1,101 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';
import { computed, h, ref } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'Register' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string()
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class:
'cursor-pointer text-primary ml-1 hover:text-primary-hover',
href: '',
},
[
$t('authentication.privacyPolicy'),
'&',
$t('authentication.terms'),
],
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: LoginAndRegisterParams) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
}
</script>
<template>
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback403Demo' });
</script>
<template>
<Fallback status="403" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback500Demo' });
</script>
<template>
<Fallback status="500" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback404Demo' });
</script>
<template>
<Fallback status="404" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'FallbackOfflineDemo' });
</script>
<template>
<Fallback status="offline" />
</template>

View File

@ -0,0 +1,100 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
},
series: [
{
areaStyle: {},
data: [
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
111,
],
itemStyle: {
color: '#5ab1ef',
},
smooth: true,
type: 'line',
},
{
areaStyle: {},
data: [
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
],
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
width: 1,
},
},
trigger: 'axis',
},
// xAxis: {
// axisTick: {
// show: false,
// },
// boundaryGap: false,
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
// type: 'category',
// },
xAxis: {
axisTick: {
show: false,
},
boundaryGap: false,
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category',
},
yAxis: [
{
axisTick: {
show: false,
},
max: 80_000,
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value',
},
],
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@ -0,0 +1,84 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
legend: {
bottom: 0,
data: ['访问', '趋势'],
},
radar: {
indicator: [
{
name: '网页',
},
{
name: '移动端',
},
{
name: 'Ipad',
},
{
name: '客户端',
},
{
name: '第三方',
},
{
name: '其它',
},
],
radius: '60%',
splitNumber: 8,
},
series: [
{
areaStyle: {
opacity: 1,
shadowBlur: 0,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
},
data: [
{
itemStyle: {
color: '#b6a2de',
},
name: '访问',
value: [90, 50, 86, 40, 50, 20],
},
{
itemStyle: {
color: '#5ab1ef',
},
name: '趋势',
value: [70, 75, 70, 76, 20, 85],
},
],
itemStyle: {
// borderColor: '#fff',
borderRadius: 10,
borderWidth: 2,
},
symbolSize: 0,
type: 'radar',
},
],
tooltip: {},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@ -0,0 +1,48 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
series: [
{
animationDelay() {
return Math.random() * 400;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
center: ['50%', '50%'],
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{ name: '外包', value: 500 },
{ name: '定制', value: 310 },
{ name: '技术支持', value: 274 },
{ name: '远程', value: 400 },
].sort((a, b) => {
return a.value - b.value;
}),
name: '商业占比',
radius: '80%',
roseType: 'radius',
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@ -0,0 +1,67 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
legend: {
bottom: '2%',
left: 'center',
},
series: [
{
animationDelay() {
return Math.random() * 100;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
avoidLabelOverlap: false,
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{ name: '搜索引擎', value: 1048 },
{ name: '直接访问', value: 735 },
{ name: '邮件营销', value: 580 },
{ name: '联盟广告', value: 484 },
],
emphasis: {
label: {
fontSize: '12',
fontWeight: 'bold',
show: true,
},
},
itemStyle: {
// borderColor: '#fff',
borderRadius: 10,
borderWidth: 2,
},
label: {
position: 'center',
show: false,
},
labelLine: {
show: false,
},
name: '访问来源',
radius: ['40%', '65%'],
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@ -0,0 +1,57 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
},
series: [
{
barMaxWidth: 80,
// color: '#4f69fd',
data: [
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
3200, 4800,
],
type: 'bar',
},
],
tooltip: {
axisPointer: {
lineStyle: {
// color: '#4f69fd',
width: 1,
},
},
trigger: 'axis',
},
xAxis: {
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}`),
type: 'category',
},
yAxis: {
max: 8000,
splitNumber: 4,
type: 'value',
},
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types';
import {
AnalysisChartCard,
AnalysisChartsTabs,
AnalysisOverview,
} from '@vben/common-ui';
import {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisits from './analytics-visits.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
const overviewItems: AnalysisOverviewItem[] = [
{
icon: SvgCardIcon,
title: '用户量',
totalTitle: '总用户量',
totalValue: 120_000,
value: 2000,
},
{
icon: SvgCakeIcon,
title: '访问量',
totalTitle: '总访问量',
totalValue: 500_000,
value: 20_000,
},
{
icon: SvgDownloadIcon,
title: '下载量',
totalTitle: '总下载量',
totalValue: 120_000,
value: 8000,
},
{
icon: SvgBellIcon,
title: '使用量',
totalTitle: '总使用量',
totalValue: 50_000,
value: 5000,
},
];
const chartTabs: TabOption[] = [
{
label: '流量趋势',
value: 'trends',
},
{
label: '月访问量',
value: 'visits',
},
];
</script>
<template>
<div class="p-5">
<AnalysisOverview :items="overviewItems" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
</AnalysisChartsTabs>
<div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div class="text-white">我是示例组件1</div>
</template>

Some files were not shown because too many files have changed in this diff Show More