优化问题
This commit is contained in:
parent
0730e89935
commit
06ae572098
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
PORT=5320
|
||||||
|
ACCESS_TOKEN_SECRET=access_token_secret
|
||||||
|
REFRESH_TOKEN_SECRET=refresh_token_secret
|
|
@ -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
|
||||||
|
```
|
|
@ -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);
|
||||||
|
});
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
|
@ -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('');
|
||||||
|
});
|
|
@ -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;
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default eventHandler((event) => {
|
||||||
|
const { status } = getQuery(event);
|
||||||
|
setResponseStatus(event, Number(status));
|
||||||
|
return useResponseError(`${status}`);
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export default defineEventHandler(() => 'Test get handler');
|
|
@ -0,0 +1 @@
|
||||||
|
export default defineEventHandler(() => 'Test post handler');
|
|
@ -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);
|
||||||
|
});
|
|
@ -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;
|
|
@ -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';
|
||||||
|
}
|
||||||
|
});
|
|
@ -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': '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
`;
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "./.nitro/types/tsconfig.json"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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',
|
||||||
|
},
|
||||||
|
];
|
|
@ -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');
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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' },
|
||||||
]),
|
]),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div>break</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div>change</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div>payment</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div>relieve</div>
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=Vben Admin Antd
|
||||||
|
|
||||||
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
|
VITE_APP_NAMESPACE=vben-web-antd
|
|
@ -0,0 +1,7 @@
|
||||||
|
# public path
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# Basic interface address SPA
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_VISUALIZER=true
|
|
@ -0,0 +1,16 @@
|
||||||
|
# 端口号
|
||||||
|
VITE_PORT=5666
|
||||||
|
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||||
|
VITE_NITRO_MOCK=true
|
||||||
|
|
||||||
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
|
@ -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
|
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from '@vben/tailwind-config/postcss';
|
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -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 };
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './form';
|
|
@ -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');
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './auth';
|
||||||
|
export * from './menu';
|
||||||
|
export * from './user';
|
|
@ -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');
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './core';
|
|
@ -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 });
|
|
@ -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>
|
|
@ -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 };
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-white">123123</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-white">1231231231</div>
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 };
|
|
@ -0,0 +1,3 @@
|
||||||
|
# locale
|
||||||
|
|
||||||
|
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
|
@ -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 };
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"demos": {
|
||||||
|
"title": "Demos",
|
||||||
|
"antd": "Ant Design Vue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"demos": {
|
||||||
|
"title": "演示",
|
||||||
|
"antd": "Ant Design Vue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './auth';
|
|
@ -0,0 +1,3 @@
|
||||||
|
# \_core
|
||||||
|
|
||||||
|
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { About } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'About' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<About />
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="coming-soon" />
|
||||||
|
</template>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback403Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="403" />
|
||||||
|
</template>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback500Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="500" />
|
||||||
|
</template>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback404Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="404" />
|
||||||
|
</template>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="offline" />
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue