Merge branch 'dev' of https://gitee.com/fessor/yudao-ui-admin-vue3 into dev
						commit
						29fd92e60c
					
				|  | @ -6,3 +6,4 @@ dist-ssr | |||
| /dist* | ||||
| *-lock.* | ||||
| pnpm-debug | ||||
| .idea | ||||
|  |  | |||
|  | @ -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() | ||||
|   ] | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										61
									
								
								package.json
								
								
								
								
							
							
						
						
									
										61
									
								
								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": { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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' | ||||
|   }) | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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' }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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' }) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|   }) | ||||
|  |  | |||
|  | @ -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' }) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|   }) | ||||
| } | ||||
|  |  | |||
|  | @ -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' }) | ||||
|   }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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 }) | ||||
| } | ||||
|  |  | |||
|  | @ -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' }) | ||||
| } | ||||
|  |  | |||
|  | @ -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" | ||||
|     > | ||||
|  |  | |||
|  | @ -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()) | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -128,7 +128,7 @@ const getColumnsConfig = (options: XTableProps) => { | |||
|     proxyForm = true | ||||
|     options.formConfig = { | ||||
|       enabled: true, | ||||
|       titleWidth: 180, | ||||
|       titleWidth: 110, | ||||
|       titleAlign: 'right', | ||||
|       items: allSchemas.searchSchema | ||||
|     } | ||||
|  |  | |||
|  | @ -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}> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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' | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|  |  | |||
|  | @ -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' // 消息类型
 | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  | @ -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 | ||||
|     }) | ||||
|     // 情况二:业务表单 | ||||
|  |  | |||
|  | @ -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 | ||||
|       }) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
| 
 | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
|  | @ -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> | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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>--> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	 fengjingtao
						fengjingtao