diff --git a/.vscode/settings.json b/.vscode/settings.json index f1a0e9db..9a2b0e6a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -220,5 +220,6 @@ "i18n-ally.keystyle": "nested", "commentTranslate.multiLineMerge": true, "vue.server.hybridMode": true, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "svn.ignoreMissingSvnWarning": true } diff --git a/apps/backend-mock/.env b/apps/backend-mock/.env new file mode 100644 index 00000000..b20c4a65 --- /dev/null +++ b/apps/backend-mock/.env @@ -0,0 +1,3 @@ +PORT=5320 +ACCESS_TOKEN_SECRET=access_token_secret +REFRESH_TOKEN_SECRET=refresh_token_secret diff --git a/apps/backend-mock/README.md b/apps/backend-mock/README.md new file mode 100644 index 00000000..401bda76 --- /dev/null +++ b/apps/backend-mock/README.md @@ -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 +``` diff --git a/apps/backend-mock/api/auth/codes.ts b/apps/backend-mock/api/auth/codes.ts new file mode 100644 index 00000000..7ba01270 --- /dev/null +++ b/apps/backend-mock/api/auth/codes.ts @@ -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); +}); diff --git a/apps/backend-mock/api/auth/login.post.ts b/apps/backend-mock/api/auth/login.post.ts new file mode 100644 index 00000000..e002c97f --- /dev/null +++ b/apps/backend-mock/api/auth/login.post.ts @@ -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, + }); +}); diff --git a/apps/backend-mock/api/auth/logout.post.ts b/apps/backend-mock/api/auth/logout.post.ts new file mode 100644 index 00000000..ac6afe94 --- /dev/null +++ b/apps/backend-mock/api/auth/logout.post.ts @@ -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(''); +}); diff --git a/apps/backend-mock/api/auth/refresh.post.ts b/apps/backend-mock/api/auth/refresh.post.ts new file mode 100644 index 00000000..7df4d34f --- /dev/null +++ b/apps/backend-mock/api/auth/refresh.post.ts @@ -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; +}); diff --git a/apps/backend-mock/api/menu/all.ts b/apps/backend-mock/api/menu/all.ts new file mode 100644 index 00000000..b27b7ea4 --- /dev/null +++ b/apps/backend-mock/api/menu/all.ts @@ -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); +}); diff --git a/apps/backend-mock/api/status.ts b/apps/backend-mock/api/status.ts new file mode 100644 index 00000000..41773e1d --- /dev/null +++ b/apps/backend-mock/api/status.ts @@ -0,0 +1,5 @@ +export default eventHandler((event) => { + const { status } = getQuery(event); + setResponseStatus(event, Number(status)); + return useResponseError(`${status}`); +}); diff --git a/apps/backend-mock/api/test.get.ts b/apps/backend-mock/api/test.get.ts new file mode 100644 index 00000000..ca4a500b --- /dev/null +++ b/apps/backend-mock/api/test.get.ts @@ -0,0 +1 @@ +export default defineEventHandler(() => 'Test get handler'); diff --git a/apps/backend-mock/api/test.post.ts b/apps/backend-mock/api/test.post.ts new file mode 100644 index 00000000..698cf211 --- /dev/null +++ b/apps/backend-mock/api/test.post.ts @@ -0,0 +1 @@ +export default defineEventHandler(() => 'Test post handler'); diff --git a/apps/backend-mock/api/user/info.ts b/apps/backend-mock/api/user/info.ts new file mode 100644 index 00000000..e3526ae5 --- /dev/null +++ b/apps/backend-mock/api/user/info.ts @@ -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); +}); diff --git a/apps/backend-mock/error.ts b/apps/backend-mock/error.ts new file mode 100644 index 00000000..e20beac4 --- /dev/null +++ b/apps/backend-mock/error.ts @@ -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; diff --git a/apps/backend-mock/middleware/1.api.ts b/apps/backend-mock/middleware/1.api.ts new file mode 100644 index 00000000..84e2ce0e --- /dev/null +++ b/apps/backend-mock/middleware/1.api.ts @@ -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'; + } +}); diff --git a/apps/backend-mock/nitro.config.ts b/apps/backend-mock/nitro.config.ts new file mode 100644 index 00000000..b3013298 --- /dev/null +++ b/apps/backend-mock/nitro.config.ts @@ -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': '*', + }, + }, + }, +}); diff --git a/apps/backend-mock/package.json b/apps/backend-mock/package.json new file mode 100644 index 00000000..b74013d6 --- /dev/null +++ b/apps/backend-mock/package.json @@ -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" + } +} diff --git a/apps/backend-mock/routes/[...].ts b/apps/backend-mock/routes/[...].ts new file mode 100644 index 00000000..70c5f7c7 --- /dev/null +++ b/apps/backend-mock/routes/[...].ts @@ -0,0 +1,12 @@ +export default defineEventHandler(() => { + return ` +

