diff --git a/.gitignore b/.gitignore index 0b05e6a6..2500fdef 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist-ssr /dist* *-lock.* pnpm-debug +.idea diff --git a/build/vite/index.ts b/build/vite/index.ts index 53f66ec0..dcec1aed 100644 --- a/build/vite/index.ts +++ b/build/vite/index.ts @@ -15,7 +15,7 @@ import vueSetupExtend from 'vite-plugin-vue-setup-extend' import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' -export function createVitePlugins(VITE_APP_TITLE: string) { +export function createVitePlugins() { const root = process.cwd() // 路径查找 function pathResolve(dir: string) { @@ -95,8 +95,6 @@ export function createVitePlugins(VITE_APP_TITLE: string) { ext: '.gz', // 生成的压缩包后缀 deleteOriginFile: false //压缩后是否删除源文件 }), - ViteEjsPlugin({ - title: VITE_APP_TITLE - }) + ViteEjsPlugin() ] } diff --git a/index.html b/index.html index cce65bdc..8cfcbefa 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ name="description" content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!" /> - <title><%= title %></title> + <title>%VITE_APP_TITLE%</title> </head> <body> <div id="app"> @@ -137,7 +137,7 @@ <div class="app-loading-wrap"> <div class="app-loading-title"> <img src="/logo.gif" class="app-loading-logo" alt="Logo" /> - <div class="app-loading-title"><%= title %></div> + <div class="app-loading-title">%VITE_APP_TITLE%</div> </div> <div class="app-loading-item"> <div class="app-loading-outter"></div> diff --git a/package.json b/package.json index d067560f..8f1d24e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yudao-ui-admin-vue3", - "version": "1.7.1-snapshot.1941", + "version": "1.7.1-snapshot.1961", "description": "基于vue3、vite4、element-plus、typesScript", "author": "xingyu", "private": false, @@ -35,6 +35,7 @@ "@zxcvbn-ts/core": "^2.2.1", "animate.css": "^4.1.1", "axios": "^1.3.4", + "benz-amr-recorder": "^1.1.5", "bpmn-js-token-simulation": "^0.10.0", "camunda-bpmn-moddle": "^7.0.1", "cropperjs": "^1.5.13", @@ -43,7 +44,7 @@ "diagram-js": "^11.6.0", "echarts": "^5.4.1", "echarts-wordcloud": "^2.1.0", - "element-plus": "2.2.34", + "element-plus": "2.3.1", "fast-xml-parser": "^4.1.3", "highlight.js": "^11.7.0", "intro.js": "^6.0.0", @@ -62,57 +63,57 @@ "vue-router": "^4.1.6", "vue-types": "^5.0.2", "vuedraggable": "^4.1.0", - "vxe-table": "^4.3.10", + "vxe-table": "^4.3.11", "web-storage-cache": "^1.1.1", "xe-utils": "^3.5.7", "xml-js": "^1.6.11" }, "devDependencies": { - "@commitlint/cli": "^17.4.4", + "@commitlint/cli": "^17.5.0", "@commitlint/config-conventional": "^17.4.4", - "@iconify/json": "^2.2.31", - "@intlify/unplugin-vue-i18n": "^0.8.2", + "@iconify/json": "^2.2.38", + "@intlify/unplugin-vue-i18n": "^0.10.0", "@purge-icons/generated": "^0.9.0", "@types/intro.js": "^5.1.1", - "@types/lodash-es": "^4.17.6", - "@types/node": "^18.14.6", + "@types/lodash-es": "^4.17.7", + "@types/node": "^18.15.5", "@types/nprogress": "^0.2.0", "@types/qrcode": "^1.5.0", "@types/qs": "^6.9.7", - "@typescript-eslint/eslint-plugin": "^5.54.1", - "@typescript-eslint/parser": "^5.54.1", - "@vitejs/plugin-legacy": "^4.0.1", - "@vitejs/plugin-vue": "^4.0.0", - "@vitejs/plugin-vue-jsx": "^3.0.0", - "autoprefixer": "^10.4.13", + "@typescript-eslint/eslint-plugin": "^5.56.0", + "@typescript-eslint/parser": "^5.56.0", + "@vitejs/plugin-legacy": "^4.0.2", + "@vitejs/plugin-vue": "^4.1.0", + "@vitejs/plugin-vue-jsx": "^3.0.1", + "autoprefixer": "^10.4.14", "bpmn-js": "^8.9.0", "bpmn-js-properties-panel": "^0.46.0", "consola": "^2.15.3", - "eslint": "^8.35.0", - "eslint-config-prettier": "^8.7.0", - "eslint-define-config": "^1.15.0", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-define-config": "^1.17.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-vue": "^9.9.0", - "lint-staged": "^13.1.2", + "lint-staged": "^13.2.0", "postcss": "^8.4.21", "postcss-html": "^1.5.0", "postcss-scss": "^4.0.6", - "prettier": "^2.8.4", - "rimraf": "^4.3.1", - "rollup": "^3.18.0", - "sass": "^1.58.3", - "stylelint": "^15.2.0", + "prettier": "^2.8.6", + "rimraf": "^4.4.1", + "rollup": "^3.20.0", + "sass": "^1.59.3", + "stylelint": "^15.3.0", "stylelint-config-html": "^1.1.0", "stylelint-config-prettier": "^9.0.5", - "stylelint-config-recommended": "^10.0.1", - "stylelint-config-standard": "^30.0.1", - "stylelint-order": "^6.0.2", - "terser": "^5.16.5", - "typescript": "4.9.5", + "stylelint-config-recommended": "^11.0.0", + "stylelint-config-standard": "^31.0.0", + "stylelint-order": "^6.0.3", + "terser": "^5.16.6", + "typescript": "5.0.2", "unplugin-auto-import": "^0.15.1", "unplugin-element-plus": "^0.7.0", "unplugin-vue-components": "^0.24.1", - "vite": "4.1.4", + "vite": "4.2.1", "vite-plugin-compression": "^0.5.1", "vite-plugin-ejs": "^1.6.4", "vite-plugin-eslint": "^1.8.1", @@ -125,7 +126,7 @@ "windicss": "^3.5.6" }, "engines": { - "node": ">=16.0.0" + "node": ">=16.18.0" }, "license": "MIT", "repository": { diff --git a/src/api/bpm/definition/index.ts b/src/api/bpm/definition/index.ts index 477d6729..c0e51fab 100644 --- a/src/api/bpm/definition/index.ts +++ b/src/api/bpm/definition/index.ts @@ -1,19 +1,19 @@ import request from '@/config/axios' -export const getProcessDefinitionBpmnXMLApi = async (id: number) => { +export const getProcessDefinitionBpmnXML = async (id: number) => { return await request.get({ url: '/bpm/process-definition/get-bpmn-xml?id=' + id }) } -export const getProcessDefinitionPageApi = async (params) => { +export const getProcessDefinitionPage = async (params) => { return await request.get({ url: '/bpm/process-definition/page', params }) } -export const getProcessDefinitionListApi = async (params) => { +export const getProcessDefinitionList = async (params) => { return await request.get({ url: '/bpm/process-definition/list', params diff --git a/src/api/bpm/form/index.ts b/src/api/bpm/form/index.ts index c745201f..142ed24c 100644 --- a/src/api/bpm/form/index.ts +++ b/src/api/bpm/form/index.ts @@ -11,7 +11,7 @@ export type FormVO = { } // 创建工作流的表单定义 -export const createFormApi = async (data: FormVO) => { +export const createForm = async (data: FormVO) => { return await request.post({ url: '/bpm/form/create', data: data @@ -19,7 +19,7 @@ export const createFormApi = async (data: FormVO) => { } // 更新工作流的表单定义 -export const updateFormApi = async (data: FormVO) => { +export const updateForm = async (data: FormVO) => { return await request.put({ url: '/bpm/form/update', data: data @@ -27,21 +27,21 @@ export const updateFormApi = async (data: FormVO) => { } // 删除工作流的表单定义 -export const deleteFormApi = async (id: number) => { +export const deleteForm = async (id: number) => { return await request.delete({ url: '/bpm/form/delete?id=' + id }) } // 获得工作流的表单定义 -export const getFormApi = async (id: number) => { +export const getForm = async (id: number) => { return await request.get({ url: '/bpm/form/get?id=' + id }) } // 获得工作流的表单定义分页 -export const getFormPageApi = async (params) => { +export const getFormPage = async (params) => { return await request.get({ url: '/bpm/form/page', params @@ -49,7 +49,7 @@ export const getFormPageApi = async (params) => { } // 获得动态表单的精简列表 -export const getSimpleFormsApi = async () => { +export const getSimpleFormList = async () => { return await request.get({ url: '/bpm/form/list-all-simple' }) diff --git a/src/api/bpm/model/index.ts b/src/api/bpm/model/index.ts index 0335a3db..2e1d4e64 100644 --- a/src/api/bpm/model/index.ts +++ b/src/api/bpm/model/index.ts @@ -25,20 +25,20 @@ export type ModelVO = { bpmnXml: string } -export const getModelPageApi = async (params) => { +export const getModelPage = async (params) => { return await request.get({ url: '/bpm/model/page', params }) } -export const getModelApi = async (id: number) => { +export const getModel = async (id: number) => { return await request.get({ url: '/bpm/model/get?id=' + id }) } -export const updateModelApi = async (data: ModelVO) => { +export const updateModel = async (data: ModelVO) => { return await request.put({ url: '/bpm/model/update', data: data }) } // 任务状态修改 -export const updateModelStateApi = async (id: number, state: number) => { +export const updateModelState = async (id: number, state: number) => { const data = { id: id, state: state @@ -46,14 +46,14 @@ export const updateModelStateApi = async (id: number, state: number) => { return await request.put({ url: '/bpm/model/update-state', data: data }) } -export const createModelApi = async (data: ModelVO) => { +export const createModel = async (data: ModelVO) => { return await request.post({ url: '/bpm/model/create', data: data }) } -export const deleteModelApi = async (id: number) => { +export const deleteModel = async (id: number) => { return await request.delete({ url: '/bpm/model/delete?id=' + id }) } -export const deployModelApi = async (id: number) => { +export const deployModel = async (id: number) => { return await request.post({ url: '/bpm/model/deploy?id=' + id }) } diff --git a/src/api/bpm/userGroup/index.ts b/src/api/bpm/userGroup/index.ts index 88ee9619..035762bf 100644 --- a/src/api/bpm/userGroup/index.ts +++ b/src/api/bpm/userGroup/index.ts @@ -11,7 +11,7 @@ export type UserGroupVO = { } // 创建用户组 -export const createUserGroupApi = async (data: UserGroupVO) => { +export const createUserGroup = async (data: UserGroupVO) => { return await request.post({ url: '/bpm/user-group/create', data: data @@ -19,7 +19,7 @@ export const createUserGroupApi = async (data: UserGroupVO) => { } // 更新用户组 -export const updateUserGroupApi = async (data: UserGroupVO) => { +export const updateUserGroup = async (data: UserGroupVO) => { return await request.put({ url: '/bpm/user-group/update', data: data @@ -27,21 +27,21 @@ export const updateUserGroupApi = async (data: UserGroupVO) => { } // 删除用户组 -export const deleteUserGroupApi = async (id: number) => { +export const deleteUserGroup = async (id: number) => { return await request.delete({ url: '/bpm/user-group/delete?id=' + id }) } // 获得用户组 -export const getUserGroupApi = async (id: number) => { +export const getUserGroup = async (id: number) => { return await request.get({ url: '/bpm/user-group/get?id=' + id }) } // 获得用户组分页 -export const getUserGroupPageApi = async (params) => { +export const getUserGroupPage = async (params) => { return await request.get({ url: '/bpm/user-group/page', params }) } // 获取用户组精简信息列表 -export const listSimpleUserGroupsApi = async () => { +export const getSimpleUserGroupList = async (): Promise<UserGroupVO[]> => { return await request.get({ url: '/bpm/user-group/list-all-simple' }) } diff --git a/src/api/infra/apiAccessLog/index.ts b/src/api/infra/apiAccessLog/index.ts index b46199e4..c6b4b45f 100644 --- a/src/api/infra/apiAccessLog/index.ts +++ b/src/api/infra/apiAccessLog/index.ts @@ -19,32 +19,12 @@ export interface ApiAccessLogVO { createTime: Date } -export interface ApiAccessLogPageReqVO extends PageParam { - userId?: number - userType?: number - applicationName?: string - requestUrl?: string - beginTime?: Date[] - duration?: number - resultCode?: number -} - -export interface ApiAccessLogExportReqVO { - userId?: number - userType?: number - applicationName?: string - requestUrl?: string - beginTime?: Date[] - duration?: number - resultCode?: number -} - // 查询列表API 访问日志 -export const getApiAccessLogPageApi = (params: ApiAccessLogPageReqVO) => { +export const getApiAccessLogPage = (params: PageParam) => { return request.get({ url: '/infra/api-access-log/page', params }) } // 导出API 访问日志 -export const exportApiAccessLogApi = (params: ApiAccessLogExportReqVO) => { +export const exportApiAccessLog = (params) => { return request.download({ url: '/infra/api-access-log/export-excel', params }) } diff --git a/src/api/infra/apiErrorLog/index.ts b/src/api/infra/apiErrorLog/index.ts index 06515c36..59ee2143 100644 --- a/src/api/infra/apiErrorLog/index.ts +++ b/src/api/infra/apiErrorLog/index.ts @@ -27,38 +27,20 @@ export interface ApiErrorLogVO { createTime: Date } -export interface ApiErrorLogPageReqVO extends PageParam { - userId?: number - userType?: number - applicationName?: string - requestUrl?: string - exceptionTime?: Date[] - processStatus: number -} - -export interface ApiErrorLogExportReqVO { - userId?: number - userType?: number - applicationName?: string - requestUrl?: string - exceptionTime?: Date[] - processStatus: number -} - // 查询列表API 访问日志 -export const getApiErrorLogPageApi = (params: ApiErrorLogPageReqVO) => { +export const getApiErrorLogPage = (params: PageParam) => { return request.get({ url: '/infra/api-error-log/page', params }) } // 更新 API 错误日志的处理状态 -export const updateApiErrorLogPageApi = (id: number, processStatus: number) => { +export const updateApiErrorLogPage = (id: number, processStatus: number) => { return request.put({ url: '/infra/api-error-log/update-status?id=' + id + '&processStatus=' + processStatus }) } // 导出API 访问日志 -export const exportApiErrorLogApi = (params: ApiErrorLogExportReqVO) => { +export const exportApiErrorLog = (params) => { return request.download({ url: '/infra/api-error-log/export-excel', params diff --git a/src/api/mp/account/index.ts b/src/api/mp/account/index.ts index cbdb1422..d641ef3c 100644 --- a/src/api/mp/account/index.ts +++ b/src/api/mp/account/index.ts @@ -1,5 +1,10 @@ import request from '@/config/axios' +export interface AccountVO { + id?: number + name: string +} + // 创建公众号账号 export const createAccount = async (data) => { return await request.post({ url: '/mp/account/create', data }) @@ -26,7 +31,7 @@ export const getAccountPage = async (query) => { } // 获取公众号账号精简信息列表 -export const getSimpleAccounts = async () => { +export const getSimpleAccountList = async () => { return request.get({ url: '/mp/account/list-all-simple' }) } diff --git a/src/api/mp/message/index.ts b/src/api/mp/message/index.ts index 8b7d3cbd..ad9b95dd 100644 --- a/src/api/mp/message/index.ts +++ b/src/api/mp/message/index.ts @@ -1,7 +1,7 @@ import request from '@/config/axios' // 获得公众号消息分页 -export const getMessagePage = (query) => { +export const getMessagePage = (query: PageParam) => { return request.get({ url: '/mp/message/page', params: query diff --git a/src/api/mp/tag/index.ts b/src/api/mp/tag/index.ts index e681e2e1..50183a51 100644 --- a/src/api/mp/tag/index.ts +++ b/src/api/mp/tag/index.ts @@ -1,7 +1,14 @@ import request from '@/config/axios' +export interface TagVO { + id?: number + name: string + accountId: number + createTime: Date +} + // 创建公众号标签 -export const createTag = (data) => { +export const createTag = (data: TagVO) => { return request.post({ url: '/mp/tag/create', data: data @@ -9,7 +16,7 @@ export const createTag = (data) => { } // 更新公众号标签 -export const updateTag = (data) => { +export const updateTag = (data: TagVO) => { return request.put({ url: '/mp/tag/update', data: data @@ -17,21 +24,21 @@ export const updateTag = (data) => { } // 删除公众号标签 -export const deleteTag = (id) => { +export const deleteTag = (id: number) => { return request.delete({ url: '/mp/tag/delete?id=' + id }) } // 获得公众号标签 -export const getTag = (id) => { +export const getTag = (id: number) => { return request.get({ url: '/mp/tag/get?id=' + id }) } // 获得公众号标签分页 -export const getTagPage = (query) => { +export const getTagPage = (query: PageParam) => { return request.get({ url: '/mp/tag/page', params: query @@ -39,14 +46,14 @@ export const getTagPage = (query) => { } // 获取公众号标签精简信息列表 -export const getSimpleTags = () => { +export const getSimpleTagList = () => { return request.get({ url: '/mp/tag/list-all-simple' }) } // 同步公众号标签 -export const syncTag = (accountId) => { +export const syncTag = (accountId: number) => { return request.post({ url: '/mp/tag/sync?accountId=' + accountId }) diff --git a/src/api/system/dept/index.ts b/src/api/system/dept/index.ts index d66de3f1..e9c31fd7 100644 --- a/src/api/system/dept/index.ts +++ b/src/api/system/dept/index.ts @@ -18,7 +18,7 @@ export interface DeptPageReqVO { } // 查询部门(精简)列表 -export const listSimpleDeptApi = async () => { +export const getSimpleDeptList = async (): Promise<DeptVO[]> => { return await request.get({ url: '/system/dept/list-all-simple' }) } diff --git a/src/api/system/loginLog/index.ts b/src/api/system/loginLog/index.ts index cadaeaf3..f275c3e2 100644 --- a/src/api/system/loginLog/index.ts +++ b/src/api/system/loginLog/index.ts @@ -13,18 +13,12 @@ export interface LoginLogVO { createTime: Date } -export interface LoginLogReqVO extends PageParam { - userIp?: string - username?: string - status?: boolean - createTime?: Date[] -} - // 查询登录日志列表 -export const getLoginLogPageApi = (params: LoginLogReqVO) => { +export const getLoginLogPage = (params: PageParam) => { return request.get({ url: '/system/login-log/page', params }) } + // 导出登录日志 -export const exportLoginLogApi = (params: LoginLogReqVO) => { +export const exportLoginLog = (params) => { return request.download({ url: '/system/login-log/export', params }) } diff --git a/src/api/system/menu/index.ts b/src/api/system/menu/index.ts index 5913972b..13736215 100644 --- a/src/api/system/menu/index.ts +++ b/src/api/system/menu/index.ts @@ -18,18 +18,13 @@ export interface MenuVO { createTime: Date } -export interface MenuPageReqVO { - name?: string - status?: number -} - // 查询菜单(精简)列表 -export const listSimpleMenusApi = () => { +export const getSimpleMenusList = () => { return request.get({ url: '/system/menu/list-all-simple' }) } // 查询菜单列表 -export const getMenuListApi = (params: MenuPageReqVO) => { +export const getMenuList = (params) => { return request.get({ url: '/system/menu/list', params }) } @@ -39,16 +34,16 @@ export const getMenuApi = (id: number) => { } // 新增菜单 -export const createMenuApi = (data: MenuVO) => { +export const createMenu = (data: MenuVO) => { return request.post({ url: '/system/menu/create', data }) } // 修改菜单 -export const updateMenuApi = (data: MenuVO) => { +export const updateMenu = (data: MenuVO) => { return request.put({ url: '/system/menu/update', data }) } // 删除菜单 -export const deleteMenuApi = (id: number) => { +export const deleteMenu = (id: number) => { return request.delete({ url: '/system/menu/delete?id=' + id }) } diff --git a/src/api/system/permission/index.ts b/src/api/system/permission/index.ts index aa355dfc..c529f884 100644 --- a/src/api/system/permission/index.ts +++ b/src/api/system/permission/index.ts @@ -37,6 +37,6 @@ export const listUserRolesApi = async (userId: number) => { } // 赋予用户角色 -export const aassignUserRoleApi = async (data: PermissionAssignUserRoleReqVO) => { +export const assignUserRoleApi = async (data: PermissionAssignUserRoleReqVO) => { return await request.post({ url: '/system/permission/assign-user-role', data }) } diff --git a/src/api/system/post/index.ts b/src/api/system/post/index.ts index 9e2540f0..405db387 100644 --- a/src/api/system/post/index.ts +++ b/src/api/system/post/index.ts @@ -10,49 +10,37 @@ export interface PostVO { createTime?: Date } -export interface PostPageReqVO extends PageParam { - code?: string - name?: string - status?: number -} - -export interface PostExportReqVO { - code?: string - name?: string - status?: number -} - // 查询岗位列表 -export const getPostPageApi = async (params: PostPageReqVO) => { +export const getPostPage = async (params: PageParam) => { return await request.get({ url: '/system/post/page', params }) } // 获取岗位精简信息列表 -export const listSimplePostsApi = async () => { +export const getSimplePostList = async (): Promise<PostVO[]> => { return await request.get({ url: '/system/post/list-all-simple' }) } // 查询岗位详情 -export const getPostApi = async (id: number) => { +export const getPost = async (id: number) => { return await request.get({ url: '/system/post/get?id=' + id }) } // 新增岗位 -export const createPostApi = async (data: PostVO) => { +export const createPost = async (data: PostVO) => { return await request.post({ url: '/system/post/create', data }) } // 修改岗位 -export const updatePostApi = async (data: PostVO) => { +export const updatePost = async (data: PostVO) => { return await request.put({ url: '/system/post/update', data }) } // 删除岗位 -export const deletePostApi = async (id: number) => { +export const deletePost = async (id: number) => { return await request.delete({ url: '/system/post/delete?id=' + id }) } // 导出岗位 -export const exportPostApi = async (params: PostExportReqVO) => { +export const exportPost = async (params) => { return await request.download({ url: '/system/post/export', params }) } diff --git a/src/api/system/role/index.ts b/src/api/system/role/index.ts index 0d477555..902d5ca6 100644 --- a/src/api/system/role/index.ts +++ b/src/api/system/role/index.ts @@ -10,49 +10,49 @@ export interface RoleVO { createTime: Date } -export interface RolePageReqVO extends PageParam { - name?: string - code?: string - status?: number - createTime?: Date[] -} - export interface UpdateStatusReqVO { id: number status: number } // 查询角色列表 -export const getRolePageApi = async (params: RolePageReqVO) => { +export const getRolePage = async (params: PageParam) => { return await request.get({ url: '/system/role/page', params }) } // 查询角色(精简)列表 -export const listSimpleRolesApi = async () => { +export const getSimpleRoleList = async (): Promise<RoleVO[]> => { return await request.get({ url: '/system/role/list-all-simple' }) } // 查询角色详情 -export const getRoleApi = async (id: number) => { +export const getRole = async (id: number) => { return await request.get({ url: '/system/role/get?id=' + id }) } // 新增角色 -export const createRoleApi = async (data: RoleVO) => { +export const createRole = async (data: RoleVO) => { return await request.post({ url: '/system/role/create', data }) } // 修改角色 -export const updateRoleApi = async (data: RoleVO) => { +export const updateRole = async (data: RoleVO) => { return await request.put({ url: '/system/role/update', data }) } // 修改角色状态 -export const updateRoleStatusApi = async (data: UpdateStatusReqVO) => { +export const updateRoleStatus = async (data: UpdateStatusReqVO) => { return await request.put({ url: '/system/role/update-status', data }) } // 删除角色 -export const deleteRoleApi = async (id: number) => { +export const deleteRole = async (id: number) => { return await request.delete({ url: '/system/role/delete?id=' + id }) } +// 导出角色 +export const exportRole = (params) => { + return request.download({ + url: '/system/role/export-excel', + params + }) +} diff --git a/src/api/system/sensitiveWord/index.ts b/src/api/system/sensitiveWord/index.ts index ffda89c0..1116226f 100644 --- a/src/api/system/sensitiveWord/index.ts +++ b/src/api/system/sensitiveWord/index.ts @@ -1,4 +1,5 @@ import request from '@/config/axios' +import qs from 'qs' export interface SensitiveWordVO { id: number @@ -9,56 +10,49 @@ export interface SensitiveWordVO { createTime: Date } -export interface SensitiveWordPageReqVO extends PageParam { - name?: string - tag?: string - status?: number - createTime?: Date[] -} - -export interface SensitiveWordExportReqVO { - name?: string - tag?: string - status?: number - createTime?: Date[] +export interface SensitiveWordTestReqVO { + text: string + tag: string[] } // 查询敏感词列表 -export const getSensitiveWordPageApi = (params: SensitiveWordPageReqVO) => { +export const getSensitiveWordPage = (params: PageParam) => { return request.get({ url: '/system/sensitive-word/page', params }) } // 查询敏感词详情 -export const getSensitiveWordApi = (id: number) => { +export const getSensitiveWord = (id: number) => { return request.get({ url: '/system/sensitive-word/get?id=' + id }) } // 新增敏感词 -export const createSensitiveWordApi = (data: SensitiveWordVO) => { +export const createSensitiveWord = (data: SensitiveWordVO) => { return request.post({ url: '/system/sensitive-word/create', data }) } // 修改敏感词 -export const updateSensitiveWordApi = (data: SensitiveWordVO) => { +export const updateSensitiveWord = (data: SensitiveWordVO) => { return request.put({ url: '/system/sensitive-word/update', data }) } // 删除敏感词 -export const deleteSensitiveWordApi = (id: number) => { +export const deleteSensitiveWord = (id: number) => { return request.delete({ url: '/system/sensitive-word/delete?id=' + id }) } // 导出敏感词 -export const exportSensitiveWordApi = (params: SensitiveWordExportReqVO) => { +export const exportSensitiveWord = (params) => { return request.download({ url: '/system/sensitive-word/export-excel', params }) } // 获取所有敏感词的标签数组 -export const getSensitiveWordTagsApi = () => { +export const getSensitiveWordTagList = () => { return request.get({ url: '/system/sensitive-word/get-tags' }) } // 获得文本所包含的不合法的敏感词数组 -export const validateTextApi = (id: number) => { - return request.get({ url: '/system/sensitive-word/validate-text?' + id }) +export const validateText = (query: SensitiveWordTestReqVO) => { + return request.get({ + url: '/system/sensitive-word/validate-text?' + qs.stringify(query, { arrayFormat: 'repeat' }) + }) } diff --git a/src/api/system/sms/smsChannel/index.ts b/src/api/system/sms/smsChannel/index.ts index 176d075f..f335628f 100644 --- a/src/api/system/sms/smsChannel/index.ts +++ b/src/api/system/sms/smsChannel/index.ts @@ -12,39 +12,32 @@ export interface SmsChannelVO { createTime: Date } -export interface SmsChannelPageReqVO extends PageParam { - signature?: string - code?: string - status?: number - createTime?: Date[] -} - // 查询短信渠道列表 -export const getSmsChannelPageApi = (params: SmsChannelPageReqVO) => { +export const getSmsChannelPage = (params: PageParam) => { return request.get({ url: '/system/sms-channel/page', params }) } // 获得短信渠道精简列表 -export function getSimpleSmsChannels() { +export function getSimpleSmsChannelList() { return request.get({ url: '/system/sms-channel/list-all-simple' }) } // 查询短信渠道详情 -export const getSmsChannelApi = (id: number) => { +export const getSmsChannel = (id: number) => { return request.get({ url: '/system/sms-channel/get?id=' + id }) } // 新增短信渠道 -export const createSmsChannelApi = (data: SmsChannelVO) => { +export const createSmsChannel = (data: SmsChannelVO) => { return request.post({ url: '/system/sms-channel/create', data }) } // 修改短信渠道 -export const updateSmsChannelApi = (data: SmsChannelVO) => { +export const updateSmsChannel = (data: SmsChannelVO) => { return request.put({ url: '/system/sms-channel/update', data }) } // 删除短信渠道 -export const deleteSmsChannelApi = (id: number) => { +export const deleteSmsChannel = (id: number) => { return request.delete({ url: '/system/sms-channel/delete?id=' + id }) } diff --git a/src/api/system/sms/smsLog/index.ts b/src/api/system/sms/smsLog/index.ts index 863eabb6..3d54fac1 100644 --- a/src/api/system/sms/smsLog/index.ts +++ b/src/api/system/sms/smsLog/index.ts @@ -1,57 +1,39 @@ import request from '@/config/axios' export interface SmsLogVO { - id: number - channelId: number + id: number | null + channelId: number | null channelCode: string - templateId: number + templateId: number | null templateCode: string - templateType: number + templateType: number | null templateContent: string - templateParams: Map<string, object> + templateParams: Map<string, object> | null + apiTemplateId: string mobile: string - userId: number - userType: number - sendStatus: number - sendTime: Date - sendCode: number + userId: number | null + userType: number | null + sendStatus: number | null + sendTime: Date | null + sendCode: number | null sendMsg: string apiSendCode: string apiSendMsg: string apiRequestId: string apiSerialNo: string - receiveStatus: number - receiveTime: Date + receiveStatus: number | null + receiveTime: Date | null apiReceiveCode: string apiReceiveMsg: string - createTime: Date -} - -export interface SmsLogPageReqVO extends PageParam { - channelId?: number - templateId?: number - mobile?: string - sendStatus?: number - sendTime?: Date[] - receiveStatus?: number - receiveTime?: Date[] -} -export interface SmsLogExportReqVO { - channelId?: number - templateId?: number - mobile?: string - sendStatus?: number - sendTime?: Date[] - receiveStatus?: number - receiveTime?: Date[] + createTime: Date | null } // 查询短信日志列表 -export const getSmsLogPageApi = (params: SmsLogPageReqVO) => { +export const getSmsLogPage = (params: PageParam) => { return request.get({ url: '/system/sms-log/page', params }) } // 导出短信日志 -export const exportSmsLogApi = (params: SmsLogExportReqVO) => { +export const exportSmsLog = (params) => { return request.download({ url: '/system/sms-log/export-excel', params }) } diff --git a/src/api/system/sms/smsTemplate/index.ts b/src/api/system/sms/smsTemplate/index.ts index 0433fe3a..35cb489d 100644 --- a/src/api/system/sms/smsTemplate/index.ts +++ b/src/api/system/sms/smsTemplate/index.ts @@ -1,18 +1,18 @@ import request from '@/config/axios' export interface SmsTemplateVO { - id: number - type: number + id: number | null + type: number | null status: number code: string name: string content: string remark: string apiTemplateId: string - channelId: number - channelCode: string - params: string[] - createTime: Date + channelId: number | null + channelCode?: string + params?: string[] + createTime?: Date } export interface SendSmsReqVO { @@ -21,60 +21,40 @@ export interface SendSmsReqVO { templateParams: Map<String, Object> } -export interface SmsTemplatePageReqVO { - type?: number - status?: number - code?: string - content?: string - apiTemplateId?: string - channelId?: number - createTime?: Date[] -} - -export interface SmsTemplateExportReqVO { - type?: number - status?: number - code?: string - content?: string - apiTemplateId?: string - channelId?: number - createTime?: Date[] -} - // 查询短信模板列表 -export const getSmsTemplatePageApi = (params: SmsTemplatePageReqVO) => { +export const getSmsTemplatePage = (params: PageParam) => { return request.get({ url: '/system/sms-template/page', params }) } // 查询短信模板详情 -export const getSmsTemplateApi = (id: number) => { +export const getSmsTemplate = (id: number) => { return request.get({ url: '/system/sms-template/get?id=' + id }) } // 新增短信模板 -export const createSmsTemplateApi = (data: SmsTemplateVO) => { +export const createSmsTemplate = (data: SmsTemplateVO) => { return request.post({ url: '/system/sms-template/create', data }) } // 修改短信模板 -export const updateSmsTemplateApi = (data: SmsTemplateVO) => { +export const updateSmsTemplate = (data: SmsTemplateVO) => { return request.put({ url: '/system/sms-template/update', data }) } // 删除短信模板 -export const deleteSmsTemplateApi = (id: number) => { +export const deleteSmsTemplate = (id: number) => { return request.delete({ url: '/system/sms-template/delete?id=' + id }) } -// 发送短信 -export const sendSmsApi = (data: SendSmsReqVO) => { - return request.post({ url: '/system/sms-template/send-sms', data }) -} - // 导出短信模板 -export const exportPostApi = (params: SmsTemplateExportReqVO) => { +export const exportSmsTemplate = (params) => { return request.download({ url: '/system/sms-template/export-excel', params }) } + +// 发送短信 +export const sendSms = (data: SendSmsReqVO) => { + return request.post({ url: '/system/sms-template/send-sms', data }) +} diff --git a/src/api/system/tenant/index.ts b/src/api/system/tenant/index.ts index d79fb7b2..176c3757 100644 --- a/src/api/system/tenant/index.ts +++ b/src/api/system/tenant/index.ts @@ -32,31 +32,31 @@ export interface TenantExportReqVO { } // 查询租户列表 -export const getTenantPageApi = (params: TenantPageReqVO) => { +export const getTenantPage = (params: TenantPageReqVO) => { return request.get({ url: '/system/tenant/page', params }) } // 查询租户详情 -export const getTenantApi = (id: number) => { +export const getTenant = (id: number) => { return request.get({ url: '/system/tenant/get?id=' + id }) } // 新增租户 -export const createTenantApi = (data: TenantVO) => { +export const createTenant = (data: TenantVO) => { return request.post({ url: '/system/tenant/create', data }) } // 修改租户 -export const updateTenantApi = (data: TenantVO) => { +export const updateTenant = (data: TenantVO) => { return request.put({ url: '/system/tenant/update', data }) } // 删除租户 -export const deleteTenantApi = (id: number) => { +export const deleteTenant = (id: number) => { return request.delete({ url: '/system/tenant/delete?id=' + id }) } // 导出租户 -export const exportTenantApi = (params: TenantExportReqVO) => { +export const exportTenant = (params: TenantExportReqVO) => { return request.download({ url: '/system/tenant/export-excel', params }) } diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts index e505921d..e488c0d7 100644 --- a/src/api/system/user/index.ts +++ b/src/api/system/user/index.ts @@ -43,12 +43,12 @@ export const getUserApi = (id: number) => { } // 新增用户 -export const createUserApi = (data: UserVO) => { +export const createUserApi = (data: UserVO | Recordable) => { return request.post({ url: '/system/user/create', data }) } // 修改用户 -export const updateUserApi = (data: UserVO) => { +export const updateUserApi = (data: UserVO | Recordable) => { return request.put({ url: '/system/user/update', data }) } @@ -86,6 +86,6 @@ export const updateUserStatusApi = (id: number, status: number) => { } // 获取用户精简信息列表 -export const getListSimpleUsersApi = () => { +export const getSimpleUserList = (): Promise<UserVO[]> => { return request.get({ url: '/system/user/list-all-simple' }) } diff --git a/src/components/Descriptions/src/Descriptions.vue b/src/components/Descriptions/src/Descriptions.vue index fca37000..f1e77ddf 100644 --- a/src/components/Descriptions/src/Descriptions.vue +++ b/src/components/Descriptions/src/Descriptions.vue @@ -76,7 +76,7 @@ const toggleClick = () => { v-if="title" :class="[ `${prefixCls}-header`, - 'h-50px flex justify-between items-center mb-10px border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]' + 'h-50px flex justify-between items-center border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]' ]" @click="toggleClick" > diff --git a/src/components/DictTag/src/DictTag.vue b/src/components/DictTag/src/DictTag.vue index ecbfedb4..e3ba78d2 100644 --- a/src/components/DictTag/src/DictTag.vue +++ b/src/components/DictTag/src/DictTag.vue @@ -34,7 +34,7 @@ export default defineComponent({ return null } // 解决自定义字典标签值为零时标签不渲染的问题 - if (props.value === undefined) { + if (props.value === undefined || props.value === null) { return null } getDictObj(props.type, props.value.toString()) diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue index 85b849fb..4d8e7dde 100644 --- a/src/components/Editor/src/Editor.vue +++ b/src/components/Editor/src/Editor.vue @@ -178,7 +178,7 @@ defineExpose({ </script> <template> - <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-3000"> + <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-99"> <!-- 工具栏 --> <Toolbar :editor="editorRef" diff --git a/src/components/XTable/src/XTable.vue b/src/components/XTable/src/XTable.vue index 55c7b129..fac292d4 100644 --- a/src/components/XTable/src/XTable.vue +++ b/src/components/XTable/src/XTable.vue @@ -128,7 +128,7 @@ const getColumnsConfig = (options: XTableProps) => { proxyForm = true options.formConfig = { enabled: true, - titleWidth: 180, + titleWidth: 110, titleAlign: 'right', items: allSchemas.searchSchema } diff --git a/src/layout/components/Breadcrumb/src/Breadcrumb.vue b/src/layout/components/Breadcrumb/src/Breadcrumb.vue index 19be7400..de036654 100644 --- a/src/layout/components/Breadcrumb/src/Breadcrumb.vue +++ b/src/layout/components/Breadcrumb/src/Breadcrumb.vue @@ -37,7 +37,7 @@ export default defineComponent({ }) const getBreadcrumb = () => { - const currentPath = currentRoute.value.path + const currentPath = currentRoute.value.matched.slice(-1)[0].path levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => { return node.path === currentPath @@ -47,7 +47,7 @@ export default defineComponent({ const renderBreadcrumb = () => { const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList)) return breadcrumbList.map((v) => { - const disabled = v.redirect === 'noredirect' + const disabled = !v.redirect || v.redirect === 'noredirect' const meta = v.meta as RouteMeta return ( <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}> diff --git a/src/layout/components/Message/src/Message.vue b/src/layout/components/Message/src/Message.vue index 94347812..4ac85860 100644 --- a/src/layout/components/Message/src/Message.vue +++ b/src/layout/components/Message/src/Message.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import dayjs from 'dayjs' +import { parseTime } from '@/utils/formatTime' import * as NotifyMessageApi from '@/api/system/notify/message' const { push } = useRouter() @@ -57,7 +57,7 @@ onMounted(() => { {{ item.templateNickname }}:{{ item.templateContent }} </span> <span class="message-date"> - {{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }} + {{ parseTime(item.createTime) }} </span> </div> </div> diff --git a/src/layout/components/UserInfo/src/UserInfo.vue b/src/layout/components/UserInfo/src/UserInfo.vue index 73e048ab..7ccadb57 100644 --- a/src/layout/components/UserInfo/src/UserInfo.vue +++ b/src/layout/components/UserInfo/src/UserInfo.vue @@ -66,9 +66,9 @@ const toDocument = () => { <Icon icon="ep:menu" /> <div @click="toDocument">{{ t('common.document') }}</div> </ElDropdownItem> - <ElDropdownItem divided> + <ElDropdownItem divided @click="loginOut"> <Icon icon="ep:switch-button" /> - <div @click="loginOut">{{ t('common.loginOut') }}</div> + <div>{{ t('common.loginOut') }}</div> </ElDropdownItem> </ElDropdownMenu> </template> diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index d5970267..58d5601b 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -225,26 +225,26 @@ const remainingRouter: AppRouteRecordRaw[] = [ children: [ { path: '/manager/form/edit', - component: () => import('@/views/bpm/form/formEditor.vue'), + component: () => import('@/views/bpm/form/editor/index.vue'), name: 'bpmFormEditor', meta: { noCache: true, hidden: true, canTo: true, title: '设计流程表单', - activeMenu: 'bpm/manager/form/formEditor' + activeMenu: '/bpm/manager/form' } }, { path: '/manager/model/edit', - component: () => import('@/views/bpm/model/modelEditor.vue'), + component: () => import('@/views/bpm/model/editor/index.vue'), name: 'modelEditor', meta: { noCache: true, hidden: true, canTo: true, title: '设计流程', - activeMenu: 'bpm/manager/model/design' + activeMenu: '/bpm/manager/model' } }, { @@ -256,7 +256,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ hidden: true, canTo: true, title: '流程定义', - activeMenu: 'bpm/definition/index' + activeMenu: '/bpm/manager/model' } }, { diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 15e57ff2..05c70dad 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -70,6 +70,23 @@ export const getDictObj = (dictType: string, value: any) => { }) } +/** + * 获得字典数据的文本展示 + * + * @param dictType 字典类型 + * @param value 字典数据的值 + */ +export const getDictLabel = (dictType: string, value: any) => { + const dictOptions: DictDataType[] = getDictOptions(dictType) + const dictLabel = ref('') + dictOptions.forEach((dict: DictDataType) => { + if (dict.value === value) { + dictLabel.value = dict.label + } + }) + return dictLabel.value +} + export enum DICT_TYPE { USER_TYPE = 'user_type', COMMON_STATUS = 'common_status', @@ -123,5 +140,9 @@ export enum DICT_TYPE { PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态 PAY_REFUND_ORDER_STATUS = 'pay_refund_order_status', // 退款订单状态 - PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type' // 退款订单类别 + PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type', // 退款订单类别 + + // ========== MP 模块 ========== + MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 + MP_MESSAGE_TYPE = 'mp_message_type' // 消息类型 } diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index 2582beee..008bc929 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -11,10 +11,65 @@ import dayjs from 'dayjs' * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" * @returns 返回拼接后的时间字符串 */ -export function formatDate(date: Date, format: string): string { +export function formatDate(date: Date, format?: string): string { + // 日期不存在,则返回空 + if (!date) { + return '' + } + // 日期存在,则进行格式化 + if (format === undefined) { + format = 'YYYY-MM-DD HH:mm:ss' + } return dayjs(date).format(format) } +// TODO 芋艿:稍后去掉 +// 日期格式化 +export function parseTime(time: any, pattern?: string) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if (typeof time === 'string' && /^[0-9]+$/.test(time)) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time + .replace(new RegExp(/-/gm), '/') + .replace('T', ' ') + .replace(new RegExp(/\.\d{3}/gm), '') + } + if (typeof time === 'number' && time.toString().length === 10) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + /** * 获取当前日期是第几周 * @param dateTime 当前传入的日期值 @@ -139,5 +194,5 @@ export const dateFormatter = (row, column, cellValue) => { if (!cellValue) { return } - return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss') + return formatDate(cellValue) } diff --git a/src/views/Profile/components/ProfileUser.vue b/src/views/Profile/components/ProfileUser.vue index 2f5a77b2..015440fc 100644 --- a/src/views/Profile/components/ProfileUser.vue +++ b/src/views/Profile/components/ProfileUser.vue @@ -34,13 +34,13 @@ </li> <li class="list-group-item"> <Icon icon="ep:calendar" class="mr-5px" />{{ t('profile.user.createTime') }} - <div class="pull-right">{{ dayjs(userInfo?.createTime).format('YYYY-MM-DD') }}</div> + <div class="pull-right">{{ parseTime(userInfo?.createTime) }}</div> </li> </ul> </div> </template> <script setup lang="ts"> -import dayjs from 'dayjs' +import { parseTime } from '@/utils/formatTime' import UserAvatar from './UserAvatar.vue' import { getUserProfileApi, ProfileVO } from '@/api/system/user/profile' diff --git a/src/views/bpm/definition/definition.data.ts b/src/views/bpm/definition/definition.data.ts deleted file mode 100644 index 14a0c319..00000000 --- a/src/views/bpm/definition/definition.data.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: null, - action: true, - columns: [ - { - title: '定义编号', - field: 'id', - table: { - width: 360 - } - }, - { - title: '定义名称', - field: 'name', - table: { - // width: 120, - slots: { - default: 'name_default' - } - } - }, - { - title: '定义分类', - field: 'category', - // dictType: DICT_TYPE.BPM_MODEL_CATEGORY, - // dictClass: 'number', - table: { - // width: 120, - slots: { - default: 'category_default' - } - } - }, - { - title: '表单信息', - field: 'formId', - table: { - // width: 200, - slots: { - default: 'formId_default' - } - } - }, - { - title: '流程版本', - field: 'version', - table: { - // width: 80, - slots: { - default: 'version_default' - } - } - }, - { - title: '激活状态', - field: 'suspensionState', - table: { - // width: 80, - slots: { - default: 'suspensionState_default' - } - } - }, - { - title: '部署时间', - field: 'deploymentTime', - isForm: false, - formatter: 'formatDate' - // table: { - // width: 180 - // } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/bpm/definition/index.vue b/src/views/bpm/definition/index.vue index f2ef640c..ce643ff6 100644 --- a/src/views/bpm/definition/index.vue +++ b/src/views/bpm/definition/index.vue @@ -1,92 +1,138 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 流程名称 --> - <template #name_default="{ row }"> - <XTextButton :title="row.name" @click="handleBpmnDetail(row.id)" /> - </template> - <!-- 流程分类 --> - <template #category_default="{ row }"> - <DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" /> - </template> - <!-- 表单信息 --> - <template #formId_default="{ row }"> - <XTextButton - v-if="row.formType === 10" - :title="row.formName" - @click="handleFormDetail(row)" - /> - <XTextButton v-else :title="row.formCustomCreatePath" @click="handleFormDetail(row)" /> - </template> - <!-- 流程版本 --> - <template #version_default="{ row }"> - <el-tag>v{{ row.version }}</el-tag> - </template> - <!-- 激活状态 --> - <template #suspensionState_default="{ row }"> - <el-tag type="success" v-if="row.suspensionState === 1">激活</el-tag> - <el-tag type="warning" v-if="row.suspensionState === 2">挂起</el-tag> - </template> - <!-- 操作 --> - <template #actionbtns_default="{ row }"> - <XTextButton - preIcon="ep:user" - title="分配规则" - v-hasPermi="['bpm:task-assign-rule:query']" - @click="handleAssignRule(row)" - /> - </template> - </XTable> - - <!-- 表单详情的弹窗 --> - <XModal v-model="formDetailVisible" width="800" title="表单详情" :show-footer="false"> - <form-create - :rule="formDetailPreview.rule" - :option="formDetailPreview.option" - v-if="formDetailVisible" + <el-table v-loading="loading" :data="list"> + <el-table-column label="定义编号" align="center" prop="id" width="400" /> + <el-table-column label="流程名称" align="center" prop="name" width="200"> + <template #default="scope"> + <el-button type="text" @click="handleBpmnDetail(scope.row)"> + <span>{{ scope.row.name }}</span> + </el-button> + </template> + </el-table-column> + <el-table-column label="定义分类" align="center" prop="category" width="100"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> + </template> + </el-table-column> + <el-table-column label="表单信息" align="center" prop="formType" width="200"> + <template #default="scope"> + <el-button + v-if="scope.row.formType === 10" + type="text" + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formName }}</span> + </el-button> + <el-button v-else type="text" @click="handleFormDetail(scope.row)"> + <span>{{ scope.row.formCustomCreatePath }}</span> + </el-button> + </template> + </el-table-column> + <el-table-column label="流程版本" align="center" prop="processDefinition.version" width="80"> + <template #default="scope"> + <el-tag v-if="scope.row">v{{ scope.row.version }}</el-tag> + <el-tag type="warning" v-else>未部署</el-tag> + </template> + </el-table-column> + <el-table-column label="状态" align="center" prop="version" width="80"> + <template #default="scope"> + <el-tag type="success" v-if="scope.row.suspensionState === 1">激活</el-tag> + <el-tag type="warning" v-if="scope.row.suspensionState === 2">挂起</el-tag> + </template> + </el-table-column> + <el-table-column + label="部署时间" + align="center" + prop="deploymentTime" + width="180" + :formatter="dateFormatter" /> - </XModal> - <!-- 流程模型图的预览 --> - <XModal title="流程图" v-model="showBpmnOpen" width="80%" height="90%"> - <my-process-viewer - key="designer" - v-model="bpmnXML" - :value="bpmnXML" - v-bind="bpmnControlForm" - :prefix="bpmnControlForm.prefix" + <el-table-column + label="定义描述" + align="center" + prop="description" + width="300" + show-overflow-tooltip /> - </XModal> + <el-table-column label="操作" align="center" width="150" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="handleAssignRule(scope.row)" + v-hasPermi="['bpm:task-assign-rule:query']" + > + 分配规则 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> </ContentWrap> -</template> -<script setup lang="ts"> -// 业务相关的 import -import * as DefinitionApi from '@/api/bpm/definition' -// import * as ModelApi from '@/api/bpm/model' -import { allSchemas } from './definition.data' -import { setConfAndFields2 } from '@/utils/formCreate' -import { DICT_TYPE } from '@/utils/dict' -const bpmnXML = ref(null) -const showBpmnOpen = ref(false) -const bpmnControlForm = ref({ - prefix: 'flowable' -}) -// const message = useMessage() // 消息弹窗 -const router = useRouter() // 路由 + <!-- 弹窗:表单详情 --> + <Dialog title="表单详情" v-model="formDetailVisible" width="800"> + <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> + </Dialog> + + <!-- 弹窗:流程模型图的预览 --> + <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> + <my-process-viewer + key="designer" + v-model="bpmnXML" + :value="bpmnXML" + v-bind="bpmnControlForm" + :prefix="bpmnControlForm.prefix" + /> + </Dialog> +</template> + +<script setup lang="ts" name="Form"> +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import * as DefinitionApi from '@/api/bpm/definition' +import { setConfAndFields2 } from '@/utils/formCreate' +const { push } = useRouter() // 路由 const { query } = useRoute() // 查询参数 -// ========== 列表相关 ========== +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 const queryParams = reactive({ + pageNo: 1, + pageSize: 10, key: query.key }) -const [registerTable] = useXTable({ - allSchemas: allSchemas, - getListApi: DefinitionApi.getProcessDefinitionPageApi, - params: queryParams -}) -// 流程表单的详情按钮操作 +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await DefinitionApi.getProcessDefinitionPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 点击任务分配按钮 */ +const handleAssignRule = (row) => { + push({ + name: 'BpmTaskAssignRuleList', + query: { + modelId: row.id + } + }) +} + +/** 流程表单的详情按钮操作 */ const formDetailVisible = ref(false) const formDetailPreview = ref({ rule: [], @@ -99,32 +145,25 @@ const handleFormDetail = async (row) => { // 弹窗打开 formDetailVisible.value = true } else { - await router.push({ + await push({ path: row.formCustomCreatePath }) } } -// 流程图的详情按钮操作 -const handleBpmnDetail = (row) => { - // TODO 芋艿:流程组件开发中 - console.log(row) - DefinitionApi.getProcessDefinitionBpmnXMLApi(row).then((response) => { - console.log(response, 'response') - bpmnXML.value = response - // 弹窗打开 - showBpmnOpen.value = true - }) - // message.success('流程组件开发中,预计 2 月底完成') +/** 流程图的详情按钮操作 */ +const bpmnDetailVisible = ref(false) +const bpmnXML = ref(null) +const bpmnControlForm = ref({ + prefix: 'flowable' +}) +const handleBpmnDetail = async (row) => { + bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id) + bpmnDetailVisible.value = true } -// 点击任务分配按钮 -const handleAssignRule = (row) => { - router.push({ - name: 'BpmTaskAssignRuleList', - query: { - processDefinitionId: row.id - } - }) -} +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/bpm/form/editor/index.vue b/src/views/bpm/form/editor/index.vue new file mode 100644 index 00000000..cb7d023f --- /dev/null +++ b/src/views/bpm/form/editor/index.vue @@ -0,0 +1,106 @@ +<template> + <ContentWrap> + <!-- 表单设计器 --> + <fc-designer ref="designer" height="780px"> + <template #handle> + <el-button round size="small" type="primary" @click="handleSave"> + <Icon icon="ep:plus" class="mr-5px" /> 保存 + </el-button> + </template> + </fc-designer> + </ContentWrap> + + <!-- 表单保存的弹窗 --> + <Dialog title="保存表单" v-model="modelVisible" width="600"> + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px"> + <el-form-item label="表单名" prop="name"> + <el-input v-model="formData.name" placeholder="请输入表单名" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { CommonStatusEnum } from '@/utils/constants' +import * as FormApi from '@/api/bpm/form' +import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate' + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息 +const { query } = useRoute() // 路由 + +const designer = ref() // 表单设计器 +const modelVisible = ref(false) // 弹窗是否展示 +const formLoading = ref(false) // 表单的加载中:提交的按钮禁用 +const formData = ref({ + name: '', + status: CommonStatusEnum.ENABLE, + remark: '' +}) +const formRules = reactive({ + name: [{ required: true, message: '表单名不能为空', trigger: 'blur' }], + status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 处理保存按钮 */ +const handleSave = () => { + modelVisible.value = true +} + +/** 提交表单 */ +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as FormApi.FormVO + data.conf = encodeConf(designer) // 表单配置 + data.fields = encodeFields(designer) // 表单字段 + if (!data.id) { + await FormApi.createForm(data) + message.success(t('common.createSuccess')) + } else { + await FormApi.updateForm(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + } finally { + formLoading.value = false + } +} + +/** 初始化 **/ +onMounted(async () => { + // 场景一:新增表单 + const id = query.id as unknown as number + if (!id) { + return + } + // 场景二:修改表单 + const data = await FormApi.getForm(id) + formData.value = data + setConfAndFields(designer, data.conf, data.fields) +}) +</script> diff --git a/src/views/bpm/form/form.data.ts b/src/views/bpm/form/form.data.ts deleted file mode 100644 index 43c93dd7..00000000 --- a/src/views/bpm/form/form.data.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -const { t } = useI18n() // 国际化 - -// 表单校验 -export const rules = reactive({ - name: [required] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '表单编号', - action: true, - columns: [ - { - title: '表单名', - field: 'name', - isSearch: true - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number' - }, - { - title: '备注', - field: 'remark' - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false, - table: { - width: 180 - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/bpm/form/formEditor.vue b/src/views/bpm/form/formEditor.vue deleted file mode 100644 index 1070739e..00000000 --- a/src/views/bpm/form/formEditor.vue +++ /dev/null @@ -1,157 +0,0 @@ -<template> - <ContentWrap> - <!-- 表单设计器 --> - <fc-designer ref="designer" height="780px"> - <template #handle> - <XButton type="primary" title="生成JSON" @click="showJson" /> - <XButton type="primary" title="生成Options" @click="showOption" /> - <XButton type="primary" :title="t('action.save')" @click="handleSave" /> - </template> - </fc-designer> - <Dialog :title="dialogTitle" v-model="dialogVisible1" maxHeight="600"> - <div ref="editor" v-if="dialogVisible1"> - <XTextButton style="float: right" :title="t('common.copy')" @click="copy(formValue)" /> - <el-scrollbar height="580"> - <pre> - {{ formValue }} - </pre> - </el-scrollbar> - </div> - </Dialog> - <!-- 表单保存的弹窗 --> - <XModal v-model="dialogVisible" title="保存表单"> - <el-form ref="formRef" :model="formValues" :rules="formRules" label-width="80px"> - <el-form-item label="表单名" prop="name"> - <el-input v-model="formValues.name" placeholder="请输入表单名" /> - </el-form-item> - <el-form-item label="开启状态" prop="status"> - <el-radio-group v-model="formValues.status"> - <el-radio - v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - <el-form-item label="备注" prop="remark"> - <el-input v-model="formValues.remark" type="textarea" placeholder="请输入备注" /> - </el-form-item> - </el-form> - <!-- 操作按钮 --> - <template #footer> - <!-- 按钮:保存 --> - <XButton - type="primary" - :title="t('action.save')" - :loading="dialogLoading" - @click="submitForm" - /> - <!-- 按钮:关闭 --> - <XButton :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> - </ContentWrap> -</template> -<script setup lang="ts" name="BpmFormEditor"> -import { FormInstance } from 'element-plus' -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { CommonStatusEnum } from '@/utils/constants' -import * as FormApi from '@/api/bpm/form' -import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate' -import { useClipboard } from '@vueuse/core' - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息 -const { query } = useRoute() // 路由 - -const designer = ref() // 表单设计器 -const type = ref(-1) -const formValue = ref('') -const dialogTitle = ref('') -const dialogVisible = ref(false) // 弹窗是否展示 -const dialogVisible1 = ref(false) // 弹窗是否展示 -const dialogLoading = ref(false) // 弹窗的加载中 -const formRef = ref<FormInstance>() -const formRules = reactive({ - name: [{ required: true, message: '表单名不能为空', trigger: 'blur' }], - status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }] -}) -const formValues = ref({ - name: '', - status: CommonStatusEnum.ENABLE, - remark: '' -}) - -// 处理保存按钮 -const handleSave = () => { - dialogVisible.value = true -} - -// 提交保存表单 -const submitForm = async () => { - // 参数校验 - const elForm = unref(formRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return - - // 提交请求 - dialogLoading.value = true - try { - const data = formValues.value as FormApi.FormVO - data.conf = encodeConf(designer) // 表单配置 - data.fields = encodeFields(designer) // 表单字段 - if (!data.id) { - await FormApi.createFormApi(data) - message.success(t('common.createSuccess')) - } else { - await FormApi.updateFormApi(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - dialogLoading.value = false - } -} -const showJson = () => { - openModel('生成JSON') - type.value = 0 - formValue.value = designer.value.getRule() -} -const showOption = () => { - openModel('生成Options') - type.value = 1 - formValue.value = designer.value.getOption() -} -const openModel = (title: string) => { - dialogVisible1.value = true - dialogTitle.value = title -} -/** 复制 **/ -const copy = async (text: string) => { - const { copy, copied, isSupported } = useClipboard({ source: text }) - if (!isSupported) { - message.error(t('common.copyError')) - } else { - await copy() - if (unref(copied)) { - message.success(t('common.copySuccess')) - } - } -} -// ========== 初始化 ========== -onMounted(() => { - // 场景一:新增表单 - const id = query.id as unknown as number - if (!id) { - return - } - // 场景二:修改表单 - FormApi.getFormApi(id).then((data) => { - formValues.value = data - setConfAndFields(designer, data.conf, data.fields) - }) -}) -</script> diff --git a/src/views/bpm/form/index.vue b/src/views/bpm/form/index.vue index b4b208a9..7e14b3e3 100644 --- a/src/views/bpm/form/index.vue +++ b/src/views/bpm/form/index.vue @@ -1,93 +1,171 @@ <template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 操作:新增 --> - <template #toolbar_buttons> - <XButton - type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:post:create']" - @click="handleCreate()" + <content-wrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="表单名" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入表单名" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['bpm:form:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['bpm:form:query']" - @click="handleDetail(row.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['bpm:form:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> - <!-- 表单详情的弹窗 --> - <XModal v-model="detailOpen" width="800" title="表单详情"> - <form-create :rule="detailPreview.rule" :option="detailPreview.option" v-if="detailOpen" /> - </XModal> - </ContentWrap> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button type="primary" @click="openForm()" v-hasPermi="['bpm:form:create']"> + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + </el-form-item> + </el-form> + </content-wrap> + + <!-- 列表 --> + <content-wrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="编号" align="center" prop="id" /> + <el-table-column label="表单名" align="center" prop="name" /> + <el-table-column label="状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column label="备注" align="center" prop="remark" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm(scope.row.id)" + v-hasPermi="['bpm:form:update']" + > + 编辑 + </el-button> + <el-button link @click="openDetail(scope.row.id)" v-hasPermi="['bpm:form:query']"> + 详情 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['bpm:form:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </content-wrap> + + <!-- 表单详情的弹窗 --> + <Dialog title="表单详情" v-model="detailVisible" width="800"> + <form-create :rule="detailData.rule" :option="detailData.option" /> + </Dialog> </template> -<script setup lang="ts" name="BpmForm"> -// 业务相关的 import +<script setup lang="ts" name="Form"> +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' import * as FormApi from '@/api/bpm/form' -import { allSchemas } from './form.data' -// 表单详情相关的变量和 import import { setConfAndFields2 } from '@/utils/formCreate' +const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 const { push } = useRouter() // 路由 -// 列表相关的变量 -const [registerTable, { deleteData }] = useXTable({ - allSchemas: allSchemas, - getListApi: FormApi.getFormPageApi, - deleteApi: FormApi.deleteFormApi +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: null }) +const queryFormRef = ref() // 搜索的表单 -// 新增操作 -const handleCreate = () => { - push({ - name: 'bpmFormEditor' - }) +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await FormApi.getFormPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } } -// 修改操作 -const handleUpdate = async (rowId: number) => { - await push({ +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const openForm = (id?: number) => { + push({ name: 'bpmFormEditor', query: { - id: rowId + id } }) } -// 详情操作 -const detailOpen = ref(false) -const detailPreview = ref({ +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await FormApi.deleteForm(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 详情操作 */ +const detailVisible = ref(false) +const detailData = ref({ rule: [], option: {} }) -const handleDetail = async (rowId: number) => { +const openDetail = async (rowId: number) => { // 设置表单 - const data = await FormApi.getFormApi(rowId) - setConfAndFields2(detailPreview, data.conf, data.fields) + const data = await FormApi.getForm(rowId) + setConfAndFields2(detailData, data.conf, data.fields) // 弹窗打开 - detailOpen.value = true + detailVisible.value = true } + +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/bpm/group/UserGroupForm.vue b/src/views/bpm/group/UserGroupForm.vue new file mode 100644 index 00000000..9496ad84 --- /dev/null +++ b/src/views/bpm/group/UserGroupForm.vue @@ -0,0 +1,130 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="组名" prop="name"> + <el-input v-model="formData.name" placeholder="请输入组名" /> + </el-form-item> + <el-form-item label="描述"> + <el-input type="textarea" v-model="formData.name" placeholder="请输入描述" /> + </el-form-item> + <el-form-item label="成员" prop="memberUserIds"> + <el-select v-model="formData.memberUserIds" multiple placeholder="请选择成员"> + <el-option + v-for="user in userList" + :key="user.id" + :label="user.nickname" + :value="user.id" + /> + </el-select> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { CommonStatusEnum } from '@/utils/constants' +import * as UserGroupApi from '@/api/bpm/userGroup' +import * as UserApi from '@/api/system/user' + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: undefined, + description: undefined, + memberUserIds: undefined, + status: CommonStatusEnum.ENABLE +}) +const formRules = reactive({ + name: [{ required: true, message: '组名不能为空', trigger: 'blur' }], + description: [{ required: true, message: '描述不能为空', trigger: 'blur' }], + memberUserIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }], + status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref +const userList = ref([]) // 用户列表 + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await UserGroupApi.getUserGroup(id) + } finally { + formLoading.value = false + } + } + // 加载用户列表 + userList.value = await UserApi.getSimpleUserList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as UserGroupApi.UserGroupVO + if (formType.value === 'create') { + await UserGroupApi.createUserGroup(data) + message.success(t('common.createSuccess')) + } else { + await UserGroupApi.updateUserGroup(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: undefined, + description: undefined, + memberUserIds: undefined, + status: CommonStatusEnum.ENABLE + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/group/group.data.ts b/src/views/bpm/group/group.data.ts deleted file mode 100644 index 613a7290..00000000 --- a/src/views/bpm/group/group.data.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -const { t } = useI18n() // 国际化 - -// 表单校验 -export const rules = reactive({ - name: [required], - description: [required], - memberUserIds: [required], - status: [required] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '编号', - action: true, - searchSpan: 8, - columns: [ - { - title: '组名', - field: 'name', - isSearch: true - }, - { - title: '成员', - field: 'memberUserIds', - table: { - slots: { - default: 'memberUserIds_default' - } - } - }, - { - title: '描述', - field: 'description' - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false, - isSearch: true, - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - }, - table: { - width: 180 - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/bpm/group/index.vue b/src/views/bpm/group/index.vue index b0f08516..5829ebad 100644 --- a/src/views/bpm/group/index.vue +++ b/src/views/bpm/group/index.vue @@ -1,182 +1,184 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="组名" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入组名" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="yyyy-MM-dd HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" + @click="openForm('create')" v-hasPermi="['bpm:user-group:create']" - @click="handleCreate()" - /> - </template> - <template #memberUserIds_default="{ row }"> - <span v-for="userId in row.memberUserIds" :key="userId"> - {{ getUserNickname(userId) }} - </span> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['bpm:user-group:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['bpm:user-group:query']" - @click="handleDetail(row.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['bpm:user-group:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + </el-form-item> + </el-form> </ContentWrap> - <XModal v-model="dialogVisible" :title="dialogTitle" :mask-closable="false"> - <!-- 对话框(添加 / 修改) --> - <Form - v-if="['create', 'update'].includes(actionType)" - :schema="allSchemas.formSchema" - :rules="rules" - ref="formRef" - > - <template #memberUserIds="form"> - <el-select v-model="form.memberUserIds" multiple> - <el-option v-for="item in users" :key="item.id" :label="item.nickname" :value="item.id" /> - </el-select> - </template> - </Form> - <!-- 对话框(详情) --> - <Descriptions - v-if="actionType === 'detail'" - :schema="allSchemas.detailSchema" - :data="detailData" - > - <template #memberUserIds="{ row }"> - <span v-for="userId in row.memberUserIds" :key="userId"> - {{ getUserNickname(userId) }} - </span> - </template> - </Descriptions> - <!-- 操作按钮 --> - <template #footer> - <!-- 按钮:保存 --> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitForm" + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="编号" align="center" prop="id" /> + <el-table-column label="组名" align="center" prop="name" /> + <el-table-column label="描述" align="center" prop="description" /> + <el-table-column label="成员" align="center"> + <template #default="scope"> + <span v-for="userId in scope.row.memberUserIds" :key="userId" class="pr-5px"> + {{ userList.find((user) => user.id === userId)?.nickname }} + </span> + </template> + </el-table-column> + <el-table-column label="状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" /> - <!-- 按钮:关闭 --> - <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['bpm:user-group:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['bpm:user-group:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <UserGroupForm ref="formRef" @success="getList" /> </template> -<script setup lang="ts"> -// 业务相关的 import +<script setup lang="ts" name="UserGroup"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' import * as UserGroupApi from '@/api/bpm/userGroup' -import { getListSimpleUsersApi, UserVO } from '@/api/system/user' -import { allSchemas, rules } from './group.data' -import { FormExpose } from '@/components/Form' - -const { t } = useI18n() // 国际化 +import * as UserApi from '@/api/system/user' +import UserGroupForm from './UserGroupForm.vue' const message = useMessage() // 消息弹窗 -// 列表相关的变量 -const [registerTable, { reload, deleteData }] = useXTable({ - allSchemas: allSchemas, - getListApi: UserGroupApi.getUserGroupPageApi, - deleteApi: UserGroupApi.deleteUserGroupApi +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: null, + status: null, + createTime: [] }) -// 用户列表 -const users = ref<UserVO[]>([]) +const queryFormRef = ref() // 搜索的表单 +const userList = ref([]) // 用户列表 -const getUserNickname = (userId) => { - for (const user of users.value) { - if (user.id === userId) { - return user.nickname - } +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await UserGroupApi.getUserGroupPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false } - return '未知(' + userId + ')' } -// ========== CRUD 相关 ========== -const actionLoading = ref(false) // 遮罩层 -const actionType = ref('') // 操作按钮的类型 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('edit') // 弹出层标题 -const formRef = ref<FormExpose>() // 表单 Ref -const detailData = ref() // 详情 Ref - -// 设置标题 -const setDialogTile = (type: string) => { - dialogTitle.value = t('action.' + type) - actionType.value = type - dialogVisible.value = true +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() } -// 新增操作 -const handleCreate = () => { - setDialogTile('create') +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() } -// 修改操作 -const handleUpdate = async (rowId: number) => { - setDialogTile('update') - // 设置数据 - const res = await UserGroupApi.getUserGroupApi(rowId) - unref(formRef)?.setValues(res) +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) } -// 详情操作 -const handleDetail = async (rowId: number) => { - setDialogTile('detail') - detailData.value = await UserGroupApi.getUserGroupApi(rowId) +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await UserGroupApi.deleteUserGroup(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} } -// 提交按钮 -const submitForm = async () => { - const elForm = unref(formRef)?.getElFormRef() - if (!elForm) return - elForm.validate(async (valid) => { - if (valid) { - actionLoading.value = true - // 提交请求 - try { - const data = unref(formRef)?.formModel as UserGroupApi.UserGroupVO - if (actionType.value === 'create') { - await UserGroupApi.createUserGroupApi(data) - message.success(t('common.createSuccess')) - } else { - await UserGroupApi.updateUserGroupApi(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - actionLoading.value = false - // 刷新列表 - await reload() - } - } - }) -} - -// ========== 初始化 ========== -onMounted(() => { - getListSimpleUsersApi().then((data) => { - users.value = data - }) +/** 初始化 **/ +onMounted(async () => { + await getList() + // 加载用户列表 + userList.value = await UserApi.getSimpleUserList() }) </script> diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue new file mode 100644 index 00000000..ac536958 --- /dev/null +++ b/src/views/bpm/model/ModelForm.vue @@ -0,0 +1,227 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible" width="600"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="110px" + v-loading="formLoading" + > + <el-form-item label="流程标识" prop="key"> + <el-input + v-model="formData.key" + placeholder="请输入流标标识" + style="width: 330px" + :disabled="!!formData.id" + /> + <el-tooltip + v-if="!formData.id" + class="item" + effect="light" + content="新建后,流程标识不可修改!" + placement="top" + > + <i style="padding-left: 5px" class="el-icon-question"></i> + </el-tooltip> + <el-tooltip v-else class="item" effect="light" content="流程标识不可修改!" placement="top"> + <i style="padding-left: 5px" class="el-icon-question"></i> + </el-tooltip> + </el-form-item> + <el-form-item label="流程名称" prop="name"> + <el-input + v-model="formData.name" + placeholder="请输入流程名称" + :disabled="!!formData.id" + clearable + /> + </el-form-item> + <el-form-item v-if="formData.id" label="流程分类" prop="category"> + <el-select + v-model="formData.category" + placeholder="请选择流程分类" + clearable + style="width: 100%" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="流程描述" prop="description"> + <el-input type="textarea" v-model="formData.description" clearable /> + </el-form-item> + <div v-if="formData.id"> + <el-form-item label="表单类型" prop="formType"> + <el-radio-group v-model="formData.formType"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId"> + <el-select v-model="formData.formId" clearable style="width: 100%"> + <el-option + v-for="form in formList" + :key="form.id" + :label="form.name" + :value="form.id" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="formData.formType === 20" + label="表单提交路由" + prop="formCustomCreatePath" + > + <el-input + v-model="formData.formCustomCreatePath" + placeholder="请输入表单提交路由" + style="width: 330px" + /> + <el-tooltip + class="item" + effect="light" + content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create" + placement="top" + > + <i style="padding-left: 5px" class="el-icon-question"></i> + </el-tooltip> + </el-form-item> + <el-form-item + v-if="formData.formType === 20" + label="表单查看路由" + prop="formCustomViewPath" + > + <el-input + v-model="formData.formCustomViewPath" + placeholder="请输入表单查看路由" + style="width: 330px" + /> + <el-tooltip + class="item" + effect="light" + content="自定义表单的查看路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/view" + placement="top" + > + <i style="padding-left: 5px" class="el-icon-question"></i> + </el-tooltip> + </el-form-item> + </div> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { ElMessageBox } from 'element-plus' +import * as ModelApi from '@/api/bpm/model' +import * as FormApi from '@/api/bpm/form' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + formType: 10, + name: '', + category: undefined, + description: '', + formId: '', + formCustomCreatePath: '', + formCustomViewPath: '' +}) +const formRules = reactive({ + category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }], + name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }], + key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }], + value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }], + visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref +const formList = ref([]) // 流程表单的下拉框的数据 + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await ModelApi.getModel(id) + } finally { + formLoading.value = false + } + } + // 获得流程表单的下拉框的数据 + formList.value = await FormApi.getSimpleFormList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as ModelApi.ModelVO + if (formType.value === 'create') { + await ModelApi.createModel(data) + // 提示,引导用户做后续的操作 + await ElMessageBox.alert( + '<strong>新建模型成功!</strong>后续需要执行如下 4 个步骤:' + + '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' + + '<div>2. 点击【设计流程】按钮,绘制流程图</div>' + + '<div>3. 点击【分配规则】按钮,设置每个用户任务的审批人</div>' + + '<div>4. 点击【发布流程】按钮,完成流程的最终发布</div>' + + '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', + '重要提示', + { + dangerouslyUseHTMLString: true, + type: 'success' + } + ) + } else { + await ModelApi.updateModel(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + formType: 10, + name: '', + category: undefined, + description: '', + formId: '', + formCustomCreatePath: '', + formCustomViewPath: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/model/ModelImportForm.vue b/src/views/bpm/model/ModelImportForm.vue new file mode 100644 index 00000000..ac26ac08 --- /dev/null +++ b/src/views/bpm/model/ModelImportForm.vue @@ -0,0 +1,137 @@ +<template> + <Dialog title="导入流程" v-model="modelVisible" width="400"> + <div> + <el-upload + ref="uploadRef" + :action="importUrl" + :headers="uploadHeaders" + :data="formData" + name="bpmnFile" + v-model:file-list="fileList" + :drag="true" + :auto-upload="false" + accept=".bpmn, .xml" + :limit="1" + :on-exceed="handleExceed" + :on-success="submitFormSuccess" + :on-error="submitFormError" + :disabled="formLoading" + > + <Icon class="el-icon--upload" icon="ep:upload-filled" /> + <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> + <template #tip> + <div class="el-upload__tip" style="color: red"> + 提示:仅允许导入“bpm”或“xml”格式文件! + </div> + <div> + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px"> + <el-form-item label="流程标识" prop="key"> + <el-input + v-model="formData.key" + placeholder="请输入流标标识" + style="width: 250px" + /> + </el-form-item> + <el-form-item label="流程名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入流程名称" clearable /> + </el-form-item> + <el-form-item label="流程描述" prop="description"> + <el-input type="textarea" v-model="formData.description" clearable /> + </el-form-item> + </el-form> + </div> + </template> + </el-upload> + </div> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { getAccessToken, getTenantId } from '@/utils/auth' +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中 +const formData = ref({ + key: '', + name: '', + description: '' +}) +const formRules = reactive({ + key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], + name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref +const uploadRef = ref() // 上传 Ref +const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import' +const uploadHeaders = ref() // 上传 Header 头 +const fileList = ref([]) // 文件列表 + +/** 打开弹窗 */ +const open = async () => { + modelVisible.value = true + resetForm() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 重置表单 */ +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + if (fileList.value.length == 0) { + message.error('请上传文件') + return + } + // 提交请求 + uploadHeaders.value = { + Authorization: 'Bearer ' + getAccessToken(), + 'tenant-id': getTenantId() + } + formLoading.value = true + uploadRef.value!.submit() +} + +/** 文件上传成功 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitFormSuccess = async (response: any): Promise<void> => { + if (response.code !== 0) { + message.error(response.msg) + formLoading.value = false + return + } + // 提示成功 + message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】') + // 发送操作成功的事件 + emit('success') +} + +/** 上传错误提示 */ +const submitFormError = (): void => { + message.error('导入流程失败,请您重新上传!') + formLoading.value = false +} + +/** 重置表单 */ +const resetForm = () => { + // 重置上传状态和文件 + formLoading.value = false + uploadRef.value?.clearFiles() + // 重置表单 + formData.value = { + key: '', + name: '', + description: '' + } + formRef.value?.resetFields() +} + +/** 文件数超出提示 */ +const handleExceed = (): void => { + message.error('最多只能上传一个文件!') +} +</script> diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue new file mode 100644 index 00000000..7e3d8413 --- /dev/null +++ b/src/views/bpm/model/editor/index.vue @@ -0,0 +1,102 @@ +<template> + <ContentWrap> + <!-- 流程设计器,负责绘制流程等 --> + <my-process-designer + key="designer" + v-if="xmlString !== undefined" + v-model="xmlString" + :value="xmlString" + v-bind="controlForm" + keyboard + ref="processDesigner" + @init-finished="initModeler" + :additionalModel="controlForm.additionalModel" + @save="save" + /> + <!-- 流程属性器,负责编辑每个流程节点的属性 --> + <my-properties-panel + key="penal" + :bpmnModeler="modeler" + :prefix="controlForm.prefix" + class="process-panel" + :model="model" + /> + </ContentWrap> +</template> + +<script setup lang="ts"> +// 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务) +import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad' +// 自定义左侧菜单(修改 默认任务 为 用户任务) +import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette' +import * as ModelApi from '@/api/bpm/model' + +const router = useRouter() // 路由 +const { query } = useRoute() // 路由的查询 +const message = useMessage() // 国际化 + +const xmlString = ref(undefined) // BPMN XML +const modeler = ref(null) // BPMN Modeler +const controlForm = ref({ + simulation: true, + labelEditing: false, + labelVisible: false, + prefix: 'flowable', + headerButtonSize: 'mini', + additionalModel: [CustomContentPadProvider, CustomPaletteProvider] +}) +const model = ref<ModelApi.ModelVO>() // 流程模型的信息 + +/** 初始化 modeler */ +const initModeler = (item) => { + setTimeout(() => { + modeler.value = item + }, 10) +} + +/** 添加/修改模型 */ +const save = async (bpmnXml) => { + const data = { + ...model.value, + bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 + } as unknown as ModelApi.ModelVO + // 提交 + if (data.id) { + await ModelApi.updateModel(data) + message.success('修改成功') + } else { + await ModelApi.createModel(data) + message.success('新增成功') + } + // 跳转回去 + close() +} + +/** 关闭按钮 */ +const close = () => { + router.push({ path: '/bpm/manager/model' }) +} + +/** 初始化 */ +onMounted(async () => { + const modelId = query.modelId as unknown as number + if (!modelId) { + message.error('缺少模型 modelId 编号') + return + } + // 查询模型 + const data = await ModelApi.getModel(modelId) + xmlString.value = data.bpmnXml + model.value = { + ...data, + bpmnXml: undefined // 清空 bpmnXml 属性 + } +}) +</script> +<style lang="scss"> +.process-panel__container { + position: absolute; + right: 60px; + top: 90px; +} +</style> diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue index 01a97d3f..b19ed956 100644 --- a/src/views/bpm/model/index.vue +++ b/src/views/bpm/model/index.vue @@ -1,353 +1,324 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton - type="primary" - preIcon="ep:zoom-in" - title="新建流程" - v-hasPermi="['bpm:model:create']" - @click="handleCreate" + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="流程标识" prop="key"> + <el-input + v-model="queryParams.key" + placeholder="请输入流程标识" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - <!-- 操作:导入 --> - <XButton - type="warning" - preIcon="ep:upload" - :title="'导入流程'" - @click="handleImport" - style="margin-left: 10px" + </el-form-item> + <el-form-item label="流程名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入流程名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - </template> - <!-- 流程名称 --> - <template #name_default="{ row }"> - <XTextButton :title="row.name" @click="handleBpmnDetail(row.id)" /> - </template> - <!-- 流程分类 --> - <template #category_default="{ row }"> - <DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" /> - </template> - <!-- 表单信息 --> - <template #formId_default="{ row }"> - <XTextButton - v-if="row.formType === 10" - :title="forms.find((form) => form.id === row.formId)?.name || row.formId" - @click="handleFormDetail(row)" - /> - <XTextButton v-else :title="row.formCustomCreatePath" @click="handleFormDetail(row)" /> - </template> - <!-- 流程版本 --> - <template #version_default="{ row }"> - <el-tag v-if="row.processDefinition">v{{ row.processDefinition.version }}</el-tag> - <el-tag type="warning" v-else>未部署</el-tag> - </template> - <!-- 激活状态 --> - <template #status_default="{ row }"> - <el-switch - v-if="row.processDefinition" - v-model="row.processDefinition.suspensionState" - :active-value="1" - :inactive-value="2" - @change="handleChangeState(row)" - /> - </template> - <!-- 操作 --> - <template #actionbtns_default="{ row }"> - <XTextButton - preIcon="ep:edit" - title="修改流程" - v-hasPermi="['bpm:model:update']" - @click="handleUpdate(row.id)" - /> - <XTextButton - preIcon="ep:setting" - title="设计流程" - v-hasPermi="['bpm:model:update']" - @click="handleDesign(row)" - /> - <XTextButton - preIcon="ep:user" - title="分配规则" - v-hasPermi="['bpm:task-assign-rule:query']" - @click="handleAssignRule(row)" - /> - <XTextButton - preIcon="ep:position" - title="发布流程" - v-hasPermi="['bpm:model:deploy']" - @click="handleDeploy(row)" - /> - <XTextButton - preIcon="ep:aim" - title="流程定义" - v-hasPermi="['bpm:process-definition:query']" - @click="handleDefinitionList(row)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['bpm:model:delete']" - @click="handleDelete(row.id)" - /> - </template> - </XTable> - - <!-- 对话框(添加 / 修改流程) --> - <XModal v-model="dialogVisible" :title="dialogTitle" width="600"> - <el-form - :loading="dialogLoading" - el-form - ref="saveFormRef" - :model="saveForm" - :rules="rules" - label-width="110px" - > - <el-form-item label="流程标识" prop="key"> - <el-input - v-model="saveForm.key" - placeholder="请输入流标标识" - style="width: 330px" - :disabled="!!saveForm.id" - /> - <el-tooltip - v-if="!saveForm.id" - class="item" - effect="light" - content="新建后,流程标识不可修改!" - placement="top" - > - <i style="padding-left: 5px" class="el-icon-question"></i> - </el-tooltip> - <el-tooltip - v-else - class="item" - effect="light" - content="流程标识不可修改!" - placement="top" - > - <i style="padding-left: 5px" class="el-icon-question"></i> - </el-tooltip> - </el-form-item> - <el-form-item label="流程名称" prop="name"> - <el-input - v-model="saveForm.name" - placeholder="请输入流程名称" - :disabled="!!saveForm.id" - clearable - /> - </el-form-item> - <el-form-item v-if="saveForm.id" label="流程分类" prop="category"> - <el-select - v-model="saveForm.category" - placeholder="请选择流程分类" - clearable - style="width: 100%" - > - <el-option - v-for="dict in getDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="流程描述" prop="description"> - <el-input type="textarea" v-model="saveForm.description" clearable /> - </el-form-item> - <div v-if="saveForm.id"> - <el-form-item label="表单类型" prop="formType"> - <el-radio-group v-model="saveForm.formType"> - <el-radio - v-for="dict in getDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" - :key="parseInt(dict.value)" - :label="parseInt(dict.value)" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - <el-form-item v-if="saveForm.formType === 10" label="流程表单" prop="formId"> - <el-select v-model="saveForm.formId" clearable style="width: 100%"> - <el-option v-for="form in forms" :key="form.id" :label="form.name" :value="form.id" /> - </el-select> - </el-form-item> - <el-form-item - v-if="saveForm.formType === 20" - label="表单提交路由" - prop="formCustomCreatePath" - > - <el-input - v-model="saveForm.formCustomCreatePath" - placeholder="请输入表单提交路由" - style="width: 330px" - /> - <el-tooltip - class="item" - effect="light" - content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create" - placement="top" - > - <i style="padding-left: 5px" class="el-icon-question"></i> - </el-tooltip> - </el-form-item> - <el-form-item - v-if="saveForm.formType === 20" - label="表单查看路由" - prop="formCustomViewPath" - > - <el-input - v-model="saveForm.formCustomViewPath" - placeholder="请输入表单查看路由" - style="width: 330px" - /> - <el-tooltip - class="item" - effect="light" - content="自定义表单的查看路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/view" - placement="top" - > - <i style="padding-left: 5px" class="el-icon-question"></i> - </el-tooltip> - </el-form-item> - </div> - </el-form> - <template #footer> - <!-- 按钮:保存 --> - <XButton - type="primary" - :loading="dialogLoading" - @click="submitForm" - :title="t('action.save')" - /> - <!-- 按钮:关闭 --> - <XButton - :loading="dialogLoading" - @click="dialogVisible = false" - :title="t('dialog.close')" - /> - </template> - </XModal> - - <!-- 导入流程 --> - <XModal v-model="importDialogVisible" width="400" title="导入流程"> - <div> - <el-upload - ref="uploadRef" - :action="importUrl" - :headers="uploadHeaders" - :drag="true" - :limit="1" - :multiple="true" - :show-file-list="true" - :disabled="uploadDisabled" - :on-exceed="handleExceed" - :on-success="handleFileSuccess" - :on-error="excelUploadError" - :auto-upload="false" - accept=".bpmn, .xml" - name="bpmnFile" - :data="importForm" + </el-form-item> + <el-form-item label="流程分类" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程分类" + clearable + class="!w-240px" > - <Icon class="el-icon--upload" icon="ep:upload-filled" /> - <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> - <template #tip> - <div class="el-upload__tip" style="color: red"> - 提示:仅允许导入“bpm”或“xml”格式文件! - </div> - <div> - <el-form - ref="importFormRef" - :model="importForm" - :rules="rules" - label-width="120px" - status-icon - > - <el-form-item label="流程标识" prop="key"> - <el-input - v-model="importForm.key" - placeholder="请输入流标标识" - style="width: 250px" - /> - </el-form-item> - <el-form-item label="流程名称" prop="name"> - <el-input v-model="importForm.name" placeholder="请输入流程名称" clearable /> - </el-form-item> - <el-form-item label="流程描述" prop="description"> - <el-input type="textarea" v-model="importForm.description" clearable /> - </el-form-item> - </el-form> - </div> - </template> - </el-upload> - </div> - <template #footer> - <!-- 按钮:保存 --> - <XButton - type="warning" - preIcon="ep:upload-filled" - :title="t('action.save')" - @click="submitFileForm" - /> - <XButton title="取 消" @click="uploadClose" /> - </template> - </XModal> - - <!-- 表单详情的弹窗 --> - <XModal v-model="formDetailVisible" width="800" title="表单详情" :show-footer="false"> - <form-create - :rule="formDetailPreview.rule" - :option="formDetailPreview.option" - v-if="formDetailVisible" - /> - </XModal> - - <!-- 流程模型图的预览 --> - <XModal title="流程图" v-model="showBpmnOpen" width="80%" height="90%"> - <my-process-viewer - key="designer" - v-model="bpmnXML" - :value="bpmnXML" - v-bind="bpmnControlForm" - :prefix="bpmnControlForm.prefix" - /> - </XModal> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['bpm:model:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新建流程 + </el-button> + <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']"> + <Icon icon="ep:upload" class="mr-5px" /> 导入流程 + </el-button> + </el-form-item> + </el-form> </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="流程标识" align="center" prop="key" width="200" /> + <el-table-column label="流程名称" align="center" prop="name" width="200"> + <template #default="scope"> + <el-button type="text" @click="handleBpmnDetail(scope.row)"> + <span>{{ scope.row.name }}</span> + </el-button> + </template> + </el-table-column> + <el-table-column label="流程分类" align="center" prop="category" width="100"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> + </template> + </el-table-column> + <el-table-column label="表单信息" align="center" prop="formType" width="200"> + <template #default="scope"> + <el-button + v-if="scope.row.formType === 10" + type="text" + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formName }}</span> + </el-button> + <el-button + v-else-if="scope.row.formType === 20" + type="text" + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formCustomCreatePath }}</span> + </el-button> + <label v-else>暂无表单</label> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="最新部署的流程定义" align="center"> + <el-table-column + label="流程版本" + align="center" + prop="processDefinition.version" + width="100" + > + <template #default="scope"> + <el-tag v-if="scope.row.processDefinition"> + v{{ scope.row.processDefinition.version }} + </el-tag> + <el-tag v-else type="warning">未部署</el-tag> + </template> + </el-table-column> + <el-table-column + label="激活状态" + align="center" + prop="processDefinition.version" + width="85" + > + <template #default="scope"> + <el-switch + v-if="scope.row.processDefinition" + v-model="scope.row.processDefinition.suspensionState" + :active-value="1" + :inactive-value="2" + @change="handleChangeState(scope.row)" + /> + </template> + </el-table-column> + <el-table-column label="部署时间" align="center" prop="deploymentTime" width="180"> + <template #default="scope"> + <span v-if="scope.row.processDefinition"> + {{ formatDate(scope.row.processDefinition.deploymentTime) }} + </span> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="操作" align="center" width="240" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['bpm:model:update']" + > + 修改流程 + </el-button> + <el-button + link + type="primary" + @click="handleDesign(scope.row)" + v-hasPermi="['bpm:model:update']" + > + 设计流程 + </el-button> + <el-button + link + type="primary" + @click="handleAssignRule(scope.row)" + v-hasPermi="['bpm:task-assign-rule:query']" + > + 分配规则 + </el-button> + <el-button + link + type="primary" + @click="handleDeploy(scope.row)" + v-hasPermi="['bpm:model:deploy']" + > + 发布流程 + </el-button> + <el-button + link + type="primary" + v-hasPermi="['bpm:process-definition:query']" + @click="handleDefinitionList(scope.row)" + > + 流程定义 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['bpm:model:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改流程 --> + <ModelForm ref="formRef" @success="getList" /> + + <!-- 表单弹窗:导入流程 --> + <ModelImportForm ref="importFormRef" @success="getList" /> + + <!-- 弹窗:表单详情 --> + <Dialog title="表单详情" v-model="formDetailVisible" width="800"> + <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> + </Dialog> + + <!-- 弹窗:流程模型图的预览 --> + <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> + <my-process-viewer + key="designer" + v-model="bpmnXML" + :value="bpmnXML" + v-bind="bpmnControlForm" + :prefix="bpmnControlForm.prefix" + /> + </Dialog> </template> -<script setup lang="ts"> -// 全局相关的 import -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import { FormInstance, UploadInstance } from 'element-plus' - -// 业务相关的 import -import { getAccessToken, getTenantId } from '@/utils/auth' -import * as FormApi from '@/api/bpm/form' +<script setup lang="ts" name="Form"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter, formatDate } from '@/utils/formatTime' import * as ModelApi from '@/api/bpm/model' -import { allSchemas, rules } from './model.data' +import * as FormApi from '@/api/bpm/form' +import ModelForm from './ModelForm.vue' +import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue' import { setConfAndFields2 } from '@/utils/formCreate' - -const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 -const router = useRouter() // 路由 +const { t } = useI18n() // 国际化 +const { push } = useRouter() // 路由 -const showBpmnOpen = ref(false) -const bpmnXML = ref(null) -const bpmnControlForm = ref({ - prefix: 'flowable' +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + key: undefined, + name: undefined, + category: undefined }) -// ========== 列表相关 ========== -const [registerTable, { reload }] = useXTable({ - allSchemas: allSchemas, - getListApi: ModelApi.getModelPageApi -}) -const forms = ref() // 流程表单的下拉框的数据 +const queryFormRef = ref() // 搜索的表单 -// 设计流程 +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ModelApi.getModelPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 添加/修改操作 */ +const importFormRef = ref() +const openImportForm = () => { + importFormRef.value.open() +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ModelApi.deleteModel(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 更新状态操作 */ +const handleChangeState = async (row) => { + const state = row.processDefinition.suspensionState + try { + // 修改状态的二次确认 + const id = row.id + const statusState = state === 1 ? '激活' : '挂起' + const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' + await message.confirm(content) + // 发起修改状态 + await ModelApi.updateModelState(id, state) + // 刷新列表 + await getList() + } catch { + // 取消后,进行恢复按钮 + row.processDefinition.suspensionState = state === 1 ? 2 : 1 + } +} + +/** 设计流程 */ const handleDesign = (row) => { - console.log(row, '设计流程') - router.push({ + push({ name: 'modelEditor', query: { modelId: row.id @@ -355,9 +326,32 @@ const handleDesign = (row) => { }) } -// 跳转到指定流程定义列表 +/** 发布流程 */ +const handleDeploy = async (row) => { + try { + // 删除的二次确认 + await message.confirm('是否部署该流程!!') + // 发起部署 + await ModelApi.deployModel(row.id) + message.success(t('部署成功')) + // 刷新列表 + await getList() + } catch {} +} + +/** 点击任务分配按钮 */ +const handleAssignRule = (row) => { + push({ + name: 'BpmTaskAssignRuleList', + query: { + modelId: row.id + } + }) +} + +/** 跳转到指定流程定义列表 */ const handleDefinitionList = (row) => { - router.push({ + push({ name: 'BpmProcessDefinitionList', query: { key: row.key @@ -365,7 +359,7 @@ const handleDefinitionList = (row) => { }) } -// 流程表单的详情按钮操作 +/** 流程表单的详情按钮操作 */ const formDetailVisible = ref(false) const formDetailPreview = ref({ rule: [], @@ -374,222 +368,31 @@ const formDetailPreview = ref({ const handleFormDetail = async (row) => { if (row.formType == 10) { // 设置表单 - const data = await FormApi.getFormApi(row.formId) + const data = await FormApi.getForm(row.formId) setConfAndFields2(formDetailPreview, data.conf, data.fields) // 弹窗打开 formDetailVisible.value = true } else { - await router.push({ + await push({ path: row.formCustomCreatePath }) } } -// 流程图的详情按钮操作 -const handleBpmnDetail = (row) => { - // TODO 芋艿:流程组件开发中 - console.log(row) - ModelApi.getModelApi(row).then((response) => { - console.log(response, 'response') - bpmnXML.value = response.bpmnXml - // 弹窗打开 - showBpmnOpen.value = true - }) - // message.success('流程组件开发中,预计 2 月底完成') -} - -// 点击任务分配按钮 -const handleAssignRule = (row) => { - router.push({ - name: 'BpmTaskAssignRuleList', - query: { - modelId: row.id - } - }) -} - -// ========== 新建/修改流程 ========== -const dialogVisible = ref(false) -const dialogTitle = ref('新建模型') -const dialogLoading = ref(false) -const saveForm = ref() -const saveFormRef = ref<FormInstance>() - -// 设置标题 -const setDialogTile = async (type: string) => { - dialogTitle.value = t('action.' + type) - dialogVisible.value = true -} - -// 新增操作 -const handleCreate = async () => { - resetForm() - await setDialogTile('create') -} - -// 修改操作 -const handleUpdate = async (rowId: number) => { - resetForm() - await setDialogTile('edit') - // 设置数据 - saveForm.value = await ModelApi.getModelApi(rowId) - if (saveForm.value.category == null) { - saveForm.value.category = 1 - } else { - saveForm.value.category = Number(saveForm.value.category) - } -} - -// 提交按钮 -const submitForm = async () => { - // 参数校验 - const elForm = unref(saveFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return - - // 提交请求 - dialogLoading.value = true - try { - const data = saveForm.value as ModelApi.ModelVO - if (!data.id) { - await ModelApi.createModelApi(data) - message.success(t('common.createSuccess')) - } else { - await ModelApi.updateModelApi(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - // 刷新列表 - await reload() - dialogLoading.value = false - } -} - -// 重置表单 -const resetForm = () => { - saveForm.value = { - formType: 10, - name: '', - courseSort: '', - description: '', - formId: '', - formCustomCreatePath: '', - formCustomViewPath: '' - } - saveFormRef.value?.resetFields() -} - -// ========== 删除 / 更新状态 / 发布流程 ========== -// 删除流程 -const handleDelete = (rowId) => { - message.delConfirm('是否删除该流程!!').then(async () => { - await ModelApi.deleteModelApi(rowId) - message.success(t('common.delSuccess')) - // 刷新列表 - reload() - }) -} - -// 更新状态操作 -const handleChangeState = (row) => { - const id = row.id - const state = row.processDefinition.suspensionState - const statusState = state === 1 ? '激活' : '挂起' - const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' - message - .confirm(content) - .then(async () => { - await ModelApi.updateModelStateApi(id, state) - message.success(t('部署成功')) - // 刷新列表 - reload() - }) - .catch(() => { - // 取消后,进行恢复按钮 - row.processDefinition.suspensionState = state === 1 ? 2 : 1 - }) -} - -// 发布流程 -const handleDeploy = (row) => { - message.confirm('是否部署该流程!!').then(async () => { - await ModelApi.deployModelApi(row.id) - message.success(t('部署成功')) - // 刷新列表 - reload() - }) -} - -// ========== 导入流程 ========== -const uploadRef = ref<UploadInstance>() -let importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import' -const uploadHeaders = ref() -const importDialogVisible = ref(false) -const uploadDisabled = ref(false) -const importFormRef = ref<FormInstance>() -const importForm = ref({ - key: '', - name: '', - description: '' +/** 流程图的详情按钮操作 */ +const bpmnDetailVisible = ref(false) +const bpmnXML = ref(null) +const bpmnControlForm = ref({ + prefix: 'flowable' }) - -// 导入流程弹窗显示 -const handleImport = () => { - importDialogVisible.value = true -} -// 文件数超出提示 -const handleExceed = (): void => { - message.error('最多只能上传一个文件!') -} -// 上传错误提示 -const excelUploadError = (): void => { - message.error('导入流程失败,请您重新上传!') +const handleBpmnDetail = async (row) => { + const data = await ModelApi.getModel(row.id) + bpmnXML.value = data.bpmnXml || '' + bpmnDetailVisible.value = true } -// 提交文件上传 -const submitFileForm = () => { - uploadHeaders.value = { - Authorization: 'Bearer ' + getAccessToken(), - 'tenant-id': getTenantId() - } - uploadDisabled.value = true - uploadRef.value!.submit() -} -// 文件上传成功 -const handleFileSuccess = async (response: any): Promise<void> => { - if (response.code !== 0) { - message.error(response.msg) - return - } - // 重置表单 - uploadClose() - // 提示,并刷新 - message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】') - await reload() -} -// 关闭文件上传 -const uploadClose = () => { - // 关闭弹窗 - importDialogVisible.value = false - // 重置上传状态和文件 - uploadDisabled.value = false - uploadRef.value!.clearFiles() - // 重置表单 - importForm.value = { - key: '', - name: '', - description: '' - } - importFormRef.value?.resetFields() -} - -// ========== 初始化 ========== +/** 初始化 **/ onMounted(() => { - // 获得流程表单的下拉框的数据 - FormApi.getSimpleFormsApi().then((data) => { - forms.value = data - }) + getList() }) </script> diff --git a/src/views/bpm/model/model.data.ts b/src/views/bpm/model/model.data.ts deleted file mode 100644 index 89e886cc..00000000 --- a/src/views/bpm/model/model.data.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -const { t } = useI18n() // 国际化 - -// 表单校验 -export const rules = reactive({ - key: [required], - name: [required], - category: [required], - formType: [required], - formId: [required], - formCustomCreatePath: [required], - formCustomViewPath: [required] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'key', - primaryType: null, - action: true, - actionWidth: '540px', - columns: [ - { - title: '流程标识', - field: 'key', - isSearch: true, - table: { - width: 120 - } - }, - { - title: '流程名称', - field: 'name', - isSearch: true, - table: { - width: 120, - slots: { - default: 'name_default' - } - } - }, - { - title: '流程分类', - field: 'category', - dictType: DICT_TYPE.BPM_MODEL_CATEGORY, - dictClass: 'number', - isSearch: true, - table: { - slots: { - default: 'category_default' - } - } - }, - { - title: '表单信息', - field: 'formId', - table: { - width: 180, - slots: { - default: 'formId_default' - } - } - }, - { - title: '最新部署的流程定义', - field: 'processDefinition', - isForm: false, - table: { - children: [ - { - title: '流程版本', - field: 'version', - slots: { - default: 'version_default' - }, - width: 80 - }, - { - title: '激活状态', - field: 'status', - slots: { - default: 'status_default' - }, - width: 80 - }, - { - title: '部署时间', - field: 'processDefinition.deploymentTime', - formatter: 'formatDate', - width: 180 - } - ] - } - }, - { - title: t('common.createTime'), - field: 'createTime', - isForm: false, - formatter: 'formatDate', - table: { - width: 180 - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/bpm/model/modelEditor.vue b/src/views/bpm/model/modelEditor.vue deleted file mode 100644 index 43836976..00000000 --- a/src/views/bpm/model/modelEditor.vue +++ /dev/null @@ -1,204 +0,0 @@ -<template> - <div class="app-container"> - <!-- 流程设计器,负责绘制流程等 --> - <!-- <myProcessDesigner --> - <my-process-designer - :key="`designer-${reloadIndex}`" - v-if="xmlString !== undefined" - v-model="xmlString" - :value="xmlString" - v-bind="controlForm" - keyboard - ref="processDesigner" - @init-finished="initModeler" - :additionalModel="controlForm.additionalModel" - @save="save" - /> - <!-- 流程属性器,负责编辑每个流程节点的属性 --> - <!-- <MyProcessPalette --> - <my-properties-panel - :key="`penal-${reloadIndex}`" - :bpmnModeler="modeler" - :prefix="controlForm.prefix" - class="process-panel" - :model="model" - /> - </div> -</template> - -<script setup lang="ts"> -// import { translations } from '@/components/bpmnProcessDesigner/src/translations' -// 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务) -import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad' -// 自定义左侧菜单(修改 默认任务 为 用户任务) -import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette' -// import xmlObj2json from "./utils/xml2json"; -// import myProcessDesigner from '@/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue' -// import MyProcessPalette from '@/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue' -import { createModelApi, getModelApi, updateModelApi, ModelVO } from '@/api/bpm/model' - -const router = useRouter() -const message = useMessage() - -// 自定义侧边栏 -// import MyProcessPanel from "../package/process-panel/ProcessPanel"; - -const xmlString = ref(undefined) // BPMN XML -const modeler = ref(null) -const reloadIndex = ref(0) -// const controlDrawerVisible = ref(false) -// const translationsSelf = translations -const controlForm = ref({ - simulation: true, - labelEditing: false, - labelVisible: false, - prefix: 'flowable', - headerButtonSize: 'mini', - additionalModel: [CustomContentPadProvider, CustomPaletteProvider] -}) -// const addis = ref({ -// CustomContentPadProvider, -// CustomPaletteProvider -// }) -// 流程模型的信息 -const model = ref<ModelVO>() -onMounted(() => { - // 如果 modelId 非空,说明是修改流程模型 - const modelId = router.currentRoute.value.query && router.currentRoute.value.query.modelId - console.log(modelId, 'modelId') - if (modelId) { - // let data = '4b4909d8-97e7-11ec-8e20-862bc1a4a054' - getModelApi(modelId as unknown as number).then((data) => { - console.log(data, 'response') - xmlString.value = data.bpmnXml - model.value = { - ...data, - bpmnXml: undefined // 清空 bpmnXml 属性 - } - // this.controlForm.processId = data.key - - // xmlString.value = - // '<?xml version="1.0" encoding="UTF-8"?>\n<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="diagram_Process_1645980650311" targetNamespace="http://activiti.org/bpmn"><bpmn2:process id="flowable_01" name="flowable测试" isExecutable="true"><bpmn2:startEvent id="Event_1iruxim"><bpmn2:outgoing>Flow_0804gmo</bpmn2:outgoing></bpmn2:startEvent><bpmn2:userTask id="task01" name="task01"><bpmn2:incoming>Flow_0804gmo</bpmn2:incoming><bpmn2:outgoing>Flow_0cx479x</bpmn2:outgoing></bpmn2:userTask><bpmn2:sequenceFlow id="Flow_0804gmo" sourceRef="Event_1iruxim" targetRef="task01" /><bpmn2:endEvent id="Event_1mdsccz"><bpmn2:incoming>Flow_0cx479x</bpmn2:incoming></bpmn2:endEvent><bpmn2:sequenceFlow id="Flow_0cx479x" sourceRef="task01" targetRef="Event_1mdsccz" /></bpmn2:process><bpmndi:BPMNDiagram id="BPMNDiagram_1"><bpmndi:BPMNPlane id="flowable_01_di" bpmnElement="flowable_01"><bpmndi:BPMNEdge id="Flow_0cx479x_di" bpmnElement="Flow_0cx479x"><di:waypoint x="440" y="350" /><di:waypoint x="492" y="350" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0804gmo_di" bpmnElement="Flow_0804gmo"><di:waypoint x="288" y="350" /><di:waypoint x="340" y="350" /></bpmndi:BPMNEdge><bpmndi:BPMNShape id="Event_1iruxim_di" bpmnElement="Event_1iruxim"><dc:Bounds x="252" y="332" width="36" height="36" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="task01_di" bpmnElement="task01"><dc:Bounds x="340" y="310" width="100" height="80" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Event_1mdsccz_di" bpmnElement="Event_1mdsccz"><dc:Bounds x="492" y="332" width="36" height="36" /></bpmndi:BPMNShape></bpmndi:BPMNPlane></bpmndi:BPMNDiagram></bpmn2:definitions>' - - // model.value = { - // key: 'flowable_01', - // name: 'flowable测试', - // description: 'ooxx', - // category: '1', - // formType: 10, - // formId: 11, - // formCustomCreatePath: null, - // formCustomViewPath: null, - // id: '4b4909d8-97e7-11ec-8e20-862bc1a4a054', - // createTime: 1645978019795, - // bpmnXml: undefined // 清空 bpmnXml 属性 - // } - // console.log(modeler.value, 'modeler11111111') - }) - } -}) -const initModeler = (item) => { - setTimeout(() => { - modeler.value = item - console.log(item, 'initModeler方法modeler') - console.log(modeler.value, 'initModeler方法modeler') - // controlForm.value.prefix = '2222' - }, 10) -} - -const save = (bpmnXml) => { - const data: ModelVO = { - ...(model.value ?? ({} as ModelVO)), - bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 - } - console.log(data, 'data') - - // 修改的提交 - if (data.id) { - updateModelApi(data).then((response) => { - console.log(response, 'response') - message.success('修改成功') - // 跳转回去 - close() - }) - return - } - // 添加的提交 - createModelApi(data).then((response) => { - console.log(response, 'response1') - message.success('保存成功') - // 跳转回去 - close() - }) -} -/** 关闭按钮 */ -const close = () => { - router.push({ path: '/bpm/manager/model' }) -} -</script> - -<style lang="scss"> -//body { -// overflow: hidden; -// margin: 0; -// box-sizing: border-box; -//} -//.app { -// width: 100%; -// height: 100%; -// box-sizing: border-box; -// display: inline-grid; -// grid-template-columns: 100px auto max-content; -//} -.demo-control-bar { - position: fixed; - right: 8px; - bottom: 8px; - z-index: 1; - .open-control-dialog { - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - font-size: 32px; - background: rgba(64, 158, 255, 1); - color: #ffffff; - cursor: pointer; - } -} - -// TODO 芋艿:去掉多余的 faq -//.info-tip { -// position: fixed; -// top: 40px; -// right: 500px; -// z-index: 10; -// color: #999999; -//} - -.control-form { - .el-radio { - width: 100%; - line-height: 32px; - } -} -.element-overlays { - box-sizing: border-box; - padding: 8px; - background: rgba(0, 0, 0, 0.6); - border-radius: 4px; - color: #fafafa; -} - -.my-process-designer { - height: calc(100vh - 84px); -} -.process-panel__container { - position: absolute; - right: 0; - top: 55px; - height: calc(100vh - 84px); -} -</style> diff --git a/src/views/bpm/processInstance/create.vue b/src/views/bpm/processInstance/create.vue index 1b59ec7c..b6fc0f49 100644 --- a/src/views/bpm/processInstance/create.vue +++ b/src/views/bpm/processInstance/create.vue @@ -72,7 +72,7 @@ const [registerTable] = useXTable({ params: { suspensionState: 1 }, - getListApi: DefinitionApi.getProcessDefinitionListApi, + getListApi: DefinitionApi.getProcessDefinitionList, isList: true }) @@ -99,7 +99,7 @@ const handleSelect = async (row) => { setConfAndFields2(detailForm, row.formConf, row.formFields) // 加载流程图 - DefinitionApi.getProcessDefinitionBpmnXMLApi(row.id).then((response) => { + DefinitionApi.getProcessDefinitionBpmnXML(row.id).then((response) => { bpmnXML.value = response }) // 情况二:业务表单 diff --git a/src/views/bpm/processInstance/detail.vue b/src/views/bpm/processInstance/detail.vue index b6d52a4c..a34d8a1a 100644 --- a/src/views/bpm/processInstance/detail.vue +++ b/src/views/bpm/processInstance/detail.vue @@ -112,13 +112,13 @@ </label> <label style="font-weight: normal" v-if="item.createTime">创建时间:</label> <label style="color: #8a909c; font-weight: normal"> - {{ dayjs(item?.createTime).format('YYYY-MM-DD HH:mm:ss') }} + {{ parseTime(item?.createTime) }} </label> <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal"> 审批时间: </label> <label v-if="item.endTime" style="color: #8a909c; font-weight: normal"> - {{ dayjs(item?.endTime).format('YYYY-MM-DD HH:mm:ss') }} + {{ parseTime(item?.endTime) }} </label> <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal"> 耗时: @@ -192,7 +192,7 @@ </ContentWrap> </template> <script setup lang="ts"> -import dayjs from 'dayjs' +import { parseTime } from '@/utils/formatTime' import * as UserApi from '@/api/system/user' import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as DefinitionApi from '@/api/bpm/definition' @@ -378,7 +378,7 @@ onMounted(() => { // 加载详情 getDetail() // 加载用户的列表 - UserApi.getListSimpleUsersApi().then((data) => { + UserApi.getSimpleUserList().then((data) => { userOptions.value.push(...data) }) }) @@ -411,7 +411,7 @@ const getDetail = () => { } // 加载流程图 - DefinitionApi.getProcessDefinitionBpmnXMLApi(processDefinition.id).then((data) => { + DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id).then((data) => { bpmnXML.value = data }) diff --git a/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue b/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue new file mode 100644 index 00000000..a452cab9 --- /dev/null +++ b/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue @@ -0,0 +1,247 @@ +<template> + <Dialog title="修改任务规则" v-model="modelVisible" width="600"> + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px"> + <el-form-item label="任务名称" prop="taskDefinitionName"> + <el-input v-model="formData.taskDefinitionName" placeholder="请输入流标标识" disabled /> + </el-form-item> + <el-form-item label="任务标识" prop="taskDefinitionKey"> + <el-input v-model="formData.taskDefinitionKey" placeholder="请输入任务标识" disabled /> + </el-form-item> + <el-form-item label="规则类型" prop="type"> + <el-select v-model="formData.type" clearable style="width: 100%"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds"> + <el-select v-model="formData.roleIds" multiple clearable style="width: 100%"> + <el-option + v-for="item in roleOptions" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item + label="指定部门" + prop="deptIds" + span="24" + v-if="formData.type === 20 || formData.type === 21" + > + <el-tree-select + ref="treeRef" + v-model="formData.deptIds" + node-key="id" + show-checkbox + :props="defaultProps" + :data="deptTreeOptions" + empty-text="加载中,请稍后" + multiple + /> + </el-form-item> + <el-form-item label="指定岗位" prop="postIds" span="24" v-if="formData.type === 22"> + <el-select v-model="formData.postIds" multiple clearable style="width: 100%"> + <el-option + v-for="item in postOptions" + :key="parseInt(item.id)" + :label="item.name" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + <el-form-item + label="指定用户" + prop="userIds" + span="24" + v-if="formData.type === 30 || formData.type === 31 || formData.type === 32" + > + <el-select v-model="formData.userIds" multiple clearable style="width: 100%"> + <el-option + v-for="item in userOptions" + :key="parseInt(item.id)" + :label="item.nickname" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + <el-form-item label="指定用户组" prop="userGroupIds" v-if="formData.type === 40"> + <el-select v-model="formData.userGroupIds" multiple clearable style="width: 100%"> + <el-option + v-for="item in userGroupOptions" + :key="parseInt(item.id)" + :label="item.name" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + <el-form-item label="指定脚本" prop="scripts" v-if="formData.type === 50"> + <el-select v-model="formData.scripts" multiple clearable style="width: 100%"> + <el-option + v-for="dict in taskAssignScriptDictDatas" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + </el-form> + <!-- 操作按钮 --> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { handleTree, defaultProps } from '@/utils/tree' +import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule' +import * as RoleApi from '@/api/system/role' +import * as DeptApi from '@/api/system/dept' +import * as PostApi from '@/api/system/post' +import * as UserApi from '@/api/system/user' +import * as UserGroupApi from '@/api/bpm/userGroup' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref({ + type: Number(undefined), + modelId: '', + options: [], + roleIds: [], + deptIds: [], + postIds: [], + userIds: [], + userGroupIds: [], + scripts: [] +}) +const formRules = reactive({ + type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }], + roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }], + deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }], + postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }], + userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }], + userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }], + scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }] +}) +const formRef = ref() // 表单 Ref +const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 +const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 +const deptTreeOptions = ref() // 部门树 +const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 +const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 +const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 +const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) + +/** 打开弹窗 */ +const open = async (modelId: string, row: TaskAssignRuleApi.TaskAssignVO) => { + // 1. 先重置表单 + resetForm() + // 2. 再设置表单 + formData.value = { + ...row, + modelId: modelId, + options: [], + roleIds: [], + deptIds: [], + postIds: [], + userIds: [], + userGroupIds: [], + scripts: [] + } + // 将 options 赋值到对应的 roleIds 等选项 + if (row.type === 10) { + formData.value.roleIds.push(...row.options) + } else if (row.type === 20 || row.type === 21) { + formData.value.deptIds.push(...row.options) + } else if (row.type === 22) { + formData.value.postIds.push(...row.options) + } else if (row.type === 30 || row.type === 31 || row.type === 32) { + formData.value.userIds.push(...row.options) + } else if (row.type === 40) { + formData.value.userGroupIds.push(...row.options) + } else if (row.type === 50) { + formData.value.scripts.push(...row.options) + } + // 打开弹窗 + modelVisible.value = true + + // 获得角色列表 + roleOptions.value = await RoleApi.getSimpleRoleList() + // 获得部门列表 + deptOptions.value = await DeptApi.getSimpleDeptList() + deptTreeOptions.value = handleTree(deptOptions.value, 'id') + // 获得岗位列表 + postOptions.value = await PostApi.getSimplePostList() + // 获得用户列表 + userOptions.value = await UserApi.getSimpleUserList() + // 获得用户组列表 + userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + + // 构建表单 + const form = { + ...formData.value, + taskDefinitionName: undefined + } + // 将 roleIds 等选项赋值到 options 中 + if (form.type === 10) { + form.options = form.roleIds + } else if (form.type === 20 || form.type === 21) { + form.options = form.deptIds + } else if (form.type === 22) { + form.options = form.postIds + } else if (form.type === 30 || form.type === 31 || form.type === 32) { + form.options = form.userIds + } else if (form.type === 40) { + form.options = form.userGroupIds + } else if (form.type === 50) { + form.options = form.scripts + } + form.roleIds = undefined + form.deptIds = undefined + form.postIds = undefined + form.userIds = undefined + form.userGroupIds = undefined + form.scripts = undefined + + // 提交请求 + formLoading.value = true + try { + const data = form as unknown as TaskAssignRuleApi.TaskAssignVO + if (!data.id) { + await TaskAssignRuleApi.createTaskAssignRule(data) + message.success(t('common.createSuccess')) + } else { + await TaskAssignRuleApi.updateTaskAssignRule(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/taskAssignRule/index.vue b/src/views/bpm/taskAssignRule/index.vue index 012e6f68..feea80a2 100644 --- a/src/views/bpm/taskAssignRule/index.vue +++ b/src/views/bpm/taskAssignRule/index.vue @@ -1,186 +1,73 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable" ref="xGrid"> - <template #options_default="{ row }"> - <span :key="option" v-for="option in row.options"> - <el-tag> - {{ getAssignRuleOptionName(row.type, option) }} + <el-table v-loading="loading" :data="list"> + <el-table-column label="任务名" align="center" prop="taskDefinitionName" /> + <el-table-column label="任务标识" align="center" prop="taskDefinitionKey" /> + <el-table-column label="规则类型" align="center" prop="type"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE" :value="scope.row.type" /> + </template> + </el-table-column> + <el-table-column label="规则范围" align="center" prop="options"> + <template #default="scope"> + <el-tag class="mr-5px" :key="option" v-for="option in scope.row.options"> + {{ getAssignRuleOptionName(scope.row.type, option) }} </el-tag> - - </span> - </template> - <!-- 操作 --> - <template #actionbtns_default="{ row }" v-if="modelId"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['bpm:task-assign-rule:update']" - @click="handleUpdate(row)" - /> - </template> - </XTable> - - <!-- 添加/修改弹窗 --> - <XModal v-model="dialogVisible" title="修改任务规则" width="800" height="35%"> - <el-form ref="formRef" :model="formData" :rules="rules" label-width="80px"> - <el-form-item label="任务名称" prop="taskDefinitionName"> - <el-input v-model="formData.taskDefinitionName" placeholder="请输入流标标识" disabled /> - </el-form-item> - <el-form-item label="任务标识" prop="taskDefinitionKey"> - <el-input v-model="formData.taskDefinitionKey" placeholder="请输入任务标识" disabled /> - </el-form-item> - <el-form-item label="规则类型" prop="type"> - <el-select v-model="formData.type" clearable style="width: 100%"> - <el-option - v-for="dict in getDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)" - :key="parseInt(dict.value)" - :label="dict.label" - :value="parseInt(dict.value)" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds"> - <el-select v-model="formData.roleIds" multiple clearable style="width: 100%"> - <el-option - v-for="item in roleOptions" - :key="item.id" - :label="item.name" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item - label="指定部门" - prop="deptIds" - span="24" - v-if="formData.type === 20 || formData.type === 21" - > - <el-tree-select - ref="treeRef" - v-model="formData.deptIds" - node-key="id" - show-checkbox - :props="defaultProps" - :data="deptTreeOptions" - empty-text="加载中,请稍后" - multiple - /> - </el-form-item> - <el-form-item label="指定岗位" prop="postIds" span="24" v-if="formData.type === 22"> - <el-select v-model="formData.postIds" multiple clearable style="width: 100%"> - <el-option - v-for="item in postOptions" - :key="parseInt(item.id)" - :label="item.name" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item - label="指定用户" - prop="userIds" - span="24" - v-if="formData.type === 30 || formData.type === 31 || formData.type === 32" - > - <el-select v-model="formData.userIds" multiple clearable style="width: 100%"> - <el-option - v-for="item in userOptions" - :key="parseInt(item.id)" - :label="item.nickname" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item label="指定用户组" prop="userGroupIds" v-if="formData.type === 40"> - <el-select v-model="formData.userGroupIds" multiple clearable style="width: 100%"> - <el-option - v-for="item in userGroupOptions" - :key="parseInt(item.id)" - :label="item.name" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item label="指定脚本" prop="scripts" v-if="formData.type === 50"> - <el-select v-model="formData.scripts" multiple clearable style="width: 100%"> - <el-option - v-for="dict in taskAssignScriptDictDatas" - :key="parseInt(dict.value)" - :label="dict.label" - :value="parseInt(dict.value)" - /> - </el-select> - </el-form-item> - </el-form> - <!-- 操作按钮 --> - <template #footer> - <!-- 按钮:保存 --> - <XButton - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitForm" - /> - <!-- 按钮:关闭 --> - <XButton - :loading="actionLoading" - :title="t('dialog.close')" - @click="dialogVisible = false" - /> - </template> - </XModal> + </template> + </el-table-column> + <el-table-column v-if="queryParams.modelId" label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm(scope.row)" + v-hasPermi="['bpm:task-assign-rule:update']" + > + 修改 + </el-button> + </template> + </el-table-column> + </el-table> </ContentWrap> + <!-- 添加/修改弹窗 --> + <TaskAssignRuleForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="TaskAssignRule"> -// 全局相关的 import -import { FormInstance } from 'element-plus' -// 业务相关的 import +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule' -import { listSimpleRolesApi } from '@/api/system/role' -import { listSimplePostsApi } from '@/api/system/post' -import { getListSimpleUsersApi } from '@/api/system/user' -import { listSimpleUserGroupsApi } from '@/api/bpm/userGroup' -import { listSimpleDeptApi } from '@/api/system/dept' -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import { handleTree, defaultProps } from '@/utils/tree' -import { allSchemas, rules, idShowActionClick } from './taskAssignRule.data' +import * as RoleApi from '@/api/system/role' +import * as DeptApi from '@/api/system/dept' +import * as PostApi from '@/api/system/post' +import * as UserApi from '@/api/system/user' +import * as UserGroupApi from '@/api/bpm/userGroup' +import TaskAssignRuleForm from './TaskAssignRuleForm.vue' +const { query } = useRoute() // 查询参数 -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 -const { query } = useRoute() -const xGrid = ref() - -// ========== 列表相关 ========== - -const roleOptions = ref() // 角色列表 -const deptOptions = ref() // 部门列表 -const deptTreeOptions = ref() -const postOptions = ref() // 岗位列表 -const userOptions = ref() // 用户列表 -const userGroupOptions = ref() // 用户组列表 -const taskAssignScriptDictDatas = getDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) - -// 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置 -const modelId = query.modelId -// 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置 -const processDefinitionId = query.processDefinitionId -let isShow = idShowActionClick(modelId) - -// 查询参数 +const loading = ref(true) // 列表的加载中 +const list = ref([]) // 列表的数据 const queryParams = reactive({ - modelId: modelId, - processDefinitionId: processDefinitionId -}) -const [registerTable, { reload }] = useXTable({ - allSchemas: allSchemas, - params: queryParams, - getListApi: TaskAssignRuleApi.getTaskAssignRuleList, - isList: true + modelId: query.modelId, // 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置 + processDefinitionId: query.processDefinitionId // 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置 }) +const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 +const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 +const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 +const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 +const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 +const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) -// 翻译规则范围 +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + list.value = await TaskAssignRuleApi.getTaskAssignRuleList(queryParams) + } finally { + loading.value = false + } +} + +/** 翻译规则范围 */ +// TODO 芋艿:各种 ts 报错 const getAssignRuleOptionName = (type, option) => { if (type === 10) { for (const roleOption of roleOptions.value) { @@ -223,136 +110,24 @@ const getAssignRuleOptionName = (type, option) => { return '未知(' + option + ')' } -// ========== 修改相关 ========== - -// 修改任务责任表单 -const actionLoading = ref(false) // 遮罩层 -const dialogVisible = ref(false) // 是否显示弹出层 -const formRef = ref<FormInstance>() -const formData = ref() // 表单数据 - -// 提交按钮 -const submitForm = async () => { - // 参数校验 - const elForm = unref(formRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return - // 构建表单 - let form = { - ...formData.value, - taskDefinitionName: undefined - } - // 将 roleIds 等选项赋值到 options 中 - if (form.type === 10) { - form.options = form.roleIds - } else if (form.type === 20 || form.type === 21) { - form.options = form.deptIds - } else if (form.type === 22) { - form.options = form.postIds - } else if (form.type === 30 || form.type === 31 || form.type === 32) { - form.options = form.userIds - } else if (form.type === 40) { - form.options = form.userGroupIds - } else if (form.type === 50) { - form.options = form.scripts - } - form.roleIds = undefined - form.deptIds = undefined - form.postIds = undefined - form.userIds = undefined - form.userGroupIds = undefined - form.scripts = undefined - // 设置提交中 - actionLoading.value = true - // 提交请求 - try { - const data = form as TaskAssignRuleApi.TaskAssignVO - // 新增 - if (!data.id) { - await TaskAssignRuleApi.createTaskAssignRule(data) - message.success(t('common.createSuccess')) - // 修改 - } else { - await TaskAssignRuleApi.updateTaskAssignRule(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - actionLoading.value = false - // 刷新列表 - await reload() - } +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (row: TaskAssignRuleApi.TaskAssignVO) => { + formRef.value.open(queryParams.modelId, row) } -// 修改任务分配规则 -const handleUpdate = (row) => { - // 1. 先重置表单 - formData.value = {} - // 2. 再设置表单 - formData.value = { - ...row, - modelId: modelId, - options: [], - roleIds: [], - deptIds: [], - postIds: [], - userIds: [], - userGroupIds: [], - scripts: [] - } - // 将 options 赋值到对应的 roleIds 等选项 - if (row.type === 10) { - formData.value.roleIds.push(...row.options) - } else if (row.type === 20 || row.type === 21) { - formData.value.deptIds.push(...row.options) - } else if (row.type === 22) { - formData.value.postIds.push(...row.options) - } else if (row.type === 30 || row.type === 31 || row.type === 32) { - formData.value.userIds.push(...row.options) - } else if (row.type === 40) { - formData.value.userGroupIds.push(...row.options) - } else if (row.type === 50) { - formData.value.scripts.push(...row.options) - } - // 打开弹窗 - dialogVisible.value = true - actionLoading.value = false -} - -// ========== 初始化 ========== -onMounted(() => { +/** 初始化 */ +onMounted(async () => { + await getList() // 获得角色列表 - roleOptions.value = [] - listSimpleRolesApi().then((data) => { - roleOptions.value.push(...data) - }) + roleOptions.value = await RoleApi.getSimpleRoleList() // 获得部门列表 - deptOptions.value = [] - deptTreeOptions.value = [] - listSimpleDeptApi().then((data) => { - deptOptions.value.push(...data) - deptTreeOptions.value.push(...handleTree(data, 'id')) - }) + deptOptions.value = await DeptApi.getSimpleDeptList() // 获得岗位列表 - postOptions.value = [] - listSimplePostsApi().then((data) => { - postOptions.value.push(...data) - }) + postOptions.value = await PostApi.getSimplePostList() // 获得用户列表 - userOptions.value = [] - getListSimpleUsersApi().then((data) => { - userOptions.value.push(...data) - }) + userOptions.value = await UserApi.getSimpleUserList() // 获得用户组列表 - userGroupOptions.value = [] - listSimpleUserGroupsApi().then((data) => { - userGroupOptions.value.push(...data) - }) - if (!isShow) { - setTimeout(() => { - xGrid.value.Ref.hideColumn('actionbtns') - }, 100) - } + userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList() }) </script> diff --git a/src/views/bpm/taskAssignRule/taskAssignRule.data.ts b/src/views/bpm/taskAssignRule/taskAssignRule.data.ts deleted file mode 100644 index cad74325..00000000 --- a/src/views/bpm/taskAssignRule/taskAssignRule.data.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -// 表单校验 -export const rules = reactive({ - type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }], - roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }], - deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }], - postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }], - userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }], - userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }], - scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: null, - action: true, - actionWidth: '200px', - columns: [ - { - title: '任务名', - field: 'taskDefinitionName' - }, - { - title: '任务标识', - field: 'taskDefinitionKey' - }, - { - title: '规则类型', - field: 'type', - dictType: DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE, - dictClass: 'number' - }, - { - title: '规则范围', - field: 'options', - table: { - slots: { - default: 'options_default' - } - } - } - ] -}) - -export const idShowActionClick = (modelId?: any) => { - if (modelId) { - return true - } else { - return false - } -} -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue b/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue new file mode 100644 index 00000000..d046a521 --- /dev/null +++ b/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue @@ -0,0 +1,65 @@ +<template> + <Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="500" width="800"> + <el-descriptions border :column="1"> + <el-descriptions-item label="日志主键" min-width="120"> + {{ detailData.id }} + </el-descriptions-item> + <el-descriptions-item label="链路追踪"> + {{ detailData.traceId }} + </el-descriptions-item> + <el-descriptions-item label="应用名"> + {{ detailData.applicationName }} + </el-descriptions-item> + <el-descriptions-item label="用户信息"> + {{ detailData.userId }} + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" /> + </el-descriptions-item> + <el-descriptions-item label="用户 IP"> + {{ detailData.userIp }} + </el-descriptions-item> + <el-descriptions-item label="用户 UA"> + {{ detailData.userAgent }} + </el-descriptions-item> + <el-descriptions-item label="请求信息"> + {{ detailData.requestMethod }} {{ detailData.requestUrl }} + </el-descriptions-item> + <el-descriptions-item label="请求参数"> + {{ detailData.requestParams }} + </el-descriptions-item> + <el-descriptions-item label="请求时间"> + {{ formatDate(detailData.beginTime) }} ~ {{ formatDate(detailData.endTime) }} + </el-descriptions-item> + <el-descriptions-item label="请求耗时">{{ detailData.duration }} ms</el-descriptions-item> + <el-descriptions-item label="操作结果"> + <div v-if="detailData.resultCode === 0">正常</div> + <div v-else-if="detailData.resultCode > 0" + >失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}</div + > + </el-descriptions-item> + </el-descriptions> + </Dialog> +</template> + +<script setup lang="ts"> +import { DICT_TYPE } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import * as ApiAccessLog from '@/api/infra/apiAccessLog' + +const modelVisible = ref(false) // 弹窗的是否展示 +const detailLoading = ref(false) // 表单地加载中 +const detailData = ref() // 详情数据 + +/** 打开弹窗 */ +const open = async (data: ApiAccessLog.ApiAccessLogVO) => { + modelVisible.value = true + // 设置数据 + detailLoading.value = true + try { + detailData.value = data + } finally { + detailLoading.value = false + } +} + +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> diff --git a/src/views/infra/apiAccessLog/apiAccessLog.data.ts b/src/views/infra/apiAccessLog/apiAccessLog.data.ts deleted file mode 100644 index cb9e97a7..00000000 --- a/src/views/infra/apiAccessLog/apiAccessLog.data.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '日志编号', - action: true, - actionWidth: '80px', - columns: [ - { - title: '链路追踪', - field: 'traceId', - isTable: false - }, - { - title: '用户编号', - field: 'userId', - isSearch: true - }, - { - title: '用户类型', - field: 'userType', - dictType: DICT_TYPE.USER_TYPE, - dictClass: 'number', - isSearch: true - }, - { - title: '应用名', - field: 'applicationName', - isSearch: true - }, - { - title: '请求方法名', - field: 'requestMethod' - }, - { - title: '请求地址', - field: 'requestUrl', - isSearch: true - }, - { - title: '请求时间', - field: 'beginTime', - formatter: 'formatDate', - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - }, - { - title: '执行时长', - field: 'duration', - table: { - slots: { - default: 'duration_default' - } - } - }, - { - title: '操作结果', - field: 'resultCode', - isSearch: true, - table: { - slots: { - default: 'resultCode_default' - } - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/infra/apiAccessLog/index.vue b/src/views/infra/apiAccessLog/index.vue index 6a09927d..3102d39d 100644 --- a/src/views/infra/apiAccessLog/index.vue +++ b/src/views/infra/apiAccessLog/index.vue @@ -1,62 +1,220 @@ <template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #duration_default="{ row }"> - <span>{{ row.duration + 'ms' }}</span> - </template> - <template #resultCode_default="{ row }"> - <span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['infra:api-access-log:query']" - @click="handleDetail(row)" + <content-wrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="用户编号" prop="userId"> + <el-input + v-model="queryParams.userId" + placeholder="请输入用户编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - </template> - </XTable> - </ContentWrap> - <XModal v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(详情) --> - <Descriptions :schema="allSchemas.detailSchema" :data="detailData"> - <template #duration="{ row }"> - <span>{{ row.duration + 'ms' }}</span> - </template> - <template #resultCode="{ row }"> - <span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span> - </template> - </Descriptions> - <!-- 操作按钮 --> - <template #footer> - <XButton :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> + </el-form-item> + <el-form-item label="用户类型" prop="userType"> + <el-select + v-model="queryParams.userType" + placeholder="请选择用户类型" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getDictOptions(DICT_TYPE.USER_TYPE)" + :key="parseInt(dict.value)" + :label="dict.label" + :value="parseInt(dict.value)" + /> + </el-select> + </el-form-item> + <el-form-item label="应用名" prop="applicationName"> + <el-input + v-model="queryParams.applicationName" + placeholder="请输入应用名" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="请求时间" prop="beginTime"> + <el-date-picker + v-model="queryParams.beginTime" + value-format="yyyy-MM-dd HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="执行时长" prop="duration"> + <el-input + v-model="queryParams.duration" + placeholder="请输入执行时长" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="结果码" prop="resultCode"> + <el-input + v-model="queryParams.resultCode" + placeholder="请输入结果码" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['infra:api-error-log:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> + </content-wrap> + + <!-- 列表 --> + <content-wrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="日志编号" align="center" prop="id" /> + <el-table-column label="用户编号" align="center" prop="userId" /> + <el-table-column label="用户类型" align="center" prop="userType"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> + </template> + </el-table-column> + <el-table-column label="应用名" align="center" prop="applicationName" /> + <el-table-column label="请求方法" align="center" prop="requestMethod" width="80" /> + <el-table-column label="请求地址" align="center" prop="requestUrl" width="250" /> + <el-table-column label="请求时间" align="center" prop="beginTime" width="180"> + <template #default="scope"> + <span>{{ formatDate(scope.row.beginTime) }}</span> + </template> + </el-table-column> + <el-table-column label="执行时长" align="center" prop="duration" width="180"> + <template #default="scope"> + <span>{{ scope.row.duration }} ms</span> + </template> + </el-table-column> + <el-table-column label="操作结果" align="center" prop="status"> + <template #default="scope"> + <span>{{ + scope.row.resultCode === 0 ? '成功' : '失败(' + scope.row.resultMsg + ')' + }}</span> + </template> + </el-table-column> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openDetail(scope.row)" + v-hasPermi="['infra:api-access-log:query']" + > + 详细 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页组件 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </content-wrap> + + <!-- 表单弹窗:详情 --> + <ApiAccessLogDetail ref="detailRef" /> </template> + <script setup lang="ts" name="ApiAccessLog"> -import { allSchemas } from './apiAccessLog.data' +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import download from '@/utils/download' +import { formatDate } from '@/utils/formatTime' import * as ApiAccessLogApi from '@/api/infra/apiAccessLog' +import ApiAccessLogDetail from './ApiAccessLogDetail.vue' -const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 -// 列表相关的变量 -const [registerTable] = useXTable({ - allSchemas: allSchemas, - topActionSlots: false, - getListApi: ApiAccessLogApi.getApiAccessLogPageApi +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + userId: null, + userType: null, + applicationName: null, + requestUrl: null, + duration: null, + resultCode: null, + beginTime: [] }) -// ========== 详情相关 ========== -const detailData = ref() // 详情 Ref -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('') // 弹出层标题 +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 -// 详情操作 -const handleDetail = (row: ApiAccessLogApi.ApiAccessLogVO) => { - // 设置数据 - detailData.value = row - dialogTitle.value = t('action.detail') - dialogVisible.value = true +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ApiAccessLogApi.getApiAccessLogPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } } + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 详情操作 */ +const detailRef = ref() +const openDetail = (data: ApiAccessLogApi.ApiAccessLogVO) => { + detailRef.value.open(data) +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await ApiAccessLogApi.exportApiAccessLog(queryParams) + download.excel(data, 'API 访问日志.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue b/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue new file mode 100644 index 00000000..5076fe00 --- /dev/null +++ b/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue @@ -0,0 +1,79 @@ +<template> + <Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="500" width="800"> + <el-descriptions border :column="1"> + <el-descriptions-item label="日志主键" min-width="120"> + {{ detailData.id }} + </el-descriptions-item> + <el-descriptions-item label="链路追踪"> + {{ detailData.traceId }} + </el-descriptions-item> + <el-descriptions-item label="应用名"> + {{ detailData.applicationName }} + </el-descriptions-item> + <el-descriptions-item label="用户编号"> + {{ detailData.userId }} + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" /> + </el-descriptions-item> + <el-descriptions-item label="用户 IP"> + {{ detailData.userIp }} + </el-descriptions-item> + <el-descriptions-item label="用户 UA"> + {{ detailData.userAgent }} + </el-descriptions-item> + <el-descriptions-item label="请求信息"> + {{ detailData.requestMethod }} {{ detailData.requestUrl }} + </el-descriptions-item> + <el-descriptions-item label="请求参数"> + {{ detailData.requestParams }} + </el-descriptions-item> + <el-descriptions-item label="异常时间"> + {{ formatDate(detailData.exceptionTime) }} + </el-descriptions-item> + <el-descriptions-item label="异常名"> + {{ detailData.exceptionName }} + </el-descriptions-item> + <el-descriptions-item label="异常堆栈" v-if="detailData.exceptionStackTrace"> + <el-input + type="textarea" + :readonly="true" + :autosize="{ maxRows: 20 }" + v-model="detailData.exceptionStackTrace" + /> + </el-descriptions-item> + <el-descriptions-item label="处理状态"> + <dict-tag + :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" + :value="detailData.processStatus" + /> + </el-descriptions-item> + <el-descriptions-item label="处理人" v-if="detailData.processUserId"> + {{ detailData.processUserId }} + </el-descriptions-item> + <el-descriptions-item label="处理时间" v-if="detailData.processTime"> + {{ formatDate(detailData.processTime) }} + </el-descriptions-item> + </el-descriptions> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import * as ApiErrorLog from '@/api/infra/apiErrorLog' + +const modelVisible = ref(false) // 弹窗的是否展示 +const detailLoading = ref(false) // 表单的加载中 +const detailData = ref() // 详情数据 + +/** 打开弹窗 */ +const open = async (data: ApiErrorLog.ApiErrorLogVO) => { + modelVisible.value = true + // 设置数据 + detailLoading.value = true + try { + detailData.value = data + } finally { + detailLoading.value = false + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> diff --git a/src/views/infra/apiErrorLog/apiErrorLog.data.ts b/src/views/infra/apiErrorLog/apiErrorLog.data.ts deleted file mode 100644 index a539c167..00000000 --- a/src/views/infra/apiErrorLog/apiErrorLog.data.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '日志编号', - action: true, - actionWidth: '300', - columns: [ - { - title: '链路追踪', - field: 'traceId', - isTable: false - }, - { - title: '用户编号', - field: 'userId', - isSearch: true - }, - { - title: '用户类型', - field: 'userType', - dictType: DICT_TYPE.USER_TYPE, - isSearch: true - }, - { - title: '应用名', - field: 'applicationName', - isSearch: true - }, - { - title: '请求方法名', - field: 'requestMethod' - }, - { - title: '请求地址', - field: 'requestUrl', - isSearch: true - }, - { - title: '异常发生时间', - field: 'exceptionTime', - formatter: 'formatDate', - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - }, - { - title: '异常名', - field: 'exceptionName' - }, - { - title: '处理状态', - field: 'processStatus', - dictType: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: '处理人', - field: 'processUserId', - isTable: false - }, - { - title: '处理时间', - field: 'processTime', - formatter: 'formatDate', - isTable: false - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/infra/apiErrorLog/index.vue b/src/views/infra/apiErrorLog/index.vue index 4193351a..cfe4adb1 100644 --- a/src/views/infra/apiErrorLog/index.vue +++ b/src/views/infra/apiErrorLog/index.vue @@ -1,99 +1,248 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 操作:导出 --> - <template #toolbar_buttons> - <XButton - type="warning" - preIcon="ep:download" - :title="t('action.export')" - @click="exportList('错误数据.xls')" + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="用户编号" prop="userId"> + <el-input + v-model="queryParams.userId" + placeholder="请输入用户编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - </template> - <template #duration_default="{ row }"> - <span>{{ row.duration + 'ms' }}</span> - </template> - <template #resultCode_default="{ row }"> - <span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['infra:api-access-log:query']" - @click="handleDetail(row)" + </el-form-item> + <el-form-item label="用户类型" prop="userType"> + <el-select + v-model="queryParams.userType" + placeholder="请选择用户类型" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="应用名" prop="applicationName"> + <el-input + v-model="queryParams.applicationName" + placeholder="请输入应用名" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - <XTextButton - preIcon="ep:cpu" - title="已处理" - v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" - v-hasPermi="['infra:api-error-log:update-status']" - @click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.DONE, '已处理')" + </el-form-item> + <el-form-item label="异常时间" prop="exceptionTime"> + <el-date-picker + v-model="queryParams.exceptionTime" + value-format="yyyy-MM-dd HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" /> - <XTextButton - preIcon="ep:mute-notification" - title="已忽略" - v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" - v-hasPermi="['infra:api-error-log:update-status']" - @click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.IGNORE, '已忽略')" - /> - </template> - </XTable> + </el-form-item> + <el-form-item label="处理状态" prop="processStatus"> + <el-select + v-model="queryParams.processStatus" + placeholder="请选择处理状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['infra:api-error-log:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> </ContentWrap> - <XModal v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(详情) --> - <Descriptions :schema="allSchemas.detailSchema" :data="detailData" /> - <!-- 操作按钮 --> - <template #footer> - <XButton :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="日志编号" align="center" prop="id" /> + <el-table-column label="用户编号" align="center" prop="userId" /> + <el-table-column label="用户类型" align="center" prop="userType"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> + </template> + </el-table-column> + <el-table-column label="应用名" align="center" prop="applicationName" width="200" /> + <el-table-column label="请求方法" align="center" prop="requestMethod" width="80" /> + <el-table-column label="请求地址" align="center" prop="requestUrl" width="180" /> + <el-table-column + label="异常发生时间" + align="center" + prop="exceptionTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="异常名" align="center" prop="exceptionName" width="180" /> + <el-table-column label="处理状态" align="center" prop="processStatus"> + <template #default="scope"> + <dict-tag + :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" + :value="scope.row.processStatus" + /> + </template> + </el-table-column> + <el-table-column label="操作" align="center" width="200"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openDetail(scope.row)" + v-hasPermi="['infra:api-error-log:query']" + > + 详细 + </el-button> + <el-button + link + type="primary" + v-if="scope.row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" + @click="handleProcess(scope.row.id, InfraApiErrorLogProcessStatusEnum.DONE)" + v-hasPermi="['infra:api-error-log:update-status']" + > + 已处理 + </el-button> + <el-button + link + type="primary" + v-if="scope.row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" + @click="handleProcess(scope.row.id, InfraApiErrorLogProcessStatusEnum.IGNORE)" + v-hasPermi="['infra:api-error-log:update-status']" + > + 已忽略 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页组件 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:详情 --> + <ApiErrorLogDetail ref="detailRef" /> </template> + <script setup lang="ts" name="ApiErrorLog"> -import { allSchemas } from './apiErrorLog.data' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' import * as ApiErrorLogApi from '@/api/infra/apiErrorLog' +import ApiErrorLogDetail from './ApiErrorLogDetail.vue' import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants' -const { t } = useI18n() // 国际化 -const message = useMessage() +const message = useMessage() // 消息弹窗 -// ========== 列表相关 ========== -const [registerTable, { reload, exportList }] = useXTable({ - allSchemas: allSchemas, - getListApi: ApiErrorLogApi.getApiErrorLogPageApi, - exportListApi: ApiErrorLogApi.exportApiErrorLogApi +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + userId: null, + userType: null, + applicationName: null, + requestUrl: null, + processStatus: null, + exceptionTime: [] }) -// ========== 详情相关 ========== -const detailData = ref() // 详情 Ref -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('') // 弹出层标题 +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 -// 详情操作 -const handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => { - // 设置数据 - detailData.value = row - dialogTitle.value = t('action.detail') - dialogVisible.value = true +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ApiErrorLogApi.getApiErrorLogPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } } -// 异常处理操作 -const handleProcessClick = ( - row: ApiErrorLogApi.ApiErrorLogVO, - processSttatus: number, - type: string -) => { - message - .confirm('确认标记为' + type + '?', t('common.reminder')) - .then(async () => { - await ApiErrorLogApi.updateApiErrorLogPageApi(row.id, processSttatus) - message.success(t('common.updateSuccess')) - }) - .finally(async () => { - // 刷新列表 - await reload() - }) - .catch(() => {}) +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() } + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 详情操作 */ +const detailRef = ref() +const openDetail = (data: ApiErrorLogApi.ApiErrorLogVO) => { + detailRef.value.open(data) +} + +/** 处理已处理 / 已忽略的操作 **/ +const handleProcess = async (id: number, processStatus: number) => { + try { + // 操作的二次确认 + const type = processStatus === InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略' + await message.confirm('确认标记为' + type + '?') + // 执行操作 + await ApiErrorLogApi.updateApiErrorLogPage(id, processStatus) + await message.success(type) + // 刷新列表 + await getList() + } catch {} +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await ApiErrorLogApi.exportApiErrorLog(queryParams) + download.excel(data, '异常日志.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/infra/build/index.vue b/src/views/infra/build/index.vue index 6f577e95..00b56fea 100644 --- a/src/views/infra/build/index.vue +++ b/src/views/infra/build/index.vue @@ -3,77 +3,99 @@ <el-row> <el-col> <div class="mb-2 float-right"> - <el-button size="small" @click="setJson"> 导入JSON</el-button> - <el-button size="small" @click="setOption"> 导入Options</el-button> - <el-button size="small" type="primary" @click="showJson">生成JSON</el-button> - <el-button size="small" type="success" @click="showOption">生成Options</el-button> + <el-button size="small" type="primary" @click="showJson">生成 JSON</el-button> + <el-button size="small" type="success" @click="showOption">生成O ptions</el-button> <el-button size="small" type="danger" @click="showTemplate">生成组件</el-button> - <!-- <el-button size="small" @click="changeLocale">中英切换</el-button> --> </div> </el-col> + <!-- 表单设计器 --> <el-col> <fc-designer ref="designer" height="780px" /> </el-col> </el-row> - <Dialog :title="dialogTitle" v-model="dialogVisible" maxHeight="600"> - <div ref="editor" v-if="dialogVisible"> - <XTextButton style="float: right" :title="t('common.copy')" @click="copy(formValue)" /> - <el-scrollbar height="580"> - <div v-highlight> - <code class="hljs"> - {{ formValue }} - </code> - </div> - </el-scrollbar> - </div> - <span style="color: red" v-if="err">输入内容格式有误!</span> - </Dialog> </ContentWrap> + + <!-- 弹窗:表单预览 --> + <Dialog :title="dialogTitle" v-model="dialogVisible" max-height="600"> + <div ref="editor" v-if="dialogVisible"> + <el-button style="float: right" @click="copy(formData)"> + {{ t('common.copy') }} + </el-button> + <el-scrollbar height="580"> + <div v-highlight> + <code class="hljs"> + {{ formData }} + </code> + </div> + </el-scrollbar> + </div> + </Dialog> </template> <script setup lang="ts" name="Build"> import formCreate from '@form-create/element-ui' import { useClipboard } from '@vueuse/core' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息 -const { t } = useI18n() -const message = useMessage() - -const designer = ref() - -const dialogVisible = ref(false) -const dialogTitle = ref('') -const err = ref(false) -const type = ref(-1) -const formValue = ref('') +const designer = ref() // 表单设计器 +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formType = ref(-1) // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件 +const formData = ref('') // 表单数据 +/** 打开弹窗 */ const openModel = (title: string) => { dialogVisible.value = true dialogTitle.value = title } -const setJson = () => { - openModel('导入JSON--未实现') -} -const setOption = () => { - openModel('导入Options--未实现') -} +/** 生成 JSON */ const showJson = () => { - openModel('生成JSON') - type.value = 0 - formValue.value = designer.value.getRule() + openModel('生成 JSON') + formType.value = 0 + formData.value = designer.value.getRule() } + +/** 生成 Options */ const showOption = () => { - openModel('生成Options') - type.value = 1 - formValue.value = designer.value.getOption() + openModel('生成 Options') + formType.value = 1 + formData.value = designer.value.getOption() } + +/** 生成组件 */ const showTemplate = () => { openModel('生成组件') - type.value = 2 - formValue.value = makeTemplate() + formType.value = 2 + formData.value = makeTemplate() +} + +const makeTemplate = () => { + const rule = designer.value.getRule() + const opt = designer.value.getOption() + return `<template> + <form-create + v-model="fapi" + :rule="rule" + :option="option" + @submit="onSubmit" + ></form-create> + </template> + <script setup lang=ts> + import formCreate from "@form-create/element-ui"; + const faps = ref(null) + const rule = ref('') + const option = ref('') + const init = () => { + rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}') + option.value = formCreate.parseJson('${JSON.stringify(opt)}') + } + const onSubmit = (formData) => { + //todo 提交表单 + } + init() + <\/script>` } -// const changeLocale = () => { -// console.info('changeLocale') -// } /** 复制 **/ const copy = async (text: string) => { @@ -87,31 +109,4 @@ const copy = async (text: string) => { } } } - -const makeTemplate = () => { - const rule = designer.value.getRule() - const opt = designer.value.getOption() - return `<template> - <form-create - v-model="fapi" - :rule="rule" - :option="option" - @submit="onSubmit" - ></form-create> -</template> -<script setup lang=ts> - import formCreate from "@form-create/element-ui"; - const faps = ref(null) - const rule = ref('') - const option = ref('') - const init = () => { - rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}') - option.value = formCreate.parseJson('${JSON.stringify(opt)}') - } - const onSubmit = (formData) => { - //todo 提交表单 - } - init() -<\/script>` -} </script> diff --git a/src/views/infra/codegen/components/BasicInfoForm.vue b/src/views/infra/codegen/components/BasicInfoForm.vue index 2009553f..5ab820a2 100644 --- a/src/views/infra/codegen/components/BasicInfoForm.vue +++ b/src/views/infra/codegen/components/BasicInfoForm.vue @@ -6,7 +6,7 @@ import { useForm } from '@/hooks/web/useForm' import { FormSchema } from '@/types/form' import { CodegenTableVO } from '@/api/infra/codegen/types' import { getIntDictOptions } from '@/utils/dict' -import { listSimpleMenusApi } from '@/api/system/menu' +import { getSimpleMenusList } from '@/api/system/menu' import { handleTree, defaultProps } from '@/utils/tree' import { PropType } from 'vue' @@ -21,7 +21,7 @@ const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_T const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE) const menuOptions = ref<any>([]) // 树形结构 const getTree = async () => { - const res = await listSimpleMenusApi() + const res = await getSimpleMenusList() menuOptions.value = handleTree(res) } diff --git a/src/views/infra/codegen/components/ImportTable.vue b/src/views/infra/codegen/components/ImportTable.vue index 38a81541..aebe7a8f 100644 --- a/src/views/infra/codegen/components/ImportTable.vue +++ b/src/views/infra/codegen/components/ImportTable.vue @@ -41,10 +41,8 @@ <vxe-column field="comment" title="表描述" /> </vxe-table> <template #footer> - <div class="dialog-footer"> - <XButton type="primary" :title="t('action.import')" @click="handleImportTable()" /> - <XButton :title="t('dialog.close')" @click="handleClose()" /> - </div> + <XButton type="primary" :title="t('action.import')" @click="handleImportTable()" /> + <XButton :title="t('dialog.close')" @click="handleClose()" /> </template> </XModal> </template> @@ -52,7 +50,7 @@ import { VxeTableInstance } from 'vxe-table' import type { DatabaseTableVO } from '@/api/infra/codegen/types' import { getSchemaTableListApi, createCodegenListApi } from '@/api/infra/codegen' -import { getDataSourceConfigListApi, DataSourceConfigVO } from '@/api/infra/dataSourceConfig' +import { getDataSourceConfigList, DataSourceConfigVO } from '@/api/infra/dataSourceConfig' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -63,13 +61,13 @@ const dbLoading = ref(true) const queryParams = reactive({ name: undefined, comment: undefined, - dataSourceConfigId: 0 + dataSourceConfigId: 0 as number | undefined }) const dataSourceConfigs = ref<DataSourceConfigVO[]>([]) const show = async () => { - const res = await getDataSourceConfigListApi() + const res = await getDataSourceConfigList() dataSourceConfigs.value = res - queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id + queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id as number visible.value = true await getList() } diff --git a/src/views/infra/config/form.vue b/src/views/infra/config/form.vue index 30e2f4d9..8d96b629 100644 --- a/src/views/infra/config/form.vue +++ b/src/views/infra/config/form.vue @@ -35,10 +35,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/infra/dataSourceConfig/form.vue b/src/views/infra/dataSourceConfig/form.vue index ea699b57..cd79e24b 100644 --- a/src/views/infra/dataSourceConfig/form.vue +++ b/src/views/infra/dataSourceConfig/form.vue @@ -21,10 +21,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/infra/file/form.vue b/src/views/infra/file/form.vue index 15ef02fd..b0a76e0e 100644 --- a/src/views/infra/file/form.vue +++ b/src/views/infra/file/form.vue @@ -23,10 +23,8 @@ </template> </el-upload> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitFileForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitFileForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/infra/fileConfig/form.vue b/src/views/infra/fileConfig/form.vue index a23aac9f..f08ba4c4 100644 --- a/src/views/infra/fileConfig/form.vue +++ b/src/views/infra/fileConfig/form.vue @@ -93,10 +93,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/infra/job/JobLog.vue b/src/views/infra/job/JobLog.vue index 1bf9d745..ba397d51 100644 --- a/src/views/infra/job/JobLog.vue +++ b/src/views/infra/job/JobLog.vue @@ -12,11 +12,7 @@ /> </template> <template #beginTime_default="{ row }"> - <span>{{ - dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') + - ' ~ ' + - dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss') - }}</span> + <span>{{ parseTime(row.beginTime) + ' ~ ' + parseTime(row.endTime) }}</span> </template> <template #duration_default="{ row }"> <span>{{ row.duration + ' 毫秒' }}</span> @@ -48,7 +44,7 @@ </XModal> </template> <script setup lang="ts" name="JobLog"> -import dayjs from 'dayjs' +import { parseTime } from '@/utils/formatTime' import * as JobLogApi from '@/api/infra/jobLog' import { allSchemas } from './jobLog.data' diff --git a/src/views/infra/webSocket/index.vue b/src/views/infra/webSocket/index.vue index 655045c0..f090ba9b 100644 --- a/src/views/infra/webSocket/index.vue +++ b/src/views/infra/webSocket/index.vue @@ -44,7 +44,7 @@ <li v-for="item in getList" class="mt-2" :key="item.time"> <div class="flex items-center"> <span class="mr-2 text-primary font-medium">收到消息:</span> - <span>{{ dayjs(item.time).format('YYYY-MM-DD HH:mm:ss') }}</span> + <span>{{ parseTime(item.time) }}</span> </div> <div> {{ item.res }} @@ -56,7 +56,7 @@ </div> </template> <script setup lang="ts"> -import dayjs from 'dayjs' +import { parseTime } from '@/utils/formatTime' import { useUserStore } from '@/store/modules/user' import { useWebSocket } from '@vueuse/core' diff --git a/src/views/mp/account/AccountForm.vue b/src/views/mp/account/AccountForm.vue new file mode 100644 index 00000000..406db8fe --- /dev/null +++ b/src/views/mp/account/AccountForm.vue @@ -0,0 +1,157 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="rules" + label-width="120px" + v-loading="formLoading" + > + <el-form-item label="名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入名称" /> + </el-form-item> + <el-form-item label="微信号" prop="account"> + <template #label> + <span> + <el-tooltip + content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 账号详情] 中能找到「微信号」" + placement="top" + > + <Icon icon="ep:question-filled" style="vertical-align: middle" /> + </el-tooltip> + 微信号 + </span> + </template> + <el-input v-model="formData.account" placeholder="请输入微信号" /> + </el-form-item> + <el-form-item label="appId" prop="appId"> + <template #label> + <span> + <el-tooltip + content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者ID(AppID)」" + placement="top" + > + <Icon icon="ep:question-filled" style="vertical-align: middle" /> + </el-tooltip> + appId + </span> + </template> + <el-input v-model="formData.appId" placeholder="请输入公众号 appId" /> + </el-form-item> + <el-form-item label="appSecret" prop="appSecret"> + <template #label> + <span> + <el-tooltip + content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者密码(AppSecret)」" + placement="top" + > + <Icon icon="ep:question-filled" style="vertical-align: middle" /> + </el-tooltip> + appSecret + </span> + </template> + <el-input v-model="formData.appSecret" placeholder="请输入公众号 appSecret" /> + </el-form-item> + <el-form-item label="token" prop="token"> + <el-input v-model="formData.token" placeholder="请输入公众号token" /> + </el-form-item> + <el-form-item label="消息加解密密钥" prop="aesKey"> + <el-input v-model="formData.aesKey" placeholder="请输入消息加解密密钥" /> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import * as AccountApi from '@/api/mp/account' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: '', + account: '', + appId: '', + appSecret: '', + token: '', + aesKey: '', + remark: '' +}) +const rules = reactive({ + name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], + account: [{ required: true, message: '公众号账号不能为空', trigger: 'blur' }], + appId: [{ required: true, message: '公众号 appId 不能为空', trigger: 'blur' }], + appSecret: [{ required: true, message: '公众号密钥不能为空', trigger: 'blur' }], + token: [{ required: true, message: '公众号 token 不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await AccountApi.getAccount(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value + if (formType.value === 'create') { + await AccountApi.createAccount(data) + message.success(t('common.createSuccess')) + } else { + await AccountApi.updateAccount(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 表单重置 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: '', + account: '', + appId: '', + appSecret: '', + token: '', + aesKey: '', + remark: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/mp/account/index.vue b/src/views/mp/account/index.vue index 497f72ec..3489998b 100644 --- a/src/views/mp/account/index.vue +++ b/src/views/mp/account/index.vue @@ -1,3 +1,192 @@ <template> - <span>开发中</span> + <!-- 搜索工作栏 --> + <content-wrap> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button> + <el-button type="primary" @click="openForm('create')" v-hasPermi="['mp:account:create']"> + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + </el-form-item> + </el-form> + </content-wrap> + + <!-- 列表 --> + <content-wrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="名称" align="center" prop="name" /> + <el-table-column label="微信号" align="center" prop="account" width="180" /> + <el-table-column label="appId" align="center" prop="appId" width="180" /> + <el-table-column label="服务器地址(URL)" align="center" prop="appId" width="360"> + <template #default="scope"> + {{ 'http://服务端地址/mp/open/' + scope.row.appId }} + </template> + </el-table-column> + <el-table-column label="二维码" align="center" prop="qrCodeUrl"> + <template #default="scope"> + <img + v-if="scope.row.qrCodeUrl" + :src="scope.row.qrCodeUrl" + alt="二维码" + style="height: 100px; display: inline-block" + /> + <el-button + link + type="primary" + @click="handleGenerateQrCode(scope.row)" + v-hasPermi="['mp:account:qr-code']" + > + 生成二维码 + </el-button> + </template> + </el-table-column> + <el-table-column label="备注" align="center" prop="remark" /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['mp:account:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['mp:account:delete']" + > + 删除 + </el-button> + <el-button + link + type="danger" + @click="handleCleanQuota(scope.row)" + v-hasPermi="['mp:account:clear-quota']" + > + 清空 API 配额 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页组件 --> + <pagination + v-show="total > 0" + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </content-wrap> + + <!-- 对话框(添加 / 修改) --> + <AccountForm ref="formRef" @success="getList" /> </template> +<script setup lang="ts" name="MpAccount"> +import * as AccountApi from '@/api/mp/account' +import AccountForm from './AccountForm.vue' + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: null, + account: null, + appId: null +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + // 处理查询参数 + let params = { ...queryParams } + // 执行查询 + const data = await AccountApi.getAccountPage(params) + list.value = data.list + total.value = data.total + loading.value = false +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await AccountApi.deleteAccount(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 生成二维码的按钮操作 */ +const handleGenerateQrCode = async (row) => { + try { + // 生成二维码的二次确认 + await message.confirm('是否确认生成公众号账号编号为"' + row.name + '"的二维码?') + // 发起生成二维码 + await AccountApi.generateAccountQrCode(row.id) + message.success('生成二维码成功') + // 刷新列表 + await getList() + } catch {} +} + +/** 清空二维码 API 配额的按钮操作 */ +const handleCleanQuota = async (row) => { + try { + // 清空 API 配额的二次确认 + await message.confirm('是否确认清空生成公众号账号编号为"' + row.name + '"的 API 配额?') + // 发起清空 API 配额 + await AccountApi.clearAccountQuota(row.id) + message.success('清空 API 配额成功') + } catch {} +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/mp/components/img.png b/src/views/mp/components/img.png new file mode 100644 index 00000000..c25a6e76 Binary files /dev/null and b/src/views/mp/components/img.png differ diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue new file mode 100644 index 00000000..47eab571 --- /dev/null +++ b/src/views/mp/components/wx-location/main.vue @@ -0,0 +1,71 @@ +<!-- + 【微信消息 - 定位】 +--> +<template> + <div> + <el-link + type="primary" + target="_blank" + :href=" + 'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx=' + + locationY + + '&pointy=' + + locationX + + '&name=' + + label + + '&ref=yudao' + " + > + <el-col> + <el-row> + <img + :src=" + 'https://apis.map.qq.com/ws/staticmap/v2/?zoom=10&markers=color:blue|label:A|' + + locationX + + ',' + + locationY + + '&key=' + + qqMapKey + + '&size=250*180' + " + /> + </el-row> + <el-row> + <el-icon><Location /></el-icon> + <Icon icon="ep:location" /> + {{ label }} + </el-row> + </el-col> + </el-link> + </div> +</template> +<script setup lang="ts" name="WxLocation"> +const props = defineProps({ + locationX: { + required: true, + type: Number + }, + locationY: { + required: true, + type: Number + }, + label: { + // 地名 + required: true, + type: String + }, + qqMapKey: { + // QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc + required: false, + type: String, + default: 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E' // 需要自定义 + } +}) + +defineExpose({ + locationX: props.locationX, + locationY: props.locationY, + label: props.label, + qqMapKey: props.qqMapKey +}) +</script> diff --git a/src/views/mp/components/wx-msg/card.scss b/src/views/mp/components/wx-msg/card.scss new file mode 100644 index 00000000..67ac9219 --- /dev/null +++ b/src/views/mp/components/wx-msg/card.scss @@ -0,0 +1,101 @@ +.avue-card{ + &__item{ + margin-bottom: 16px; + border: 1px solid #e8e8e8; + background-color: #fff; + box-sizing: border-box; + color: rgba(0,0,0,.65); + font-size: 14px; + font-variant: tabular-nums; + line-height: 1.5; + list-style: none; + font-feature-settings: "tnum"; + cursor: pointer; + height:200px; + &:hover{ + border-color: rgba(0,0,0,.09); + box-shadow: 0 2px 8px rgba(0,0,0,.09); + } + &--add{ + border:1px dashed #000; + width: 100%; + color: rgba(0,0,0,.45); + background-color: #fff; + border-color: #d9d9d9; + border-radius: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + i{ + margin-right: 10px; + } + &:hover{ + color: #40a9ff; + background-color: #fff; + border-color: #40a9ff; + } + } + } + &__body{ + display: flex; + padding: 24px; + } + &__detail{ + flex:1 + } + &__avatar{ + width: 48px; + height: 48px; + border-radius: 48px; + overflow: hidden; + margin-right: 12px; + img{ + width: 100%; + height: 100%; + } + } + &__title{ + color: rgba(0,0,0,.85); + margin-bottom: 12px; + font-size: 16px; + &:hover{ + color:#1890ff; + } + } + &__info{ + color: rgba(0,0,0,.45); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + height: 64px; + } + &__menu{ + display: flex; + justify-content:space-around; + height: 50px; + background: #f7f9fa; + color: rgba(0,0,0,.45); + text-align: center; + line-height: 50px; + &:hover{ + color:#1890ff; + } + } +} + +/** joolun 额外加的 */ +.avue-comment__main { + flex: unset!important; + border-radius: 5px!important; + margin: 0 8px!important; +} +.avue-comment__header { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} +.avue-comment__body { + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} diff --git a/src/views/mp/components/wx-msg/comment.scss b/src/views/mp/components/wx-msg/comment.scss new file mode 100644 index 00000000..3f1341b2 --- /dev/null +++ b/src/views/mp/components/wx-msg/comment.scss @@ -0,0 +1,88 @@ +/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */ +.avue-comment{ + margin-bottom: 30px; + display: flex; + align-items: flex-start; + &--reverse{ + flex-direction:row-reverse; + .avue-comment__main{ + &:before,&:after{ + left: auto; + right: -8px; + border-width: 8px 0 8px 8px; + } + &:before{ + border-left-color: #dedede; + } + &:after{ + border-left-color: #f8f8f8; + margin-right: 1px; + margin-left: auto; + } + } + } + &__avatar{ + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid transparent; + box-sizing: border-box; + vertical-align: middle; + } + &__header{ + padding: 5px 15px; + background: #f8f8f8; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + justify-content: space-between; + } + &__author{ + font-weight: 700; + font-size: 14px; + color: #999; + } + &__main{ + flex:1; + margin: 0 20px; + position: relative; + border: 1px solid #dedede; + border-radius: 2px; + &:before,&:after{ + position: absolute; + top: 10px; + left: -8px; + right: 100%; + width: 0; + height: 0; + display: block; + content: " "; + border-color: transparent; + border-style: solid solid outset; + border-width: 8px 8px 8px 0; + pointer-events: none; + } + &:before { + border-right-color: #dedede; + z-index: 1; + } + &:after{ + border-right-color: #f8f8f8; + margin-left: 1px; + z-index: 2; + } + } + &__body{ + padding: 15px; + overflow: hidden; + background: #fff; + font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333; + font-size: 14px; + } + blockquote{ + margin:0; + font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif; + padding: 1px 0 1px 15px; + border-left: 4px solid #ddd; + } +} diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue new file mode 100644 index 00000000..b514a73e --- /dev/null +++ b/src/views/mp/components/wx-msg/main.vue @@ -0,0 +1,338 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 芋道源码: + ① 移除暂时用不到的 websocket + ② 代码优化,补充注释,提升阅读性 +--> +<template> + <div class="msg-main"> + <div class="msg-div" :id="'msg-div' + nowStr"> + <!-- 加载更多 --> + <div v-loading="loading"></div> + <div v-if="!loading"> + <div class="el-table__empty-block" v-if="loadMore" @click="loadingMore" + ><span class="el-table__empty-text">点击加载更多</span></div + > + <div class="el-table__empty-block" v-if="!loadMore" + ><span class="el-table__empty-text">没有更多了</span></div + > + </div> + <!-- 消息列表 --> + <div class="execution" v-for="item in list" :key="item.id"> + <div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''"> + <div class="avatar-div"> + <img + :src="item.sendFrom === 1 ? user.avatar : mp.avatar" + class="avue-comment__avatar" + /> + <div class="avue-comment__author">{{ + item.sendFrom === 1 ? user.nickname : mp.nickname + }}</div> + </div> + <div class="avue-comment__main"> + <div class="avue-comment__header"> + <div class="avue-comment__create_time">{{ parseTime(item.createTime) }}</div> + </div> + <div + class="avue-comment__body" + :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''" + > + <!-- 【事件】区域 --> + <div v-if="item.type === 'event' && item.event === 'subscribe'"> + <el-tag type="success" size="mini">关注</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'unsubscribe'"> + <el-tag type="danger" size="mini">取消关注</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'CLICK'"> + <el-tag size="mini">点击菜单</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'VIEW'"> + <el-tag size="mini">点击菜单链接</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'"> + <el-tag size="mini">扫码结果</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'scancode_push'"> + <el-tag size="mini">扫码结果</el-tag>【{{ item.eventKey }}】 + </div> + <div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'"> + <el-tag size="mini">系统拍照发图</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'"> + <el-tag size="mini">拍照或者相册</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'pic_weixin'"> + <el-tag size="mini">微信相册</el-tag> + </div> + <div v-else-if="item.type === 'event' && item.event === 'location_select'"> + <el-tag size="mini">选择地理位置</el-tag> + </div> + <div v-else-if="item.type === 'event'"> + <el-tag type="danger" size="mini">未知事件类型</el-tag> + </div> + <!-- 【消息】区域 --> + <div v-else-if="item.type === 'text'">{{ item.content }}</div> + <div v-else-if="item.type === 'voice'"> + <wx-voice-player :url="item.mediaUrl" :content="item.recognition" /> + </div> + <div v-else-if="item.type === 'image'"> + <a target="_blank" :href="item.mediaUrl"> + <img :src="item.mediaUrl" style="width: 100px" /> + </a> + </div> + <div + v-else-if="item.type === 'video' || item.type === 'shortvideo'" + style="text-align: center" + > + <wx-video-player :url="item.mediaUrl" /> + </div> + <div v-else-if="item.type === 'link'" class="avue-card__detail"> + <el-link type="success" :underline="false" target="_blank" :href="item.url"> + <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> + </el-link> + <div class="avue-card__info" style="height: unset">{{ item.description }}</div> + </div> + <!-- TODO 芋艿:待完善 --> + <div v-else-if="item.type === 'location'"> + <wx-location + :label="item.label" + :location-y="item.locationY" + :location-x="item.locationX" + /> + </div> + <div v-else-if="item.type === 'news'" style="width: 300px"> + <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> + <wx-news :articles="item.articles" /> + </div> + <div v-else-if="item.type === 'music'"> + <wx-music + :title="item.title" + :description="item.description" + :thumb-media-url="item.thumbMediaUrl" + :music-url="item.musicUrl" + :hq-music-url="item.hqMusicUrl" + /> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="msg-send" v-loading="sendLoading"> + <wx-reply-select ref="replySelect" :objData="objData" /> + <el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button> + </div> + </div> +</template> + +<script> +import { getMessagePage, sendMessage } from '@/api/mp/message' +import WxReplySelect from '@/views/mp/components/wx-reply/main.vue' +import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' +import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' +import WxNews from '@/views/mp/components/wx-news/main.vue' +import WxLocation from '@/views/mp/components/wx-location/main.vue' +import WxMusic from '@/views/mp/components/wx-music/main.vue' +import { getUser } from '@/api/mp/mpuser' + +export default { + name: 'WxMsg', + components: { + WxReplySelect, + WxVideoPlayer, + WxVoicePlayer, + WxNews, + WxLocation, + WxMusic + }, + props: { + userId: { + type: Number, + required: true + } + }, + data() { + return { + nowStr: new Date().getTime(), // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 + loading: false, // 消息列表是否正在加载中 + loadMore: true, // 是否可以加载更多 + list: [], // 消息列表 + queryParams: { + pageNo: 1, // 当前页数 + pageSize: 14, // 每页显示多少条 + accountId: undefined + }, + user: { + // 由于微信不再提供昵称,直接使用“用户”展示 + nickname: '用户', + avatar: require('@/assets/images/profile.jpg'), + accountId: 0 // 公众号账号编号 + }, + mp: { + nickname: '公众号', + avatar: require('@/assets/images/wechat.png') + }, + + // ========= 消息发送 ========= + sendLoading: false, // 发送消息是否加载中 + objData: { + // 微信发送消息 + type: 'text' + } + } + }, + created() { + // 获得用户信息 + getUser(this.userId).then((response) => { + this.user.nickname = + response.data.nickname && response.data.nickname.length > 0 + ? response.data.nickname + : this.user.nickname + this.user.avatar = + response.data.avatar && this.user.avatar.length > 0 + ? response.data.avatar + : this.user.avatar + this.user.accountId = response.data.accountId + // 设置公众号账号编号 + this.queryParams.accountId = response.data.accountId + this.objData.accountId = response.data.accountId + + // 加载消息 + console.log(this.queryParams) + this.refreshChange() + }) + }, + methods: { + sendMsg() { + if (!this.objData) { + return + } + // 公众号限制:客服消息,公众号只允许发送一条 + if (this.objData.type === 'news' && this.objData.articles.length > 1) { + this.objData.articles = [this.objData.articles[0]] + this.$message({ + showClose: true, + message: '图文消息条数限制在 1 条以内,已默认发送第一条', + type: 'success' + }) + } + + // 执行发送 + this.sendLoading = true + sendMessage( + Object.assign( + { + userId: this.userId + }, + { + ...this.objData + } + ) + ) + .then((response) => { + this.sendLoading = false + // 添加到消息列表,并滚动 + this.list = [...this.list, ...[response.data]] + this.scrollToBottom() + // 重置 objData 状态 + this.$refs['replySelect'].deleteObj() // 重置,避免 tab 的数据未清理 + }) + .catch(() => { + this.sendLoading = false + }) + }, + loadingMore() { + this.queryParams.pageNo++ + this.getPage(this.queryParams) + }, + getPage(page, params) { + this.loading = true + getMessagePage( + Object.assign( + { + pageNo: page.pageNo, + pageSize: page.pageSize, + userId: this.userId, + accountId: page.accountId + }, + params + ) + ).then((response) => { + // 计算当前的滚动高度 + const msgDiv = document.getElementById('msg-div' + this.nowStr) + let scrollHeight = 0 + if (msgDiv) { + scrollHeight = msgDiv.scrollHeight + } + + // 处理数据 + const data = response.data.list.reverse() + this.list = [...data, ...this.list] + this.loading = false + if (data.length < this.queryParams.pageSize || data.length === 0) { + this.loadMore = false + } + this.queryParams.pageNo = page.pageNo + this.queryParams.pageSize = page.pageSize + + // 滚动到原来的位置 + if (this.queryParams.pageNo === 1) { + // 定位到消息底部 + this.scrollToBottom() + } else if (data.length !== 0) { + // 定位滚动条 + this.$nextTick(() => { + if (scrollHeight !== 0) { + msgDiv.scrollTop = + document.getElementById('msg-div' + this.nowStr).scrollHeight - scrollHeight - 100 + } + }) + } + }) + }, + /** + * 刷新回调 + */ + refreshChange() { + this.getPage(this.queryParams) + }, + /** 定位到消息底部 */ + scrollToBottom: function () { + this.$nextTick(() => { + let div = document.getElementById('msg-div' + this.nowStr) + div.scrollTop = div.scrollHeight + }) + } + } +} +</script> +<style lang="scss" scoped> +/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ +@import './comment.scss'; +@import './card.scss'; + +.msg-main { + margin-top: -30px; + padding: 10px; +} +.msg-div { + height: 50vh; + overflow: auto; + background-color: #eaeaea; + margin-left: 10px; + margin-right: 10px; +} +.msg-send { + padding: 10px; +} +.avatar-div { + text-align: center; + width: 80px; +} +.send-but { + float: right; + margin-top: 8px !important; +} +</style> diff --git a/src/views/mp/components/wx-music/main.vue b/src/views/mp/components/wx-music/main.vue new file mode 100644 index 00000000..52555f15 --- /dev/null +++ b/src/views/mp/components/wx-music/main.vue @@ -0,0 +1,60 @@ +<!-- + 【微信消息 - 音乐】 +--> +<template> + <div> + <el-link + type="success" + :underline="false" + target="_blank" + :href="hqMusicUrl ? hqMusicUrl : musicUrl" + > + <div + class="avue-card__body" + style="padding: 10px; background-color: #fff; border-radius: 5px" + > + <div class="avue-card__avatar"> + <img :src="thumbMediaUrl" alt="" /> + </div> + <div class="avue-card__detail"> + <div class="avue-card__title" style="margin-bottom: unset">{{ title }}</div> + <div class="avue-card__info" style="height: unset">{{ description }}</div> + </div> + </div> + </el-link> + </div> +</template> + +<script setup lang="ts" name="WxMusic"> +const props = defineProps({ + title: { + required: false, + type: String + }, + description: { + required: false, + type: String + }, + musicUrl: { + required: false, + type: String + }, + hqMusicUrl: { + required: false, + type: String + }, + thumbMediaUrl: { + required: true, + type: String + } +}) + +defineExpose({ + musicUrl: props.musicUrl +}) +</script> + +<style lang="scss" scoped> +/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */ +@import '../wx-msg/card.scss'; +</style> diff --git a/src/views/mp/components/wx-news/main.vue b/src/views/mp/components/wx-news/main.vue new file mode 100644 index 00000000..d08e2813 --- /dev/null +++ b/src/views/mp/components/wx-news/main.vue @@ -0,0 +1,107 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 【微信消息 - 图文】 + 芋道源码: + ① 代码优化,补充注释,提升阅读性 +--> +<template> + <div class="news-home"> + <div v-for="(article, index) in articles" :key="index" class="news-div"> + <!-- 头条 --> + <a target="_blank" :href="article.url" v-if="index === 0"> + <div class="news-main"> + <div class="news-content"> + <el-image + class="material-img" + style="width: 100%; height: 120px" + :src="article.picUrl" + /> + <div class="news-content-title"> + <span>{{ article.title }}</span> + </div> + </div> + </div> + </a> + <!-- 二条/三条等等 --> + <a target="_blank" :href="article.url" v-else> + <div class="news-main-item"> + <div class="news-content-item"> + <div class="news-content-item-title">{{ article.title }}</div> + <div class="news-content-item-img"> + <img class="material-img" :src="article.picUrl" height="100%" /> + </div> + </div> + </div> + </a> + </div> + </div> +</template> + +<script setup lang="ts"> +const props = defineProps({ + articles: { + type: Array, + default: () => null + } +}) + +defineExpose({ + articles: props.articles +}) +</script> + +<style lang="scss" scoped> +.news-home { + background-color: #ffffff; + width: 100%; + margin: auto; +} +.news-main { + width: 100%; + margin: auto; +} +.news-content { + background-color: #acadae; + width: 100%; + position: relative; +} +.news-content-title { + display: inline-block; + font-size: 12px; + color: #ffffff; + position: absolute; + left: 0; + bottom: 0; + background-color: black; + width: 98%; + padding: 1%; + opacity: 0.65; + white-space: normal; + box-sizing: unset !important; +} +.news-main-item { + background-color: #ffffff; + padding: 5px 0; + border-top: 1px solid #eaeaea; +} +.news-content-item { + position: relative; +} +.news-content-item-title { + display: inline-block; + font-size: 10px; + width: 70%; + margin-left: 1%; + white-space: normal; +} +.news-content-item-img { + display: inline-block; + width: 25%; + background-color: #acadae; + margin-right: 1%; +} +.material-img { + width: 100%; +} +</style> diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue new file mode 100644 index 00000000..57a3cd84 --- /dev/null +++ b/src/views/mp/components/wx-reply/main.vue @@ -0,0 +1,634 @@ +<!--<!–--> +<!-- - Copyright (C) 2018-2019--> +<!-- - All rights reserved, Designed By www.joolun.com--> +<!-- 芋道源码:--> +<!-- ① 移除多余的 rep 为前缀的变量,让 message 消息更简单--> +<!-- ② 代码优化,补充注释,提升阅读性--> +<!-- ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入--> +<!-- ④ 支持发送【视频】消息时,支持新建视频--> +<!--–>--> +<!--<template>--> +<!-- <el-tabs type="border-card" v-model="objData.type" @tab-click="handleClick">--> +<!-- <!– 类型 1:文本 –>--> +<!-- <el-tab-pane name="text">--> +<!-- <span slot="label"><i class="el-icon-document"></i> 文本</span>--> +<!-- <el-input--> +<!-- type="textarea"--> +<!-- :rows="5"--> +<!-- placeholder="请输入内容"--> +<!-- v-model="objData.content"--> +<!-- @input="inputContent"--> +<!-- />--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 2:图片 –>--> +<!-- <el-tab-pane name="image">--> +<!-- <span slot="label"><i class="el-icon-picture"></i> 图片</span>--> +<!-- <el-row>--> +<!-- <!– 情况一:已经选择好素材、或者上传好图片 –>--> +<!-- <div class="select-item" v-if="objData.url">--> +<!-- <img class="material-img" :src="objData.url" />--> +<!-- <p class="item-name" v-if="objData.name">{{ objData.name }}</p>--> +<!-- <el-row class="ope-row">--> +<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <!– 情况二:未做完上述操作 –>--> +<!-- <div v-else>--> +<!-- <el-row style="text-align: center">--> +<!-- <!– 选择素材 –>--> +<!-- <el-col :span="12" class="col-select">--> +<!-- <el-button type="success" @click="openMaterial">--> +<!-- 素材库选择<i class="el-icon-circle-check el-icon--right"></i>--> +<!-- </el-button>--> +<!-- <el-dialog--> +<!-- title="选择图片"--> +<!-- v-model:visible="dialogImageVisible"--> +<!-- width="90%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <wx-material-select :obj-data="objData" @selectMaterial="selectMaterial" />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <!– 文件上传 –>--> +<!-- <el-col :span="12" class="col-add">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeImageUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button type="primary">上传图片</el-button>--> +<!-- <div slot="tip" class="el-upload__tip"--> +<!-- >支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div--> +<!-- >--> +<!-- </el-upload>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </div>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 3:语音 –>--> +<!-- <el-tab-pane name="voice">--> +<!-- <span slot="label"><i class="el-icon-phone"></i> 语音</span>--> +<!-- <el-row>--> +<!-- <div class="select-item2" v-if="objData.url">--> +<!-- <p class="item-name">{{ objData.name }}</p>--> +<!-- <div class="item-infos">--> +<!-- <wx-voice-player :url="objData.url" />--> +<!-- </div>--> +<!-- <el-row class="ope-row">--> +<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <div v-else>--> +<!-- <el-row style="text-align: center">--> +<!-- <!– 选择素材 –>--> +<!-- <el-col :span="12" class="col-select">--> +<!-- <el-button type="success" @click="openMaterial">--> +<!-- 素材库选择<i class="el-icon-circle-check el-icon--right"></i>--> +<!-- </el-button>--> +<!-- <el-dialog--> +<!-- title="选择语音"--> +<!-- v-model:visible="dialogVoiceVisible"--> +<!-- width="90%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <WxMaterialSelect :objData="objData" @selectMaterial="selectMaterial" />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <!– 文件上传 –>--> +<!-- <el-col :span="12" class="col-add">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeVoiceUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button type="primary">点击上传</el-button>--> +<!-- <div slot="tip" class="el-upload__tip"--> +<!-- >格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s</div--> +<!-- >--> +<!-- </el-upload>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </div>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 4:视频 –>--> +<!-- <el-tab-pane name="video">--> +<!-- <span slot="label"><i class="el-icon-share"></i> 视频</span>--> +<!-- <el-row>--> +<!-- <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <div style="text-align: center">--> +<!-- <wx-video-player v-if="objData.url" :url="objData.url" />--> +<!-- </div>--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-row style="text-align: center">--> +<!-- <!– 选择素材 –>--> +<!-- <el-col :span="12">--> +<!-- <el-button type="success" @click="openMaterial">--> +<!-- 素材库选择<i class="el-icon-circle-check el-icon--right"></i>--> +<!-- </el-button>--> +<!-- <el-dialog--> +<!-- title="选择视频"--> +<!-- v-model:visible="dialogVideoVisible"--> +<!-- width="90%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <wx-material-select :objData="objData" @selectMaterial="selectMaterial" />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <!– 文件上传 –>--> +<!-- <el-col :span="12">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeVideoUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button type="primary"--> +<!-- >新建视频<i class="el-icon-upload el-icon--right"></i--> +<!-- ></el-button>--> +<!-- </el-upload>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 5:图文 –>--> +<!-- <el-tab-pane name="news">--> +<!-- <span slot="label"><i class="el-icon-news"></i> 图文</span>--> +<!-- <el-row>--> +<!-- <div class="select-item" v-if="objData.articles">--> +<!-- <wx-news :articles="objData.articles" />--> +<!-- <el-row class="ope-row">--> +<!-- <el-button type="danger" icon="el-icon-delete" circle @click="deleteObj" />--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <!– 选择素材 –>--> +<!-- <div v-if="!objData.content">--> +<!-- <el-row style="text-align: center">--> +<!-- <el-col :span="24">--> +<!-- <el-button type="success" @click="openMaterial"--> +<!-- >{{ newsType === '1' ? '选择已发布图文' : '选择草稿箱图文'--> +<!-- }}<i class="el-icon-circle-check el-icon--right"></i--> +<!-- ></el-button>--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- </div>--> +<!-- <el-dialog title="选择图文" v-model:visible="dialogNewsVisible" width="90%" append-to-body>--> +<!-- <wx-material-select--> +<!-- :objData="objData"--> +<!-- @selectMaterial="selectMaterial"--> +<!-- :newsType="newsType"--> +<!-- />--> +<!-- </el-dialog>--> +<!-- </el-row>--> +<!-- </el-tab-pane>--> +<!-- <!– 类型 6:音乐 –>--> +<!-- <el-tab-pane name="music">--> +<!-- <span slot="label"><i class="el-icon-service"></i> 音乐</span>--> +<!-- <el-row>--> +<!-- <el-col :span="6">--> +<!-- <div class="thumb-div">--> +<!-- <img style="width: 100px" v-if="objData.thumbMediaUrl" :src="objData.thumbMediaUrl" />--> +<!-- <i v-else class="el-icon-plus avatar-uploader-icon"></i>--> +<!-- <div class="thumb-but">--> +<!-- <el-upload--> +<!-- :action="actionUrl"--> +<!-- :headers="headers"--> +<!-- multiple--> +<!-- :limit="1"--> +<!-- :file-list="fileList"--> +<!-- :data="uploadData"--> +<!-- :before-upload="beforeThumbImageUpload"--> +<!-- :on-success="handleUploadSuccess"--> +<!-- >--> +<!-- <el-button slot="trigger" size="mini" type="text">本地上传</el-button>--> +<!-- <el-button size="mini" type="text" @click="openMaterial" style="margin-left: 5px"--> +<!-- >素材库选择</el-button--> +<!-- >--> +<!-- </el-upload>--> +<!-- </div>--> +<!-- </div>--> +<!-- <el-dialog--> +<!-- title="选择图片"--> +<!-- v-model:visible="dialogThumbVisible"--> +<!-- width="80%"--> +<!-- append-to-body--> +<!-- >--> +<!-- <wx-material-select--> +<!-- :objData="{ type: 'image', accountId: objData.accountId }"--> +<!-- @selectMaterial="selectMaterial"--> +<!-- />--> +<!-- </el-dialog>--> +<!-- </el-col>--> +<!-- <el-col :span="18">--> +<!-- <el-input v-model="objData.title" placeholder="请输入标题" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input v-model="objData.description" placeholder="请输入描述" @input="inputContent" />--> +<!-- </el-col>--> +<!-- </el-row>--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input v-model="objData.musicUrl" placeholder="请输入音乐链接" @input="inputContent" />--> +<!-- <div style="margin: 20px 0"></div>--> +<!-- <el-input--> +<!-- v-model="objData.hqMusicUrl"--> +<!-- placeholder="请输入高质量音乐链接"--> +<!-- @input="inputContent"--> +<!-- />--> +<!-- </el-tab-pane>--> +<!-- </el-tabs>--> +<!--</template>--> + +<!--<script>--> +<!--import WxNews from '@/views/mp/components/wx-news/main.vue'--> +<!--import WxMaterialSelect from '@/views/mp/components/wx-material-select/main.vue'--> +<!--import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'--> +<!--import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'--> + +<!--import { getAccessToken } from '@/utils/auth'--> + +<!--export default {--> +<!-- name: 'WxReplySelect',--> +<!-- components: {--> +<!-- WxNews,--> +<!-- WxMaterialSelect,--> +<!-- WxVoicePlayer,--> +<!-- WxVideoPlayer--> +<!-- },--> +<!-- props: {--> +<!-- objData: {--> +<!-- // 消息对象。--> +<!-- type: Object, // 设置为 Object 的原因,方便属性的传递--> +<!-- required: true--> +<!-- },--> +<!-- newsType: {--> +<!-- // 图文类型:1、已发布图文;2、草稿箱图文--> +<!-- type: String,--> +<!-- default: '1'--> +<!-- }--> +<!-- },--> +<!-- data() {--> +<!-- return {--> +<!-- tempPlayerObj: {--> +<!-- type: '2'--> +<!-- },--> + +<!-- tempObj: new Map().set(--> +<!-- // 临时缓存,切换消息类型的 tab 的时候,可以保存对应的数据;--> +<!-- this.objData.type, // 消息类型--> +<!-- Object.assign({}, this.objData)--> +<!-- ), // 消息内容--> + +<!-- // ========== 素材选择的弹窗,是否可见 ==========--> +<!-- dialogNewsVisible: false, // 图文--> +<!-- dialogImageVisible: false, // 图片--> +<!-- dialogVoiceVisible: false, // 语音--> +<!-- dialogVideoVisible: false, // 视频--> +<!-- dialogThumbVisible: false, // 缩略图--> + +<!-- // ========== 文件上传(图片、语音、视频) ==========--> +<!-- fileList: [], // 文件列表--> +<!-- uploadData: {--> +<!-- accountId: undefined,--> +<!-- type: this.objData.type,--> +<!-- title: '',--> +<!-- introduction: ''--> +<!-- },--> +<!-- actionUrl: process.env.VUE_APP_BASE_API + '/admin-api/mp/material/upload-temporary',--> +<!-- headers: { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部--> +<!-- }--> +<!-- },--> +<!-- methods: {--> +<!-- beforeThumbImageUpload(file) {--> +<!-- const isType =--> +<!-- file.type === 'image/jpeg' ||--> +<!-- file.type === 'image/png' ||--> +<!-- file.type === 'image/gif' ||--> +<!-- file.type === 'image/bmp' ||--> +<!-- file.type === 'image/jpg'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传图片格式不对!')--> +<!-- return false--> +<!-- }--> +<!-- const isLt = file.size / 1024 / 1024 < 2--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传图片大小不能超过 2M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- beforeVoiceUpload(file) {--> +<!-- // 校验格式--> +<!-- const isType =--> +<!-- file.type === 'audio/mp3' ||--> +<!-- file.type === 'audio/mpeg' ||--> +<!-- file.type === 'audio/wma' ||--> +<!-- file.type === 'audio/wav' ||--> +<!-- file.type === 'audio/amr'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传语音格式不对!' + file.type)--> +<!-- return false--> +<!-- }--> +<!-- // 校验大小--> +<!-- const isLt = file.size / 1024 / 1024 < 2--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传语音大小不能超过 2M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- beforeImageUpload(file) {--> +<!-- // 校验格式--> +<!-- const isType =--> +<!-- file.type === 'image/jpeg' ||--> +<!-- file.type === 'image/png' ||--> +<!-- file.type === 'image/gif' ||--> +<!-- file.type === 'image/bmp' ||--> +<!-- file.type === 'image/jpg'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传图片格式不对!')--> +<!-- return false--> +<!-- }--> +<!-- // 校验大小--> +<!-- const isLt = file.size / 1024 / 1024 < 2--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传图片大小不能超过 2M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- beforeVideoUpload(file) {--> +<!-- // 校验格式--> +<!-- const isType = file.type === 'video/mp4'--> +<!-- if (!isType) {--> +<!-- this.$message.error('上传视频格式不对!')--> +<!-- return false--> +<!-- }--> +<!-- // 校验大小--> +<!-- const isLt = file.size / 1024 / 1024 < 10--> +<!-- if (!isLt) {--> +<!-- this.$message.error('上传视频大小不能超过 10M!')--> +<!-- return false--> +<!-- }--> +<!-- this.uploadData.accountId = this.objData.accountId--> +<!-- return true--> +<!-- },--> +<!-- handleUploadSuccess(response, file, fileList) {--> +<!-- if (response.code !== 0) {--> +<!-- this.$message.error('上传出错:' + response.msg)--> +<!-- return false--> +<!-- }--> + +<!-- // 清空上传时的各种数据--> +<!-- this.fileList = []--> +<!-- this.uploadData.title = ''--> +<!-- this.uploadData.introduction = ''--> + +<!-- // 上传好的文件,本质是个素材,所以可以进行选中--> +<!-- let item = response.data--> +<!-- this.selectMaterial(item)--> +<!-- },--> +<!-- /**--> +<!-- * 切换消息类型的 tab--> +<!-- *--> +<!-- * @param tab tab--> +<!-- */--> +<!-- handleClick(tab) {--> +<!-- // 设置后续文件上传的文件类型--> +<!-- this.uploadData.type = this.objData.type--> +<!-- if (this.uploadData.type === 'music') {--> +<!-- // 【音乐】上传的是缩略图--> +<!-- this.uploadData.type = 'thumb'--> +<!-- }--> + +<!-- // 从 tempObj 临时缓存中,获取对应的数据,并设置回 objData--> +<!-- let tempObjItem = this.tempObj.get(this.objData.type)--> +<!-- if (tempObjItem) {--> +<!-- this.objData.content = tempObjItem.content ? tempObjItem.content : null--> +<!-- this.objData.mediaId = tempObjItem.mediaId ? tempObjItem.mediaId : null--> +<!-- this.objData.url = tempObjItem.url ? tempObjItem.url : null--> +<!-- this.objData.name = tempObjItem.url ? tempObjItem.name : null--> +<!-- this.objData.title = tempObjItem.title ? tempObjItem.title : null--> +<!-- this.objData.description = tempObjItem.description ? tempObjItem.description : null--> +<!-- return--> +<!-- }--> +<!-- // 如果获取不到,需要把 objData 复原--> +<!-- // 必须使用 $set 赋值,不然 input 无法输入内容--> +<!-- this.$set(this.objData, 'content', '')--> +<!-- this.$delete(this.objData, 'mediaId')--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.$set(this.objData, 'title', '')--> +<!-- this.$set(this.objData, 'description', '')--> +<!-- },--> +<!-- /**--> +<!-- * 选择素材,将设置设置到 objData 变量--> +<!-- *--> +<!-- * @param item 素材--> +<!-- */--> +<!-- selectMaterial(item) {--> +<!-- // 选择好素材,所以隐藏弹窗--> +<!-- this.closeMaterial()--> + +<!-- // 创建 tempObjItem 对象,并设置对应的值--> +<!-- let tempObjItem = {}--> +<!-- tempObjItem.type = this.objData.type--> +<!-- if (this.objData.type === 'news') {--> +<!-- tempObjItem.articles = item.content.newsItem--> +<!-- this.objData.articles = item.content.newsItem--> +<!-- } else if (this.objData.type === 'music') {--> +<!-- // 音乐需要特殊处理,因为选择的是图片的缩略图--> +<!-- tempObjItem.thumbMediaId = item.mediaId--> +<!-- this.objData.thumbMediaId = item.mediaId--> +<!-- tempObjItem.thumbMediaUrl = item.url--> +<!-- this.objData.thumbMediaUrl = item.url--> +<!-- // title、introduction、musicUrl、hqMusicUrl:从 objData 到 tempObjItem,避免上传素材后,被覆盖掉--> +<!-- tempObjItem.title = this.objData.title || ''--> +<!-- tempObjItem.introduction = this.objData.introduction || ''--> +<!-- tempObjItem.musicUrl = this.objData.musicUrl || ''--> +<!-- tempObjItem.hqMusicUrl = this.objData.hqMusicUrl || ''--> +<!-- } else if (this.objData.type === 'image' || this.objData.type === 'voice') {--> +<!-- tempObjItem.mediaId = item.mediaId--> +<!-- this.objData.mediaId = item.mediaId--> +<!-- tempObjItem.url = item.url--> +<!-- this.objData.url = item.url--> +<!-- tempObjItem.name = item.name--> +<!-- this.objData.name = item.name--> +<!-- } else if (this.objData.type === 'video') {--> +<!-- tempObjItem.mediaId = item.mediaId--> +<!-- this.objData.mediaId = item.mediaId--> +<!-- tempObjItem.url = item.url--> +<!-- this.objData.url = item.url--> +<!-- tempObjItem.name = item.name--> +<!-- this.objData.name = item.name--> +<!-- // title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction--> +<!-- if (item.title) {--> +<!-- this.objData.title = item.title || ''--> +<!-- tempObjItem.title = item.title || ''--> +<!-- }--> +<!-- if (item.introduction) {--> +<!-- this.objData.description = item.introduction || '' // 消息使用的是 description,素材使用的是 introduction,所以转换下--> +<!-- tempObjItem.description = item.introduction || ''--> +<!-- }--> +<!-- } else if (this.objData.type === 'text') {--> +<!-- this.objData.content = item.content || ''--> +<!-- }--> +<!-- // 最终设置到临时缓存--> +<!-- this.tempObj.set(this.objData.type, tempObjItem)--> +<!-- },--> +<!-- openMaterial() {--> +<!-- if (this.objData.type === 'news') {--> +<!-- this.dialogNewsVisible = true--> +<!-- } else if (this.objData.type === 'image') {--> +<!-- this.dialogImageVisible = true--> +<!-- } else if (this.objData.type === 'voice') {--> +<!-- this.dialogVoiceVisible = true--> +<!-- } else if (this.objData.type === 'video') {--> +<!-- this.dialogVideoVisible = true--> +<!-- } else if (this.objData.type === 'music') {--> +<!-- this.dialogThumbVisible = true--> +<!-- }--> +<!-- },--> +<!-- closeMaterial() {--> +<!-- this.dialogNewsVisible = false--> +<!-- this.dialogImageVisible = false--> +<!-- this.dialogVoiceVisible = false--> +<!-- this.dialogVideoVisible = false--> +<!-- this.dialogThumbVisible = false--> +<!-- },--> +<!-- deleteObj() {--> +<!-- if (this.objData.type === 'news') {--> +<!-- this.$delete(this.objData, 'articles')--> +<!-- } else if (this.objData.type === 'image') {--> +<!-- this.objData.mediaId = null--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.objData.name = null--> +<!-- } else if (this.objData.type === 'voice') {--> +<!-- this.objData.mediaId = null--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.objData.name = null--> +<!-- } else if (this.objData.type === 'video') {--> +<!-- this.objData.mediaId = null--> +<!-- this.$delete(this.objData, 'url')--> +<!-- this.objData.name = null--> +<!-- this.objData.title = null--> +<!-- this.objData.description = null--> +<!-- } else if (this.objData.type === 'music') {--> +<!-- this.objData.thumbMediaId = null--> +<!-- this.objData.thumbMediaUrl = null--> +<!-- this.objData.title = null--> +<!-- this.objData.description = null--> +<!-- this.objData.musicUrl = null--> +<!-- this.objData.hqMusicUrl = null--> +<!-- } else if (this.objData.type === 'text') {--> +<!-- this.objData.content = null--> +<!-- }--> +<!-- // 覆盖缓存--> +<!-- this.tempObj.set(this.objData.type, Object.assign({}, this.objData))--> +<!-- },--> +<!-- /**--> +<!-- * 输入时,缓存每次 objData 到 tempObj 中--> +<!-- *--> +<!-- * why?不确定为什么 v-model="objData.content" 不能自动缓存,所以通过这样的方式--> +<!-- */--> +<!-- inputContent(str) {--> +<!-- // 覆盖缓存--> +<!-- this.tempObj.set(this.objData.type, Object.assign({}, this.objData))--> +<!-- }--> +<!-- }--> +<!--}--> +<!--</script>--> + +<!--<style lang="scss" scoped>--> +<!--.public-account-management {--> +<!-- .el-input {--> +<!-- width: 70%;--> +<!-- margin-right: 2%;--> +<!-- }--> +<!--}--> +<!--.pagination {--> +<!-- text-align: right;--> +<!-- margin-right: 25px;--> +<!--}--> +<!--.select-item {--> +<!-- width: 280px;--> +<!-- padding: 10px;--> +<!-- margin: 0 auto 10px auto;--> +<!-- border: 1px solid #eaeaea;--> +<!--}--> +<!--.select-item2 {--> +<!-- padding: 10px;--> +<!-- margin: 0 auto 10px auto;--> +<!-- border: 1px solid #eaeaea;--> +<!--}--> +<!--.ope-row {--> +<!-- padding-top: 10px;--> +<!-- text-align: center;--> +<!--}--> +<!--.item-name {--> +<!-- font-size: 12px;--> +<!-- overflow: hidden;--> +<!-- text-overflow: ellipsis;--> +<!-- white-space: nowrap;--> +<!-- text-align: center;--> +<!--}--> +<!--.el-form-item__content {--> +<!-- line-height: unset !important;--> +<!--}--> +<!--.col-select {--> +<!-- border: 1px solid rgb(234, 234, 234);--> +<!-- padding: 50px 0px;--> +<!-- height: 160px;--> +<!-- width: 49.5%;--> +<!--}--> +<!--.col-select2 {--> +<!-- border: 1px solid rgb(234, 234, 234);--> +<!-- padding: 50px 0px;--> +<!-- height: 160px;--> +<!--}--> +<!--.col-add {--> +<!-- border: 1px solid rgb(234, 234, 234);--> +<!-- padding: 50px 0px;--> +<!-- height: 160px;--> +<!-- width: 49.5%;--> +<!-- float: right;--> +<!--}--> +<!--.avatar-uploader-icon {--> +<!-- border: 1px solid #d9d9d9;--> +<!-- font-size: 28px;--> +<!-- color: #8c939d;--> +<!-- width: 100px !important;--> +<!-- height: 100px !important;--> +<!-- line-height: 100px !important;--> +<!-- text-align: center;--> +<!--}--> +<!--.material-img {--> +<!-- width: 100%;--> +<!--}--> +<!--.thumb-div {--> +<!-- display: inline-block;--> +<!-- text-align: center;--> +<!--}--> +<!--.item-infos {--> +<!-- width: 30%;--> +<!-- margin: auto;--> +<!--}--> +<!--</style>--> diff --git a/src/views/mp/components/wx-video-play/main.vue b/src/views/mp/components/wx-video-play/main.vue new file mode 100644 index 00000000..880d10f8 --- /dev/null +++ b/src/views/mp/components/wx-video-play/main.vue @@ -0,0 +1,117 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 【微信消息 - 视频】 + 芋道源码: + ① bug 修复: + 1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容; + 存在的问题:mediaId 有效期是 3 天,超过时间后无法播放 + 2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。 + ② 体验优化:弹窗关闭后,自动暂停视频的播放 +--> +<template> + <div> + <!-- 提示 --> + <div @click="playVideo()"> + <el-icon> + <VideoPlay /> + </el-icon> + <p>点击播放视频</p> + </div> + + <!-- 弹窗播放 --> + <el-dialog + title="视频播放" + v-model:visible="dialogVideo" + width="40%" + append-to-body + @close="closeDialog" + > + <video-player + v-if="playerOptions.sources[0].src" + class="video-player vjs-custom-skin" + ref="videoPlayerRef" + :playsinline="true" + :options="playerOptions" + @play="onPlayerPlay($event)" + @pause="onPlayerPause($event)" + /> + </el-dialog> + </div> +</template> + +<script setup lang="ts" name="WxVideoPlayer"> +// 引入 videoPlayer 相关组件。教程:https://juejin.cn/post/6923056942281654285 +import { videoPlayer } from 'vue-video-player' +import 'video.js/dist/video-js.css' +import 'vue-video-player/src/custom-theme.css' +import { VideoPlay } from '@element-plus/icons-vue' + +const props = defineProps({ + url: { + // 视频地址,例如说:https://www.iocoder.cn/xxx.mp4 + type: String, + required: true + } +}) +const videoPlayerRef = ref() +const dialogVideo = ref(false) +const playerOptions = reactive({ + playbackRates: [0.5, 1.0, 1.5, 2.0], // 播放速度 + autoplay: false, // 如果 true,浏览器准备好时开始回放。 + muted: false, // 默认情况下将会消除任何音频。 + loop: false, // 导致视频一结束就重新开始。 + preload: 'auto', // 建议浏览器在 <video> 加载元素后是否应该开始下载视频数据。auto 浏览器选择最佳行为,立即开始加载视频(如果浏览器支持) + language: 'zh-CN', + aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3") + fluid: true, // 当true时,Video.js player 将拥有流体大小。换句话说,它将按比例缩放以适应其容器。 + sources: [ + { + type: 'video/mp4', + src: '' // 你的视频地址(必填)【重要】 + } + ], + poster: '', // 你的封面地址 + width: document.documentElement.clientWidth, + notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖 Video.js 无法播放媒体源时显示的默认信息。 + controlBar: { + timeDivider: true, + durationDisplay: true, + remainingTimeDisplay: false, + fullscreenToggle: true //全屏按钮 + } +}) + +const playVideo = () => { + dialogVideo.value = true + playerOptions.sources[0].src = props.url +} +const closeDialog = () => { + // 暂停播放 + // videoPlayerRef.player.pause() +} +// onPlayerPlay(player) {}, +// // // eslint-disable-next-line @typescript-eslint/no-unused-vars +// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +// onPlayerPause(player) {} + +// methods: { +// playVideo() { +// this.dialogVideo = true +// // 设置地址 +// this.playerOptions.sources[0]['src'] = this.url +// }, +// closeDialog() { +// // 暂停播放 +// this.$refs.videoPlayer.player.pause() +// }, +// +// //todo player组件引入可能有问题 +// +// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +// onPlayerPlay(player) {}, +// // // eslint-disable-next-line @typescript-eslint/no-unused-vars +// // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +// onPlayerPause(player) {} +// } +</script> diff --git a/src/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue new file mode 100644 index 00000000..3260fc05 --- /dev/null +++ b/src/views/mp/components/wx-voice-play/main.vue @@ -0,0 +1,97 @@ +<!-- + - Copyright (C) 2018-2019 + - All rights reserved, Designed By www.joolun.com + 【微信消息 - 语音】 + 芋道源码: + ① bug 修复: + 1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容; + 存在的问题:mediaId 有效期是 3 天,超过时间后无法播放 + 2)重构后的做法:后端接收到微信公众号的视频消息后,将视频消息的 media_id 的文件内容保存到文件服务器中,这样前端可以直接使用 URL 播放。 + ② 代码优化:将 props 中的 objData 调成为 data 中对应的属性,并补充相关注释 +--> +<template> + <div class="wx-voice-div" @click="playVoice"> + <el-icon> + <Icon v-if="playing !== true" icon="ep:video-play" /> + <Icon v-else icon="ep:video-pause" /> + <span class="amr-duration" v-if="duration">{{ duration }} 秒</span> + </el-icon> + <div v-if="content"> + <el-tag type="success" size="mini">语音识别</el-tag> + {{ content }} + </div> + </div> +</template> + +<script setup lang="ts" name="WxVoicePlayer"> +// 因为微信语音是 amr 格式,所以需要用到 amr 解码器:https://www.npmjs.com/package/benz-amr-recorder +import BenzAMRRecorder from 'benz-amr-recorder' + +const props = defineProps({ + url: { + type: String, // 语音地址,例如说:https://www.iocoder.cn/xxx.amr + required: true + }, + content: { + type: String, // 语音文本 + required: false + } +}) + +const amr = ref() +const playing = ref(false) +const duration = ref() + +/** 处理点击,播放或暂停 */ +const playVoice = () => { + // 情况一:未初始化,则创建 BenzAMRRecorder + if (amr.value === undefined) { + amrInit() + return + } + // 情况二:已经初始化,则根据情况播放或暂时 + if (amr.value.isPlaying()) { + amrStop() + } else { + amrPlay() + } +} + +/** 音频初始化 */ +const amrInit = () => { + amr.value = new BenzAMRRecorder() + // 设置播放 + amr.value.initWithUrl(props.url).then(function () { + amrPlay() + duration.value = amr.value.getDuration() + }) + // 监听暂停 + amr.value.onEnded(function () { + playing.value = false + }) +} + +/** 音频播放 */ +const amrPlay = () => { + playing.value = true + amr.value.play() +} + +/** 音频暂停 */ +const amrStop = () => { + playing.value = false + amr.value.stop() +} +// TODO 芋艿:下面样式有点问题 +</script> +<style lang="scss" scoped> +.wx-voice-div { + padding: 5px; + background-color: #eaeaea; + border-radius: 10px; +} +.amr-duration { + font-size: 11px; + margin-left: 5px; +} +</style> diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue index 497f72ec..6ef4a303 100644 --- a/src/views/mp/freePublish/index.vue +++ b/src/views/mp/freePublish/index.vue @@ -1,3 +1,392 @@ <template> - <span>开发中</span> + <!-- 搜索工作栏 --> + <content-wrap> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> + <el-option + v-for="item in accountList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </content-wrap> + + <!-- 列表 --> + <content-wrap> + <div class="waterfall" v-loading="loading"> + <div + class="waterfall-item" + v-show="item.content && item.content.newsItem" + v-for="item in list" + :key="item.articleId" + > + <wx-news :articles="item.content.newsItem" /> + <!-- 操作 --> + <el-row justify="center" class="ope-row"> + <el-button + type="danger" + circle + @click="handleDelete(item)" + v-hasPermi="['mp:free-publish:delete']" + > + <Icon icon="ep:delete" /> + </el-button> + </el-row> + </div> + </div> + <!-- 分页组件 --> + <pagination + v-show="total > 0" + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </content-wrap> </template> + +<script setup lang="ts" name="freePublish"> +import { getFreePublishPage, deleteFreePublish } from '@/api/mp/freePublish' +import * as MpAccountApi from '@/api/mp/account' +import WxNews from '@/views/mp/components/wx-news/main.vue' +const message = useMessage() // 消息弹窗 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + currentPage: 1, // 当前页数 + pageNo: 1, // 当前页数 + accountId: undefined // 当前页数 +}) +const queryFormRef = ref() // 搜索的表单 +const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 + +/** 查询列表 */ +const getList = async () => { + // 如果没有选中公众号账号,则进行提示。 + if (!queryParams.accountId) { + message.error('未选中公众号,无法查询已发表图文') + return false + } + // TODO 改成 await 形式 + loading.value = true + getFreePublishPage(queryParams) + .then((data) => { + console.log(data) + // 将 thumbUrl 转成 picUrl,保证 wx-news 组件可以预览封面 + data.list.forEach((item) => { + console.log(item) + const newsItem = item.content.newsItem + newsItem.forEach((article) => { + article.picUrl = article.thumbUrl + }) + }) + list.value = data.list + total.value = data.total + }) + .finally(() => { + loading.value = false + }) +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + // 默认选中第一个 + if (accountList.value.length > 0) { + // @ts-ignore + queryParams.accountId = accountList.value[0].id + } + handleQuery() +} + +/** 删除按钮操作 */ +const handleDelete = async (item) => { + { + // TODO 改成 await 形式 + const articleId = item.articleId + const accountId = queryParams.accountId + message + .confirm('删除后用户将无法访问此页面,确定删除?') + .then(function () { + return deleteFreePublish(accountId, articleId) + }) + .then(() => { + getList() + message.success('删除成功') + }) + .catch(() => {}) + } +} + +onMounted(async () => { + accountList.value = await MpAccountApi.getSimpleAccountList() + // 选中第一个 + if (accountList.value.length > 0) { + // @ts-ignore + queryParams.accountId = accountList.value[0].id + } + await getList() +}) +</script> +<style lang="scss" scoped> +.pagination { + float: right; + margin-right: 25px; +} + +.add_but { + padding: 10px; +} + +.ope-row { + margin-top: 5px; + text-align: center; + border-top: 1px solid #eaeaea; + padding-top: 5px; +} + +.item-name { + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; +} + +.el-upload__tip { + margin-left: 5px; +} + +/* 新增图文 */ +.left { + display: inline-block; + width: 35%; + vertical-align: top; + margin-top: 200px; +} + +.right { + display: inline-block; + width: 60%; + margin-top: -40px; +} + +.avatar-uploader { + width: 20%; + display: inline-block; +} + +.avatar-uploader .el-upload { + border-radius: 6px; + cursor: pointer; + position: relative; + overflow: hidden; + text-align: unset !important; +} + +.avatar-uploader .el-upload:hover { + border-color: #165dff; +} + +.avatar-uploader-icon { + border: 1px solid #d9d9d9; + font-size: 28px; + color: #8c939d; + width: 120px; + height: 120px; + line-height: 120px; + text-align: center; +} + +.avatar { + width: 230px; + height: 120px; +} + +.avatar1 { + width: 120px; + height: 120px; +} + +.digest { + width: 60%; + display: inline-block; + vertical-align: top; +} + +/*新增图文*/ +/*瀑布流样式*/ +.waterfall { + width: 100%; + column-gap: 10px; + column-count: 5; + margin: 0 auto; +} + +.waterfall-item { + padding: 10px; + margin-bottom: 10px; + break-inside: avoid; + border: 1px solid #eaeaea; +} + +p { + line-height: 30px; +} + +@media (min-width: 992px) and (max-width: 1300px) { + .waterfall { + column-count: 3; + } + p { + color: red; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .waterfall { + column-count: 2; + } + p { + color: orange; + } +} + +@media (max-width: 767px) { + .waterfall { + column-count: 1; + } +} + +/*瀑布流样式*/ +.news-main { + background-color: #ffffff; + width: 100%; + margin: auto; + height: 120px; +} + +.news-content { + background-color: #acadae; + width: 100%; + height: 120px; + position: relative; +} + +.news-content-title { + display: inline-block; + font-size: 15px; + color: #ffffff; + position: absolute; + left: 0px; + bottom: 0px; + background-color: black; + width: 98%; + padding: 1%; + opacity: 0.65; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + height: 25px; +} + +.news-main-item { + background-color: #ffffff; + padding: 5px 0px; + border-top: 1px solid #eaeaea; + width: 100%; + margin: auto; +} + +.news-content-item { + position: relative; + margin-left: -3px; +} + +.news-content-item-title { + display: inline-block; + font-size: 12px; + width: 70%; +} + +.news-content-item-img { + display: inline-block; + width: 25%; + background-color: #acadae; +} + +.input-tt { + padding: 5px; +} + +.activeAddNews { + border: 5px solid #2bb673; +} + +.news-main-plus { + width: 280px; + text-align: center; + margin: auto; + height: 50px; +} + +.icon-plus { + margin: 10px; + font-size: 25px; +} + +.select-item { + width: 60%; + padding: 10px; + margin: 0 auto 10px auto; + border: 1px solid #eaeaea; +} + +.father .child { + display: none; + text-align: center; + position: relative; + bottom: 25px; +} + +.father:hover .child { + display: block; +} + +.thumb-div { + display: inline-block; + width: 30%; + text-align: center; +} + +.thumb-but { + margin: 5px; +} + +.material-img { + width: 100%; + height: 100%; +} +</style> diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue index 497f72ec..10145221 100644 --- a/src/views/mp/message/index.vue +++ b/src/views/mp/message/index.vue @@ -1,3 +1,261 @@ <template> - <span>开发中</span> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> + <el-option + v-for="item in accountList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="消息类型" prop="type"> + <el-select v-model="queryParams.type" placeholder="请选择消息类型" class="!w-240px"> + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="用户标识" prop="openid"> + <el-input + v-model="queryParams.openid" + placeholder="请输入用户标识" + clearable + :v-on="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + style="width: 240px" + value-format="yyyy-MM-dd HH:mm:ss" + type="daterange" + range-separator="-" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="['00:00:00', '23:59:59']" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column + label="发送时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="消息类型" align="center" prop="type" width="80" /> + <el-table-column label="发送方" align="center" prop="sendFrom" width="80"> + <template #default="scope"> + <el-tag v-if="scope.row.sendFrom === 1" type="success">粉丝</el-tag> + <el-tag v-else type="info">公众号</el-tag> + </template> + </el-table-column> + <el-table-column label="用户标识" align="center" prop="openid" width="300" /> + <el-table-column label="内容" prop="content"> + <template #default="scope"> + <!-- 【事件】区域 --> + <div v-if="scope.row.type === 'event' && scope.row.event === 'subscribe'"> + <el-tag type="success">关注</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'unsubscribe'"> + <el-tag type="danger">取消关注</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'CLICK'"> + <el-tag>点击菜单</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'VIEW'"> + <el-tag>点击菜单链接</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_waitmsg'"> + <el-tag>扫码结果</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'scancode_push'"> + <el-tag>扫码结果</el-tag>【{{ scope.row.eventKey }}】 + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_sysphoto'"> + <el-tag>系统拍照发图</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_photo_or_album'"> + <el-tag>拍照或者相册</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'pic_weixin'"> + <el-tag>微信相册</el-tag> + </div> + <div v-else-if="scope.row.type === 'event' && scope.row.event === 'location_select'"> + <el-tag>选择地理位置</el-tag> + </div> + <div v-else-if="scope.row.type === 'event'"> + <el-tag type="danger">未知事件类型</el-tag> + </div> + <!-- 【消息】区域 --> + <div v-else-if="scope.row.type === 'text'">{{ scope.row.content }}</div> + <div v-else-if="scope.row.type === 'voice'"> + <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" /> + </div> + <div v-else-if="scope.row.type === 'image'"> + <a target="_blank" :href="scope.row.mediaUrl"> + <img :src="scope.row.mediaUrl" style="width: 100px" /> + </a> + </div> + <div v-else-if="scope.row.type === 'video' || scope.row.type === 'shortvideo'"> + <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" /> + </div> + <div v-else-if="scope.row.type === 'link'"> + <el-tag>链接</el-tag>: + <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a> + </div> + <div v-else-if="scope.row.type === 'location'"> + <wx-location + :label="scope.row.label" + :location-y="scope.row.locationY" + :location-x="scope.row.locationX" + /> + </div> + <div v-else-if="scope.row.type === 'music'"> + <wx-music + :title="scope.row.title" + :description="scope.row.description" + :thumb-media-url="scope.row.thumbMediaUrl" + :music-url="scope.row.musicUrl" + :hq-music-url="scope.row.hqMusicUrl" + /> + </div> + <div v-else-if="scope.row.type === 'news'"> + <wx-news :articles="scope.row.articles" /> + </div> + <div v-else> + <el-tag type="danger">未知消息类型</el-tag> + </div> + </template> + </el-table-column> + <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> + <template #default="scope"> + <el-button + link + type="primary" + @click="handleSend(scope.row)" + v-hasPermi="['mp:message:send']" + > + 消息 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页组件 --> + <pagination + v-show="total > 0" + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + + <!-- 发送消息的弹窗 --> + <el-dialog title="粉丝消息列表" v-model:visible="open" width="50%"> + <wx-msg :user-id="userId" v-if="open" /> + </el-dialog> + </ContentWrap> </template> +<script setup lang="ts" name="MpMessage"> +import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +// import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue' +import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue' +// import WxMsg from '@/views/mp/components/wx-msg/main.vue' +import WxLocation from '@/views/mp/components/wx-location/main.vue' +// import WxMusic from '@/views/mp/components/wx-music/main.vue' +// import WxNews from '@/views/mp/components/wx-news/main.vue' +import * as MpAccountApi from '@/api/mp/account' +import * as MpMessageApi from '@/api/mp/message' +const message = useMessage() // 消息弹窗 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + openid: null, + accountId: null, + type: null, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 +// TODO 芋艿:下面应该移除 +const open = ref(false) // 是否显示弹出层 +const userId = ref(0) // 操作的用户编号 +const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 + +/** 查询参数列表 */ +const getList = async () => { + // 如果没有选中公众号账号,则进行提示。 + if (!queryParams.accountId) { + await message.error('未选中公众号,无法查询消息') + return + } + try { + loading.value = true + const data = await MpMessageApi.getMessagePage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = async () => { + queryFormRef.value.resetFields() + // 默认选中第一个 + if (accountList.value.length > 0) { + // @ts-ignore + queryParams.accountId = accountList.value[0].id + } + handleQuery() +} +const handleSend = async (row) => { + userId.value = row.userId + open.value = true +} + +/** 初始化 **/ +onMounted(async () => { + accountList.value = await MpAccountApi.getSimpleAccountList() + // 选中第一个 + if (accountList.value.length > 0) { + // @ts-ignore + queryParams.accountId = accountList.value[0].id + } + await getList() +}) +</script> diff --git a/src/views/mp/tag/TagForm.vue b/src/views/mp/tag/TagForm.vue new file mode 100644 index 00000000..db251cdf --- /dev/null +++ b/src/views/mp/tag/TagForm.vue @@ -0,0 +1,91 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="标签名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入标签名称" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import * as MpTagApi from '@/api/mp/tag' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + accountId: -1, + name: '' +}) +const formRules = reactive({ + name: [{ required: true, message: '请输入标签名称', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, accountId: number, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + formData.value.accountId = accountId + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await MpTagApi.getTag(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as MpTagApi.TagVO + if (formType.value === 'create') { + await MpTagApi.createTag(data) + message.success(t('common.createSuccess')) + } else { + await MpTagApi.updateTag(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + accountId: -1, + name: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue new file mode 100644 index 00000000..84e6fc17 --- /dev/null +++ b/src/views/mp/tag/index.vue @@ -0,0 +1,183 @@ +<template> + <!-- 搜索工作栏 --> + <ContentWrap> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="公众号" prop="accountId"> + <el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px"> + <el-option + v-for="item in accountList" + :key="parseInt(item.id)" + :label="item.name" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + <el-form-item label="标签名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入标签名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mp:tag:create']"> + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button type="success" plain @click="handleSync" v-hasPermi="['mp:tag:sync']"> + <Icon icon="ep:refresh" class="mr-5px" /> 同步 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="编号" align="center" prop="id" /> + <el-table-column label="标签名称" align="center" prop="name" /> + <el-table-column label="粉丝数" align="center" prop="count" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['mp:tag:update']" + > + 修改 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['mp:tag:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <TagForm ref="formRef" @success="getList" /> +</template> +<script setup lang="ts" name="MpTag"> +import { dateFormatter } from '@/utils/formatTime' +import * as MpTagApi from '@/api/mp/tag' +import * as MpAccountApi from '@/api/mp/account' +import TagForm from './TagForm.vue' +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + accountId: undefined, + name: null +}) +const queryFormRef = ref() // 搜索的表单 +const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 + +/** 查询参数列表 */ +const getList = async () => { + // 如果没有选中公众号账号,则进行提示。 + if (!queryParams.accountId) { + await message.error('未选中公众号,无法查询标签') + return + } + try { + loading.value = true + const data = await MpTagApi.getTagPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + // 默认选中第一个 + if (accountList.value.length > 0) { + // @ts-ignore + queryParams.accountId = accountList.value[0].id + } + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, queryParams.accountId, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await MpTagApi.deleteTag(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 同步操作 */ +const handleSync = async () => { + try { + await message.confirm('是否确认同步标签?') + // @ts-ignore + await MpTagApi.syncTag(queryParams.accountId) + message.success('同步标签成功') + await getList() + } catch {} +} + +/** 初始化 **/ +onMounted(async () => { + accountList.value = await MpAccountApi.getSimpleAccountList() + // 选中第一个 + if (accountList.value.length > 0) { + // @ts-ignore + queryParams.accountId = accountList.value[0].id + } + await getList() +}) +</script> diff --git a/src/views/system/area/form.vue b/src/views/system/area/form.vue index 4e61fe32..f0cff434 100644 --- a/src/views/system/area/form.vue +++ b/src/views/system/area/form.vue @@ -15,10 +15,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/system/dept/DeptForm.vue b/src/views/system/dept/DeptForm.vue new file mode 100644 index 00000000..f2c3bc02 --- /dev/null +++ b/src/views/system/dept/DeptForm.vue @@ -0,0 +1,174 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="上级部门" prop="parentId"> + <el-tree-select + v-model="formData.parentId" + :data="deptTree" + :props="{ value: 'id', label: 'name', children: 'children' }" + value-key="deptId" + placeholder="请选择上级部门" + check-strictly + default-expand-all + /> + </el-form-item> + <el-form-item label="部门名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入部门名称" /> + </el-form-item> + <el-form-item label="显示排序" prop="sort"> + <el-input-number v-model="formData.sort" controls-position="right" :min="0" /> + </el-form-item> + <el-form-item label="负责人" prop="leaderUserId"> + <el-select + v-model="formData.leaderUserId" + placeholder="请输入负责人" + clearable + style="width: 100%" + > + <el-option + v-for="item in userList" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="联系电话" prop="phone"> + <el-input v-model="formData.phone" placeholder="请输入联系电话" maxlength="11" /> + </el-form-item> + <el-form-item label="邮箱" prop="email"> + <el-input v-model="formData.email" placeholder="请输入邮箱" maxlength="50" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="formData.status" placeholder="请选择状态" clearable> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + </el-form> + <template #footer> + <el-button type="primary" @click="submitForm">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { handleTree } from '@/utils/tree' +import * as DeptApi from '@/api/system/dept' +import * as UserApi from '@/api/system/user' +import { CommonStatusEnum } from '@/utils/constants' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + title: '', + parentId: undefined, + name: undefined, + sort: undefined, + leaderUserId: undefined, + phone: undefined, + email: undefined, + status: CommonStatusEnum.ENABLE +}) +const formRules = reactive({ + parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }], + name: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }], + sort: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }], + email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }], + phone: [ + { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' } + ] +}) +const formRef = ref() // 表单 Ref +const deptTree = ref() // 树形结构 +const userList = ref<UserApi.UserVO[]>([]) // 用户列表 + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await DeptApi.getDeptApi(id) + } finally { + formLoading.value = false + } + } + // 获得用户列表 + userList.value = await UserApi.getSimpleUserList() + // 获得部门树 + await getTree() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as DeptApi.DeptVO + if (formType.value === 'create') { + await DeptApi.createDeptApi(data) + message.success(t('common.createSuccess')) + } else { + await DeptApi.updateDeptApi(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + title: '', + parentId: undefined, + name: undefined, + sort: undefined, + leaderUserId: undefined, + phone: undefined, + email: undefined, + status: CommonStatusEnum.ENABLE + } + formRef.value?.resetFields() +} + +/** 获得部门树 */ +const getTree = async () => { + deptTree.value = [] + const data = await DeptApi.getSimpleDeptList() + let dept: Tree = { id: 0, name: '顶级部门', children: [] } + dept.children = handleTree(data) + deptTree.value.push(dept) +} +</script> diff --git a/src/views/system/dept/dept.data.ts b/src/views/system/dept/dept.data.ts deleted file mode 100644 index c6945841..00000000 --- a/src/views/system/dept/dept.data.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -const { t } = useI18n() // 国际化 - -// 表单校验 -export const rules = reactive({ - name: [required], - sort: [required], - // email: [required], - email: [ - { required: true, message: t('profile.rules.mail'), trigger: 'blur' }, - { - type: 'email', - message: t('profile.rules.truemail'), - trigger: ['blur', 'change'] - } - ], - phone: [ - { - len: 11, - trigger: 'blur', - message: '请输入正确的手机号码' - } - ] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: null, - action: true, - columns: [ - { - title: '上级部门', - field: 'parentId', - isTable: false - }, - { - title: '部门名称', - field: 'name', - isSearch: true, - table: { - treeNode: true, - align: 'left' - } - }, - { - title: '负责人', - field: 'leaderUserId', - table: { - slots: { - default: 'leaderUserId_default' - } - } - }, - { - title: '联系电话', - field: 'phone' - }, - { - title: '邮箱', - field: 'email', - isTable: false - }, - { - title: '显示排序', - field: 'sort' - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue index 3b182e2a..ead319ce 100644 --- a/src/views/system/dept/index.vue +++ b/src/views/system/dept/index.vue @@ -1,189 +1,188 @@ <template> + <!-- 搜索工作栏 --> <ContentWrap> - <!-- 列表 --> - <XTable ref="xGrid" @register="registerTable" show-overflow> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton - type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:dept:create']" - @click="handleCreate()" + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="部门名称" prop="title"> + <el-input + v-model="queryParams.name" + placeholder="请输入部门名称" + clearable + class="!w-240px" /> - <XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" /> - <XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" /> - </template> - <template #leaderUserId_default="{ row }"> - <span>{{ userNicknameFormat(row) }}</span> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['system:dept:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['system:dept:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> - </ContentWrap> - <!-- 添加或修改菜单对话框 --> - <XModal id="deptModel" v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(添加 / 修改) --> - <Form ref="formRef" :schema="allSchemas.formSchema" :rules="rules"> - <template #parentId="form"> - <el-tree-select - node-key="id" - v-model="form['parentId']" - :props="defaultProps" - :data="deptOptions" - :default-expanded-keys="[100]" - check-strictly - /> - </template> - <template #leaderUserId="form"> - <el-select v-model="form['leaderUserId']"> + </el-form-item> + <el-form-item label="部门状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择不么你状态" + clearable + class="!w-240px" + > <el-option - v-for="item in userOption" - :key="item.id" - :label="item.nickname" - :value="item.id" + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" /> </el-select> - </template> - </Form> - <template #footer> - <!-- 按钮:保存 --> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :loading="actionLoading" - @click="submitForm()" - :title="t('action.save')" + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['system:dept:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button type="danger" plain @click="toggleExpandAll"> + <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table + v-loading="loading" + :data="list" + row-key="id" + v-if="refreshTable" + :default-expand-all="isExpandAll" + > + <el-table-column prop="name" label="部门名称" width="260" /> + <el-table-column prop="leader" label="负责人" width="120"> + <template #default="scope"> + {{ userList.find((user) => user.id === scope.row.leaderUserId)?.nickname }} + </template> + </el-table-column> + <el-table-column prop="sort" label="排序" width="200" /> + <el-table-column prop="status" label="状态" width="100"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" /> - <!-- 按钮:关闭 --> - <XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" /> - </template> - </XModal> + <el-table-column label="操作" align="center" class-name="fixed-width"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['system:dept:update']" + > + 修改 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['system:dept:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <DeptForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="Dept"> -import { handleTree, defaultProps } from '@/utils/tree' -import type { FormExpose } from '@/components/Form' -import { allSchemas, rules } from './dept.data' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import { handleTree } from '@/utils/tree' import * as DeptApi from '@/api/system/dept' -import { getListSimpleUsersApi, UserVO } from '@/api/system/user' - -const { t } = useI18n() // 国际化 +import DeptForm from './DeptForm.vue' +import * as UserApi from '@/api/system/user' const message = useMessage() // 消息弹窗 -// 列表相关的变量 -const xGrid = ref<any>() // 列表 Grid Ref -const treeConfig = { - transform: true, - rowField: 'id', - parentField: 'parentId', - expandAll: true -} +const { t } = useI18n() // 国际化 -// 弹窗相关的变量 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('edit') // 弹出层标题 -const actionType = ref('') // 操作按钮的类型 -const actionLoading = ref(false) // 遮罩层 -const formRef = ref<FormExpose>() // 表单 Ref -const deptOptions = ref() // 树形结构 -const userOption = ref<UserVO[]>([]) - -const getUserList = async () => { - const res = await getListSimpleUsersApi() - userOption.value = res -} -// 获取下拉框[上级]的数据 -const getTree = async () => { - deptOptions.value = [] - const res = await DeptApi.listSimpleDeptApi() - let dept: Tree = { id: 0, name: '顶级部门', children: [] } - dept.children = handleTree(res) - deptOptions.value.push(dept) -} -const [registerTable, { reload, deleteData }] = useXTable({ - allSchemas: allSchemas, - treeConfig: treeConfig, - getListApi: DeptApi.getDeptPageApi, - deleteApi: DeptApi.deleteDeptApi +const loading = ref(true) // 列表的加载中 +const list = ref() // 列表的数据 +const queryParams = reactive({ + title: '', + name: undefined, + status: undefined, + pageNo: 1, + pageSize: 100 }) -// ========== 新增/修改 ========== +const queryFormRef = ref() // 搜索的表单 +const isExpandAll = ref(true) // 是否展开,默认全部展开 +const refreshTable = ref(true) // 重新渲染表格状态 +const userList = ref<UserApi.UserVO[]>([]) // 用户列表 -// 设置标题 -const setDialogTile = (type: string) => { - dialogTitle.value = t('action.' + type) - actionType.value = type - dialogVisible.value = true +/** 查询部门列表 */ +const getList = async () => { + loading.value = true + try { + const data = await DeptApi.getDeptPageApi(queryParams) + list.value = handleTree(data) + } finally { + loading.value = false + } } -// 新增操作 -const handleCreate = async () => { - setDialogTile('create') -} - -// 修改操作 -const handleUpdate = async (rowId: number) => { - setDialogTile('update') - // 设置数据 - const res = await DeptApi.getDeptApi(rowId) - await nextTick() - unref(formRef)?.setValues(res) -} - -// 提交新增/修改的表单 -const submitForm = async () => { - const elForm = unref(formRef)?.getElFormRef() - if (!elForm) return - elForm.validate(async (valid) => { - if (valid) { - actionLoading.value = true - // 提交请求 - try { - const data = unref(formRef)?.formModel as DeptApi.DeptVO - if (actionType.value === 'create') { - await DeptApi.createDeptApi(data) - message.success(t('common.createSuccess')) - } else if (actionType.value === 'update') { - await DeptApi.updateDeptApi(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - actionLoading.value = false - await getTree() - await reload() - } - } +/** 展开/折叠操作 */ +const toggleExpandAll = () => { + refreshTable.value = false + isExpandAll.value = !isExpandAll.value + console.log(isExpandAll.value) + nextTick(() => { + refreshTable.value = true }) } -const userNicknameFormat = (row) => { - if (!row || !row.leaderUserId) { - return '未设置' - } - for (const user of userOption.value) { - if (row.leaderUserId === user.id) { - return user.nickname - } - } - return '未知【' + row.leaderUserId + '】' +/** 搜索按钮操作 */ +const handleQuery = () => { + getList() } -// ========== 初始化 ========== +/** 重置按钮操作 */ +const resetQuery = () => { + queryParams.pageNo = 1 + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await DeptApi.deleteDeptApi(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 初始化 **/ onMounted(async () => { - await getUserList() - await getTree() + await getList() + // 获取用户列表 + userList.value = await UserApi.getSimpleUserList() }) </script> diff --git a/src/views/system/dict/data.form.vue b/src/views/system/dict/data.form.vue index c0b70b8e..9271e8a9 100644 --- a/src/views/system/dict/data.form.vue +++ b/src/views/system/dict/data.form.vue @@ -51,10 +51,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/system/dict/form.vue b/src/views/system/dict/form.vue index af02c597..179656de 100644 --- a/src/views/system/dict/form.vue +++ b/src/views/system/dict/form.vue @@ -33,10 +33,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/system/errorCode/form.vue b/src/views/system/errorCode/form.vue index 9544c6ab..f261ced1 100644 --- a/src/views/system/errorCode/form.vue +++ b/src/views/system/errorCode/form.vue @@ -21,10 +21,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/system/errorCode/index.vue b/src/views/system/errorCode/index.vue index fc152903..c95d652c 100644 --- a/src/views/system/errorCode/index.vue +++ b/src/views/system/errorCode/index.vue @@ -99,7 +99,7 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> + <el-table-column label="操作" align="center" class-name="small-paddingfixed-width"> <template #default="scope"> <el-button link diff --git a/src/views/system/loginlog/LoginLogDetail.vue b/src/views/system/loginlog/LoginLogDetail.vue new file mode 100644 index 00000000..f0890eca --- /dev/null +++ b/src/views/system/loginlog/LoginLogDetail.vue @@ -0,0 +1,49 @@ +<template> + <Dialog title="详情" v-model="modelVisible" width="800"> + <el-descriptions border :column="1"> + <el-descriptions-item label="日志编号" min-width="120"> + {{ detailData.id }} + </el-descriptions-item> + <el-descriptions-item label="操作类型"> + <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="detailData.logType" /> + </el-descriptions-item> + <el-descriptions-item label="用户名称"> + {{ detailData.username }} + </el-descriptions-item> + <el-descriptions-item label="登录地址"> + {{ detailData.userIp }} + </el-descriptions-item> + <el-descriptions-item label="浏览器"> + {{ detailData.userAgent }} + </el-descriptions-item> + <el-descriptions-item label="登陆结果"> + <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="detailData.result" /> + </el-descriptions-item> + <el-descriptions-item label="登录日期"> + {{ formatDate(detailData.createTime) }} + </el-descriptions-item> + </el-descriptions> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import * as LoginLogApi from '@/api/system/loginLog' + +const modelVisible = ref(false) // 弹窗的是否展示 +const detailLoading = ref(false) // 表单的加载中 +const detailData = ref() // 详情数据 + +/** 打开弹窗 */ +const open = async (data: LoginLogApi.LoginLogVO) => { + modelVisible.value = true + // 设置数据 + detailLoading.value = true + try { + detailData.value = data + } finally { + detailLoading.value = false + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> diff --git a/src/views/system/loginlog/index.vue b/src/views/system/loginlog/index.vue index f2bb8c67..7b1aca5f 100644 --- a/src/views/system/loginlog/index.vue +++ b/src/views/system/loginlog/index.vue @@ -1,53 +1,175 @@ <template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 操作:导出 --> - <template #toolbar_buttons> - <XButton - type="warning" - preIcon="ep:download" - :title="t('action.export')" - @click="exportList('登录列表.xls')" + <content-wrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="用户名称" prop="username"> + <el-input + v-model="queryParams.username" + placeholder="请输入用户名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:详情 --> - <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" /> - </template> - </XTable> - </ContentWrap> - <!-- 弹窗 --> - <XModal id="postModel" v-model="dialogVisible" :title="dialogTitle"> - <!-- 表单:详情 --> - <Descriptions :schema="allSchemas.detailSchema" :data="detailData" /> - <template #footer> - <!-- 按钮:关闭 --> - <XButton :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> + </el-form-item> + <el-form-item label="登录地址" prop="userIp"> + <el-input + v-model="queryParams.userIp" + placeholder="请输入登录地址" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="登录日期" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['infra:config:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> + </content-wrap> + + <!-- 列表 --> + <content-wrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="日志编号" align="center" prop="id" /> + <el-table-column label="操作类型" align="center" prop="logType"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="scope.row.logType" /> + </template> + </el-table-column> + <el-table-column label="用户名称" align="center" prop="username" width="180" /> + <el-table-column label="登录地址" align="center" prop="userIp" width="180" /> + <el-table-column label="浏览器" align="center" prop="userAgent" /> + <el-table-column label="登陆结果" align="center" prop="result"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="scope.row.result" /> + </template> + </el-table-column> + <el-table-column + label="登录日期" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openDetail(scope.row)" + v-hasPermi="['infra:config:query']" + > + 详情 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </content-wrap> + + <!-- 表单弹窗:详情 --> + <LoginLogDetail ref="detailRef" /> </template> -<script setup lang="ts" name="Loginlog"> -// 业务相关的 import -import { allSchemas } from './loginLog.data' -import { getLoginLogPageApi, exportLoginLogApi, LoginLogVO } from '@/api/system/loginLog' +<script setup lang="ts" name="LoginLog"> +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' +import * as LoginLogApi from '@/api/system/loginLog' +import LoginLogDetail from './LoginLogDetail.vue' +const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 -// 列表相关的变量 -const [registerTable, { exportList }] = useXTable({ - allSchemas: allSchemas, - getListApi: getLoginLogPageApi, - exportListApi: exportLoginLogApi +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + username: undefined, + userIp: undefined, + createTime: [] }) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 -// 详情操作 -const detailData = ref() // 详情 Ref -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref(t('action.detail')) // 弹出层标题 -// 详情 -const handleDetail = async (row: LoginLogVO) => { - // 设置数据 - detailData.value = row - dialogVisible.value = true +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await LoginLogApi.getLoginLogPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } } + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 详情操作 */ +const detailRef = ref() +const openDetail = (data: LoginLogApi.LoginLogVO) => { + detailRef.value.open(data) +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await LoginLogApi.exportLoginLog(queryParams) + download.excel(data, '登录日志.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/system/loginlog/loginLog.data.ts b/src/views/system/loginlog/loginLog.data.ts deleted file mode 100644 index c0a51fbe..00000000 --- a/src/views/system/loginlog/loginLog.data.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '日志编号', - action: true, - actionWidth: '100px', - columns: [ - { - title: '日志类型', - field: 'logType', - dictType: DICT_TYPE.SYSTEM_LOGIN_TYPE, - dictClass: 'number' - }, - { - title: '用户名称', - field: 'username', - isSearch: true - }, - { - title: '登录地址', - field: 'userIp', - isSearch: true - }, - { - title: '浏览器', - field: 'userAgent' - }, - { - title: '登陆结果', - field: 'result', - dictType: DICT_TYPE.SYSTEM_LOGIN_RESULT, - dictClass: 'number' - }, - { - title: '登录日期', - field: 'createTime', - formatter: 'formatDate', - table: { - width: 150 - }, - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/mail/template/send.vue b/src/views/system/mail/template/send.vue index 94aaf004..b4b411b9 100644 --- a/src/views/system/mail/template/send.vue +++ b/src/views/system/mail/template/send.vue @@ -26,10 +26,8 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue new file mode 100644 index 00000000..45bcedfb --- /dev/null +++ b/src/views/system/menu/MenuForm.vue @@ -0,0 +1,253 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="上级菜单"> + <el-tree-select + node-key="id" + v-model="formData.parentId" + :props="defaultProps" + :data="menuTree" + :default-expanded-keys="[0]" + check-strictly + /> + </el-form-item> + <el-form-item label="菜单名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入菜单名称" clearable /> + </el-form-item> + <el-form-item label="菜单类型" prop="type"> + <el-radio-group v-model="formData.type"> + <el-radio-button + v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)" + :key="dict.label" + :label="dict.value" + > + {{ dict.label }} + </el-radio-button> + </el-radio-group> + </el-form-item> + <el-form-item label="菜单图标" v-if="formData.type !== 3"> + <IconSelect v-model="formData.icon" clearable /> + </el-form-item> + <el-form-item label="路由地址" prop="path" v-if="formData.type !== 3"> + <template #label> + <Tooltip + titel="路由地址" + message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头" + /> + </template> + <el-input v-model="formData.path" placeholder="请输入路由地址" clearable /> + </el-form-item> + <el-form-item label="组件地址" prop="component" v-if="formData.type === 2"> + <el-input v-model="formData.component" placeholder="例如说:system/user/index" clearable /> + </el-form-item> + <el-form-item label="组件名字" prop="componentName" v-if="formData.type === 2"> + <el-input v-model="formData.componentName" placeholder="例如说:SystemUser" clearable /> + </el-form-item> + <el-form-item label="权限标识" prop="permission" v-if="formData.type !== 1"> + <template #label> + <Tooltip + titel="权限标识" + message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)" + /> + </template> + <el-input v-model="formData.permission" placeholder="请输入权限标识" clearable /> + </el-form-item> + <el-form-item label="显示排序" prop="sort"> + <el-input-number v-model="formData.sort" controls-position="right" :min="0" clearable /> + </el-form-item> + <el-form-item label="菜单状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.label" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="显示状态" prop="visible" v-if="formData.type !== 3"> + <template #label> + <Tooltip titel="显示状态" message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" /> + </template> + <el-radio-group v-model="formData.visible"> + <el-radio border key="true" :label="true">显示</el-radio> + <el-radio border key="false" :label="false">隐藏</el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="总是显示" prop="alwaysShow" v-if="formData.type !== 3"> + <template #label> + <Tooltip + titel="总是显示" + message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单" + /> + </template> + <el-radio-group v-model="formData.alwaysShow"> + <el-radio border key="true" :label="true">总是</el-radio> + <el-radio border key="false" :label="false">不是</el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="缓存状态" prop="keepAlive" v-if="formData.type === 2"> + <template #label> + <Tooltip + titel="缓存状态" + message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段" + /> + </template> + <el-radio-group v-model="formData.keepAlive"> + <el-radio border key="true" :label="true">缓存</el-radio> + <el-radio border key="false" :label="false">不缓存</el-radio> + </el-radio-group> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import * as MenuApi from '@/api/system/menu' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants' +import { handleTree, defaultProps } from '@/utils/tree' +const { wsCache } = useCache() +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: 0, + name: '', + permission: '', + type: SystemMenuTypeEnum.DIR, + sort: Number(undefined), + parentId: 0, + path: '', + icon: '', + component: '', + componentName: '', + status: CommonStatusEnum.ENABLE, + visible: true, + keepAlive: true, + alwaysShow: true +}) +const formRules = reactive({ + name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }], + sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }], + path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }], + status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number, parentId?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + if (parentId) { + formData.value.parentId = parentId + } + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await MenuApi.getMenuApi(id) + } finally { + formLoading.value = false + } + } + // 获得菜单列表 + await getTree() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + if ( + formData.value.type === SystemMenuTypeEnum.DIR || + formData.value.type === SystemMenuTypeEnum.MENU + ) { + if (!isExternal(formData.value.path)) { + if (formData.value.parentId === 0 && formData.value.path.charAt(0) !== '/') { + message.error('路径必须以 / 开头') + return + } else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') { + message.error('路径不能以 / 开头') + return + } + } + } + const data = formData.value as unknown as MenuApi.MenuVO + if (formType.value === 'create') { + await MenuApi.createMenu(data) + message.success(t('common.createSuccess')) + } else { + await MenuApi.updateMenu(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + // 清空,从而触发刷新 + wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + } +} + +/** 获取下拉框[上级菜单]的数据 */ +const menuTree = ref<Tree[]>([]) // 树形结构 +const getTree = async () => { + menuTree.value = [] + const res = await MenuApi.getSimpleMenusList() + let menu: Tree = { id: 0, name: '主类目', children: [] } + menu.children = handleTree(res) + menuTree.value.push(menu) +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: 0, + name: '', + permission: '', + type: SystemMenuTypeEnum.DIR, + sort: Number(undefined), + parentId: 0, + path: '', + icon: '', + component: '', + componentName: '', + status: CommonStatusEnum.ENABLE, + visible: true, + keepAlive: true, + alwaysShow: true + } + formRef.value?.resetFields() +} + +/** 判断 path 是不是外部的 HTTP 等链接 */ +const isExternal = (path: string) => { + return /^(https?:|mailto:|tel:)/.test(path) +} +</script> diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 0604aa93..3baf3148 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -1,351 +1,183 @@ <template> + <!-- 搜索工作栏 --> <ContentWrap> - <!-- 列表 --> - <XTable ref="xGrid" @register="registerTable" show-overflow> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton - type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:menu:create']" - @click="handleCreate()" - /> - <XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" /> - <XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" /> - </template> - <template #name_default="{ row }"> - <Icon :icon="row.icon" /> - <span class="ml-3">{{ row.name }}</span> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['system:menu:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['system:menu:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> - </ContentWrap> - <!-- 添加或修改菜单对话框 --> - <XModal id="menuModel" v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(添加 / 修改) --> <el-form - ref="formRef" - :model="menuForm" - :rules="rules" - label-width="100px" - label-position="right" + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" > - <el-form-item label="上级菜单"> - <el-tree-select - node-key="id" - v-model="menuForm.parentId" - :props="defaultProps" - :data="menuOptions" - :default-expanded-keys="[0]" - check-strictly + <el-form-item label="菜单名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入菜单名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> </el-form-item> - <el-col :span="16"> - <el-form-item label="菜单名称" prop="name"> - <el-input v-model="menuForm.name" placeholder="请输入菜单名称" clearable /> - </el-form-item> - </el-col> - <el-form-item label="菜单类型" prop="type"> - <el-radio-group v-model="menuForm.type"> - <el-radio-button - v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)" - :key="dict.label" - :label="dict.value" - > - {{ dict.label }} - </el-radio-button> - </el-radio-group> + <el-form-item label="状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择菜单状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['system:menu:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button type="danger" plain @click="toggleExpandAll"> + <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠 + </el-button> </el-form-item> - <template v-if="menuForm.type !== 3"> - <el-form-item label="菜单图标"> - <IconSelect v-model="menuForm.icon" clearable /> - </el-form-item> - <el-col :span="16"> - <el-form-item label="路由地址" prop="path"> - <template #label> - <Tooltip - titel="路由地址" - message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头" - /> - </template> - <el-input v-model="menuForm.path" placeholder="请输入路由地址" clearable /> - </el-form-item> - </el-col> - </template> - <template v-if="menuForm.type === 2"> - <el-col :span="16"> - <el-form-item label="组件地址" prop="component"> - <el-input - v-model="menuForm.component" - placeholder="例如说:system/user/index" - clearable - /> - </el-form-item> - </el-col> - <el-col :span="16"> - <el-form-item label="组件名字" prop="componentName"> - <el-input v-model="menuForm.componentName" placeholder="例如说:SystemUser" clearable /> - </el-form-item> - </el-col> - </template> - <template v-if="menuForm.type !== 1"> - <el-col :span="16"> - <el-form-item label="权限标识" prop="permission"> - <template #label> - <Tooltip - titel="权限标识" - message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)" - /> - </template> - <el-input v-model="menuForm.permission" placeholder="请输入权限标识" clearable /> - </el-form-item> - </el-col> - </template> - <el-col :span="16"> - <el-form-item label="显示排序" prop="sort"> - <el-input-number v-model="menuForm.sort" controls-position="right" :min="0" clearable /> - </el-form-item> - </el-col> - <el-col :span="16"> - <el-form-item label="菜单状态" prop="status"> - <el-radio-group v-model="menuForm.status"> - <el-radio - border - v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="dict.label" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - </el-col> - <template v-if="menuForm.type !== 3"> - <el-col :span="16"> - <el-form-item label="显示状态" prop="visible"> - <template #label> - <Tooltip - titel="显示状态" - message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" - /> - </template> - <el-radio-group v-model="menuForm.visible"> - <el-radio border key="true" :label="true">显示</el-radio> - <el-radio border key="false" :label="false">隐藏</el-radio> - </el-radio-group> - </el-form-item> - </el-col> - </template> - <template v-if="menuForm.type !== 3"> - <el-col :span="16"> - <el-form-item label="总是显示" prop="alwaysShow"> - <template #label> - <Tooltip - titel="总是显示" - message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单" - /> - </template> - <el-radio-group v-model="menuForm.alwaysShow"> - <el-radio border key="true" :label="true">总是</el-radio> - <el-radio border key="false" :label="false">不是</el-radio> - </el-radio-group> - </el-form-item> - </el-col> - </template> - <template v-if="menuForm.type === 2"> - <el-col :span="16"> - <el-form-item label="缓存状态" prop="keepAlive"> - <template #label> - <Tooltip - titel="缓存状态" - message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段" - /> - </template> - <el-radio-group v-model="menuForm.keepAlive"> - <el-radio border key="true" :label="true">缓存</el-radio> - <el-radio border key="false" :label="false">不缓存</el-radio> - </el-radio-group> - </el-form-item> - </el-col> - </template> </el-form> - <template #footer> - <!-- 按钮:保存 --> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :loading="actionLoading" - @click="submitForm()" - :title="t('action.save')" - /> - <!-- 按钮:关闭 --> - <XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" /> - </template> - </XModal> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table + v-loading="loading" + :data="list" + row-key="id" + v-if="refreshTable" + :default-expand-all="isExpandAll" + > + <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="250" /> + <el-table-column prop="icon" label="图标" align="center" width="100"> + <template #default="scope"> + <Icon :icon="scope.row.icon" /> + </template> + </el-table-column> + <el-table-column prop="sort" label="排序" width="60" /> + <el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true" /> + <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true" /> + <el-table-column prop="componentName" label="组件名称" :show-overflow-tooltip="true" /> + <el-table-column prop="status" label="状态" width="80"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['system:menu:update']" + > + 修改 + </el-button> + <el-button + link + type="primary" + @click="openForm('create', undefined, scope.row.id)" + v-hasPermi="['system:menu:create']" + > + 新增 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['system:menu:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <MenuForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="Menu"> -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' -import { FormInstance } from 'element-plus' -// 业务相关的 import import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants' -import { handleTree, defaultProps } from '@/utils/tree' +import { handleTree } from '@/utils/tree' import * as MenuApi from '@/api/system/menu' -import { allSchemas, rules } from './menu.data' - +import MenuForm from './MenuForm.vue' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 -const { wsCache } = useCache() -const xGrid = ref<any>(null) - -// 列表相关的变量 -const treeConfig = { - transform: true, - rowField: 'id', - parentField: 'parentId', - expandAll: false -} -const [registerTable, { reload, deleteData }] = useXTable({ - allSchemas: allSchemas, - treeConfig: treeConfig, - getListApi: MenuApi.getMenuListApi, - deleteApi: MenuApi.deleteMenuApi -}) -// 弹窗相关的变量 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('edit') // 弹出层标题 -const actionType = ref('') // 操作按钮的类型 -const actionLoading = ref(false) // 遮罩层 -// 新增和修改的表单值 -const formRef = ref<FormInstance>() -const menuForm = ref<MenuApi.MenuVO>({ - id: 0, - name: '', - permission: '', - type: SystemMenuTypeEnum.DIR, - sort: 1, - parentId: 0, - path: '', - icon: '', - component: '', - componentName: '', - status: CommonStatusEnum.ENABLE, - visible: true, - keepAlive: true, - alwaysShow: true, - createTime: new Date() +const loading = ref(true) // 列表的加载中 +const list = ref<any>([]) // 列表的数据 +const queryParams = reactive({ + name: undefined, + status: undefined }) +const queryFormRef = ref() // 搜索的表单 +const isExpandAll = ref(false) // 是否展开,默认全部折叠 +const refreshTable = ref(true) // 重新渲染表格状态 -// ========== 下拉框[上级菜单] ========== -const menuOptions = ref<any[]>([]) // 树形结构 -// 获取下拉框[上级菜单]的数据 -const getTree = async () => { - menuOptions.value = [] - const res = await MenuApi.listSimpleMenusApi() - let menu: Tree = { id: 0, name: '主类目', children: [] } - menu.children = handleTree(res) - menuOptions.value.push(menu) -} - -// ========== 新增/修改 ========== - -// 设置标题 -const setDialogTile = async (type: string) => { - await getTree() - dialogTitle.value = t('action.' + type) - actionType.value = type - dialogVisible.value = true -} - -// 新增操作 -const handleCreate = () => { - setDialogTile('create') - // 重置表单 - formRef.value?.resetFields() - menuForm.value = { - id: 0, - name: '', - permission: '', - type: SystemMenuTypeEnum.DIR, - sort: 1, - parentId: 0, - path: '', - icon: '', - component: '', - componentName: '', - status: CommonStatusEnum.ENABLE, - visible: true, - keepAlive: true, - alwaysShow: true, - createTime: new Date() - } -} - -// 修改操作 -const handleUpdate = async (rowId: number) => { - await setDialogTile('update') - // 设置数据 - const res = await MenuApi.getMenuApi(rowId) - menuForm.value = res - // TODO 芋艿:这块要优化下,部分字段未重置,无法修改 - menuForm.value.componentName = res.componentName || '' - menuForm.value.alwaysShow = res.alwaysShow !== undefined ? res.alwaysShow : true -} - -// 提交新增/修改的表单 -const submitForm = async () => { - actionLoading.value = true - // 提交请求 +/** 查询参数列表 */ +const getList = async () => { + loading.value = true try { - if ( - menuForm.value.type === SystemMenuTypeEnum.DIR || - menuForm.value.type === SystemMenuTypeEnum.MENU - ) { - if (!isExternal(menuForm.value.path)) { - if (menuForm.value.parentId === 0 && menuForm.value.path.charAt(0) !== '/') { - message.error('路径必须以 / 开头') - return - } else if (menuForm.value.parentId !== 0 && menuForm.value.path.charAt(0) === '/') { - message.error('路径不能以 / 开头') - return - } - } - } - if (actionType.value === 'create') { - await MenuApi.createMenuApi(menuForm.value) - message.success(t('common.createSuccess')) - } else { - await MenuApi.updateMenuApi(menuForm.value) - message.success(t('common.updateSuccess')) - } + const data = await MenuApi.getMenuList(queryParams) + list.value = handleTree(data) } finally { - dialogVisible.value = false - actionLoading.value = false - wsCache.delete(CACHE_KEY.ROLE_ROUTERS) - // 操作成功,重新加载列表 - await reload() + loading.value = false } } -// 判断 path 是不是外部的 HTTP 等链接 -const isExternal = (path: string) => { - return /^(https?:|mailto:|tel:)/.test(path) +/** 搜索按钮操作 */ +const handleQuery = () => { + getList() } + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number, parentId?: number) => { + formRef.value.open(type, id, parentId) +} + +/** 展开/折叠操作 */ +const toggleExpandAll = () => { + refreshTable.value = false + isExpandAll.value = !isExpandAll.value + nextTick(() => { + refreshTable.value = true + }) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await MenuApi.deleteMenu(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/system/menu/menu.data.ts b/src/views/system/menu/menu.data.ts deleted file mode 100644 index 753c1211..00000000 --- a/src/views/system/menu/menu.data.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -const { t } = useI18n() // 国际化 - -// 新增和修改的表单校验 -export const rules = reactive({ - name: [required], - sort: [required], - path: [required], - status: [required] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: null, - action: true, - columns: [ - { - title: '上级菜单', - field: 'parentId', - isTable: false - }, - { - title: '菜单名称', - field: 'name', - isSearch: true, - table: { - treeNode: true, - align: 'left', - width: '200px', - slots: { - default: 'name_default' - } - } - }, - { - title: '菜单类型', - field: 'type', - dictType: DICT_TYPE.SYSTEM_MENU_TYPE - }, - { - title: '路由地址', - field: 'path' - }, - { - title: '组件路径', - field: 'component' - }, - { - title: '组件名字', - field: 'componentName' - }, - { - title: '权限标识', - field: 'permission' - }, - { - title: '排序', - field: 'sort' - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isTable: false - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/notice/form.vue b/src/views/system/notice/form.vue index b8a49586..87e75623 100644 --- a/src/views/system/notice/form.vue +++ b/src/views/system/notice/form.vue @@ -38,17 +38,14 @@ </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> <script setup lang="ts"> import { DICT_TYPE, getDictOptions } from '@/utils/dict' import * as NoticeApi from '@/api/system/notice' - const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 diff --git a/src/views/system/notify/template/index.vue b/src/views/system/notify/template/index.vue index 4ec16a0a..c4113924 100644 --- a/src/views/system/notify/template/index.vue +++ b/src/views/system/notify/template/index.vue @@ -119,7 +119,7 @@ import { FormExpose } from '@/components/Form' // 业务相关的 import import { rules, allSchemas } from './template.data' import * as NotifyTemplateApi from '@/api/system/notify/template' -import { getListSimpleUsersApi, UserVO } from '@/api/system/user' +import { getSimpleUserList, UserVO } from '@/api/system/user' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -244,7 +244,7 @@ const sendTest = async () => { // ========== 初始化 ========== onMounted(() => { - getListSimpleUsersApi().then((data) => { + getSimpleUserList().then((data) => { userOption.value = data }) }) diff --git a/src/views/system/oauth2/client/client.data.ts b/src/views/system/oauth2/client/client.data.ts deleted file mode 100644 index 52ee8895..00000000 --- a/src/views/system/oauth2/client/client.data.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -const { t } = useI18n() // 国际化 - -const authorizedGrantOptions = getStrDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE) - -// 表单校验 -export const rules = reactive({ - clientId: [required], - secret: [required], - name: [required], - status: [required], - accessTokenValiditySeconds: [required], - refreshTokenValiditySeconds: [required], - redirectUris: [required], - authorizedGrantTypes: [required] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'clientId', - primaryType: null, - action: true, - columns: [ - { - title: '客户端端号', - field: 'clientId' - }, - { - title: '客户端密钥', - field: 'secret' - }, - { - title: '应用名', - field: 'name', - isSearch: true - }, - { - title: '应用图标', - field: 'logo', - table: { - cellRender: { - name: 'XImg' - } - }, - form: { - component: 'UploadImg' - } - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: '访问令牌的有效期', - field: 'accessTokenValiditySeconds', - form: { - component: 'InputNumber' - }, - table: { - slots: { - default: 'accessTokenValiditySeconds_default' - } - } - }, - { - title: '刷新令牌的有效期', - field: 'refreshTokenValiditySeconds', - form: { - component: 'InputNumber' - }, - table: { - slots: { - default: 'refreshTokenValiditySeconds_default' - } - } - }, - { - title: '授权类型', - field: 'authorizedGrantTypes', - table: { - width: 400, - slots: { - default: 'authorizedGrantTypes_default' - } - }, - form: { - component: 'Select', - componentProps: { - options: authorizedGrantOptions, - multiple: true, - filterable: true - } - } - }, - { - title: '授权范围', - field: 'scopes', - isTable: false, - form: { - component: 'Select', - componentProps: { - options: [], - multiple: true, - filterable: true, - allowCreate: true, - defaultFirstOption: true - } - } - }, - { - title: '自动授权范围', - field: 'autoApproveScopes', - isTable: false, - form: { - component: 'Select', - componentProps: { - options: [], - multiple: true, - filterable: true, - allowCreate: true, - defaultFirstOption: true - } - } - }, - { - title: '可重定向的 URI 地址', - field: 'redirectUris', - isTable: false, - form: { - component: 'Select', - componentProps: { - options: [], - multiple: true, - filterable: true, - allowCreate: true, - defaultFirstOption: true - } - } - }, - { - title: '权限', - field: 'authorities', - isTable: false, - form: { - component: 'Select', - componentProps: { - options: [], - multiple: true, - filterable: true, - allowCreate: true, - defaultFirstOption: true - } - } - }, - { - title: '资源', - field: 'resourceIds', - isTable: false, - form: { - component: 'Select', - componentProps: { - options: [], - multiple: true, - filterable: true, - allowCreate: true, - defaultFirstOption: true - } - } - }, - { - title: '附加信息', - field: 'additionalInformation', - isTable: false, - form: { - component: 'Input', - componentProps: { - type: 'textarea', - rows: 4 - }, - colProps: { - span: 24 - } - } - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/oauth2/client/form.vue b/src/views/system/oauth2/client/form.vue new file mode 100644 index 00000000..0822e59f --- /dev/null +++ b/src/views/system/oauth2/client/form.vue @@ -0,0 +1,259 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible" width="800"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="120px" + v-loading="formLoading" + > + <el-form-item label="客户端编号" prop="secret"> + <el-input v-model="formData.clientId" placeholder="请输入客户端编号" /> + </el-form-item> + <el-form-item label="客户端密钥" prop="secret"> + <el-input v-model="formData.secret" placeholder="请输入客户端密钥" /> + </el-form-item> + <el-form-item label="应用名" prop="name"> + <el-input v-model="formData.name" placeholder="请输入应用名" /> + </el-form-item> + <el-form-item label="应用图标"> + <UploadImg v-model="formData.logo" :limit="1" /> + </el-form-item> + <el-form-item label="应用描述"> + <el-input type="textarea" v-model="formData.description" placeholder="请输入应用名" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="parseInt(dict.value)" + >{{ dict.label }}</el-radio + > + </el-radio-group> + </el-form-item> + <el-form-item label="访问令牌的有效期" prop="accessTokenValiditySeconds"> + <el-input-number v-model="formData.accessTokenValiditySeconds" placeholder="单位:秒" /> + </el-form-item> + <el-form-item label="刷新令牌的有效期" prop="refreshTokenValiditySeconds"> + <el-input-number v-model="formData.refreshTokenValiditySeconds" placeholder="单位:秒" /> + </el-form-item> + <el-form-item label="授权类型" prop="authorizedGrantTypes"> + <el-select + v-model="formData.authorizedGrantTypes" + multiple + filterable + placeholder="请输入授权类型" + style="width: 500px" + > + <el-option + v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="授权范围" prop="scopes"> + <el-select + v-model="formData.scopes" + multiple + filterable + allow-create + placeholder="请输入授权范围" + style="width: 500px" + > + <el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" /> + </el-select> + </el-form-item> + <el-form-item label="自动授权范围" prop="autoApproveScopes"> + <el-select + v-model="formData.autoApproveScopes" + multiple + filterable + placeholder="请输入授权范围" + style="width: 500px" + > + <el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" /> + </el-select> + </el-form-item> + <el-form-item label="可重定向的 URI 地址" prop="redirectUris"> + <el-select + v-model="formData.redirectUris" + multiple + filterable + allow-create + placeholder="请输入可重定向的 URI 地址" + style="width: 500px" + > + <el-option + v-for="redirectUri in formData.redirectUris" + :key="redirectUri" + :label="redirectUri" + :value="redirectUri" + /> + </el-select> + </el-form-item> + <el-form-item label="权限" prop="authorities"> + <el-select + v-model="formData.authorities" + multiple + filterable + allow-create + placeholder="请输入权限" + style="width: 500px" + > + <el-option + v-for="authority in formData.authorities" + :key="authority" + :label="authority" + :value="authority" + /> + </el-select> + </el-form-item> + <el-form-item label="资源" prop="resourceIds"> + <el-select + v-model="formData.resourceIds" + multiple + filterable + allow-create + placeholder="请输入资源" + style="width: 500px" + > + <el-option + v-for="resourceId in formData.resourceIds" + :key="resourceId" + :label="resourceId" + :value="resourceId" + /> + </el-select> + </el-form-item> + <el-form-item label="附加信息" prop="additionalInformation"> + <el-input + type="textarea" + v-model="formData.additionalInformation" + placeholder="请输入附加信息,JSON 格式数据" + /> + </el-form-item> + </el-form> + <template #footer> + <div class="dialog-footer"> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </div> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import * as ClientApi from '@/api/system/oauth2/client' +import UploadImg from '@/components/UploadFile' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + clientId: undefined, + secret: undefined, + name: undefined, + logo: undefined, + description: undefined, + status: DICT_TYPE.COMMON_STATUS, + accessTokenValiditySeconds: 30 * 60, + refreshTokenValiditySeconds: 30 * 24 * 60, + redirectUris: [], + authorizedGrantTypes: [], + scopes: [], + autoApproveScopes: [], + authorities: [], + resourceIds: [], + additionalInformation: undefined +}) +const formRules = reactive({ + clientId: [{ required: true, message: '客户端编号不能为空', trigger: 'blur' }], + secret: [{ required: true, message: '客户端密钥不能为空', trigger: 'blur' }], + name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }], + logo: [{ required: true, message: '应用图标不能为空', trigger: 'blur' }], + status: [{ required: true, message: '状态不能为空', trigger: 'blur' }], + accessTokenValiditySeconds: [ + { required: true, message: '访问令牌的有效期不能为空', trigger: 'blur' } + ], + refreshTokenValiditySeconds: [ + { required: true, message: '刷新令牌的有效期不能为空', trigger: 'blur' } + ], + redirectUris: [{ required: true, message: '可重定向的 URI 地址不能为空', trigger: 'blur' }], + authorizedGrantTypes: [{ required: true, message: '授权类型不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const openModal = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await ClientApi.getOAuth2ClientApi(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as ClientApi.OAuth2ClientVO + if (formType.value === 'create') { + await ClientApi.createOAuth2ClientApi(data) + message.success(t('common.createSuccess')) + } else { + await ClientApi.updateOAuth2ClientApi(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + clientId: undefined, + secret: undefined, + name: undefined, + logo: undefined, + description: undefined, + status: DICT_TYPE.COMMON_STATUS, + accessTokenValiditySeconds: 30 * 60, + refreshTokenValiditySeconds: 30 * 24 * 60, + redirectUris: [], + authorizedGrantTypes: [], + scopes: [], + autoApproveScopes: [], + authorities: [], + resourceIds: [], + additionalInformation: undefined + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/oauth2/client/index.vue b/src/views/system/oauth2/client/index.vue index 9ff44692..c88af726 100644 --- a/src/views/system/oauth2/client/index.vue +++ b/src/views/system/oauth2/client/index.vue @@ -133,7 +133,6 @@ import type { FormExpose } from '@/components/Form' // 业务相关的 import import * as ClientApi from '@/api/system/oauth2/client' -import { rules, allSchemas } from './client.data' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 diff --git a/src/views/system/operatelog/detail.vue b/src/views/system/operatelog/detail.vue index 6c856e95..b3603e2e 100644 --- a/src/views/system/operatelog/detail.vue +++ b/src/views/system/operatelog/detail.vue @@ -41,7 +41,7 @@ {{ detailData.javaMethodArgs }} </el-descriptions-item> <el-descriptions-item label="操作时间"> - {{ formatDate(detailData.startTime, 'YYYY-MM-DD HH:mm:ss') }} + {{ formatDate(detailData.startTime) }} </el-descriptions-item> <el-descriptions-item label="执行时长">{{ detailData.duration }} ms</el-descriptions-item> <el-descriptions-item label="操作结果"> diff --git a/src/views/system/post/PostForm.vue b/src/views/system/post/PostForm.vue new file mode 100644 index 00000000..a9dec8b0 --- /dev/null +++ b/src/views/system/post/PostForm.vue @@ -0,0 +1,120 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible" width="800"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="岗位标题" prop="name"> + <el-input v-model="formData.name" placeholder="请输入岗位标题" /> + </el-form-item> + <el-form-item label="岗位编码" prop="code"> + <el-input :model-value="formData.code" placeholder="请输入岗位编码" height="150px" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="formData.status" placeholder="请选择状态" clearable> + <el-option + v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="parseInt(dict.value)" + :label="dict.label" + :value="parseInt(dict.value)" + /> + </el-select> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" type="textarea" placeholder="请输备注" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import { CommonStatusEnum } from '@/utils/constants' +import * as PostApi from '@/api/system/post' + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: '', + code: '', + sort: undefined, + status: CommonStatusEnum.ENABLE, + remark: '' +}) +const formRules = reactive({ + name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }], + code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }], + status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }], + remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const openModal = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await PostApi.getPost(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as PostApi.PostVO + if (formType.value === 'create') { + await PostApi.createPost(data) + message.success(t('common.createSuccess')) + } else { + await PostApi.updatePost(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: '', + code: '', + sort: undefined, + status: CommonStatusEnum.ENABLE, + remark: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/post/form.vue b/src/views/system/post/form.vue deleted file mode 100644 index 065aecaf..00000000 --- a/src/views/system/post/form.vue +++ /dev/null @@ -1,91 +0,0 @@ -<template> - <!-- 弹窗 --> - <XModal :title="modelTitle" :loading="modelLoading" v-model="modelVisible"> - <!-- 表单:添加/修改 --> - <Form - ref="formRef" - v-if="['create', 'update'].includes(actionType)" - :schema="allSchemas.formSchema" - :rules="rules" - /> - <!-- 表单:详情 --> - <Descriptions - v-if="actionType === 'detail'" - :schema="allSchemas.detailSchema" - :data="detailData" - /> - <template #footer> - <!-- 按钮:保存 --> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitForm()" - /> - <!-- 按钮:关闭 --> - <XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" /> - </template> - </XModal> -</template> -<script setup lang="ts"> -import type { FormExpose } from '@/components/Form' -import * as PostApi from '@/api/system/post' -import { rules, allSchemas } from './post.data' -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -// 弹窗相关的变量 -const modelVisible = ref(false) // 是否显示弹出层 -const modelTitle = ref('') // 弹出层标题 -const modelLoading = ref(false) // 弹出层loading -const actionType = ref('') // 操作按钮的类型 -const actionLoading = ref(false) // 按钮 Loading -const formRef = ref<FormExpose>() // 表单 Ref -const detailData = ref() // 详情 Ref - -// 打开弹窗 -const openModal = async (type: string, id?: number) => { - modelVisible.value = true - modelLoading.value = true - modelTitle.value = t('action.' + type) - actionType.value = type - // 设置数据 - if (id) { - const res = await PostApi.getPostApi(id) - if (type === 'update') { - unref(formRef)?.setValues(res) - } else if (type === 'detail') { - detailData.value = res - } - } - modelLoading.value = false -} -defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 - -// 提交新增/修改的表单 -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - const elForm = unref(formRef)?.getElFormRef() - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return - // 提交请求 - actionLoading.value = true - try { - const data = unref(formRef)?.formModel as PostApi.PostVO - if (actionType.value === 'create') { - await PostApi.createPostApi(data) - message.success(t('common.createSuccess')) - } else { - await PostApi.updatePostApi(data) - message.success(t('common.updateSuccess')) - } - modelVisible.value = false - emit('success') - } finally { - actionLoading.value = false - } -} -</script> diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue index c5a13e1e..03e491d0 100644 --- a/src/views/system/post/index.vue +++ b/src/views/system/post/index.vue @@ -1,71 +1,198 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="岗位名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入岗位名称" + clearable + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="岗位编码" prop="code"> + <el-input + v-model="queryParams.code" + placeholder="请输入岗位编码" + clearable + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="queryParams.status" placeholder="请选择状态" clearable> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:post:create']" @click="openModal('create')" - /> - <!-- 操作:导出 --> - <XButton - type="primary" + v-hasPermi="['system:notice:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button + type="success" plain - preIcon="ep:download" - :title="t('action.export')" - v-hasPermi="['system:post:export']" - @click="exportList('岗位列表.xls')" - /> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['system:post:update']" - @click="openModal('update', row?.id)" - /> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['system:post:query']" - @click="openModal('detail', row?.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.delete')" - v-hasPermi="['system:post:delete']" - @click="deleteData(row?.id)" - /> - </template> - </XTable> + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['infra:config:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> </ContentWrap> - <!-- 表单弹窗:添加/修改/详情 --> - <PostForm ref="modalRef" @success="reload()" /> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" align="center"> + <el-table-column label="岗位编号" align="center" prop="id" /> + <el-table-column label="岗位名称" align="center" prop="name" /> + <el-table-column label="岗位编码" align="center" prop="code" /> + <el-table-column label="岗位顺序" align="center" prop="sort" /> + <el-table-column label="岗位备注" align="center" prop="remark" /> + <el-table-column label="状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openModal('update', scope.row.id)" + v-hasPermi="['system:post:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['system:post:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <PostForm ref="formRef" @success="getList" /> </template> -<script setup lang="ts" name="Post"> +<script setup lang="tsx"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' import * as PostApi from '@/api/system/post' -import { allSchemas } from './post.data' -import PostForm from './form.vue' +import PostForm from './PostForm.vue' + +const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 -// 列表相关的变量 -const [registerTable, { reload, deleteData, exportList }] = useXTable({ - allSchemas: allSchemas, // 列表配置 - getListApi: PostApi.getPostPageApi, // 加载列表的 API - deleteApi: PostApi.deletePostApi, // 删除数据的 API - exportListApi: PostApi.exportPostApi // 导出数据的 API +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + code: '', + name: '', + status: undefined }) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 -// 表单相关的变量 -const modalRef = ref() -const openModal = (type: string, id?: number) => { - modalRef.value.openModal(type, id) +/** 查询岗位列表 */ +const getList = async () => { + loading.value = true + try { + const data = await PostApi.getPostPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } } + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openModal = (type: string, id?: number) => { + formRef.value.openModal(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await PostApi.deletePost(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await PostApi.exportPost(queryParams) + download.excel(data, '岗位列表.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) </script> diff --git a/src/views/system/post/post.data.ts b/src/views/system/post/post.data.ts deleted file mode 100644 index 4926bcc6..00000000 --- a/src/views/system/post/post.data.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -const { t } = useI18n() // 国际化 - -// 表单校验 -export const rules = reactive({ - name: [required], - code: [required], - sort: [required] -}) - -// 增删改查 CrudSchema 配置 -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '岗位编号', - action: true, - columns: [ - { - title: '岗位名称', - field: 'name', - isSearch: true - }, - { - title: '岗位编码', - field: 'code', - isSearch: true - }, - { - title: '岗位顺序', - field: 'sort', - form: { - component: 'InputNumber' - } - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: '备注', - field: 'remark', - isTable: false - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false, - table: { - width: 180 - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/role/MenuPermissionForm.vue b/src/views/system/role/MenuPermissionForm.vue new file mode 100644 index 00000000..650fb659 --- /dev/null +++ b/src/views/system/role/MenuPermissionForm.vue @@ -0,0 +1,202 @@ +<template> + <Dialog :title="dialogScopeTitle" v-model="dialogScopeVisible" width="800"> + <el-form + ref="menuPermissionFormRef" + :model="dataScopeForm" + :inline="true" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="角色名称"> + <el-tag>{{ dataScopeForm.name }}</el-tag> + </el-form-item> + <el-form-item label="角色标识"> + <el-tag>{{ dataScopeForm.code }}</el-tag> + </el-form-item> + <!-- 分配角色的数据权限对话框 --> + <el-form-item label="权限范围" v-if="actionScopeType === 'data'"> + <el-select v-model="dataScopeForm.dataScope"> + <el-option + v-for="item in dataScopeDictDatas" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + </el-form-item> + <!-- 分配角色的菜单权限对话框 --> + <el-row> + <el-col :span="24"> + <el-form-item + label="权限范围" + v-if=" + actionScopeType === 'menu' || + dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM + " + style="display: flex" + > + <el-card class="card" shadow="never"> + <template #header> + 父子联动(选中父节点,自动选择子节点): + <el-switch + v-model="checkStrictly" + inline-prompt + active-text="是" + inactive-text="否" + /> + 全选/全不选: + <el-switch + v-model="treeNodeAll" + inline-prompt + active-text="是" + inactive-text="否" + @change="handleCheckedTreeNodeAll()" + /> + </template> + <el-tree + ref="treeRef" + node-key="id" + show-checkbox + :check-strictly="!checkStrictly" + :props="defaultProps" + :data="dataScopeForm" + empty-text="加载中,请稍后" + /> + </el-card> + </el-form-item> </el-col + ></el-row> + </el-form> + <!-- 操作按钮 --> + <template #footer> + <div class="dialog-footer"> + <el-button + :title="t('action.save')" + :loading="actionLoading" + @click="submitScope()" + type="primary" + :disabled="formLoading" + > + 保存 + </el-button> + <el-button + :loading="actionLoading" + :title="t('dialog.close')" + @click="dialogScopeVisible = false" + >取 消</el-button + > + </div> + </template> + </Dialog> +</template> + +<script setup lang="ts"> +import * as RoleApi from '@/api/system/role' +import type { ElTree } from 'element-plus' +import type { FormExpose } from '@/components/Form' +import { handleTree, defaultProps } from '@/utils/tree' +import { SystemDataScopeEnum } from '@/utils/constants' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import * as MenuApi from '@/api/system/menu' +import * as DeptApi from '@/api/system/dept' +import * as PermissionApi from '@/api/system/permission' +// ========== CRUD 相关 ========== +const actionLoading = ref(false) // 遮罩层 +const menuPermissionFormRef = ref<FormExpose>() // 表单 Ref +const { t } = useI18n() // 国际化 +const dialogScopeTitle = ref('菜单权限') +const dataScopeDictDatas = ref() +const message = useMessage() // 消息弹窗 +const actionScopeType = ref('') +// 选项 +const checkStrictly = ref(true) +const treeNodeAll = ref(false) +const dialogScopeVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const treeOptions = ref<any[]>([]) // 菜单树形结构 +const treeRef = ref<InstanceType<typeof ElTree>>() +// ========== 数据权限 ========== +const dataScopeForm = reactive({ + id: 0, + name: '', + code: '', + dataScope: 0, + checkList: [] +}) + +/** 打开弹窗 */ +const openModal = async (type: string, row: RoleApi.RoleVO) => { + dataScopeForm.id = row.id + dataScopeForm.name = row.name + dataScopeForm.code = row.code + actionScopeType.value = type + dialogScopeVisible.value = true + if (type === 'menu') { + const menuRes = await MenuApi.getSimpleMenusList() + treeOptions.value = handleTree(menuRes) + const role = await PermissionApi.listRoleMenusApi(row.id) + if (role) { + role?.forEach((item: any) => { + unref(treeRef)?.setChecked(item, true, false) + }) + } + } else if (type === 'data') { + const deptRes = await DeptApi.getSimpleDeptList() + treeOptions.value = handleTree(deptRes) + const role = await RoleApi.getRole(row.id) + dataScopeForm.dataScope = role.dataScope + if (role.dataScopeDeptIds) { + role.dataScopeDeptIds?.forEach((item: any) => { + unref(treeRef)?.setChecked(item, true, false) + }) + } + } +} + +// 保存权限 +const submitScope = async () => { + if ('data' === actionScopeType.value) { + const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({ + roleId: dataScopeForm.id, + dataScope: dataScopeForm.dataScope, + dataScopeDeptIds: + dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM + ? [] + : (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>) + }) + await PermissionApi.assignRoleDataScopeApi(data.value) + } else if ('menu' === actionScopeType.value) { + const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({ + roleId: dataScopeForm.id, + menuIds: [ + ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), + ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) + ] + }) + await PermissionApi.assignRoleMenuApi(data.value) + } + message.success(t('common.updateSuccess')) + dialogScopeVisible.value = false +} + +// 全选/全不选 +const handleCheckedTreeNodeAll = () => { + treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : []) +} + +const init = () => { + dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE) +} + +defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 +// ========== 初始化 ========== +onMounted(() => { + init() +}) +</script> +<style scoped> +.card { + width: 100%; + max-height: 400px; + overflow-y: scroll; +} +</style> diff --git a/src/views/system/role/RoleForm.vue b/src/views/system/role/RoleForm.vue new file mode 100644 index 00000000..e6444a03 --- /dev/null +++ b/src/views/system/role/RoleForm.vue @@ -0,0 +1,123 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="角色名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入角色名称" /> + </el-form-item> + <el-form-item label="角色标识" prop="code"> + <el-input :model-value="formData.code" placeholder="请输入角色标识" height="150px" /> + </el-form-item> + <el-form-item label="显示顺序" prop="sort"> + <el-input :model-value="formData.sort" placeholder="请输入显示顺序" height="150px" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="formData.status" placeholder="请选择状态" clearable> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" type="textarea" placeholder="请输备注" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { CommonStatusEnum } from '@/utils/constants' +import * as RoleApi from '@/api/system/role' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: '', + code: '', + sort: undefined, + status: CommonStatusEnum.ENABLE, + remark: '' +}) +const formRules = reactive({ + name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }], + code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }], + sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }], + status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }], + remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await RoleApi.getRole(id) + } finally { + formLoading.value = false + } + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: '', + code: '', + sort: undefined, + status: CommonStatusEnum.ENABLE, + remark: '' + } + formRef.value?.resetFields() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as RoleApi.RoleVO + if (formType.value === 'create') { + await RoleApi.createRole(data) + message.success(t('common.createSuccess')) + } else { + await RoleApi.updateRole(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} +</script> diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue index da4b8389..0e75d67d 100644 --- a/src/views/system/role/index.vue +++ b/src/views/system/role/index.vue @@ -1,331 +1,242 @@ <template> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 操作:新增 --> - <template #toolbar_buttons> - <XButton - type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:role:create']" - @click="handleCreate()" + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="角色名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入角色名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" /> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:编辑 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['system:role:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['system:role:query']" - @click="handleDetail(row.id)" - /> - <!-- 操作:菜单权限 --> - <XTextButton - preIcon="ep:basketball" - title="菜单权限" - v-hasPermi="['system:permission:assign-role-menu']" - @click="handleScope('menu', row)" - /> - <!-- 操作:数据权限 --> - <XTextButton - preIcon="ep:coin" - title="数据权限" - v-hasPermi="['system:permission:assign-role-data-scope']" - @click="handleScope('data', row)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['system:role:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> - </ContentWrap> - - <XModal v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(添加 / 修改) --> - <Form - v-if="['create', 'update'].includes(actionType)" - :schema="allSchemas.formSchema" - :rules="rules" - ref="formRef" - /> - <!-- 对话框(详情) --> - <Descriptions - v-if="actionType === 'detail'" - :schema="allSchemas.detailSchema" - :data="detailData" - /> - <!-- 操作按钮 --> - <template #footer> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitForm()" - /> - <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> - - <XModal v-model="dialogScopeVisible" :title="dialogScopeTitle"> - <el-form :model="dataScopeForm" label-width="140px" :inline="true"> - <el-form-item label="角色名称"> - <el-tag>{{ dataScopeForm.name }}</el-tag> </el-form-item> - <el-form-item label="角色标识"> - <el-tag>{{ dataScopeForm.code }}</el-tag> + <el-form-item label="角色标识" prop="code"> + <el-input + v-model="queryParams.code" + placeholder="请输入角色标识" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> </el-form-item> - <!-- 分配角色的数据权限对话框 --> - <el-form-item label="权限范围" v-if="actionScopeType === 'data'"> - <el-select v-model="dataScopeForm.dataScope"> + <el-form-item label="状态" prop="status"> + <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> <el-option - v-for="item in dataScopeDictDatas" - :key="item.value" - :label="item.label" - :value="item.value" + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" /> </el-select> </el-form-item> - <!-- 分配角色的菜单权限对话框 --> - <el-row> - <el-col :span="24"> - <el-form-item - label="权限范围" - v-if=" - actionScopeType === 'menu' || - dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM - " - style="display: flex" - > - <el-card class="card" shadow="never"> - <template #header> - 父子联动(选中父节点,自动选择子节点): - <el-switch - v-model="checkStrictly" - inline-prompt - active-text="是" - inactive-text="否" - /> - 全选/全不选: - <el-switch - v-model="treeNodeAll" - inline-prompt - active-text="是" - inactive-text="否" - @change="handleCheckedTreeNodeAll()" - /> - </template> - <el-tree - ref="treeRef" - node-key="id" - show-checkbox - :check-strictly="!checkStrictly" - :props="defaultProps" - :data="treeOptions" - empty-text="加载中,请稍后" - /> - </el-card> - </el-form-item> </el-col - ></el-row> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['system:role:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['system:role:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> </el-form> - <!-- 操作按钮 --> - <template #footer> - <XButton - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitScope()" + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="角色编号" align="center" prop="id" /> + <el-table-column label="角色名称" align="center" prop="name" /> + <el-table-column label="角色类型" align="center" prop="type" /> + <el-table-column label="角色标识" align="center" prop="code" /> + <el-table-column label="显示顺序" align="center" prop="sort" /> + <el-table-column label="备注" align="center" prop="remark" /> + <el-table-column label="状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" /> - <XButton - :loading="actionLoading" - :title="t('dialog.close')" - @click="dialogScopeVisible = false" - /> - </template> - </XModal> + <el-table-column :width="300" label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['system:role:update']" + > + 编辑 + </el-button> + <el-button + link + type="primary" + preIcon="ep:basketball" + title="菜单权限" + v-hasPermi="['system:permission:assign-role-menu']" + @click="handleScope('menu', scope.row)" + > + 菜单权限 + </el-button> + <el-button + link + type="primary" + preIcon="ep:coin" + title="数据权限" + v-hasPermi="['system:permission:assign-role-data-scope']" + @click="handleScope('data', scope.row)" + > + 数据权限 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['system:role:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <RoleForm ref="formRef" @success="getList" /> + <!-- 表单弹窗:菜单权限 --> + <MenuPermissionForm ref="menuPermissionFormRef" @success="getList" /> </template> -<script setup lang="ts" name="Role"> -import type { ElTree } from 'element-plus' -import type { FormExpose } from '@/components/Form' -import { handleTree, defaultProps } from '@/utils/tree' -import { SystemDataScopeEnum } from '@/utils/constants' -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { rules, allSchemas } from './role.data' +<script setup lang="tsx"> import * as RoleApi from '@/api/system/role' -import { listSimpleMenusApi } from '@/api/system/menu' -import { listSimpleDeptApi } from '@/api/system/dept' -import * as PermissionApi from '@/api/system/permission' - -const { t } = useI18n() // 国际化 +import RoleForm from './RoleForm.vue' +import MenuPermissionForm from './MenuPermissionForm.vue' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' const message = useMessage() // 消息弹窗 -// 列表相关的变量 -const [registerTable, { reload, deleteData }] = useXTable({ - allSchemas: allSchemas, - getListApi: RoleApi.getRolePageApi, - deleteApi: RoleApi.deleteRoleApi -}) +const { t } = useI18n() // 国际化 -// ========== CRUD 相关 ========== -const actionLoading = ref(false) // 遮罩层 -const actionType = ref('') // 操作按钮的类型 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('edit') // 弹出层标题 -const formRef = ref<FormExpose>() // 表单 Ref -const detailData = ref() // 详情 Ref - -// 设置标题 -const setDialogTile = (type: string) => { - dialogTitle.value = t('action.' + type) - actionType.value = type - dialogVisible.value = true -} - -// 新增操作 -const handleCreate = () => { - setDialogTile('create') -} - -// 修改操作 -const handleUpdate = async (rowId: number) => { - setDialogTile('update') - // 设置数据 - const res = await RoleApi.getRoleApi(rowId) - unref(formRef)?.setValues(res) -} - -// 详情操作 -const handleDetail = async (rowId: number) => { - setDialogTile('detail') - // 设置数据 - const res = await RoleApi.getRoleApi(rowId) - detailData.value = res -} - -// 提交按钮 -const submitForm = async () => { - const elForm = unref(formRef)?.getElFormRef() - if (!elForm) return - elForm.validate(async (valid) => { - if (valid) { - actionLoading.value = true - // 提交请求 - try { - const data = unref(formRef)?.formModel as RoleApi.RoleVO - if (actionType.value === 'create') { - await RoleApi.createRoleApi(data) - message.success(t('common.createSuccess')) - } else { - await RoleApi.updateRoleApi(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - actionLoading.value = false - // 刷新列表 - await reload() - } - } - }) -} - -// ========== 数据权限 ========== -const dataScopeForm = reactive({ - id: 0, - name: '', +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, code: '', - dataScope: 0, - checkList: [] + name: '', + status: undefined, + createTime: [] }) -const treeOptions = ref<any[]>([]) // 菜单树形结构 -const treeRef = ref<InstanceType<typeof ElTree>>() -const dialogScopeVisible = ref(false) -const dialogScopeTitle = ref('数据权限') -const actionScopeType = ref('') -const dataScopeDictDatas = ref() -// 选项 -const checkStrictly = ref(true) -const treeNodeAll = ref(false) -// 全选/全不选 -const handleCheckedTreeNodeAll = () => { - treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : []) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询角色列表 */ +const getList = async () => { + loading.value = true + try { + const data = await RoleApi.getRolePage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } } -// 权限操作 + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 数据权限操作 */ +const menuPermissionFormRef = ref() const handleScope = async (type: string, row: RoleApi.RoleVO) => { - dataScopeForm.id = row.id - dataScopeForm.name = row.name - dataScopeForm.code = row.code - actionScopeType.value = type - dialogScopeVisible.value = true - if (type === 'menu') { - const menuRes = await listSimpleMenusApi() - treeOptions.value = handleTree(menuRes) - const role = await PermissionApi.listRoleMenusApi(row.id) - if (role) { - role?.forEach((item: any) => { - unref(treeRef)?.setChecked(item, true, false) - }) - } - } else if (type === 'data') { - const deptRes = await listSimpleDeptApi() - treeOptions.value = handleTree(deptRes) - const role = await RoleApi.getRoleApi(row.id) - dataScopeForm.dataScope = role.dataScope - if (role.dataScopeDeptIds) { - role.dataScopeDeptIds?.forEach((item: any) => { - unref(treeRef)?.setChecked(item, true, false) - }) - } + menuPermissionFormRef.value.openForm(type, row) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await RoleApi.deleteRole(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await RoleApi.exportRole(queryParams) + download.excel(data, '角色列表.xls') + } catch { + } finally { + exportLoading.value = false } } -// 保存权限 -const submitScope = async () => { - if ('data' === actionScopeType.value) { - const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({ - roleId: dataScopeForm.id, - dataScope: dataScopeForm.dataScope, - dataScopeDeptIds: - dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM - ? [] - : (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>) - }) - await PermissionApi.assignRoleDataScopeApi(data.value) - } else if ('menu' === actionScopeType.value) { - const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({ - roleId: dataScopeForm.id, - menuIds: [ - ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), - ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) - ] - }) - await PermissionApi.assignRoleMenuApi(data.value) - } - message.success(t('common.updateSuccess')) - dialogScopeVisible.value = false -} -const init = () => { - dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE) -} -// ========== 初始化 ========== + +/** 初始化 **/ onMounted(() => { - init() + getList() }) </script> -<style scoped> -.card { - width: 100%; - max-height: 400px; - overflow-y: scroll; -} -</style> diff --git a/src/views/system/role/role.data.ts b/src/views/system/role/role.data.ts deleted file mode 100644 index d55b5e21..00000000 --- a/src/views/system/role/role.data.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -// 国际化 -const { t } = useI18n() -// 表单校验 -export const rules = reactive({ - name: [required], - code: [required], - sort: [required] -}) -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - // primaryKey: 'id', - // primaryTitle: '角色编号', - // primaryType: 'seq', - action: true, - actionWidth: '400px', - columns: [ - { - title: '角色编号', - field: 'id', - table: { - width: 200 - } - }, - { - title: '角色名称', - field: 'name', - isSearch: true - }, - { - title: '角色类型', - field: 'type', - dictType: DICT_TYPE.SYSTEM_ROLE_TYPE, - dictClass: 'number', - isForm: false - }, - { - title: '角色标识', - field: 'code', - isSearch: true - }, - { - title: '显示顺序', - field: 'sort' - }, - { - title: t('form.remark'), - field: 'remark', - isTable: false, - form: { - component: 'Input', - componentProps: { - type: 'textarea', - rows: 4 - }, - colProps: { - span: 24 - } - } - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false, - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/sensitiveWord/form.vue b/src/views/system/sensitiveWord/SensitiveWordForm.vue similarity index 80% rename from src/views/system/sensitiveWord/form.vue rename to src/views/system/sensitiveWord/SensitiveWordForm.vue index 1dccd656..c069756b 100644 --- a/src/views/system/sensitiveWord/form.vue +++ b/src/views/system/sensitiveWord/SensitiveWordForm.vue @@ -33,23 +33,20 @@ placeholder="请选择文章标签" style="width: 380px" > - <el-option v-for="tag in tags" :key="tag" :label="tag" :value="tag" /> + <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" /> </el-select> </el-form-item> </el-form> <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> </template> </Dialog> </template> -<script setup lang="ts"> +<script setup lang="ts" name="SensitiveWordForm"> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as SensitiveWordApi from '@/api/system/sensitiveWord' import { CommonStatusEnum } from '@/utils/constants' - const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -69,10 +66,10 @@ const formRules = reactive({ tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const tags = ref([]) // todo @blue-syd:在 openModal 里加载下 +const tagList = ref([]) // 标签数组 /** 打开弹窗 */ -const openModal = async (type: string, id?: number) => { +const open = async (type: string, id?: number) => { modelVisible.value = true modelTitle.value = t('action.' + type) formType.value = type @@ -81,14 +78,15 @@ const openModal = async (type: string, id?: number) => { if (id) { formLoading.value = true try { - formData.value = await SensitiveWordApi.getSensitiveWordApi(id) - console.log(formData.value) + formData.value = await SensitiveWordApi.getSensitiveWord(id) } finally { formLoading.value = false } } + // 获得 Tag 标签列表 + tagList.value = await SensitiveWordApi.getSensitiveWordTagList() } -defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 /** 提交表单 */ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 @@ -102,10 +100,10 @@ const submitForm = async () => { try { const data = formData.value as unknown as SensitiveWordApi.SensitiveWordVO if (formType.value === 'create') { - await SensitiveWordApi.createSensitiveWordApi(data) // TODO @blue-syd:去掉 API 后缀 + await SensitiveWordApi.createSensitiveWord(data) message.success(t('common.createSuccess')) } else { - await SensitiveWordApi.updateSensitiveWordApi(data) // TODO @blue-syd:去掉 API 后缀 + await SensitiveWordApi.updateSensitiveWord(data) message.success(t('common.updateSuccess')) } modelVisible.value = false diff --git a/src/views/system/sensitiveWord/SensitiveWordTestForm.vue b/src/views/system/sensitiveWord/SensitiveWordTestForm.vue new file mode 100644 index 00000000..881309c8 --- /dev/null +++ b/src/views/system/sensitiveWord/SensitiveWordTestForm.vue @@ -0,0 +1,90 @@ +<template> + <Dialog title="检测敏感词" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="文本" prop="text"> + <el-input type="textarea" v-model="formData.text" placeholder="请输入测试文本" /> + </el-form-item> + <el-form-item label="标签" prop="tags"> + <el-select + v-model="formData.tags" + multiple + filterable + allow-create + placeholder="请选择标签" + style="width: 380px" + > + <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" /> + </el-select> + </el-form-item> + </el-form> + <template #footer> + <div class="dialog-footer"> + <el-button @click="submitForm" type="primary" :disabled="formLoading">检 测</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </div> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import * as SensitiveWordApi from '@/api/system/sensitiveWord' +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref({ + text: '', + tags: [] +}) +const formRules = reactive({ + text: [{ required: true, message: '测试文本不能为空', trigger: 'blur' }], + tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref +const tagList = ref([]) // 标签数组 + +/** 打开弹窗 */ +const open = async () => { + modelVisible.value = true + resetForm() + // 获得 Tag 标签列表 + tagList.value = await SensitiveWordApi.getSensitiveWordTagList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const form = formData.value as unknown as SensitiveWordApi.SensitiveWordTestReqVO + const data = await SensitiveWordApi.validateText(form) + if (data.length === 0) { + message.success('不包含敏感词!') + return + } + message.warning('包含敏感词:' + data.join(', ')) + modelVisible.value = false + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + text: '', + tags: [] + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/sensitiveWord/index.vue b/src/views/system/sensitiveWord/index.vue index 93ea29f1..cf1fdb82 100644 --- a/src/views/system/sensitiveWord/index.vue +++ b/src/views/system/sensitiveWord/index.vue @@ -1,13 +1,20 @@ <template> - <!-- 搜索 --> - <content-wrap> - <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true"> + <!-- 搜索工作栏 --> + <ContentWrap> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > <el-form-item label="敏感词" prop="name"> <el-input v-model="queryParams.name" placeholder="请输入敏感词" clearable @keyup.enter="handleQuery" + class="!w-240px" /> </el-form-item> <el-form-item label="标签" prop="tag"> @@ -16,17 +23,19 @@ placeholder="请选择标签" clearable @keyup.enter="handleQuery" + class="!w-240px" > - <el-option v-for="tag in tags" :key="tag" :label="tag" :value="tag" /> + <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" /> </el-select> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable> <el-option - v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="parseInt(dict.value)" + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" :label="dict.label" - :value="parseInt(dict.value)" + :value="dict.value" + class="!w-240px" /> </el-select> </el-form-item> @@ -38,6 +47,7 @@ start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" /> </el-form-item> <el-form-item> @@ -45,7 +55,8 @@ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button type="primary" - @click="openModal('create')" + plain + @click="openForm('create')" v-hasPermi="['system:sensitive-word:create']" > <Icon icon="ep:plus" class="mr-5px" /> 新增 @@ -59,12 +70,15 @@ > <Icon icon="ep:download" class="mr-5px" /> 导出 </el-button> + <el-button type="warning" plain @click="openTestForm"> + <Icon icon="ep:document-checked" class="mr-5px" /> 测试 + </el-button> </el-form-item> </el-form> - </content-wrap> + </ContentWrap> <!-- 列表 --> - <content-wrap> + <ContentWrap> <el-table v-loading="loading" :data="list"> <el-table-column label="编号" align="center" prop="id" /> <el-table-column label="敏感词" align="center" prop="name" /> @@ -77,15 +91,13 @@ <el-table-column label="标签" align="center" prop="tags"> <template #default="scope"> <el-tag - :disable-transitions="true" - :key="index" - v-for="(tag, index) in scope.row.tags" - :index="index" class="mr-5px" + v-for="tag in scope.row.tags" + :key="tag" + :disable-transitions="true" > {{ tag }} </el-tag> - </template> </el-table-column> <el-table-column @@ -100,7 +112,7 @@ <el-button link type="primary" - @click="openModal('update', scope.row.id)" + @click="openForm('update', scope.row.id)" v-hasPermi="['infra:config:update']" > 编辑 @@ -123,17 +135,21 @@ v-model:limit="queryParams.pageSize" @pagination="getList" /> - </content-wrap> + </ContentWrap> <!-- 表单弹窗:添加/修改 --> - <config-form ref="modalRef" @success="getList" /> + <SensitiveWordForm ref="formRef" @success="getList" /> + + <!-- 表单弹窗:测试敏感词 --> + <SensitiveWordTestForm ref="testFormRef" /> </template> <script setup lang="ts" name="SensitiveWord"> -import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import download from '@/utils/download' import * as SensitiveWordApi from '@/api/system/sensitiveWord' -import ConfigForm from './form.vue' // TODO @blue-syd:组件名不对 +import SensitiveWordForm from './SensitiveWordForm.vue' +import SensitiveWordTestForm from './SensitiveWordTestForm.vue' const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 @@ -150,13 +166,13 @@ const queryParams = reactive({ }) const queryFormRef = ref() // 搜索的表单 const exportLoading = ref(false) // 导出的加载中 -const tags = ref([]) +const tagList = ref([]) // 标签数组 /** 查询参数列表 */ const getList = async () => { loading.value = true try { - const data = await SensitiveWordApi.getSensitiveWordPageApi(queryParams) // TODO @blue-syd:去掉 API 后缀哈 + const data = await SensitiveWordApi.getSensitiveWordPage(queryParams) list.value = data.list total.value = data.total } finally { @@ -177,12 +193,16 @@ const resetQuery = () => { } /** 添加/修改操作 */ -const modalRef = ref() -const openModal = (type: string, id?: number) => { - modalRef.value.openModal(type, id) +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) } -// TODO @blue-syd:还少一个【测试】按钮的功能,参见 http://dashboard.yudao.iocoder.cn/system/sensitive-word +/** 测试敏感词按钮操作 */ +const testFormRef = ref() +const openTestForm = () => { + testFormRef.value.open() +} /** 删除按钮操作 */ const handleDelete = async (id: number) => { @@ -190,7 +210,7 @@ const handleDelete = async (id: number) => { // 删除的二次确认 await message.delConfirm() // 发起删除 - await SensitiveWordApi.deleteSensitiveWordApi(id) + await SensitiveWordApi.deleteSensitiveWord(id) message.success(t('common.delSuccess')) // 刷新列表 await getList() @@ -204,7 +224,7 @@ const handleExport = async () => { await message.exportConfirm() // 发起导出 exportLoading.value = true - const data = await SensitiveWordApi.exportSensitiveWordApi(queryParams) // TODO @blue-syd:去掉 API 后缀哈 + const data = await SensitiveWordApi.exportSensitiveWord(queryParams) download.excel(data, '敏感词.xls') } catch { } finally { @@ -212,14 +232,10 @@ const handleExport = async () => { } } -/** 获得 Tag 标签列表 */ -const getTags = async () => { - tags.value = await SensitiveWordApi.getSensitiveWordTagsApi() // TODO @blue-syd:去掉 API 后缀哈 -} - /** 初始化 **/ -onMounted(() => { - getTags() - getList() +onMounted(async () => { + await getList() + // 获得 Tag 标签列表 + tagList.value = await SensitiveWordApi.getSensitiveWordTagList() }) </script> diff --git a/src/views/system/sms/smsChannel/form.vue b/src/views/system/sms/channel/SmsChannelForm.vue similarity index 64% rename from src/views/system/sms/smsChannel/form.vue rename to src/views/system/sms/channel/SmsChannelForm.vue index 7c20a90d..3145af91 100644 --- a/src/views/system/sms/smsChannel/form.vue +++ b/src/views/system/sms/channel/SmsChannelForm.vue @@ -1,137 +1,141 @@ -<template> - <Dialog :title="modelTitle" v-model="modelVisible"> - <el-form ref="formRef" :model="form" :rules="rules" label-width="130px" v-loading="formLoading"> - <el-form-item label="短信签名" prop="signature"> - <el-input v-model="form.signature" placeholder="请输入短信签名" /> - </el-form-item> - <el-form-item label="渠道编码" prop="code"> - <el-select v-model="form.code" placeholder="请选择渠道编码" clearable> - <el-option - v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="启用状态"> - <el-radio-group v-model="form.status"> - <el-radio - v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="dict.value" - :label="parseInt(dict.value)" - >{{ dict.label }}</el-radio - > - </el-radio-group> - </el-form-item> - <el-form-item label="备注" prop="remark"> - <el-input v-model="form.remark" placeholder="请输入备注" /> - </el-form-item> - <el-form-item label="短信 API 的账号" prop="apiKey"> - <el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" /> - </el-form-item> - <el-form-item label="短信 API 的密钥" prop="apiSecret"> - <el-input v-model="form.apiSecret" placeholder="请输入短信 API 的密钥" /> - </el-form-item> - <el-form-item label="短信发送回调 URL" prop="callbackUrl"> - <el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" /> - </el-form-item> - </el-form> - <template #footer> - <div class="dialog-footer"> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> - </template> - </Dialog> -</template> -<script setup lang="ts"> -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import * as SmsChannelApi from '@/api/system/sms/smsChannel' - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const modelVisible = ref(false) // 弹窗的是否展示 -const modelTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const form = ref({ - id: undefined, - signature: '', - code: '', - status: '', - remark: '', - apiKey: '', - apiSecret: '', - callbackUrl: '' -}) -const rules = reactive({ - signature: [{ required: true, message: '短信签名不能为空', trigger: 'blur' }], - code: [{ required: true, message: '渠道编码不能为空', trigger: 'blur' }], - status: [{ required: true, message: '启用状态不能为空', trigger: 'blur' }], - apiKey: [{ required: true, message: '短信 API 的账号不能为空', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref - -/** 打开弹窗 */ -const openModal = async (type: string, id?: number) => { - modelVisible.value = true - modelTitle.value = t('action.' + type) - formType.value = type - resetForm() - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - form.value = await SmsChannelApi.getSmsChannelApi(id) - console.log(form) - } finally { - formLoading.value = false - } - } -} - -defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - const data = unref(formRef)?.formModel as SmsChannelApi.SmsChannelVO - if (formType.value === 'create') { - await SmsChannelApi.createSmsChannelApi(data) - message.success(t('common.createSuccess')) - } else { - await SmsChannelApi.updateSmsChannelApi(data) - message.success(t('common.updateSuccess')) - } - modelVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - form.value = { - id: undefined, - signature: '', - code: '', - status: '', - remark: '', - apiKey: '', - apiSecret: '', - callbackUrl: '' - } - formRef.value?.resetFields() -} -</script> +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="130px" + v-loading="formLoading" + > + <el-form-item label="短信签名" prop="signature"> + <el-input v-model="formData.signature" placeholder="请输入短信签名" /> + </el-form-item> + <el-form-item label="渠道编码" prop="code"> + <el-select v-model="formData.code" placeholder="请选择渠道编码" clearable> + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="启用状态"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" placeholder="请输入备注" /> + </el-form-item> + <el-form-item label="短信 API 的账号" prop="apiKey"> + <el-input v-model="formData.apiKey" placeholder="请输入短信 API 的账号" /> + </el-form-item> + <el-form-item label="短信 API 的密钥" prop="apiSecret"> + <el-input v-model="formData.apiSecret" placeholder="请输入短信 API 的密钥" /> + </el-form-item> + <el-form-item label="短信发送回调 URL" prop="callbackUrl"> + <el-input v-model="formData.callbackUrl" placeholder="请输入短信发送回调 URL" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' +import * as SmsChannelApi from '@/api/system/sms/smsChannel' +import { CommonStatusEnum } from '@/utils/constants' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + signature: '', + code: '', + status: CommonStatusEnum.ENABLE, + remark: '', + apiKey: '', + apiSecret: '', + callbackUrl: '' +}) +const formRules = reactive({ + signature: [{ required: true, message: '短信签名不能为空', trigger: 'blur' }], + code: [{ required: true, message: '渠道编码不能为空', trigger: 'blur' }], + status: [{ required: true, message: '启用状态不能为空', trigger: 'blur' }], + apiKey: [{ required: true, message: '短信 API 的账号不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await SmsChannelApi.getSmsChannel(id) + console.log(formData) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = unref(formRef)?.formModel as SmsChannelApi.SmsChannelVO + if (formType.value === 'create') { + await SmsChannelApi.createSmsChannel(data) + message.success(t('common.createSuccess')) + } else { + await SmsChannelApi.updateSmsChannel(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + signature: '', + code: '', + status: CommonStatusEnum.ENABLE, + remark: '', + apiKey: '', + apiSecret: '', + callbackUrl: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/sms/smsChannel/index.vue b/src/views/system/sms/channel/index.vue similarity index 71% rename from src/views/system/sms/smsChannel/index.vue rename to src/views/system/sms/channel/index.vue index bac94f25..65d18029 100644 --- a/src/views/system/sms/smsChannel/index.vue +++ b/src/views/system/sms/channel/index.vue @@ -1,6 +1,12 @@ <template> <ContentWrap> - <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > <el-form-item label="短信签名" prop="signature"> <el-input v-model="queryParams.signature" @@ -12,10 +18,10 @@ <el-form-item label="启用状态" prop="status"> <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable> <el-option - v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="parseInt(dict.value)" + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" :label="dict.label" - :value="parseInt(dict.value)" + :value="dict.value" /> </el-select> </el-form-item> @@ -34,24 +40,17 @@ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button type="primary" - @click="openModal('create')" + @click="openForm('create')" v-hasPermi="['system:sms-channel:create']" > <Icon icon="ep:plus" class="mr-5px" /> 新增</el-button > - <el-button - type="success" - plain - @click="handleExport" - :loading="exportLoading" - v-hasPermi="['system:sms-channel:export']" - > - <Icon icon="ep:download" class="mr-5px" /> 导出</el-button - > </el-form-item> </el-form> + </ContentWrap> - <!-- 列表 --> + <!-- 列表 --> + <ContentWrap> <el-table v-loading="loading" :data="list" align="center"> <el-table-column label="编号" align="center" prop="id" /> <el-table-column label="短信签名" align="center" prop="signature" /> @@ -71,18 +70,21 @@ align="center" prop="apiKey" :show-overflow-tooltip="true" + width="180" /> <el-table-column label="短信 API 的密钥" align="center" prop="apiSecret" :show-overflow-tooltip="true" + width="180" /> <el-table-column label="短信发送回调 URL" align="center" prop="callbackUrl" :show-overflow-tooltip="true" + width="180" /> <el-table-column label="创建时间" @@ -96,7 +98,7 @@ <el-button link type="primary" - @click="openModal('update', scope.row.id)" + @click="openForm('update', scope.row.id)" v-hasPermi="['system:sms-channel:update']" > 编辑 @@ -120,35 +122,22 @@ @pagination="getList" /> </ContentWrap> + <!-- 表单弹窗:添加/修改 --> - <SmsChannelForm ref="modalRef" @success="getList" /> + <SmsChannelForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="SmsChannel"> -// 业务相关的 import -import * as SmsChannelApi from '@/api/system/sms/smsChannel' -//格式化时间 +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' -//字典 -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -//表单弹窗:添加/修改 -import SmsChannelForm from './form.vue' -//下载 -// import download from '@/utils/download' - +import * as SmsChannelApi from '@/api/system/sms/smsChannel' +import SmsChannelForm from './SmsChannelForm.vue' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 -// 列表的加载中 -const loading = ref(true) -//搜索的表单 -const queryFormRef = ref() -// 列表的总页数 -const total = ref(0) -// 列表的数据 -const list = ref([]) -//导出的加载中 -const exportLoading = ref(false) -//查询参数 +const loading = ref(false) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryFormRef = ref() // 搜索的表单 const queryParams = reactive({ pageNo: 1, pageSize: 10, @@ -160,9 +149,8 @@ const queryParams = reactive({ /** 查询参数列表 */ const getList = async () => { loading.value = true - // 执行查询 try { - const data = await SmsChannelApi.getSmsChannelPageApi(queryParams) + const data = await SmsChannelApi.getSmsChannelPage(queryParams) list.value = data.list total.value = data.total } finally { @@ -183,26 +171,9 @@ const resetQuery = () => { } /** 添加/修改操作 */ -const modalRef = ref() -const openModal = (type: string, id?: number) => { - modalRef.value.openModal(type, id) -} - -/** 导出按钮操作 */ -const handleExport = async () => { - try { - // 导出的二次确认 - await message.exportConfirm() - // 发起导出 - exportLoading.value = true - await message.info('该功能目前不支持') - //导出功能先不考虑 - // const data = await SmsChannelApi.exportSmsChanelApi(queryParams) - // download.excel(data, '短信渠道.xls') - } catch { - } finally { - exportLoading.value = false - } +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) } /** 删除按钮操作 */ @@ -211,7 +182,7 @@ const handleDelete = async (id: number) => { // 删除的二次确认 await message.delConfirm() // 发起删除 - await SmsChannelApi.deleteSmsChannelApi(id) + await SmsChannelApi.deleteSmsChannel(id) message.success(t('common.delSuccess')) // 刷新列表 await getList() diff --git a/src/views/system/sms/log/SmsLogDetail.vue b/src/views/system/sms/log/SmsLogDetail.vue new file mode 100644 index 00000000..736d0b8e --- /dev/null +++ b/src/views/system/sms/log/SmsLogDetail.vue @@ -0,0 +1,87 @@ +<template> + <Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="500" width="800"> + <el-descriptions border :column="1"> + <el-descriptions-item label="日志主键" min-width="120"> + {{ detailData.id }} + </el-descriptions-item> + <el-descriptions-item label="短信渠道"> + {{ channelList.find((channel) => channel.id === detailData.channelId)?.signature }} + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="detailData.channelCode" /> + </el-descriptions-item> + <el-descriptions-item label="短信模板"> + {{ detailData.templateId }} | {{ detailData.templateCode }} + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="detailData.templateType" /> + </el-descriptions-item> + <el-descriptions-item label="API 的模板编号"> + {{ detailData.apiTemplateId }} + </el-descriptions-item> + <el-descriptions-item label="用户信息"> + {{ detailData.mobile }} + <span v-if="detailData.userType && detailData.userId"> + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" /> + ({{ detailData.userId }}) + </span> + </el-descriptions-item> + <el-descriptions-item label="短信内容"> + {{ detailData.templateContent }} + </el-descriptions-item> + <el-descriptions-item label="短信参数"> + {{ detailData.templateParams }} + </el-descriptions-item> + <el-descriptions-item label="创建时间"> + {{ formatDate(detailData.createTime) }} + </el-descriptions-item> + <el-descriptions-item label="发送状态"> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="detailData.sendStatus" /> + </el-descriptions-item> + <el-descriptions-item label="发送时间"> + {{ formatDate(detailData.sendTime) }} + </el-descriptions-item> + <el-descriptions-item label="发送结果"> + {{ detailData.sendCode }} | {{ detailData.sendMsg }} + </el-descriptions-item> + <el-descriptions-item label="API 发送结果"> + {{ detailData.apiSendCode }} | {{ detailData.apiSendMsg }} + </el-descriptions-item> + <el-descriptions-item label="API 短信编号"> + {{ detailData.apiSerialNo }} + </el-descriptions-item> + <el-descriptions-item label="API 请求编号"> + {{ detailData.apiRequestId }} + </el-descriptions-item> + <el-descriptions-item label="API 接收状态"> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="detailData.receiveStatus" /> + {{ formatDate(detailData.receiveTime) }} + </el-descriptions-item> + <el-descriptions-item label="API 接收结果"> + {{ detailData.apiReceiveCode }} | {{ detailData.apiReceiveMsg }} + </el-descriptions-item> + </el-descriptions> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import * as SmsLogApi from '@/api/system/sms/smsLog' +import * as SmsChannelApi from '@/api/system/sms/smsChannel' + +const modelVisible = ref(false) // 弹窗的是否展示 +const detailLoading = ref(false) // 表单的加载中 +const detailData = ref() // 详情数据 +const channelList = ref([]) // 短信渠道列表 + +/** 打开弹窗 */ +const open = async (data: SmsLogApi.SmsLogVO) => { + modelVisible.value = true + // 设置数据 + detailLoading.value = true + try { + detailData.value = data + } finally { + detailLoading.value = false + } + // 加载渠道列表 + channelList.value = await SmsChannelApi.getSimpleSmsChannelList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> diff --git a/src/views/system/sms/log/index.vue b/src/views/system/sms/log/index.vue new file mode 100644 index 00000000..ec8a4659 --- /dev/null +++ b/src/views/system/sms/log/index.vue @@ -0,0 +1,263 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="100px" + > + <el-form-item label="手机号" prop="mobile"> + <el-input + v-model="queryParams.mobile" + placeholder="请输入手机号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="短信渠道" prop="channelId"> + <el-select + v-model="queryParams.channelId" + placeholder="请选择短信渠道" + clearable + class="!w-240px" + > + <el-option + v-for="channel in channelList" + :key="channel.id" + :value="channel.id" + :label=" + channel.signature + + `【 ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}】` + " + /> + </el-select> + </el-form-item> + <el-form-item label="模板编号" prop="templateId"> + <el-input + v-model="queryParams.templateId" + placeholder="请输入模板编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="发送状态" prop="sendStatus"> + <el-select + v-model="queryParams.sendStatus" + placeholder="请选择发送状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="发送时间" prop="sendTime"> + <el-date-picker + v-model="queryParams.sendTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="接收状态" prop="receiveStatus"> + <el-select + v-model="queryParams.receiveStatus" + placeholder="请选择接收状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="接收时间" prop="receiveTime"> + <el-date-picker + v-model="queryParams.receiveTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['system:sms-log:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="编号" align="center" prop="id" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="手机号" align="center" prop="mobile" width="120"> + <template #default="scope"> + <div>{{ scope.row.mobile }}</div> + <div v-if="scope.row.userType && scope.row.userId"> + <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> + {{ '(' + scope.row.userId + ')' }} + </div> + </template> + </el-table-column> + <el-table-column label="短信内容" align="center" prop="templateContent" width="300" /> + <el-table-column label="发送状态" align="center" width="180"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="scope.row.sendStatus" /> + <div>{{ formatDate(scope.row.sendTime) }}</div> + </template> + </el-table-column> + <el-table-column label="接收状态" align="center" width="180"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="scope.row.receiveStatus" /> + <div>{{ formatDate(scope.row.receiveTime) }}</div> + </template> + </el-table-column> + <el-table-column label="短信渠道" align="center" width="120"> + <template #default="scope"> + <div> + {{ channelList.find((channel) => channel.id === scope.row.channelId)?.signature }} + </div> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.channelCode" /> + </template> + </el-table-column> + <el-table-column label="模板编号" align="center" prop="templateId" /> + <el-table-column label="短信类型" align="center" prop="templateType"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="scope.row.templateType" /> + </template> + </el-table-column> + <el-table-column label="操作" align="center" fixed="right" class-name="fixed-width"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openDetail(scope.row)" + v-hasPermi="['system:sms-log:query']" + > + 详情 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:详情 --> + <SmsLogDetail ref="detailRef" /> +</template> +<script setup lang="ts" name="smsLog"> +import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict' +import { dateFormatter, formatDate } from '@/utils/formatTime' +import download from '@/utils/download' +import * as SmsChannelApi from '@/api/system/sms/smsChannel' +import * as SmsLogApi from '@/api/system/sms/smsLog' +import SmsLogDetail from './SmsLogDetail.vue' +const message = useMessage() // 消息弹窗 + +const loading = ref(false) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryFormRef = ref() // 搜索的表单 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + channelId: null, + templateId: null, + mobile: '', + sendStatus: null, + receiveStatus: null, + sendTime: [], + receiveTime: [] +}) +const exportLoading = ref(false) // 导出的加载中 +const channelList = ref([]) // 短信渠道列表 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await SmsLogApi.getSmsLogPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await SmsLogApi.exportSmsLog(queryParams) + download.excel(data, '短信日志.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 详情操作 */ +const detailRef = ref() +const openDetail = (data: SmsLogApi.SmsLogVO) => { + detailRef.value.open(data) +} + +/** 初始化 **/ +onMounted(async () => { + await getList() + // 加载渠道列表 + channelList.value = await SmsChannelApi.getSimpleSmsChannelList() +}) +</script> diff --git a/src/views/system/sms/smsLog/index.vue b/src/views/system/sms/smsLog/index.vue deleted file mode 100644 index 334da2ad..00000000 --- a/src/views/system/sms/smsLog/index.vue +++ /dev/null @@ -1,57 +0,0 @@ -<template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 操作:导出 --> - <template #toolbar_buttons> - <XButton - type="warning" - preIcon="ep:download" - :title="t('action.export')" - @click="exportList('短信日志.xls')" - /> - </template> - <template #actionbtns_default="{ row }"> - <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" /> - </template> - </XTable> - </ContentWrap> - - <XModal id="smsLog" v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(详情) --> - <Descriptions - v-if="actionType === 'detail'" - :schema="allSchemas.detailSchema" - :data="detailData" - /> - <!-- 操作按钮 --> - <template #footer> - <XButton :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> -</template> -<script setup lang="ts" name="SmsLog"> -import { allSchemas } from './sms.log.data' -import * as SmsLoglApi from '@/api/system/sms/smsLog' -const { t } = useI18n() // 国际化 - -// 列表相关的变量 -const [registerTable, { exportList }] = useXTable({ - allSchemas: allSchemas, - getListApi: SmsLoglApi.getSmsLogPageApi, - exportListApi: SmsLoglApi.exportSmsLogApi -}) - -// 弹窗相关的变量 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref(t('action.detail')) // 弹出层标题 -const actionType = ref('') // 操作按钮的类型 -// ========== 详情相关 ========== -const detailData = ref() // 详情 Ref -const handleDetail = (row: SmsLoglApi.SmsLogVO) => { - // 设置数据 - actionType.value = 'detail' - detailData.value = row - dialogVisible.value = true -} -</script> diff --git a/src/views/system/sms/smsLog/sms.log.data.ts b/src/views/system/sms/smsLog/sms.log.data.ts deleted file mode 100644 index c975bb0f..00000000 --- a/src/views/system/sms/smsLog/sms.log.data.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' - -const { t } = useI18n() // 国际化 - -const authorizedGrantOptions = getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE) -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '日志编号', - action: true, - columns: [ - { - title: '手机号', - field: 'mobile', - isSearch: true - }, - { - title: '短信内容', - field: 'templateContent' - }, - { - title: '模板编号', - field: 'templateId', - isSearch: true - }, - { - title: '短信渠道', - field: 'channelId', - // dictType: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, - // dictClass: 'number', - isSearch: true, - // table: { - // component: 'Select', - componentProps: { - options: authorizedGrantOptions - // multiple: false, - // filterable: true - } - // } - }, - { - title: '发送状态', - field: 'sendStatus', - dictType: DICT_TYPE.SYSTEM_SMS_SEND_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: '发送时间', - field: 'sendTime', - formatter: 'formatDate', - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - }, - { - title: '短信类型', - field: 'templateType', - dictType: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, - dictClass: 'number', - isSearch: true - }, - { - title: '接收状态', - field: 'receiveStatus', - dictType: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: '接收时间', - field: 'receiveTime', - formatter: 'formatDate', - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate' - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/sms/smsTemplate/index.vue b/src/views/system/sms/smsTemplate/index.vue deleted file mode 100644 index bbc7c863..00000000 --- a/src/views/system/sms/smsTemplate/index.vue +++ /dev/null @@ -1,232 +0,0 @@ -<template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <!-- 操作:新增 --> - <template #toolbar_buttons> - <XButton - type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:sms-channel:create']" - @click="handleCreate()" - /> - </template> - <template #actionbtns_default="{ row }"> - <XTextButton - preIcon="ep:cpu" - :title="t('action.test')" - v-hasPermi="['system:sms-template:send-sms']" - @click="handleSendSms(row)" - /> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['system:sms-template:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['system:sms-template:query']" - @click="handleDetail(row.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['system:sms-template:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> - </ContentWrap> - <XModal id="smsTemplate" v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(添加 / 修改) --> - <Form - v-if="['create', 'update'].includes(actionType)" - :schema="allSchemas.formSchema" - :rules="rules" - ref="formRef" - /> - <!-- 对话框(详情) --> - <Descriptions - v-if="actionType === 'detail'" - :schema="allSchemas.detailSchema" - :data="detailData" - /> - <!-- 操作按钮 --> - <template #footer> - <!-- 按钮:保存 --> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitForm()" - /> - <!-- 按钮:关闭 --> - <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> - <XModal id="sendTest" v-model="sendVisible" title="测试"> - <el-form :model="sendSmsForm" :rules="sendSmsRules" label-width="200px" label-position="top"> - <el-form-item label="模板内容" prop="content"> - <el-input - v-model="sendSmsForm.content" - type="textarea" - placeholder="请输入模板内容" - readonly - /> - </el-form-item> - <el-form-item label="手机号" prop="mobile"> - <el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" /> - </el-form-item> - <el-form-item - v-for="param in sendSmsForm.params" - :key="param" - :label="'参数 {' + param + '}'" - :prop="'templateParams.' + param" - > - <el-input - v-model="sendSmsForm.templateParams[param]" - :placeholder="'请输入 ' + param + ' 参数'" - /> - </el-form-item> - </el-form> - <!-- 操作按钮 --> - <template #footer> - <XButton - type="primary" - :title="t('action.test')" - :loading="actionLoading" - @click="sendSmsTest()" - /> - <XButton :title="t('dialog.close')" @click="sendVisible = false" /> - </template> - </XModal> -</template> -<script setup lang="ts" name="SmsTemplate"> -import type { FormExpose } from '@/components/Form' -// 业务相关的 import -import * as SmsTemplateApi from '@/api/system/sms/smsTemplate' -import { rules, allSchemas } from './sms.template.data' - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -// 列表相关的变量 -const [registerTable, { reload, deleteData }] = useXTable({ - allSchemas: allSchemas, - getListApi: SmsTemplateApi.getSmsTemplatePageApi, - deleteApi: SmsTemplateApi.deleteSmsTemplateApi -}) - -// 弹窗相关的变量 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('edit') // 弹出层标题 -const actionType = ref('') // 操作按钮的类型 -const actionLoading = ref(false) // 按钮 Loading -const formRef = ref<FormExpose>() // 表单 Ref -const detailData = ref() // 详情 Ref - -// 设置标题 -const setDialogTile = (type: string) => { - dialogTitle.value = t('action.' + type) - actionType.value = type - dialogVisible.value = true -} - -// 新增操作 -const handleCreate = () => { - setDialogTile('create') -} - -// 修改操作 -const handleUpdate = async (rowId: number) => { - setDialogTile('update') - // 设置数据 - const res = await SmsTemplateApi.getSmsTemplateApi(rowId) - unref(formRef)?.setValues(res) -} - -// 详情操作 -const handleDetail = async (rowId: number) => { - setDialogTile('detail') - // 设置数据 - const res = await SmsTemplateApi.getSmsTemplateApi(rowId) - detailData.value = res -} - -// 提交按钮 -const submitForm = async () => { - const elForm = unref(formRef)?.getElFormRef() - if (!elForm) return - elForm.validate(async (valid) => { - if (valid) { - actionLoading.value = true - // 提交请求 - try { - const data = unref(formRef)?.formModel as SmsTemplateApi.SmsTemplateVO - if (actionType.value === 'create') { - await SmsTemplateApi.createSmsTemplateApi(data) - message.success(t('common.createSuccess')) - } else { - await SmsTemplateApi.updateSmsTemplateApi(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - } finally { - actionLoading.value = false - // 刷新列表 - await reload() - } - } - }) -} - -// ========== 测试相关 ========== -const sendSmsForm = ref({ - content: '', - params: {}, - mobile: '', - templateCode: '', - templateParams: {} -}) -const sendSmsRules = ref({ - mobile: [{ required: true, message: '手机不能为空', trigger: 'blur' }], - templateCode: [{ required: true, message: '模版编号不能为空', trigger: 'blur' }], - templateParams: {} -}) -const sendVisible = ref(false) - -const handleSendSms = (row: any) => { - sendSmsForm.value.content = row.content - sendSmsForm.value.params = row.params - sendSmsForm.value.templateCode = row.code - sendSmsForm.value.templateParams = row.params.reduce(function (obj, item) { - obj[item] = undefined - return obj - }, {}) - sendSmsRules.value.templateParams = row.params.reduce(function (obj, item) { - obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'change' } - return obj - }, {}) - sendVisible.value = true -} - -const sendSmsTest = async () => { - const data: SmsTemplateApi.SendSmsReqVO = { - mobile: sendSmsForm.value.mobile, - templateCode: sendSmsForm.value.templateCode, - templateParams: sendSmsForm.value.templateParams as unknown as Map<string, Object> - } - const res = await SmsTemplateApi.sendSmsApi(data) - if (res) { - message.success('提交发送成功!发送结果,见发送日志编号:' + res) - } - sendVisible.value = false -} -</script> diff --git a/src/views/system/sms/smsTemplate/sms.template.data.ts b/src/views/system/sms/smsTemplate/sms.template.data.ts deleted file mode 100644 index 6178d6c2..00000000 --- a/src/views/system/sms/smsTemplate/sms.template.data.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -import * as smsApi from '@/api/system/sms/smsChannel' -const { t } = useI18n() // 国际化 -const tenantPackageOption = [] -const getTenantPackageOptions = async () => { - const res = await smsApi.getSimpleSmsChannels() - console.log(res, 'resresres') - res.forEach((tenantPackage: TenantPackageVO) => { - tenantPackageOption.push({ - key: tenantPackage.id, - value: tenantPackage.id, - label: tenantPackage.signature - }) - }) -} -getTenantPackageOptions() -// 表单校验 -export const rules = reactive({ - type: [required], - status: [required], - code: [required], - name: [required], - content: [required], - apiTemplateId: [required], - channelId: [required] -}) - -// CrudSchema -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '模板编号', - action: true, - actionWidth: '280', - columns: [ - { - title: '短信渠道编码', - field: 'channelId', - isSearch: false, - isForm: true, - isTable: false, - form: { - component: 'Select', - componentProps: { - options: tenantPackageOption - } - } - }, - { - title: '模板编码', - field: 'code', - isSearch: true - }, - { - title: '模板名称', - field: 'name', - isSearch: true - }, - { - title: '模板内容', - field: 'content' - }, - { - title: '短信 API 的模板编号', - field: 'apiTemplateId', - isSearch: true - }, - { - title: '短信类型', - field: 'type', - dictType: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, - dictClass: 'number', - isSearch: true, - table: { - width: 80 - } - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true, - table: { - width: 80 - } - }, - { - title: t('form.remark'), - field: 'remark', - isTable: false - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false, - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/sms/template/SmsTemplateForm.vue b/src/views/system/sms/template/SmsTemplateForm.vue new file mode 100644 index 00000000..03684215 --- /dev/null +++ b/src/views/system/sms/template/SmsTemplateForm.vue @@ -0,0 +1,160 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="140px" + v-loading="formLoading" + > + <el-form-item label="短信渠道编号" prop="channelId"> + <el-select v-model="formData.channelId" placeholder="请选择短信渠道编号"> + <el-option + v-for="channel in channelList" + :key="channel.id" + :value="channel.id" + :label=" + channel.signature + + `【 ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}】` + " + /> + </el-select> + </el-form-item> + <el-form-item label="短信类型" prop="type"> + <el-select v-model="formData.type" placeholder="请选择短信类型"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="模板编号" prop="code"> + <el-input v-model="formData.code" placeholder="请输入模板编号" /> + </el-form-item> + <el-form-item label="模板名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入模板名称" /> + </el-form-item> + <el-form-item label="模板内容" prop="content"> + <el-input type="textarea" v-model="formData.content" placeholder="请输入模板内容" /> + </el-form-item> + <el-form-item label="开启状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="parseInt(dict.value as string)" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="短信 API 模板编号" prop="apiTemplateId"> + <el-input v-model="formData.apiTemplateId" placeholder="请输入短信 API 的模板编号" /> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" placeholder="请输入备注" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict' +import * as SmsTemplateApi from '@/api/system/sms/smsTemplate' +import * as SmsChannelApi from '@/api/system/sms/smsChannel' +import { CommonStatusEnum } from '@/utils/constants' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型 +const formData = ref<SmsTemplateApi.SmsTemplateVO>({ + id: null, + type: null, + status: CommonStatusEnum.ENABLE, + code: '', + name: '', + content: '', + remark: '', + apiTemplateId: '', + channelId: null +}) +const formRules = reactive({ + type: [{ required: true, message: '短信类型不能为空', trigger: 'change' }], + status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }], + code: [{ required: true, message: '模板编码不能为空', trigger: 'blur' }], + name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }], + content: [{ required: true, message: '模板内容不能为空', trigger: 'blur' }], + apiTemplateId: [{ required: true, message: '短信 API 的模板编号不能为空', trigger: 'blur' }], + channelId: [{ required: true, message: '短信渠道编号不能为空', trigger: 'change' }] +}) +const formRef = ref() // 表单 Ref +const channelList = ref<SmsChannelApi.SmsChannelVO[]>([]) // 短信渠道列表 + +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await SmsTemplateApi.getSmsTemplate(id) + } finally { + formLoading.value = false + } + } + // 加载渠道列表 + channelList.value = await SmsChannelApi.getSimpleSmsChannelList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + formLoading.value = true + try { + const data = formData.value as SmsTemplateApi.SmsTemplateVO + if (formType.value === 'create') { + await SmsTemplateApi.createSmsTemplate(data) + message.success(t('common.createSuccess')) + } else { + await SmsTemplateApi.updateSmsTemplate(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: null, + type: null, + status: CommonStatusEnum.ENABLE, + code: '', + name: '', + content: '', + remark: '', + apiTemplateId: '', + channelId: null + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/sms/template/SmsTemplateSendForm.vue b/src/views/system/sms/template/SmsTemplateSendForm.vue new file mode 100644 index 00000000..f2ecbe9f --- /dev/null +++ b/src/views/system/sms/template/SmsTemplateSendForm.vue @@ -0,0 +1,117 @@ +<template> + <Dialog title="测试" v-model="modelVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="140px" + v-loading="formLoading" + > + <el-form-item label="模板内容" prop="content"> + <el-input + v-model="formData.content" + type="textarea" + placeholder="请输入模板内容" + readonly + /> + </el-form-item> + <el-form-item label="手机号" prop="mobile"> + <el-input v-model="formData.mobile" placeholder="请输入手机号" /> + </el-form-item> + <el-form-item + v-for="param in formData.params" + :key="param" + :label="'参数 {' + param + '}'" + :prop="'templateParams.' + param" + > + <el-input + v-model="formData.templateParams[param]" + :placeholder="'请输入 ' + param + ' 参数'" + /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import * as SmsTemplateApi from '@/api/system/sms/smsTemplate' +const message = useMessage() // 消息弹窗 + +const modelVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 + +// 发送短信表单相关 +const formData = ref({ + content: '', + params: {}, + mobile: '', + templateCode: '', + templateParams: new Map() +}) +const formRules = reactive({ + mobile: [{ required: true, message: '手机不能为空', trigger: 'blur' }], + templateCode: [{ required: true, message: '模版编码不能为空', trigger: 'blur' }], + templateParams: {} +}) +const formRef = ref() // 表单 Ref + +const open = async (id: number) => { + modelVisible.value = true + resetForm() + // 设置数据 + formLoading.value = true + try { + const data = await SmsTemplateApi.getSmsTemplate(id) + // 设置动态表单 + formData.value.content = data.content + formData.value.params = data.params + formData.value.templateCode = data.code + formData.value.templateParams = data.params.reduce((obj, item) => { + obj[item] = '' // 给每个动态属性赋值,避免无法读取 + return obj + }, {}) + formRules.templateParams = data.params.reduce((obj, item) => { + obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'blur' } + return obj + }, {}) + } finally { + formLoading.value = false + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as SmsTemplateApi.SendSmsReqVO + const logId = await SmsTemplateApi.sendSms(data) + if (logId) { + message.success('提交发送成功!发送结果,见发送日志编号:' + logId) + } + modelVisible.value = false + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + content: '', + params: {}, + mobile: '', + templateCode: '', + templateParams: new Map() + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/sms/template/index.vue b/src/views/system/sms/template/index.vue new file mode 100644 index 00000000..906436a5 --- /dev/null +++ b/src/views/system/sms/template/index.vue @@ -0,0 +1,311 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="150px" + > + <el-form-item label="短信类型" prop="type"> + <el-select + v-model="queryParams.type" + placeholder="请选择短信类型" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="开启状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择开启状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="模板编码" prop="code"> + <el-input + v-model="queryParams.code" + placeholder="请输入模板编码" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="短信 API 的模板编号" prop="apiTemplateId"> + <el-input + v-model="queryParams.apiTemplateId" + placeholder="请输入短信 API 的模板编号" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="短信渠道" prop="channelId"> + <el-select + v-model="queryParams.channelId" + placeholder="请选择短信渠道" + clearable + class="!w-240px" + > + <el-option + v-for="channel in channelList" + :key="channel.id" + :value="channel.id" + :label=" + channel.signature + + `【 ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}】` + " + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + style="width: 240px" + type="daterange" + value-format="YYYY-MM-DD HH:mm:ss" + start-placeholder="开始日期" + end-placeholder="结束日期" + class="!w-240px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['system:sms-template:create']" + > + <Icon icon="ep:plus" class="mr-5px" />新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['system:sms-template:export']" + > + <Icon icon="ep:download" class="mr-5px" /> 导出 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" align="center"> + <el-table-column + label="模板编码" + align="center" + prop="code" + width="120" + :show-overflow-tooltip="true" + /> + <el-table-column + label="模板名称" + align="center" + prop="name" + width="120" + :show-overflow-tooltip="true" + /> + <el-table-column + label="模板内容" + align="center" + prop="content" + width="200" + :show-overflow-tooltip="true" + /> + <el-table-column label="短信类型" align="center" prop="type"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="scope.row.type" /> + </template> + </el-table-column> + <el-table-column label="状态" align="center" prop="status" width="80"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column label="备注" align="center" prop="remark" /> + <el-table-column + label="短信 API 的模板编号" + align="center" + prop="apiTemplateId" + width="200" + :show-overflow-tooltip="true" + /> + <el-table-column label="短信渠道" align="center" width="120"> + <template #default="scope"> + <div> + {{ channelList.find((channel) => channel.id === scope.row.channelId)?.signature }} + </div> + <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.channelCode" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="操作" align="center" width="210" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['system:sms-template:update']" + > + 修改 + </el-button> + <el-button + link + type="primary" + @click="openSendForm(scope.row.id)" + v-hasPermi="['system:sms-template:send-sms']" + > + 测试 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['system:sms-template:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <SmsTemplateForm ref="formRef" @success="getList" /> + <!-- 表单弹窗:测试发送 --> + <SmsTemplateSendForm ref="sendFormRef" /> +</template> +<script setup lang="ts" name="SmsTemplate"> +import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import * as SmsTemplateApi from '@/api/system/sms/smsTemplate' +import * as SmsChannelApi from '@/api/system/sms/smsChannel' +import download from '@/utils/download' +import SmsTemplateForm from './SmsTemplateForm.vue' +import SmsTemplateSendForm from './SmsTemplateSendForm.vue' +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(false) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryFormRef = ref() // 搜索的表单 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + type: null, + status: null, + code: '', + content: '', + apiTemplateId: '', + channelId: null, + createTime: [] +}) +const exportLoading = ref(false) // 导出的加载中 +const channelList = ref<SmsChannelApi.SmsChannelVO[]>([]) // 短信渠道列表 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await SmsTemplateApi.getSmsTemplatePage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 发送短信按钮 */ +const sendFormRef = ref() +const openSendForm = (id: number) => { + sendFormRef.value.open(id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await SmsTemplateApi.deleteSmsTemplate(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await SmsTemplateApi.exportSmsTemplate(queryParams) + download.excel(data, '短信模板.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(async () => { + await getList() + // 加载渠道列表 + channelList.value = await SmsChannelApi.getSimpleSmsChannelList() +}) +</script> diff --git a/src/views/system/tenant/form.vue b/src/views/system/tenant/form.vue new file mode 100644 index 00000000..4a6eaae4 --- /dev/null +++ b/src/views/system/tenant/form.vue @@ -0,0 +1,174 @@ +<template> + <Dialog :title="modelTitle" v-model="modelVisible" width="50%"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="80px" + v-loading="formLoading" + > + <el-form-item label="租户名" prop="name"> + <el-input v-model="formData.name" placeholder="请输入租户名" /> + </el-form-item> + <el-form-item label="租户套餐" prop="packageId"> + <el-select v-model="formData.packageId" placeholder="请选择租户套餐" clearable> + <el-option + v-for="item in packageList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="联系人" prop="contactName"> + <el-input v-model="formData.contactName" placeholder="请输入联系人" /> + </el-form-item> + <el-form-item label="联系手机" prop="contactMobile"> + <el-input v-model="formData.contactMobile" placeholder="请输入联系手机" /> + </el-form-item> + <el-form-item v-if="formData.id === undefined" label="用户名称" prop="username"> + <el-input v-model="formData.username" placeholder="请输入用户名称" /> + </el-form-item> + <el-form-item v-if="formData.id === undefined" label="用户密码" prop="password"> + <el-input + v-model="formData.password" + placeholder="请输入用户密码" + type="password" + show-password + /> + </el-form-item> + <el-form-item label="账号额度" prop="accountCount"> + <el-input-number + v-model="formData.accountCount" + placeholder="请输入账号额度" + controls-position="right" + :min="0" + /> + </el-form-item> + <el-form-item label="过期时间" prop="expireTime"> + <el-date-picker + clearable + v-model="formData.expireTime" + type="date" + value-format="x" + placeholder="请选择过期时间" + /> + </el-form-item> + <el-form-item label="绑定域名" prop="domain"> + <el-input v-model="formData.domain" placeholder="请输入绑定域名" /> + </el-form-item> + <el-form-item label="租户状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="modelVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import * as TenantApi from '@/api/system/tenant' +import { CommonStatusEnum } from '@/utils/constants' +import * as TenantPackageApi from '@/api/system/tenantPackage' + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 +const modelVisible = ref(false) // 弹窗的是否展示 +const modelTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: undefined, + packageId: undefined, + contactName: undefined, + contactMobile: undefined, + accountCount: undefined, + expireTime: undefined, + domain: undefined, + status: CommonStatusEnum.ENABLE +}) +const formRules = reactive({ + name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }], + packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }], + contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }], + status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }], + accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }], + expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }], + domain: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref +const packageList = ref([]) // 租户套餐 + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + modelVisible.value = true + modelTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await TenantApi.getTenant(id) + } finally { + formLoading.value = false + } + } + // 加载套餐列表 + packageList.value = await TenantPackageApi.getTenantPackageList() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as TenantApi.TenantVO + if (formType.value === 'create') { + await TenantApi.createTenant(data) + message.success(t('common.createSuccess')) + } else { + await TenantApi.updateTenant(data) + message.success(t('common.updateSuccess')) + } + modelVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: undefined, + packageId: undefined, + contactName: undefined, + contactMobile: undefined, + accountCount: undefined, + expireTime: undefined, + domain: undefined, + status: CommonStatusEnum.ENABLE + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/system/tenant/index.vue b/src/views/system/tenant/index.vue index bb1ca1a3..e316992d 100644 --- a/src/views/system/tenant/index.vue +++ b/src/views/system/tenant/index.vue @@ -1,197 +1,257 @@ <template> + <!-- 搜索 --> <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton - type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" - v-hasPermi="['system:tenant:create']" - @click="handleCreate()" - /> - <XButton - type="warning" - preIcon="ep:download" - :title="t('action.export')" - v-hasPermi="['system:tenant:export']" - @click="exportList('租户列表.xls')" - /> - </template> - <template #accountCount_default="{ row }"> - <el-tag> {{ row.accountCount }} </el-tag> - </template> - <template #packageId_default="{ row }"> - <el-tag v-if="row.packageId === 0" type="danger">系统租户</el-tag> - <el-tag v-else type="success"> {{ getPackageName(row.packageId) }} </el-tag> - </template> - <template #actionbtns_default="{ row }"> - <!-- 操作:修改 --> - <XTextButton - preIcon="ep:edit" - :title="t('action.edit')" - v-hasPermi="['system:tenant:update']" - @click="handleUpdate(row.id)" - /> - <!-- 操作:详情 --> - <XTextButton - preIcon="ep:view" - :title="t('action.detail')" - v-hasPermi="['system:tenant:update']" - @click="handleDetail(row.id)" - /> - <!-- 操作:删除 --> - <XTextButton - preIcon="ep:delete" - :title="t('action.del')" - v-hasPermi="['system:tenant:delete']" - @click="deleteData(row.id)" - /> - </template> - </XTable> - </ContentWrap> - <XModal v-model="dialogVisible" :title="dialogTitle"> - <!-- 对话框(添加 / 修改) --> - <Form - v-if="['create', 'update'].includes(actionType)" - :schema="allSchemas.formSchema" - :rules="rules" - ref="formRef" - /> - <!-- 对话框(详情) --> - <Descriptions - v-if="actionType === 'detail'" - :schema="allSchemas.detailSchema" - :data="detailData" + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" > - <template #packageId="{ row }"> - <el-tag v-if="row.packageId === 0" type="danger">系统租户</el-tag> - <el-tag v-else type="success"> {{ getPackageName(row.packageId) }} </el-tag> - </template> - </Descriptions> - <!-- 操作按钮 --> - <template #footer> - <!-- 按钮:保存 --> - <XButton - v-if="['create', 'update'].includes(actionType)" - type="primary" - :title="t('action.save')" - :loading="actionLoading" - @click="submitForm()" + <el-form-item label="租户名" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入租户名" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="联系人" prop="contactName"> + <el-input + v-model="queryParams.contactName" + placeholder="请输入联系人" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="联系手机" prop="contactMobile"> + <el-input + v-model="queryParams.contactMobile" + placeholder="请输入联系手机" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="租户状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择租户状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </el-form-item> + + <el-form-item> + <el-button @click="handleQuery"> + <Icon icon="ep:search" class="mr-5px" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon icon="ep:refresh" class="mr-5px" /> + 重置 + </el-button> + <el-button type="primary" @click="openForm('create')" v-hasPermi="['system:tenant:create']"> + <Icon icon="ep:plus" class="mr-5px" /> + 新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['system:tenant:export']" + > + <Icon icon="ep:download" class="mr-5px" /> + 导出 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" align="center"> + <el-table-column label="租户编号" align="center" prop="id" /> + <el-table-column label="租户名" align="center" prop="name" /> + <el-table-column label="租户套餐" align="center" prop="packageId"> + <template #default="scope"> + <el-tag v-if="scope.row.packageId === 0" type="danger">系统租户</el-tag> + <template v-else v-for="item in packageList"> + <el-tag type="success" :key="item.id" v-if="item.id === scope.row.packageId" + >{{ item.name }} + </el-tag> + </template> + </template> + </el-table-column> + <el-table-column label="联系人" align="center" prop="contactName" /> + <el-table-column label="联系手机" align="center" prop="contactMobile" /> + <el-table-column label="账号额度" align="center" prop="accountCount"> + <template #default="scope"> + <el-tag>{{ scope.row.accountCount }}</el-tag> + </template> + </el-table-column> + <el-table-column + label="过期时间" + align="center" + prop="expireTime" + width="180" + :formatter="dateFormatter" /> - <!-- 按钮:关闭 --> - <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> - </template> - </XModal> + <el-table-column label="绑定域名" align="center" prop="domain" width="180" /> + <el-table-column label="租户状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="操作" align="center" min-width="110" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['system:tenant:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['system:tenant:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <TenantForm ref="formRef" @success="getList" /> </template> <script setup lang="ts" name="Tenant"> -import type { FormExpose } from '@/components/Form' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' import * as TenantApi from '@/api/system/tenant' -import { rules, allSchemas, tenantPackageOption } from './tenant.data' +import * as TenantPackageApi from '@/api/system/tenantPackage' +import TenantForm from './form.vue' -const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 -// 列表相关的变量 -const [registerTable, { reload, deleteData, exportList }] = useXTable({ - allSchemas: allSchemas, - getListApi: TenantApi.getTenantPageApi, - deleteApi: TenantApi.deleteTenantApi, - exportListApi: TenantApi.exportTenantApi +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: undefined, + contactName: undefined, + contactMobile: undefined, + status: undefined, + createTime: [] }) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 +const packageList = ref([]) //租户套餐列表 -const actionLoading = ref(false) // 遮罩层 -const actionType = ref('') // 操作按钮的类型 -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('edit') // 弹出层标题 -const formRef = ref<FormExpose>() // 表单 Ref -const detailData = ref() // 详情 Ref -const getPackageName = (packageId: number) => { - for (let item of tenantPackageOption) { - if (item.value === packageId) { - return item.label - } - } - return '未知套餐' -} - -// 设置标题 -const setDialogTile = (type: string) => { - dialogTitle.value = t('action.' + type) - actionType.value = type - dialogVisible.value = true -} - -// 新增操作 -const handleCreate = async () => { - // 重置表单 - setDialogTile('create') - await nextTick() - console.log(allSchemas.formSchema, 'allSchemas.formSchema') - if (allSchemas.formSchema[4].field !== 'username') { - unref(formRef)?.addSchema( - { - field: 'username', - label: '用户名称', - component: 'Input' - }, - 0 - ) - unref(formRef)?.addSchema( - { - field: 'password', - label: '用户密码', - component: 'InputPassword' - }, - 1 - ) +/** 查询参数列表 */ +const getList = async () => { + loading.value = true + try { + const data = await TenantApi.getTenantPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false } } -// 修改操作 -const handleUpdate = async (rowId: number) => { - setDialogTile('update') - await nextTick() - unref(formRef)?.delSchema('username') - unref(formRef)?.delSchema('password') - // 设置数据 - const res = await TenantApi.getTenantApi(rowId) - unref(formRef)?.setValues(res) +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() } -// 详情操作 -const handleDetail = async (rowId: number) => { - // 设置数据 - const res = await TenantApi.getTenantApi(rowId) - detailData.value = res - setDialogTile('detail') +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() } -// 提交按钮 -const submitForm = async () => { - const elForm = unref(formRef)?.getElFormRef() - if (!elForm) return - elForm.validate(async (valid) => { - if (valid) { - actionLoading.value = true - // 提交请求 - try { - const data = unref(formRef)?.formModel as TenantApi.TenantVO - if (actionType.value === 'create') { - await TenantApi.createTenantApi(data) - message.success(t('common.createSuccess')) - } else { - await TenantApi.updateTenantApi(data) - message.success(t('common.updateSuccess')) - } - // 操作成功,重新加载列表 - dialogVisible.value = false - } finally { - actionLoading.value = false - // 刷新列表 - await reload() - } - } - }) +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) } + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await TenantApi.deleteTenant(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await TenantApi.exportTenant(queryParams) + download.excel(data, '租户列表.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 初始化 **/ +onMounted(async () => { + await getList() + packageList.value = await TenantPackageApi.getTenantPackageList() +}) </script> diff --git a/src/views/system/tenant/tenant.data.ts b/src/views/system/tenant/tenant.data.ts deleted file mode 100644 index 1137b44a..00000000 --- a/src/views/system/tenant/tenant.data.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -import { getTenantPackageList, TenantPackageVO } from '@/api/system/tenantPackage' -import { ComponentOptions } from '@/types/components' - -const { t } = useI18n() // 国际化 - -export const tenantPackageOption: ComponentOptions[] = [] -const getTenantPackageOptions = async () => { - const res = await getTenantPackageList() - res.forEach((tenantPackage: TenantPackageVO) => { - tenantPackageOption.push({ - key: tenantPackage.id, - value: tenantPackage.id, - label: tenantPackage.name - }) - }) - - return tenantPackageOption -} -getTenantPackageOptions() - -const validateName = (rule: any, value: any, callback: any) => { - const reg = /^[a-zA-Z0-9]{4,30}$/ - if (value === '') { - callback(new Error('请输入用户名称')) - } else { - console.log(reg.test(rule), 'reg.test(rule)') - if (!reg.test(value)) { - callback(new Error('用户名称由 数字、字母 组成')) - } else { - callback() - } - } -} -const validateMobile = (rule: any, value: any, callback: any) => { - const reg = /^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/ - if (value === '') { - callback(new Error('请输入联系手机')) - } else { - if (!reg.test(value)) { - callback(new Error('请输入正确的手机号')) - } else { - callback() - } - } -} - -// 表单校验 -export const rules = reactive({ - name: [required], - packageId: [required], - contactName: [required], - contactMobile: [ - required, - { - validator: validateMobile, - trigger: 'blur' - } - ], - accountCount: [required], - expireTime: [required], - username: [ - required, - { - min: 4, - max: 30, - trigger: 'blur', - message: '用户名称长度为 4-30 个字符' - }, - { validator: validateName, trigger: 'blur' } - ], - password: [ - required, - { - min: 4, - max: 16, - trigger: 'blur', - message: '密码长度为 4-16 位' - } - ], - domain: [required], - status: [required] -}) - -// CrudSchema. -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryTitle: '租户编号', - primaryType: 'id', - action: true, - columns: [ - { - title: '租户名称', - field: 'name', - isSearch: true - }, - { - title: '租户套餐', - field: 'packageId', - table: { - slots: { - default: 'packageId_default' - } - }, - form: { - component: 'Select', - componentProps: { - options: tenantPackageOption - } - } - }, - { - title: '联系人', - field: 'contactName', - isSearch: true - }, - { - title: '联系手机', - field: 'contactMobile', - isSearch: true - }, - { - title: '用户名称', - field: 'username', - isTable: false, - isDetail: false - }, - { - title: '用户密码', - field: 'password', - isTable: false, - isDetail: false, - form: { - component: 'InputPassword' - } - }, - { - title: '账号额度', - field: 'accountCount', - table: { - slots: { - default: 'accountCount_default' - } - }, - form: { - component: 'InputNumber' - } - }, - { - title: '过期时间', - field: 'expireTime', - formatter: 'formatDate', - form: { - component: 'DatePicker', - componentProps: { - type: 'datetime', - valueFormat: 'x' - } - } - }, - { - title: '绑定域名', - field: 'domain' - }, - { - title: '租户状态', - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true - }, - { - title: t('table.createTime'), - field: 'createTime', - formatter: 'formatDate', - isForm: false, - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/tenantPackage/index.vue b/src/views/system/tenantPackage/index.vue index c8f5756a..07ea39c6 100644 --- a/src/views/system/tenantPackage/index.vue +++ b/src/views/system/tenantPackage/index.vue @@ -71,7 +71,7 @@ import type { ElTree } from 'element-plus' // 业务相关的 import import { rules, allSchemas } from './tenantPackage.data' import * as TenantPackageApi from '@/api/system/tenantPackage' -import { listSimpleMenusApi } from '@/api/system/menu' +import { getSimpleMenusList } from '@/api/system/menu' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -102,7 +102,7 @@ const validateCategory = (rule: any, value: any, callback: any) => { rules.menuIds = [{ required: true, validator: validateCategory, trigger: 'blur' }] const getTree = async () => { - const res = await listSimpleMenusApi() + const res = await getSimpleMenusList() menuOptions.value = handleTree(res) } diff --git a/src/views/system/user/AddForm.vue b/src/views/system/user/AddForm.vue new file mode 100644 index 00000000..9a4d6029 --- /dev/null +++ b/src/views/system/user/AddForm.vue @@ -0,0 +1,223 @@ +<template> + <!-- 添加或修改参数配置对话框 --> + <el-dialog + :title="title" + :modelValue="modelValue" + width="600px" + append-to-body + @close="closeDialog" + > + <el-form ref="formRef" :model="formData" :rules="rules" label-width="80px"> + <el-row> + <el-col :span="12"> + <el-form-item label="用户昵称" prop="nickname"> + <el-input v-model="formData.nickname" placeholder="请输入用户昵称" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="归属部门" prop="deptId"> + <el-tree-select + node-key="id" + v-model="formData.deptId" + :data="deptOptions" + :props="defaultProps" + check-strictly + placeholder="请选择归属部门" + /> + </el-form-item> + </el-col> + </el-row> + <el-row> + <el-col :span="12"> + <el-form-item label="手机号码" prop="mobile"> + <el-input v-model="formData.mobile" placeholder="请输入手机号码" maxlength="11" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="邮箱" prop="email"> + <el-input v-model="formData.email" placeholder="请输入邮箱" maxlength="50" /> + </el-form-item> + </el-col> + </el-row> + <el-row> + <el-col :span="12"> + <el-form-item v-if="formData.id === undefined" label="用户名称" prop="username"> + <el-input v-model="formData.username" placeholder="请输入用户名称" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item v-if="formData.id === undefined" label="用户密码" prop="password"> + <el-input + v-model="formData.password" + placeholder="请输入用户密码" + type="password" + show-password + /> + </el-form-item> + </el-col> + </el-row> + <el-row> + <el-col :span="12"> + <el-form-item label="用户性别"> + <el-select v-model="formData.sex" placeholder="请选择"> + <el-option + v-for="dict in sexDictDatas" + :key="parseInt(dict.value)" + :label="dict.label" + :value="parseInt(dict.value)" + /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="岗位"> + <el-select v-model="formData.postIds" multiple placeholder="请选择"> + <el-option + v-for="item in postOptions" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + </el-col> + </el-row> + <el-row> + <el-col :span="24"> + <el-form-item label="备注"> + <el-input v-model="formData.remark" type="textarea" placeholder="请输入内容" /> + </el-form-item> + </el-col> + </el-row> + </el-form> + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="submitForm">确 定</el-button> + <el-button @click="cancel">取 消</el-button> + </div> + </template> + </el-dialog> +</template> +<script lang="ts" setup> +import { PostVO } from '@/api/system/post' +import { createUserApi, updateUserApi } from '@/api/system/user' +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import { defaultProps } from '@/utils/tree' +import { ElForm, FormItemRule } from 'element-plus' +import { Arrayable } from 'element-plus/es/utils' + +type Form = InstanceType<typeof ElForm> +interface Props { + deptOptions?: Tree[] + postOptions?: PostVO[] //岗位列表 + modelValue: boolean + formInitValue?: Recordable & Partial<typeof initParams> +} + +const props = withDefaults(defineProps<Props>(), { + deptOptions: () => [], + postOptions: () => [], + modelValue: false, + formInitValue: () => ({}) +}) +const emits = defineEmits(['update:modelValue', 'success']) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 +// 弹出层标题 +const title = computed(() => { + return formData.value?.id ? '修改用户' : '添加用户' +}) + +// 性别字典 +const sexDictDatas = getDictOptions(DICT_TYPE.SYSTEM_USER_SEX) + +// 表单初始化参数 +const initParams = { + nickname: '', + deptId: '', + mobile: '', + email: '', + id: undefined, + username: '', + password: '', + sex: 1, + postIds: [], + remark: '', + status: '0', + roleIds: [] +} + +// 校验规则 +const rules = { + username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }], + nickname: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }], + password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }], + email: [ + { + type: 'email', + message: "'请输入正确的邮箱地址", + trigger: ['blur', 'change'] + } + ], + mobile: [ + { + pattern: /^(?:(?:\+|00)86)?1(?:3[\d]|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8[\d]|9[189])\d{8}$/, + message: '请输入正确的手机号码', + trigger: 'blur' + } + ] +} as Partial<Record<string, Arrayable<FormItemRule>>> +const formRef = ref<Form | null>() +const formData = ref<Recordable>({ ...initParams }) +watch( + () => props.formInitValue, + (val) => { + formData.value = { ...val } + }, + { deep: true } +) + +const resetForm = () => { + let form = formRef?.value + if (!form) return + formData.value = { ...initParams } + form && (form as Form).resetFields() +} +const closeDialog = () => { + emits('update:modelValue', false) +} +// 操作成功 +const operateOk = () => { + emits('success', true) + closeDialog() +} +const submitForm = () => { + let form = formRef.value as Form + form.validate(async (valid) => { + let data = formData.value + if (valid) { + try { + if (data?.id !== undefined) { + await updateUserApi(data) + message.success(t('common.updateSuccess')) + operateOk() + } else { + await createUserApi(data) + message.success(t('common.createSuccess')) + operateOk() + } + } catch (err) { + console.error(err) + } + } + }) +} +const cancel = () => { + closeDialog() +} + +defineExpose({ + resetForm +}) +</script> diff --git a/src/views/system/user/ImportForm.vue b/src/views/system/user/ImportForm.vue new file mode 100644 index 00000000..4bfa4631 --- /dev/null +++ b/src/views/system/user/ImportForm.vue @@ -0,0 +1,153 @@ +<template> + <el-dialog + :title="upload.title" + :modelValue="modelValue" + width="400px" + append-to-body + @close="closeDialog" + > + <el-upload + ref="uploadRef" + accept=".xlsx, .xls" + :limit="1" + :headers="upload.headers" + :action="upload.url + '?updateSupport=' + upload.updateSupport" + :disabled="upload.isUploading" + :on-progress="handleFileUploadProgress" + :on-success="handleFileSuccess" + :on-exceed="handleExceed" + :on-error="excelUploadError" + :auto-upload="false" + drag + > + <Icon icon="ep:upload" /> + <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> + <template #tip> + <div class="el-upload__tip text-center"> + <div class="el-upload__tip"> + <el-checkbox v-model="upload.updateSupport" /> 是否更新已经存在的用户数据 + </div> + + <span>仅允许导入xls、xlsx格式文件。</span> + <el-link + type="primary" + :underline="false" + style="font-size: 12px; vertical-align: baseline" + @click="importTemplate" + >下载模板</el-link + > + </div> + </template> + </el-upload> + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="submitFileForm">确 定</el-button> + <el-button @click="cancel">取 消</el-button> + </div> + </template> + </el-dialog> +</template> + +<script lang="ts" setup> +import { importUserTemplateApi } from '@/api/system/user' +import { getAccessToken, getTenantId } from '@/utils/auth' +import download from '@/utils/download' + +interface Props { + modelValue: boolean +} + +// const props = +withDefaults(defineProps<Props>(), { + modelValue: false +}) + +const emits = defineEmits(['update:modelValue', 'success']) + +const message = useMessage() // 消息弹窗 + +const uploadRef = ref() + +// 用户导入参数 +const upload = reactive({ + // // 是否显示弹出层(用户导入) + // open: false, + // 弹出层标题(用户导入) + title: '用户导入', + // 是否禁用上传 + isUploading: false, + // 是否更新已经存在的用户数据 + updateSupport: 0, + // 设置上传的请求头部 + headers: { + Authorization: 'Bearer ' + getAccessToken(), + 'tenant-id': getTenantId() + }, + // 上传的地址 + url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import' +}) + +// 文件上传中处理 +const handleFileUploadProgress = () => { + upload.isUploading = true +} +// 文件上传成功处理 +const handleFileSuccess = (response: any) => { + if (response.code !== 0) { + message.error(response.msg) + return + } + upload.isUploading = false + uploadRef.value?.clearFiles() + // 拼接提示语 + const data = response.data + let text = '上传成功数量:' + data.createUsernames.length + ';' + for (let username of data.createUsernames) { + text += '< ' + username + ' >' + } + text += '更新成功数量:' + data.updateUsernames.length + ';' + for (const username of data.updateUsernames) { + text += '< ' + username + ' >' + } + text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';' + for (const username in data.failureUsernames) { + text += '< ' + username + ': ' + data.failureUsernames[username] + ' >' + } + message.alert(text) + emits('success') + closeDialog() +} + +// 文件数超出提示 +const handleExceed = (): void => { + message.error('最多只能上传一个文件!') +} +// 上传错误提示 +const excelUploadError = (): void => { + message.error('导入数据失败,请您重新上传!') +} + +/** 下载模板操作 */ +const importTemplate = async () => { + try { + const res = await importUserTemplateApi() + download.excel(res, '用户导入模版.xls') + } catch (error) { + console.error(error) + } +} + +/* 弹框按钮操作 */ +// 点击取消 +const cancel = () => { + closeDialog() +} +// 关闭弹窗 +const closeDialog = () => { + emits('update:modelValue', false) +} +// 提交上传文件 +const submitFileForm = () => { + uploadRef.value?.submit() +} +</script> diff --git a/src/views/system/user/RoleForm.vue b/src/views/system/user/RoleForm.vue new file mode 100644 index 00000000..cb5603fe --- /dev/null +++ b/src/views/system/user/RoleForm.vue @@ -0,0 +1,90 @@ +<template> + <el-dialog title="分配角色" :modelValue="show" width="500px" append-to-body @close="closeDialog"> + <el-form :model="formData" label-width="80px" ref="formRef"> + <el-form-item label="用户名称"> + <el-input v-model="formData.username" :disabled="true" /> + </el-form-item> + <el-form-item label="用户昵称"> + <el-input v-model="formData.nickname" :disabled="true" /> + </el-form-item> + <el-form-item label="角色"> + <el-select v-model="formData.roleIds" multiple placeholder="请选择"> + <el-option + v-for="item in roleOptions" + :key="parseInt(item.id)" + :label="item.name" + :value="parseInt(item.id)" + /> + </el-select> + </el-form-item> + </el-form> + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="submit">确 定</el-button> + <el-button @click="cancel">取 消</el-button> + </div> + </template> + </el-dialog> +</template> + +<script setup lang="ts"> +import { assignUserRoleApi, PermissionAssignUserRoleReqVO } from '@/api/system/permission' + +interface Props { + show: boolean + roleOptions: any[] + formInitValue?: Recordable & Partial<typeof initParams> +} + +const props = withDefaults(defineProps<Props>(), { + show: false, + roleOptions: () => [], + formInitValue: () => ({}) +}) +const emits = defineEmits(['update:show', 'success']) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +// 表单初始化参数 +const initParams = { + nickname: '', + id: 0, + username: '', + roleIds: [] as number[] +} +const formData = ref<Recordable>({ ...initParams }) +watch( + () => props.formInitValue, + (val) => { + formData.value = { ...val } + }, + { deep: true } +) +/* 弹框按钮操作 */ +// 点击取消 +const cancel = () => { + closeDialog() +} +// 关闭弹窗 +const closeDialog = () => { + emits('update:show', false) +} +// 提交 +const submit = async () => { + const data = ref<PermissionAssignUserRoleReqVO>({ + userId: formData.value.id, + roleIds: formData.value.roleIds + }) + try { + await assignUserRoleApi(data.value) + message.success(t('common.updateSuccess')) + emits('success', true) + closeDialog() + } catch (error) { + console.error(error) + } +} +</script> + +<style></style> diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue index 2b4bcc41..138c7373 100644 --- a/src/views/system/user/index.vue +++ b/src/views/system/user/index.vue @@ -1,277 +1,296 @@ <template> <div class="app-container"> - <!-- 搜索工作栏 --> - <el-row :gutter="20"> - <!--部门数据--> - <el-col :span="4" :xs="24"> - <div class="head-container"> - <el-input - v-model="deptName" - placeholder="请输入部门名称" - clearable - size="small" - style="margin-bottom: 20px" - > - <template #prefix> - <Icon icon="ep:search" /> - </template> - </el-input> - </div> - <div class="head-container"> - <el-tree - :data="deptOptions" - :props="defaultProps" - :expand-on-click-node="false" - :filter-node-method="filterNode" - ref="treeRef" - node-key="id" - default-expand-all - highlight-current - @node-click="handleDeptNodeClick" - /> - </div> - </el-col> - <!--用户数据--> - <el-col :span="20" :xs="24"> - <el-form - :model="queryParams" - ref="queryForm" - size="small" - :inline="true" - v-show="showSearch" - label-width="68px" - > - <el-form-item label="用户名称" prop="username"> + <content-wrap> + <!-- 搜索工作栏 --> + <el-row :gutter="20"> + <!--部门数据--> + <el-col :span="4" :xs="24"> + <div class="head-container"> <el-input - v-model="queryParams.username" - placeholder="请输入用户名称" + v-model="deptName" + placeholder="请输入部门名称" clearable - style="width: 240px" - @keyup.enter="handleQuery" - /> - </el-form-item> - <el-form-item label="手机号码" prop="mobile"> - <el-input - v-model="queryParams.mobile" - placeholder="请输入手机号码" - clearable - style="width: 240px" - @keyup.enter="handleQuery" - /> - </el-form-item> - <el-form-item label="状态" prop="status"> - <el-select - v-model="queryParams.status" - placeholder="用户状态" - clearable - style="width: 240px" + style="margin-bottom: 20px" > - <el-option - v-for="dict in statusDictDatas" - :key="parseInt(dict.value)" - :label="dict.label" - :value="parseInt(dict.value)" - /> - </el-select> - </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - style="width: 240px" - value-format="yyyy-MM-dd HH:mm:ss" - type="daterange" - range-separator="-" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="['00:00:00', '23:59:59']" + <template #prefix> + <Icon icon="ep:search" /> + </template> + </el-input> + </div> + <div class="head-container"> + <el-tree + :data="deptOptions" + :props="defaultProps" + :expand-on-click-node="false" + :filter-node-method="filterNode" + ref="treeRef" + node-key="id" + default-expand-all + highlight-current + @node-click="handleDeptNodeClick" /> - </el-form-item> - <el-form-item> - <el-button type="primary" @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> - </el-form-item> - </el-form> + </div> + </el-col> + <!--用户数据--> + <el-col :span="20" :xs="24"> + <el-form + :model="queryParams" + ref="queryFormRef" + :inline="true" + v-show="showSearch" + label-width="68px" + > + <el-form-item label="用户名称" prop="username"> + <el-input + v-model="queryParams.username" + placeholder="请输入用户名称" + clearable + style="width: 240px" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="手机号码" prop="mobile"> + <el-input + v-model="queryParams.mobile" + placeholder="请输入手机号码" + clearable + style="width: 240px" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="用户状态" + clearable + style="width: 240px" + > + <el-option + v-for="dict in statusDictDatas" + :key="parseInt(dict.value)" + :label="dict.label" + :value="parseInt(dict.value)" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + style="width: 240px" + value-format="YYYY-MM-DD HH:mm:ss" + type="datetimerange" + range-separator="-" + start-placeholder="开始日期" + end-placeholder="结束日期" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" @click="handleQuery" + ><Icon icon="ep:search" />搜索</el-button + > + <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> + </el-form-item> + </el-form> - <el-row :gutter="10" class="mb8"> - <el-col :span="1.5"> - <el-button - type="primary" - plain - size="small" - @click="handleAdd" - v-hasPermi="['system:user:create']" - ><Icon icon="ep:plus" />新增</el-button - > - </el-col> - <el-col :span="1.5"> - <el-button - type="info" - size="small" - @click="handleImport" - v-hasPermi="['system:user:import']" - ><Icon icon="ep:upload" />导入</el-button - > - </el-col> - <el-col :span="1.5"> - <el-button - type="warning" - size="small" - @click="handleExport" - :loading="exportLoading" - v-hasPermi="['system:user:export']" - ><Icon icon="ep:download" />导出</el-button - > - </el-col> - <!-- <right-toolbar - :showSearch.sync="showSearch" - @queryTable="getList" - :columns="columns" - ></right-toolbar> --> - </el-row> - <el-table v-loading="loading" :data="userList"> - <el-table-column - label="用户编号" - align="center" - key="id" - prop="id" - v-if="columns[0].visible" - /> - <el-table-column - label="用户名称" - align="center" - key="username" - prop="username" - v-if="columns[1].visible" - :show-overflow-tooltip="true" - /> - <el-table-column - label="用户昵称" - align="center" - key="nickname" - prop="nickname" - v-if="columns[2].visible" - :show-overflow-tooltip="true" - /> - <el-table-column - label="部门" - align="center" - key="deptName" - prop="dept.name" - v-if="columns[3].visible" - :show-overflow-tooltip="true" - /> - <el-table-column - label="手机号码" - align="center" - key="mobile" - prop="mobile" - v-if="columns[4].visible" - width="120" - /> - <el-table-column label="状态" key="status" v-if="columns[5].visible" align="center"> - <template #default="scope"> - <el-switch - v-model="scope.row.status" - :active-value="0" - :inactive-value="1" - @change="handleStatusChange(scope.row)" - /> - </template> - </el-table-column> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - v-if="columns[6].visible" - width="160" - > - <template #default="scope"> - <span>{{ parseTime(scope.row.createTime) }}</span> - </template> - </el-table-column> - <el-table-column - label="操作" - align="center" - width="160" - class-name="small-padding fixed-width" - > - <template #default="scope"> + <el-row :gutter="10" class="mb-8px"> + <el-col :span="1.5"> <el-button + type="primary" + plain size="small" - type="text" - @click="handleUpdate(scope.row)" - v-hasPermi="['system:user:update']" - ><Icon icon="ep:edit" />修改</el-button + @click="handleAdd" + v-hasPermi="['system:user:create']" + ><Icon icon="ep:plus" />新增</el-button > - <el-dropdown - @command="(command) => handleCommand(command, scope.$index, scope.row)" - v-hasPermi="[ - 'system:user:delete', - 'system:user:update-password', - 'system:permission:assign-user-role' - ]" + </el-col> + <el-col :span="1.5"> + <el-button + type="info" + size="small" + @click="handleImport" + v-hasPermi="['system:user:import']" + ><Icon icon="ep:upload" />导入</el-button > - <el-button size="small" type="text"><Icon icon="ep:d-arrow-right" />更多</el-button> - <template #dropdown> - <el-dropdown-menu> - <el-dropdown-item - command="handleDelete" - v-if="scope.row.id !== 1" - size="small" - type="text" - v-hasPermi="['system:user:delete']" - ><Icon icon="ep:delete" />删除</el-dropdown-item - > - <el-dropdown-item - command="handleResetPwd" - size="small" - type="text" - v-hasPermi="['system:user:update-password']" - ><Icon icon="ep:key" />重置密码</el-dropdown-item - > - <el-dropdown-item - command="handleRole" - size="small" - type="text" - v-hasPermi="['system:permission:assign-user-role']" - ><Icon icon="ep:circle-check" />分配角色</el-dropdown-item - > - </el-dropdown-menu> - </template> - </el-dropdown> - </template> - </el-table-column> - </el-table> - <pagination - v-show="total > 0" - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </el-col> - </el-row> + </el-col> + <el-col :span="1.5"> + <el-button + type="warning" + size="small" + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['system:user:export']" + ><Icon icon="ep:download" />导出</el-button + > + </el-col> + </el-row> + <el-table v-loading="loading" :data="userList"> + <el-table-column + label="用户编号" + align="center" + key="id" + prop="id" + v-if="columns[0].visible" + /> + <el-table-column + label="用户名称" + align="center" + key="username" + prop="username" + v-if="columns[1].visible" + :show-overflow-tooltip="true" + /> + <el-table-column + label="用户昵称" + align="center" + key="nickname" + prop="nickname" + v-if="columns[2].visible" + :show-overflow-tooltip="true" + /> + <el-table-column + label="部门" + align="center" + key="deptName" + prop="dept.name" + v-if="columns[3].visible" + :show-overflow-tooltip="true" + /> + <el-table-column + label="手机号码" + align="center" + key="mobile" + prop="mobile" + v-if="columns[4].visible" + width="120" + /> + <el-table-column label="状态" key="status" v-if="columns[5].visible" align="center"> + <template #default="scope"> + <el-switch + v-model="scope.row.status" + :active-value="0" + :inactive-value="1" + @change="handleStatusChange(scope.row)" + /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + v-if="columns[6].visible" + width="160" + > + <template #default="scope"> + <span>{{ parseTime(scope.row.createTime) }}</span> + </template> + </el-table-column> + <el-table-column + label="操作" + align="center" + width="160" + class-name="small-padding fixed-width" + > + <template #default="scope"> + <div class="flex justify-center items-center"> + <el-button + type="primary" + link + @click="handleUpdate(scope.row)" + v-hasPermi="['system:user:update']" + ><Icon icon="ep:edit" />修改</el-button + > + <el-dropdown + @command="(command) => handleCommand(command, scope.$index, scope.row)" + v-hasPermi="[ + 'system:user:delete', + 'system:user:update-password', + 'system:permission:assign-user-role' + ]" + > + <el-button type="primary" link><Icon icon="ep:d-arrow-right" />更多</el-button> + <template #dropdown> + <el-dropdown-menu> + <!-- div包住避免控制台报错:Runtime directive used on component with non-element root node --> + <div v-if="scope.row.id !== 1" v-hasPermi="['system:user:delete']"> + <el-dropdown-item command="handleDelete" type="text" + ><Icon icon="ep:delete" />删除</el-dropdown-item + > + </div> + <div v-hasPermi="['system:user:update-password']"> + <el-dropdown-item command="handleResetPwd" type="text" + ><Icon icon="ep:key" />重置密码</el-dropdown-item + ></div + > + <div v-hasPermi="['system:permission:assign-user-role']"> + <el-dropdown-item command="handleRole" type="text" + ><Icon icon="ep:circle-check" />分配角色</el-dropdown-item + ></div + > + </el-dropdown-menu> + </template> + </el-dropdown> + </div> + </template> + </el-table-column> + </el-table> + <pagination + v-show="total > 0" + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </el-col> + </el-row> + </content-wrap> + <!-- 添加或修改用户对话框 --> + <AddForm + ref="addEditFormRef" + v-model="showAddDialog" + :dept-options="deptOptions" + :post-options="postOptions" + :form-init-value="addFormInitValue" + @success="getList" + /> + <!-- 用户导入对话框 --> + <ImportForm v-model="importDialogVisible" @success="getList" /> + <!-- 分配角色 --> + <RoleForm + ref="roleFormRef" + v-model:show="roleDialogVisible" + :role-options="roleOptions" + :form-init-value="userRole" + @success="getList" + /> </div> </template> <script setup lang="ts" name="User"> import type { ElTree } from 'element-plus' import { handleTree, defaultProps } from '@/utils/tree' -import { listSimpleDeptApi } from '@/api/system/dept' -import { listSimplePostsApi, PostVO } from '@/api/system/post' +// 原vue3版本api方法都是Api结尾觉得见名知义,个人觉得这个可以形成规范 +import { getSimpleDeptList as getSimpleDeptListApi } from '@/api/system/dept' +import { getSimplePostList as getSimplePostListApi, PostVO } from '@/api/system/post' import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import { UserVO } from '@/api/system/user' import { - // createUserApi, - // updateUserStatusApi, - // deleteUserApi, - // exportUserApi, - // getUserApi, - // importUserTemplateApi, - getUserPageApi - // resetUserPwdApid, - // updateUserApi + deleteUserApi, + exportUserApi, + resetUserPwdApi, + updateUserStatusApi, + UserVO } from '@/api/system/user' +import { parseTime } from './utils' +import AddForm from './AddForm.vue' +import ImportForm from './ImportForm.vue' +import RoleForm from './RoleForm.vue' +import { getUserApi, getUserPageApi } from '@/api/system/user' +import { getSimpleRoleList as getSimpleRoleListApi } from '@/api/system/role' +import { listUserRolesApi } from '@/api/system/permission' +import { CommonStatusEnum } from '@/utils/constants' +import download from '@/utils/download' + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 const queryParams = reactive({ pageNo: 1, @@ -283,16 +302,24 @@ const queryParams = reactive({ createTime: [] }) const showSearch = ref(true) -// 数据字典 +const showAddDialog = ref(false) + +// 数据字典- const statusDictDatas = getDictOptions(DICT_TYPE.COMMON_STATUS) -// const sexDictDatas = getDictOptions(DICT_TYPE.SYSTEM_USER_SEX) // ========== 创建部门树结构 ========== const deptName = ref('') +watch( + () => deptName.value, + (val) => { + treeRef.value?.filter(val) + } +) const deptOptions = ref<Tree[]>([]) // 树形结构 const treeRef = ref<InstanceType<typeof ElTree>>() const getTree = async () => { - const res = await listSimpleDeptApi() + const res = await getSimpleDeptListApi() + deptOptions.value = [] deptOptions.value.push(...handleTree(res)) } const filterNode = (value: string, data: Tree) => { @@ -301,13 +328,13 @@ const filterNode = (value: string, data: Tree) => { } const handleDeptNodeClick = async (row: { [key: string]: any }) => { queryParams.deptId = row.id - // await reload() getList() } + // 获取岗位列表 const postOptions = ref<PostVO[]>([]) //岗位列表 const getPostOptions = async () => { - const res = await listSimplePostsApi() + const res = await getSimplePostListApi() postOptions.value.push(...res) } // 用户列表 @@ -323,6 +350,7 @@ const columns = ref([ { key: 5, label: `状态`, visible: true }, { key: 6, label: `创建时间`, visible: true } ]) +/* 查询列表 */ const getList = () => { loading.value = true getUserPageApi(queryParams).then((response) => { @@ -331,15 +359,169 @@ const getList = () => { loading.value = false }) } -const handleQuery = () => {} -const resetQuery = () => {} -const handleAdd = () => {} -const handleImport = () => {} +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const queryFormRef = ref() +const resetQuery = () => { + queryFormRef.value?.resetFields() + handleQuery() +} + +// 添加或编辑 +const addEditFormRef = ref() +// 添加用户 +const handleAdd = () => { + addEditFormRef?.value.resetForm() + // 获得下拉数据 + getTree() + // 打开表单,并设置初始化 + showAddDialog.value = true +} + +// 用户导入 +const handleImport = () => { + importDialogVisible.value = true +} + +// 用户导出 const exportLoading = ref(false) -const handleExport = () => {} -const handleStatusChange = () => {} -const handleUpdate = () => {} -const handleCommand = () => {} +const handleExport = () => { + message + .confirm('是否确认导出所有用户数据项?') + .then(() => { + // 处理查询参数 + let params = { ...queryParams } + params.pageNo = 1 + params.pageSize = 99999 + exportLoading.value = true + return exportUserApi(params) + }) + .then((response) => { + download.excel(response, '用户数据.xls') + }) + .catch(() => {}) + .finally(() => { + exportLoading.value = false + }) +} + +// 操作分发 +const handleCommand = (command: string, index: number, row: UserVO) => { + console.log(index) + switch (command) { + case 'handleUpdate': + handleUpdate(row) //修改客户信息 + break + case 'handleDelete': + handleDelete(row) //红号变更 + break + case 'handleResetPwd': + handleResetPwd(row) + break + case 'handleRole': + handleRole(row) + break + default: + break + } +} + +// 用户状态修改 +const handleStatusChange = (row: UserVO) => { + let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用' + message + .confirm('确认要"' + text + '""' + row.username + '"用户吗?', t('common.reminder')) + .then(function () { + row.status = + row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.ENABLE : CommonStatusEnum.DISABLE + return updateUserStatusApi(row.id, row.status) + }) + .then(() => { + message.success(text + '成功') + // 刷新列表 + getList() + }) + .catch(() => { + row.status = + row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE + }) +} + +// 具体数据单行操作 +const addFormInitValue = ref<Recordable>({}) +/** 修改按钮操作 */ +const handleUpdate = (row: UserVO) => { + addEditFormRef.value?.resetForm() + getTree() + const id = row.id + getUserApi(id).then((response) => { + addFormInitValue.value = response + showAddDialog.value = true + }) +} + +// 删除用户 +const handleDelete = (row: UserVO) => { + const ids = row.id + message + .confirm('是否确认删除用户编号为"' + ids + '"的数据项?') + .then(() => { + return deleteUserApi(ids) + }) + .then(() => { + getList() + message.success('删除成功') + }) + .catch(() => {}) +} + +// 重置密码 +const handleResetPwd = (row: UserVO) => { + message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => { + resetUserPwdApi(row.id, value) + .then(() => { + message.success('修改成功,新密码是:' + value) + }) + .catch((e) => { + console.error(e) + }) + }) +} + +// 分配角色 +const roleDialogVisible = ref(false) +const roleOptions = ref() +const userRole = reactive({ + id: 0, + username: '', + nickname: '', + roleIds: [] +}) +const handleRole = async (row: UserVO) => { + addEditFormRef.value?.resetForm() + userRole.id = row.id + userRole.username = row.username + userRole.nickname = row.nickname + + // 获得角色列表 + const roleOpt = await getSimpleRoleListApi() + roleOptions.value = [...roleOpt] + + // 获得角色拥有的菜单集合 + const roles = await listUserRolesApi(row.id) + userRole.roleIds = roles + + roleDialogVisible.value = true +} + +/* 用户导入 */ +const importDialogVisible = ref(false) + // ========== 初始化 ========== onMounted(async () => { getList() diff --git a/src/views/system/user/user.data.ts b/src/views/system/user/user.data.ts deleted file mode 100644 index 3a702c04..00000000 --- a/src/views/system/user/user.data.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' -// 国际化 -const { t } = useI18n() -const validateMobile = (rule: any, value: any, callback: any) => { - const reg = /^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/ - if (value === '') { - callback(new Error('请输入联系手机')) - } else { - if (!reg.test(value)) { - callback(new Error('请输入正确的手机号')) - } else { - callback() - } - } -} -// 表单校验 -export const rules = reactive({ - username: [required], - nickname: [required], - password: [required], - deptId: [required], - email: [ - { required: true, message: t('profile.rules.mail'), trigger: 'blur' }, - { - type: 'email', - message: t('profile.rules.truemail'), - trigger: ['blur', 'change'] - } - ], - status: [required], - mobile: [ - required, - { - len: 11, - trigger: 'blur', - message: '请输入正确的手机号码' - }, - { validator: validateMobile, trigger: 'blur' } - ] -}) -// crudSchemas -const crudSchemas = reactive<VxeCrudSchema>({ - primaryKey: 'id', - primaryType: 'id', - primaryTitle: '用户编号', - action: true, - actionWidth: '200px', - columns: [ - { - title: '用户账号', - field: 'username', - isSearch: true - }, - { - title: '用户密码', - field: 'password', - isDetail: false, - isTable: false, - form: { - component: 'InputPassword' - } - }, - { - title: '用户' + t('profile.user.sex'), - field: 'sex', - dictType: DICT_TYPE.SYSTEM_USER_SEX, - dictClass: 'number', - table: { show: false } - }, - { - title: '用户昵称', - field: 'nickname' - }, - { - title: '用户邮箱', - field: 'email' - }, - { - title: '手机号码', - field: 'mobile', - isSearch: true - }, - { - title: '部门', - field: 'deptId', - isTable: false - }, - { - title: '岗位', - field: 'postIds', - isTable: false - }, - { - title: t('common.status'), - field: 'status', - dictType: DICT_TYPE.COMMON_STATUS, - dictClass: 'number', - isSearch: true, - table: { - slots: { - default: 'status_default' - } - } - }, - { - title: '最后登录时间', - field: 'loginDate', - formatter: 'formatDate', - isForm: false - }, - { - title: '最后登录IP', - field: 'loginIp', - isTable: false, - isForm: false - }, - { - title: t('form.remark'), - field: 'remark', - isTable: false - }, - { - title: t('common.createTime'), - field: 'createTime', - formatter: 'formatDate', - isTable: false, - isForm: false, - search: { - show: true, - itemRender: { - name: 'XDataTimePicker' - } - } - } - ] -}) -export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/user/utils.ts b/src/views/system/user/utils.ts new file mode 100644 index 00000000..6473c2c9 --- /dev/null +++ b/src/views/system/user/utils.ts @@ -0,0 +1,44 @@ +export const parseTime = (time) => { + if (!time) { + return null + } + const format = '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if (typeof time === 'string' && /^[0-9]+$/.test(time)) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time + .replace(new RegExp(/-/gm), '/') + .replace('T', ' ') + .replace(new RegExp(/\.[\d]{3}/gm), '') + } + if (typeof time === 'number' && time.toString().length === 10) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} diff --git a/tsconfig.json b/tsconfig.json index 6b412cd8..b97c7079 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ "vite-plugin-svg-icons/client", "@form-create/element-ui/types" ], + "outDir": "target", // 请保留这个属性,防止tsconfig.json文件报错 "typeRoots": ["./node_modules/@types/", "./types"] }, "include": [ @@ -40,5 +41,5 @@ "src/types/auto-imports.d.ts", "src/types/auto-components.d.ts" ], - "exclude": ["dist", "node_modules"] + "exclude": ["dist", "target", "node_modules"] } diff --git a/types/components.d.ts b/types/components.d.ts index 85db5663..9d0ba09a 100644 --- a/types/components.d.ts +++ b/types/components.d.ts @@ -1,6 +1,7 @@ declare module 'vue' { export interface GlobalComponents { - Icon: typeof import('../components/Icon/src/Icon.vue')['default'] + Icon: typeof import('@/components/Icon')['Icon'] + DictTag: typeof import('@/components/DictTag')['DictTag'] } } diff --git a/types/global.d.ts b/types/global.d.ts index 3685ffbd..5e292687 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -1,29 +1,29 @@ export {} declare global { - declare interface Fn<T = any> { + interface Fn<T = any> { (...arg: T[]): T } - declare type Nullable<T> = T | null + type Nullable<T> = T | null - declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T> + type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T> - declare type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T> + type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T> - declare type ComponentRef<T> = InstanceType<T> + type ComponentRef<T> = InstanceType<T> - declare type LocaleType = 'zh-CN' | 'en' + type LocaleType = 'zh-CN' | 'en' - declare type AxiosHeaders = + type AxiosHeaders = | 'application/json' | 'application/x-www-form-urlencoded' | 'multipart/form-data' - declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT' + type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT' - declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' + type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' - declare interface AxiosConfig { + interface AxiosConfig { params?: any data?: any url?: string @@ -32,17 +32,17 @@ declare global { responseType?: AxiosResponseType } - declare interface IResponse<T = any> { + interface IResponse<T = any> { code: string data: T extends any ? T : T & any } - declare interface PageParam { + interface PageParam { pageSize?: number pageNo?: number } - declare interface Tree { + interface Tree { id: number name: string children?: Tree[] | any[] diff --git a/types/router.d.ts b/types/router.d.ts index 10ba0c15..9b08b805 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -54,7 +54,7 @@ type Component<T = any> = | (() => Promise<T>) declare global { - declare interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { + interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { name: string meta: RouteMeta component?: Component | string @@ -64,7 +64,7 @@ declare global { keepAlive?: boolean } - declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { + interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { icon: any name: string meta: RouteMeta diff --git a/vite.config.ts b/vite.config.ts index 6b54e183..fe2d7131 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -42,7 +42,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => { // }, }, // 项目使用的vite插件。 单独提取到build/vite/plugin中管理 - plugins: createVitePlugins(env.VITE_APP_TITLE), + plugins: createVitePlugins(), css: { preprocessorOptions: { scss: {