Hello Vben Admin

+

Mock service is starting

+ +`; +}); diff --git a/apps/backend-mock/tsconfig.build.json b/apps/backend-mock/tsconfig.build.json new file mode 100644 index 00000000..64f86c6b --- /dev/null +++ b/apps/backend-mock/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/backend-mock/tsconfig.json b/apps/backend-mock/tsconfig.json new file mode 100644 index 00000000..43008af1 --- /dev/null +++ b/apps/backend-mock/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nitro/types/tsconfig.json" +} diff --git a/apps/backend-mock/utils/cookie-utils.ts b/apps/backend-mock/utils/cookie-utils.ts new file mode 100644 index 00000000..0d92f577 --- /dev/null +++ b/apps/backend-mock/utils/cookie-utils.ts @@ -0,0 +1,26 @@ +import type { EventHandlerRequest, H3Event } from 'h3'; + +export function clearRefreshTokenCookie(event: H3Event) { + deleteCookie(event, 'jwt', { + httpOnly: true, + sameSite: 'none', + secure: true, + }); +} + +export function setRefreshTokenCookie( + event: H3Event, + refreshToken: string, +) { + setCookie(event, 'jwt', refreshToken, { + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, + sameSite: 'none', + secure: true, + }); +} + +export function getRefreshTokenFromCookie(event: H3Event) { + const refreshToken = getCookie(event, 'jwt'); + return refreshToken; +} diff --git a/apps/backend-mock/utils/jwt-utils.ts b/apps/backend-mock/utils/jwt-utils.ts new file mode 100644 index 00000000..8cfc6843 --- /dev/null +++ b/apps/backend-mock/utils/jwt-utils.ts @@ -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, +): null | Omit { + 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 { + 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; + } +} diff --git a/apps/backend-mock/utils/mock-data.ts b/apps/backend-mock/utils/mock-data.ts new file mode 100644 index 00000000..b0a2bc1a --- /dev/null +++ b/apps/backend-mock/utils/mock-data.ts @@ -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', + }, +]; diff --git a/apps/backend-mock/utils/response.ts b/apps/backend-mock/utils/response.ts new file mode 100644 index 00000000..dea14724 --- /dev/null +++ b/apps/backend-mock/utils/response.ts @@ -0,0 +1,29 @@ +import type { EventHandlerRequest, H3Event } from 'h3'; + +export function useResponseSuccess(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) { + setResponseStatus(event, 403); + return useResponseError('ForbiddenException', 'Forbidden Exception'); +} + +export function unAuthorizedResponse(event: H3Event) { + setResponseStatus(event, 401); + return useResponseError('UnauthorizedException', 'Unauthorized Exception'); +} diff --git a/apps/web-contract/src/router/routes/modules/contract.ts b/apps/web-contract/src/router/routes/modules/contract.ts index 7867292f..f0298805 100644 --- a/apps/web-contract/src/router/routes/modules/contract.ts +++ b/apps/web-contract/src/router/routes/modules/contract.ts @@ -24,442 +24,463 @@ const routes: RouteRecordRaw[] = [ }, ], }, + { + name: 'ContractApproval', + path: '/contract/approval', + component: BasicLayout, + meta: { + icon: 'lucide:area-chart', + title: '合同立项', + }, + children: [ { - name: 'ContractApproval', - path: '/contract/approval', - component: BasicLayout, - meta: { - icon: 'lucide:area-chart', - title: '合同立项', - }, - 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: '签约依据维护', - }, + 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'; } - ], - }, - { - name: 'ContractBusiness', - path: '/contract/business', - component: BasicLayout, + }, + component: () => import('#/views/contract/approval/edit/index.vue'), meta: { 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', - path: '/contract/declaration', - component: BasicLayout, + name: 'ContractApprovalTodo', + path: '/contract/approval/todo', + component: () => import('#/views/contract/approval/todo/index.vue'), meta: { 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', - path: '/contract/sign', - component: BasicLayout, + name: 'ContractApprovalList', + path: '/contract/approval/list', + component: () => import('#/views/contract/approval/list/index.vue'), meta: { 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', - path: '/contract/perform', - component: BasicLayout, + name: 'ContractApprovalSigningBasis', + path: '/contract/approval/signing-basis', + component: () => + import('#/views/contract/approval/signing-basis/index.vue'), meta: { 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', - path: '/contract/archive', - component: BasicLayout, + 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', + 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: { icon: 'lucide:area-chart', 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', - path: '/contract/sign-authorization', - component: BasicLayout, + name: 'ContractArchiveTodoRetracement', + path: '/contract/archive/todo/retracement', + component: () => + import('#/views/contract/archive/todo/retracement.vue'), meta: { 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', - path: '/contract/company', - component: BasicLayout, + name: 'ContractArchiveList', + path: '/contract/archive/list', + component: () => import('#/views/contract/archive/list/index.vue'), meta: { 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: { - // 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: 'ContractSignAuthorization', + path: '/contract/sign-authorization', + component: BasicLayout, + meta: { + icon: 'lucide:area-chart', + 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', + 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', - // path: '/contract/statistic', - // meta: { - // icon: 'lucide:area-chart', - // title: '统计分析', - // }, - // children: [ - // { - // name: 'ContractPrintBusiness', - // path: '/contract/print/business', - // component: () => import('#/views/contract/company/edit/index.vue'), - // meta: { - // icon: 'lucide:area-chart', - // title: '统计分析', - // }, - // }, - // { - // name: 'ContractPrintAbrogateList', - // path: '/contract/print/abrogate-list', - // component: () => import('#/views/contract/company/list/index.vue'), - // meta: { - // icon: 'lucide:area-chart', - // title: '废除查询', - // }, - // }, + // ], + // }, + // { + // name: 'ContractPrint', + // path: '/contract/statistic', + // meta: { + // icon: 'lucide:area-chart', + // title: '统计分析', + // }, + // children: [ + // { + // name: 'ContractPrintBusiness', + // path: '/contract/print/business', + // component: () => import('#/views/contract/company/edit/index.vue'), + // meta: { + // icon: 'lucide:area-chart', + // title: '统计分析', + // }, + // }, + // { + // name: 'ContractPrintAbrogateList', + // path: '/contract/print/abrogate-list', + // component: () => import('#/views/contract/company/list/index.vue'), + // meta: { + // icon: 'lucide:area-chart', + // title: '废除查询', + // }, + // }, - // ], - // }, + // ], + // }, ]; export default routes; diff --git a/apps/web-contract/src/utils/dict/shared.ts b/apps/web-contract/src/utils/dict/shared.ts index a639a2bf..09f65d5e 100644 --- a/apps/web-contract/src/utils/dict/shared.ts +++ b/apps/web-contract/src/utils/dict/shared.ts @@ -30,6 +30,8 @@ export enum DICT_TYPE { comprehensive_config = 'comprehensive_config', /** 合同管理-签约依据类型 */ contract_basis_type = 'contract_basis_type', + /** 合同管理-合同相对人性质 */ + counterparty_nature = 'counterparty_nature', /** 变更原因 */ cancel_reson = 'cancel_reson', /** 划分标段 */ @@ -45,5 +47,7 @@ export enum DICT_TYPE { /** 付款性质 */ 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' } diff --git a/apps/web-contract/src/utils/dict/static.data.js b/apps/web-contract/src/utils/dict/static.data.js index 9d7270b6..f46f525b 100644 --- a/apps/web-contract/src/utils/dict/static.data.js +++ b/apps/web-contract/src/utils/dict/static.data.js @@ -24,6 +24,8 @@ export default { contract_basis_type: createEntry('合同管理-签约依据类型'), + counterparty_nature: createEntry('合同管理-合同相对人性质'), + cancel_reson: createEntry('变更原因', [ { label: '未填写原因', value: '0' }, { label: '用户取消', value: '1' }, @@ -73,7 +75,7 @@ export default { { label: '待部门审核', value: 'departmentAudit' }, { label: '已结束', value: 'end' }, { label: '废除发起', value: 'abolishEdit' }, - { label: '待废除审批', value: 'abolishEdit' }, + { label: '待废除审批', value: 'abolishDepartmentAudit' }, ]), contract_abolish_flow_node: createEntry('合同立项节点流程', [ @@ -82,5 +84,4 @@ export default { { label: '已结束', value: 'end' }, { label: '废除', value: 'abolishEdit' }, ]), - }; diff --git a/apps/web-contract/src/views/contract/approval/edit/curd.tsx b/apps/web-contract/src/views/contract/approval/edit/curd.tsx index f7c7264f..66b564e8 100644 --- a/apps/web-contract/src/views/contract/approval/edit/curd.tsx +++ b/apps/web-contract/src/views/contract/approval/edit/curd.tsx @@ -46,17 +46,21 @@ export function getFormSchema(params: any = {}) { name: 'fs-dict-select', vModel: 'value', class: 'min-w-[200px]', + prototype:true, dict: dict({ async getData({ form = {} }) { return filterContractTypes(contractTypeData, '-1'); }, }), }, - valueChange({ form, value, getComponentRef }) { - form.ctrTwoType = undefined; - if (value) { + valueChange: { + immediate: true, //是否立即执行一次 + handle({ form, value, getComponentRef }) { + form.ctrTwoType = undefined; + console.log(getComponentRef('ctrTwoType')); getComponentRef('ctrTwoType').reloadDict(); // 执行city的select组件的reloadDict()方法,触发“city”重新加载字典 - } + }, + }, conditionalRender: { match({ form }) { @@ -82,6 +86,7 @@ export function getFormSchema(params: any = {}) { name: 'fs-dict-select', vModel: 'value', class: 'min-w-[200px]', + prototype:true, dict: dict({ async getData({ form = {} }) { return filterContractTypes(contractTypeData, form.ctrType); @@ -172,6 +177,8 @@ export function getFormSchema(params: any = {}) { valueChange: { immediate: true, //是否立即执行一次 handle({ form }) { + debugger; + console.log(form); form.fundAllocationName = getDictObj( DICT_TYPE.contract_fund_flow, @@ -267,7 +274,12 @@ export function getFormSchema(params: any = {}) { render({ form }) { return ( - {getDictObj(DICT_TYPE.contract_currency_unit, form.priceType)} + { + getDictObj( + DICT_TYPE.contract_currency_unit, + form.priceType || '', + )?.label + } ); }, @@ -293,7 +305,10 @@ export function getFormSchema(params: any = {}) { render({ form }) { return ( - {getDictObj(DICT_TYPE.contract_organization_form, form.organiza)} + { + getDictObj(DICT_TYPE.contract_organization_form, form.organiza) + ?.label + } ); }, diff --git a/apps/web-contract/src/views/contract/company/edit/curd.tsx b/apps/web-contract/src/views/contract/company/edit/curd.tsx index e05a5d08..e5fc07fb 100644 --- a/apps/web-contract/src/views/contract/company/edit/curd.tsx +++ b/apps/web-contract/src/views/contract/company/edit/curd.tsx @@ -1,9 +1,13 @@ -import { DICT_TYPE, getDictOptions } from '#/utils/dict'; import { dict } from '@fast-crud/fast-crud'; +import { DICT_TYPE, getDictObj, getDictOptions } from '#/utils/dict'; + export function getFormSchema(_params: any = {}) { return { labelCol: { style: { width: '140px' } }, + initialForm: { + currencyTypeId: 'CNY', + }, columns: { providerName: { title: '单位全称', @@ -14,7 +18,7 @@ export function getFormSchema(_params: any = {}) { vModel: 'value', allowClear: false, }, - rules: [{ required: true }], + rules: [{ required: true, message: '请输入单位名称' }], }, providerKind: { title: '性质', @@ -25,10 +29,10 @@ export function getFormSchema(_params: any = {}) { vModel: 'value', class: 'min-w-[200px]', dict: dict({ - data: [], + data: getDictOptions(DICT_TYPE.counterparty_nature), }), }, - rules: [{ required: true }], + rules: [{ required: true, message: '请选择单位性质' }], }, providerAddr: { title: '住所', @@ -111,6 +115,15 @@ export function getFormSchema(_params: any = {}) { data: getDictOptions(DICT_TYPE.contract_currency_unit), }), }, + valueChange: { + immediate: true, // 是否立即执行一次 + handle({ form }) { + form.currencyTypeName = getDictObj( + DICT_TYPE.contract_currency_unit, + form.currencyTypeId, + )?.label; + }, + }, }, marketScope: { title: '营业执照经营范围', @@ -262,7 +275,7 @@ export function getFormSchemaByBank(_params: any = {}) { }; } -export function getFormSchemaByCertification(params: any = {}) { +export function getFormSchemaByCertification(_params: any = {}) { return { labelCol: { style: { width: '140px' } }, initialForm: {}, @@ -292,23 +305,23 @@ export function getFormSchemaByCertification(params: any = {}) { key: 'time', col: { span: 12 }, render({ form }) { - //注意此处的v-model写法 + // 注意此处的v-model写法 return (
@@ -335,7 +348,7 @@ export function getFormSchemaByCertification(params: any = {}) { }; } -export function getFormSchemaByShareholder(params: any = {}) { +export function getFormSchemaByShareholder(_params: any = {}) { return { labelCol: { style: { width: '140px' } }, columns: { @@ -367,7 +380,7 @@ export function getFormSchemaByShareholder(params: any = {}) { name: 'fs-dict-select', vModel: 'value', dict: dict({ - data: [], + data: getDictOptions(DICT_TYPE.counterparty_nature), }), }, }, diff --git a/apps/web-contract/src/views/contract/company/edit/index.vue b/apps/web-contract/src/views/contract/company/edit/index.vue index fa767e6e..ecd22091 100644 --- a/apps/web-contract/src/views/contract/company/edit/index.vue +++ b/apps/web-contract/src/views/contract/company/edit/index.vue @@ -1,30 +1,27 @@