Commit 29fd92e6 by fengjingtao

Merge branch 'dev' of https://gitee.com/fessor/yudao-ui-admin-vue3 into dev

parents ca1b6df5 1ef65634
...@@ -6,3 +6,4 @@ dist-ssr ...@@ -6,3 +6,4 @@ dist-ssr
/dist* /dist*
*-lock.* *-lock.*
pnpm-debug pnpm-debug
.idea
...@@ -15,7 +15,7 @@ import vueSetupExtend from 'vite-plugin-vue-setup-extend' ...@@ -15,7 +15,7 @@ import vueSetupExtend from 'vite-plugin-vue-setup-extend'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export function createVitePlugins(VITE_APP_TITLE: string) { export function createVitePlugins() {
const root = process.cwd() const root = process.cwd()
// 路径查找 // 路径查找
function pathResolve(dir: string) { function pathResolve(dir: string) {
...@@ -95,8 +95,6 @@ export function createVitePlugins(VITE_APP_TITLE: string) { ...@@ -95,8 +95,6 @@ export function createVitePlugins(VITE_APP_TITLE: string) {
ext: '.gz', // 生成的压缩包后缀 ext: '.gz', // 生成的压缩包后缀
deleteOriginFile: false //压缩后是否删除源文件 deleteOriginFile: false //压缩后是否删除源文件
}), }),
ViteEjsPlugin({ ViteEjsPlugin()
title: VITE_APP_TITLE
})
] ]
} }
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
name="description" name="description"
content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!" content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
/> />
<title><%= title %></title> <title>%VITE_APP_TITLE%</title>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
...@@ -137,7 +137,7 @@ ...@@ -137,7 +137,7 @@
<div class="app-loading-wrap"> <div class="app-loading-wrap">
<div class="app-loading-title"> <div class="app-loading-title">
<img src="/logo.gif" class="app-loading-logo" alt="Logo" /> <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>
<div class="app-loading-item"> <div class="app-loading-item">
<div class="app-loading-outter"></div> <div class="app-loading-outter"></div>
......
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "1.7.1-snapshot.1941", "version": "1.7.1-snapshot.1961",
"description": "基于vue3、vite4、element-plus、typesScript", "description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu", "author": "xingyu",
"private": false, "private": false,
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
"@zxcvbn-ts/core": "^2.2.1", "@zxcvbn-ts/core": "^2.2.1",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.3.4", "axios": "^1.3.4",
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0", "bpmn-js-token-simulation": "^0.10.0",
"camunda-bpmn-moddle": "^7.0.1", "camunda-bpmn-moddle": "^7.0.1",
"cropperjs": "^1.5.13", "cropperjs": "^1.5.13",
...@@ -43,7 +44,7 @@ ...@@ -43,7 +44,7 @@
"diagram-js": "^11.6.0", "diagram-js": "^11.6.0",
"echarts": "^5.4.1", "echarts": "^5.4.1",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.2.34", "element-plus": "2.3.1",
"fast-xml-parser": "^4.1.3", "fast-xml-parser": "^4.1.3",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"intro.js": "^6.0.0", "intro.js": "^6.0.0",
...@@ -62,57 +63,57 @@ ...@@ -62,57 +63,57 @@
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue-types": "^5.0.2", "vue-types": "^5.0.2",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"vxe-table": "^4.3.10", "vxe-table": "^4.3.11",
"web-storage-cache": "^1.1.1", "web-storage-cache": "^1.1.1",
"xe-utils": "^3.5.7", "xe-utils": "^3.5.7",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.4", "@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.4.4", "@commitlint/config-conventional": "^17.4.4",
"@iconify/json": "^2.2.31", "@iconify/json": "^2.2.38",
"@intlify/unplugin-vue-i18n": "^0.8.2", "@intlify/unplugin-vue-i18n": "^0.10.0",
"@purge-icons/generated": "^0.9.0", "@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.1", "@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.7",
"@types/node": "^18.14.6", "@types/node": "^18.15.5",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.54.1", "@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.54.1", "@typescript-eslint/parser": "^5.56.0",
"@vitejs/plugin-legacy": "^4.0.1", "@vitejs/plugin-legacy": "^4.0.2",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.14",
"bpmn-js": "^8.9.0", "bpmn-js": "^8.9.0",
"bpmn-js-properties-panel": "^0.46.0", "bpmn-js-properties-panel": "^0.46.0",
"consola": "^2.15.3", "consola": "^2.15.3",
"eslint": "^8.35.0", "eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0", "eslint-config-prettier": "^8.8.0",
"eslint-define-config": "^1.15.0", "eslint-define-config": "^1.17.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0", "eslint-plugin-vue": "^9.9.0",
"lint-staged": "^13.1.2", "lint-staged": "^13.2.0",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"prettier": "^2.8.4", "prettier": "^2.8.6",
"rimraf": "^4.3.1", "rimraf": "^4.4.1",
"rollup": "^3.18.0", "rollup": "^3.20.0",
"sass": "^1.58.3", "sass": "^1.59.3",
"stylelint": "^15.2.0", "stylelint": "^15.3.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.5", "stylelint-config-prettier": "^9.0.5",
"stylelint-config-recommended": "^10.0.1", "stylelint-config-recommended": "^11.0.0",
"stylelint-config-standard": "^30.0.1", "stylelint-config-standard": "^31.0.0",
"stylelint-order": "^6.0.2", "stylelint-order": "^6.0.3",
"terser": "^5.16.5", "terser": "^5.16.6",
"typescript": "4.9.5", "typescript": "5.0.2",
"unplugin-auto-import": "^0.15.1", "unplugin-auto-import": "^0.15.1",
"unplugin-element-plus": "^0.7.0", "unplugin-element-plus": "^0.7.0",
"unplugin-vue-components": "^0.24.1", "unplugin-vue-components": "^0.24.1",
"vite": "4.1.4", "vite": "4.2.1",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4", "vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
...@@ -125,7 +126,7 @@ ...@@ -125,7 +126,7 @@
"windicss": "^3.5.6" "windicss": "^3.5.6"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.18.0"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {
......
import request from '@/config/axios' import request from '@/config/axios'
export const getProcessDefinitionBpmnXMLApi = async (id: number) => { export const getProcessDefinitionBpmnXML = async (id: number) => {
return await request.get({ return await request.get({
url: '/bpm/process-definition/get-bpmn-xml?id=' + id url: '/bpm/process-definition/get-bpmn-xml?id=' + id
}) })
} }
export const getProcessDefinitionPageApi = async (params) => { export const getProcessDefinitionPage = async (params) => {
return await request.get({ return await request.get({
url: '/bpm/process-definition/page', url: '/bpm/process-definition/page',
params params
}) })
} }
export const getProcessDefinitionListApi = async (params) => { export const getProcessDefinitionList = async (params) => {
return await request.get({ return await request.get({
url: '/bpm/process-definition/list', url: '/bpm/process-definition/list',
params params
......
...@@ -11,7 +11,7 @@ export type FormVO = { ...@@ -11,7 +11,7 @@ export type FormVO = {
} }
// 创建工作流的表单定义 // 创建工作流的表单定义
export const createFormApi = async (data: FormVO) => { export const createForm = async (data: FormVO) => {
return await request.post({ return await request.post({
url: '/bpm/form/create', url: '/bpm/form/create',
data: data data: data
...@@ -19,7 +19,7 @@ export const createFormApi = async (data: FormVO) => { ...@@ -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({ return await request.put({
url: '/bpm/form/update', url: '/bpm/form/update',
data: data data: data
...@@ -27,21 +27,21 @@ export const updateFormApi = async (data: FormVO) => { ...@@ -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({ return await request.delete({
url: '/bpm/form/delete?id=' + id url: '/bpm/form/delete?id=' + id
}) })
} }
// 获得工作流的表单定义 // 获得工作流的表单定义
export const getFormApi = async (id: number) => { export const getForm = async (id: number) => {
return await request.get({ return await request.get({
url: '/bpm/form/get?id=' + id url: '/bpm/form/get?id=' + id
}) })
} }
// 获得工作流的表单定义分页 // 获得工作流的表单定义分页
export const getFormPageApi = async (params) => { export const getFormPage = async (params) => {
return await request.get({ return await request.get({
url: '/bpm/form/page', url: '/bpm/form/page',
params params
...@@ -49,7 +49,7 @@ export const getFormPageApi = async (params) => { ...@@ -49,7 +49,7 @@ export const getFormPageApi = async (params) => {
} }
// 获得动态表单的精简列表 // 获得动态表单的精简列表
export const getSimpleFormsApi = async () => { export const getSimpleFormList = async () => {
return await request.get({ return await request.get({
url: '/bpm/form/list-all-simple' url: '/bpm/form/list-all-simple'
}) })
......
...@@ -25,20 +25,20 @@ export type ModelVO = { ...@@ -25,20 +25,20 @@ export type ModelVO = {
bpmnXml: string bpmnXml: string
} }
export const getModelPageApi = async (params) => { export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', 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 }) 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 }) 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 = { const data = {
id: id, id: id,
state: state state: state
...@@ -46,14 +46,14 @@ export const updateModelStateApi = async (id: number, state: number) => { ...@@ -46,14 +46,14 @@ export const updateModelStateApi = async (id: number, state: number) => {
return await request.put({ url: '/bpm/model/update-state', data: data }) 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 }) 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 }) 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 }) return await request.post({ url: '/bpm/model/deploy?id=' + id })
} }
...@@ -11,7 +11,7 @@ export type UserGroupVO = { ...@@ -11,7 +11,7 @@ export type UserGroupVO = {
} }
// 创建用户组 // 创建用户组
export const createUserGroupApi = async (data: UserGroupVO) => { export const createUserGroup = async (data: UserGroupVO) => {
return await request.post({ return await request.post({
url: '/bpm/user-group/create', url: '/bpm/user-group/create',
data: data data: data
...@@ -19,7 +19,7 @@ export const createUserGroupApi = async (data: UserGroupVO) => { ...@@ -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({ return await request.put({
url: '/bpm/user-group/update', url: '/bpm/user-group/update',
data: data data: data
...@@ -27,21 +27,21 @@ export const updateUserGroupApi = async (data: UserGroupVO) => { ...@@ -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 }) 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 }) 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 }) 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' }) return await request.get({ url: '/bpm/user-group/list-all-simple' })
} }
...@@ -19,32 +19,12 @@ export interface ApiAccessLogVO { ...@@ -19,32 +19,12 @@ export interface ApiAccessLogVO {
createTime: Date 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 访问日志 // 查询列表API 访问日志
export const getApiAccessLogPageApi = (params: ApiAccessLogPageReqVO) => { export const getApiAccessLogPage = (params: PageParam) => {
return request.get({ url: '/infra/api-access-log/page', params }) return request.get({ url: '/infra/api-access-log/page', params })
} }
// 导出API 访问日志 // 导出API 访问日志
export const exportApiAccessLogApi = (params: ApiAccessLogExportReqVO) => { export const exportApiAccessLog = (params) => {
return request.download({ url: '/infra/api-access-log/export-excel', params }) return request.download({ url: '/infra/api-access-log/export-excel', params })
} }
...@@ -27,38 +27,20 @@ export interface ApiErrorLogVO { ...@@ -27,38 +27,20 @@ export interface ApiErrorLogVO {
createTime: Date 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 访问日志 // 查询列表API 访问日志
export const getApiErrorLogPageApi = (params: ApiErrorLogPageReqVO) => { export const getApiErrorLogPage = (params: PageParam) => {
return request.get({ url: '/infra/api-error-log/page', params }) return request.get({ url: '/infra/api-error-log/page', params })
} }
// 更新 API 错误日志的处理状态 // 更新 API 错误日志的处理状态
export const updateApiErrorLogPageApi = (id: number, processStatus: number) => { export const updateApiErrorLogPage = (id: number, processStatus: number) => {
return request.put({ return request.put({
url: '/infra/api-error-log/update-status?id=' + id + '&processStatus=' + processStatus url: '/infra/api-error-log/update-status?id=' + id + '&processStatus=' + processStatus
}) })
} }
// 导出API 访问日志 // 导出API 访问日志
export const exportApiErrorLogApi = (params: ApiErrorLogExportReqVO) => { export const exportApiErrorLog = (params) => {
return request.download({ return request.download({
url: '/infra/api-error-log/export-excel', url: '/infra/api-error-log/export-excel',
params params
......
import request from '@/config/axios' import request from '@/config/axios'
export interface AccountVO {
id?: number
name: string
}
// 创建公众号账号 // 创建公众号账号
export const createAccount = async (data) => { export const createAccount = async (data) => {
return await request.post({ url: '/mp/account/create', data }) return await request.post({ url: '/mp/account/create', data })
...@@ -26,7 +31,7 @@ export const getAccountPage = async (query) => { ...@@ -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' }) return request.get({ url: '/mp/account/list-all-simple' })
} }
......
import request from '@/config/axios' import request from '@/config/axios'
// 获得公众号消息分页 // 获得公众号消息分页
export const getMessagePage = (query) => { export const getMessagePage = (query: PageParam) => {
return request.get({ return request.get({
url: '/mp/message/page', url: '/mp/message/page',
params: query params: query
......
import request from '@/config/axios' 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({ return request.post({
url: '/mp/tag/create', url: '/mp/tag/create',
data: data data: data
...@@ -9,7 +16,7 @@ export const createTag = (data) => { ...@@ -9,7 +16,7 @@ export const createTag = (data) => {
} }
// 更新公众号标签 // 更新公众号标签
export const updateTag = (data) => { export const updateTag = (data: TagVO) => {
return request.put({ return request.put({
url: '/mp/tag/update', url: '/mp/tag/update',
data: data data: data
...@@ -17,21 +24,21 @@ export const updateTag = (data) => { ...@@ -17,21 +24,21 @@ export const updateTag = (data) => {
} }
// 删除公众号标签 // 删除公众号标签
export const deleteTag = (id) => { export const deleteTag = (id: number) => {
return request.delete({ return request.delete({
url: '/mp/tag/delete?id=' + id url: '/mp/tag/delete?id=' + id
}) })
} }
// 获得公众号标签 // 获得公众号标签
export const getTag = (id) => { export const getTag = (id: number) => {
return request.get({ return request.get({
url: '/mp/tag/get?id=' + id url: '/mp/tag/get?id=' + id
}) })
} }
// 获得公众号标签分页 // 获得公众号标签分页
export const getTagPage = (query) => { export const getTagPage = (query: PageParam) => {
return request.get({ return request.get({
url: '/mp/tag/page', url: '/mp/tag/page',
params: query params: query
...@@ -39,14 +46,14 @@ export const getTagPage = (query) => { ...@@ -39,14 +46,14 @@ export const getTagPage = (query) => {
} }
// 获取公众号标签精简信息列表 // 获取公众号标签精简信息列表
export const getSimpleTags = () => { export const getSimpleTagList = () => {
return request.get({ return request.get({
url: '/mp/tag/list-all-simple' url: '/mp/tag/list-all-simple'
}) })
} }
// 同步公众号标签 // 同步公众号标签
export const syncTag = (accountId) => { export const syncTag = (accountId: number) => {
return request.post({ return request.post({
url: '/mp/tag/sync?accountId=' + accountId url: '/mp/tag/sync?accountId=' + accountId
}) })
......
...@@ -18,7 +18,7 @@ export interface DeptPageReqVO { ...@@ -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' }) return await request.get({ url: '/system/dept/list-all-simple' })
} }
......
...@@ -13,18 +13,12 @@ export interface LoginLogVO { ...@@ -13,18 +13,12 @@ export interface LoginLogVO {
createTime: Date 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 }) 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 }) return request.download({ url: '/system/login-log/export', params })
} }
...@@ -18,18 +18,13 @@ export interface MenuVO { ...@@ -18,18 +18,13 @@ export interface MenuVO {
createTime: Date createTime: Date
} }
export interface MenuPageReqVO {
name?: string
status?: number
}
// 查询菜单(精简)列表 // 查询菜单(精简)列表
export const listSimpleMenusApi = () => { export const getSimpleMenusList = () => {
return request.get({ url: '/system/menu/list-all-simple' }) 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 }) return request.get({ url: '/system/menu/list', params })
} }
...@@ -39,16 +34,16 @@ export const getMenuApi = (id: number) => { ...@@ -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 }) 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 }) 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 }) return request.delete({ url: '/system/menu/delete?id=' + id })
} }
...@@ -37,6 +37,6 @@ export const listUserRolesApi = async (userId: number) => { ...@@ -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 }) return await request.post({ url: '/system/permission/assign-user-role', data })
} }
...@@ -10,49 +10,37 @@ export interface PostVO { ...@@ -10,49 +10,37 @@ export interface PostVO {
createTime?: Date 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 }) 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' }) 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 }) 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 }) 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 }) 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 }) 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 }) return await request.download({ url: '/system/post/export', params })
} }
...@@ -10,49 +10,49 @@ export interface RoleVO { ...@@ -10,49 +10,49 @@ export interface RoleVO {
createTime: Date createTime: Date
} }
export interface RolePageReqVO extends PageParam {
name?: string
code?: string
status?: number
createTime?: Date[]
}
export interface UpdateStatusReqVO { export interface UpdateStatusReqVO {
id: number id: number
status: number status: number
} }
// 查询角色列表 // 查询角色列表
export const getRolePageApi = async (params: RolePageReqVO) => { export const getRolePage = async (params: PageParam) => {
return await request.get({ url: '/system/role/page', params }) 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' }) 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 }) 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 }) 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 }) 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 }) 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 }) return await request.delete({ url: '/system/role/delete?id=' + id })
} }
// 导出角色
export const exportRole = (params) => {
return request.download({
url: '/system/role/export-excel',
params
})
}
import request from '@/config/axios' import request from '@/config/axios'
import qs from 'qs'
export interface SensitiveWordVO { export interface SensitiveWordVO {
id: number id: number
...@@ -9,56 +10,49 @@ export interface SensitiveWordVO { ...@@ -9,56 +10,49 @@ export interface SensitiveWordVO {
createTime: Date createTime: Date
} }
export interface SensitiveWordPageReqVO extends PageParam { export interface SensitiveWordTestReqVO {
name?: string text: string
tag?: string tag: string[]
status?: number
createTime?: Date[]
}
export interface SensitiveWordExportReqVO {
name?: string
tag?: string
status?: number
createTime?: Date[]
} }
// 查询敏感词列表 // 查询敏感词列表
export const getSensitiveWordPageApi = (params: SensitiveWordPageReqVO) => { export const getSensitiveWordPage = (params: PageParam) => {
return request.get({ url: '/system/sensitive-word/page', params }) 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 }) 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 }) 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 }) 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 }) 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 }) 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' }) return request.get({ url: '/system/sensitive-word/get-tags' })
} }
// 获得文本所包含的不合法的敏感词数组 // 获得文本所包含的不合法的敏感词数组
export const validateTextApi = (id: number) => { export const validateText = (query: SensitiveWordTestReqVO) => {
return request.get({ url: '/system/sensitive-word/validate-text?' + id }) return request.get({
url: '/system/sensitive-word/validate-text?' + qs.stringify(query, { arrayFormat: 'repeat' })
})
} }
...@@ -12,39 +12,32 @@ export interface SmsChannelVO { ...@@ -12,39 +12,32 @@ export interface SmsChannelVO {
createTime: Date 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 }) 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' }) 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 }) 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 }) 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 }) 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 }) return request.delete({ url: '/system/sms-channel/delete?id=' + id })
} }
import request from '@/config/axios' import request from '@/config/axios'
export interface SmsLogVO { export interface SmsLogVO {
id: number id: number | null
channelId: number channelId: number | null
channelCode: string channelCode: string
templateId: number templateId: number | null
templateCode: string templateCode: string
templateType: number templateType: number | null
templateContent: string templateContent: string
templateParams: Map<string, object> templateParams: Map<string, object> | null
apiTemplateId: string
mobile: string mobile: string
userId: number userId: number | null
userType: number userType: number | null
sendStatus: number sendStatus: number | null
sendTime: Date sendTime: Date | null
sendCode: number sendCode: number | null
sendMsg: string sendMsg: string
apiSendCode: string apiSendCode: string
apiSendMsg: string apiSendMsg: string
apiRequestId: string apiRequestId: string
apiSerialNo: string apiSerialNo: string
receiveStatus: number receiveStatus: number | null
receiveTime: Date receiveTime: Date | null
apiReceiveCode: string apiReceiveCode: string
apiReceiveMsg: string apiReceiveMsg: string
createTime: Date createTime: Date | null
}
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[]
} }
// 查询短信日志列表 // 查询短信日志列表
export const getSmsLogPageApi = (params: SmsLogPageReqVO) => { export const getSmsLogPage = (params: PageParam) => {
return request.get({ url: '/system/sms-log/page', params }) 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 }) return request.download({ url: '/system/sms-log/export-excel', params })
} }
import request from '@/config/axios' import request from '@/config/axios'
export interface SmsTemplateVO { export interface SmsTemplateVO {
id: number id: number | null
type: number type: number | null
status: number status: number
code: string code: string
name: string name: string
content: string content: string
remark: string remark: string
apiTemplateId: string apiTemplateId: string
channelId: number channelId: number | null
channelCode: string channelCode?: string
params: string[] params?: string[]
createTime: Date createTime?: Date
} }
export interface SendSmsReqVO { export interface SendSmsReqVO {
...@@ -21,60 +21,40 @@ export interface SendSmsReqVO { ...@@ -21,60 +21,40 @@ export interface SendSmsReqVO {
templateParams: Map<String, Object> 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 }) 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 }) 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 }) 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 }) 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 }) 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({ return request.download({
url: '/system/sms-template/export-excel', url: '/system/sms-template/export-excel',
params params
}) })
} }
// 发送短信
export const sendSms = (data: SendSmsReqVO) => {
return request.post({ url: '/system/sms-template/send-sms', data })
}
...@@ -32,31 +32,31 @@ export interface TenantExportReqVO { ...@@ -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 }) 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 }) 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 }) 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 }) 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 }) 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 }) return request.download({ url: '/system/tenant/export-excel', params })
} }
...@@ -43,12 +43,12 @@ export const getUserApi = (id: number) => { ...@@ -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 }) 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 }) return request.put({ url: '/system/user/update', data })
} }
...@@ -86,6 +86,6 @@ export const updateUserStatusApi = (id: number, status: number) => { ...@@ -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' }) return request.get({ url: '/system/user/list-all-simple' })
} }
...@@ -76,7 +76,7 @@ const toggleClick = () => { ...@@ -76,7 +76,7 @@ const toggleClick = () => {
v-if="title" v-if="title"
:class="[ :class="[
`${prefixCls}-header`, `${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" @click="toggleClick"
> >
......
...@@ -34,7 +34,7 @@ export default defineComponent({ ...@@ -34,7 +34,7 @@ export default defineComponent({
return null return null
} }
// 解决自定义字典标签值为零时标签不渲染的问题 // 解决自定义字典标签值为零时标签不渲染的问题
if (props.value === undefined) { if (props.value === undefined || props.value === null) {
return null return null
} }
getDictObj(props.type, props.value.toString()) getDictObj(props.type, props.value.toString())
......
...@@ -178,7 +178,7 @@ defineExpose({ ...@@ -178,7 +178,7 @@ defineExpose({
</script> </script>
<template> <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 <Toolbar
:editor="editorRef" :editor="editorRef"
......
...@@ -128,7 +128,7 @@ const getColumnsConfig = (options: XTableProps) => { ...@@ -128,7 +128,7 @@ const getColumnsConfig = (options: XTableProps) => {
proxyForm = true proxyForm = true
options.formConfig = { options.formConfig = {
enabled: true, enabled: true,
titleWidth: 180, titleWidth: 110,
titleAlign: 'right', titleAlign: 'right',
items: allSchemas.searchSchema items: allSchemas.searchSchema
} }
......
...@@ -37,7 +37,7 @@ export default defineComponent({ ...@@ -37,7 +37,7 @@ export default defineComponent({
}) })
const getBreadcrumb = () => { const getBreadcrumb = () => {
const currentPath = currentRoute.value.path const currentPath = currentRoute.value.matched.slice(-1)[0].path
levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => { levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
return node.path === currentPath return node.path === currentPath
...@@ -47,7 +47,7 @@ export default defineComponent({ ...@@ -47,7 +47,7 @@ export default defineComponent({
const renderBreadcrumb = () => { const renderBreadcrumb = () => {
const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList)) const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
return breadcrumbList.map((v) => { return breadcrumbList.map((v) => {
const disabled = v.redirect === 'noredirect' const disabled = !v.redirect || v.redirect === 'noredirect'
const meta = v.meta as RouteMeta const meta = v.meta as RouteMeta
return ( return (
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}> <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
......
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import { parseTime } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message' import * as NotifyMessageApi from '@/api/system/notify/message'
const { push } = useRouter() const { push } = useRouter()
...@@ -57,7 +57,7 @@ onMounted(() => { ...@@ -57,7 +57,7 @@ onMounted(() => {
{{ item.templateNickname }}{{ item.templateContent }} {{ item.templateNickname }}{{ item.templateContent }}
</span> </span>
<span class="message-date"> <span class="message-date">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }} {{ parseTime(item.createTime) }}
</span> </span>
</div> </div>
</div> </div>
......
...@@ -66,9 +66,9 @@ const toDocument = () => { ...@@ -66,9 +66,9 @@ const toDocument = () => {
<Icon icon="ep:menu" /> <Icon icon="ep:menu" />
<div @click="toDocument">{{ t('common.document') }}</div> <div @click="toDocument">{{ t('common.document') }}</div>
</ElDropdownItem> </ElDropdownItem>
<ElDropdownItem divided> <ElDropdownItem divided @click="loginOut">
<Icon icon="ep:switch-button" /> <Icon icon="ep:switch-button" />
<div @click="loginOut">{{ t('common.loginOut') }}</div> <div>{{ t('common.loginOut') }}</div>
</ElDropdownItem> </ElDropdownItem>
</ElDropdownMenu> </ElDropdownMenu>
</template> </template>
......
...@@ -225,26 +225,26 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -225,26 +225,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [ children: [
{ {
path: '/manager/form/edit', path: '/manager/form/edit',
component: () => import('@/views/bpm/form/formEditor.vue'), component: () => import('@/views/bpm/form/editor/index.vue'),
name: 'bpmFormEditor', name: 'bpmFormEditor',
meta: { meta: {
noCache: true, noCache: true,
hidden: true, hidden: true,
canTo: true, canTo: true,
title: '设计流程表单', title: '设计流程表单',
activeMenu: 'bpm/manager/form/formEditor' activeMenu: '/bpm/manager/form'
} }
}, },
{ {
path: '/manager/model/edit', path: '/manager/model/edit',
component: () => import('@/views/bpm/model/modelEditor.vue'), component: () => import('@/views/bpm/model/editor/index.vue'),
name: 'modelEditor', name: 'modelEditor',
meta: { meta: {
noCache: true, noCache: true,
hidden: true, hidden: true,
canTo: true, canTo: true,
title: '设计流程', title: '设计流程',
activeMenu: 'bpm/manager/model/design' activeMenu: '/bpm/manager/model'
} }
}, },
{ {
...@@ -256,7 +256,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -256,7 +256,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
canTo: true, canTo: true,
title: '流程定义', title: '流程定义',
activeMenu: 'bpm/definition/index' activeMenu: '/bpm/manager/model'
} }
}, },
{ {
......
...@@ -70,6 +70,23 @@ export const getDictObj = (dictType: string, value: any) => { ...@@ -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 { export enum DICT_TYPE {
USER_TYPE = 'user_type', USER_TYPE = 'user_type',
COMMON_STATUS = 'common_status', COMMON_STATUS = 'common_status',
...@@ -123,5 +140,9 @@ export enum DICT_TYPE { ...@@ -123,5 +140,9 @@ export enum DICT_TYPE {
PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态 PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态
PAY_REFUND_ORDER_STATUS = 'pay_refund_order_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' ...@@ -11,10 +11,65 @@ import dayjs from 'dayjs'
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
* @returns 返回拼接后的时间字符串 * @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) 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 当前传入的日期值 * @param dateTime 当前传入的日期值
...@@ -139,5 +194,5 @@ export const dateFormatter = (row, column, cellValue) => { ...@@ -139,5 +194,5 @@ export const dateFormatter = (row, column, cellValue) => {
if (!cellValue) { if (!cellValue) {
return return
} }
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss') return formatDate(cellValue)
} }
...@@ -34,13 +34,13 @@ ...@@ -34,13 +34,13 @@
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<Icon icon="ep:calendar" class="mr-5px" />{{ t('profile.user.createTime') }} <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> </li>
</ul> </ul>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import { parseTime } from '@/utils/formatTime'
import UserAvatar from './UserAvatar.vue' import UserAvatar from './UserAvatar.vue'
import { getUserProfileApi, ProfileVO } from '@/api/system/user/profile' import { getUserProfileApi, ProfileVO } from '@/api/system/user/profile'
......
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)
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <el-table v-loading="loading" :data="list">
<XTable @register="registerTable"> <el-table-column label="定义编号" align="center" prop="id" width="400" />
<!-- 流程名称 --> <el-table-column label="流程名称" align="center" prop="name" width="200">
<template #name_default="{ row }"> <template #default="scope">
<XTextButton :title="row.name" @click="handleBpmnDetail(row.id)" /> <el-button type="text" @click="handleBpmnDetail(scope.row)">
</template> <span>{{ scope.row.name }}</span>
<!-- 流程分类 --> </el-button>
<template #category_default="{ row }"> </template>
<DictTag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="Number(row?.category)" /> </el-table-column>
</template> <el-table-column label="定义分类" align="center" prop="category" width="100">
<!-- 表单信息 --> <template #default="scope">
<template #formId_default="{ row }"> <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
<XTextButton </template>
v-if="row.formType === 10" </el-table-column>
:title="row.formName" <el-table-column label="表单信息" align="center" prop="formType" width="200">
@click="handleFormDetail(row)" <template #default="scope">
/> <el-button
<XTextButton v-else :title="row.formCustomCreatePath" @click="handleFormDetail(row)" /> v-if="scope.row.formType === 10"
</template> type="text"
<!-- 流程版本 --> @click="handleFormDetail(scope.row)"
<template #version_default="{ row }"> >
<el-tag>v{{ row.version }}</el-tag> <span>{{ scope.row.formName }}</span>
</template> </el-button>
<!-- 激活状态 --> <el-button v-else type="text" @click="handleFormDetail(scope.row)">
<template #suspensionState_default="{ row }"> <span>{{ scope.row.formCustomCreatePath }}</span>
<el-tag type="success" v-if="row.suspensionState === 1">激活</el-tag> </el-button>
<el-tag type="warning" v-if="row.suspensionState === 2">挂起</el-tag> </template>
</template> </el-table-column>
<!-- 操作 --> <el-table-column label="流程版本" align="center" prop="processDefinition.version" width="80">
<template #actionbtns_default="{ row }"> <template #default="scope">
<XTextButton <el-tag v-if="scope.row">v{{ scope.row.version }}</el-tag>
preIcon="ep:user" <el-tag type="warning" v-else>未部署</el-tag>
title="分配规则" </template>
v-hasPermi="['bpm:task-assign-rule:query']" </el-table-column>
@click="handleAssignRule(row)" <el-table-column label="状态" align="center" prop="version" width="80">
/> <template #default="scope">
</template> <el-tag type="success" v-if="scope.row.suspensionState === 1">激活</el-tag>
</XTable> <el-tag type="warning" v-if="scope.row.suspensionState === 2">挂起</el-tag>
</template>
<!-- 表单详情的弹窗 --> </el-table-column>
<XModal v-model="formDetailVisible" width="800" title="表单详情" :show-footer="false"> <el-table-column
<form-create label="部署时间"
:rule="formDetailPreview.rule" align="center"
:option="formDetailPreview.option" prop="deploymentTime"
v-if="formDetailVisible" width="180"
:formatter="dateFormatter"
/> />
</XModal> <el-table-column
<!-- 流程模型图的预览 --> label="定义描述"
<XModal title="流程图" v-model="showBpmnOpen" width="80%" height="90%"> align="center"
<my-process-viewer prop="description"
key="designer" width="300"
v-model="bpmnXML" show-overflow-tooltip
:value="bpmnXML"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/> />
</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> </ContentWrap>
<!-- 弹窗:表单详情 -->
<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> </template>
<script setup lang="ts">
// 业务相关的 import <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 * as DefinitionApi from '@/api/bpm/definition'
// import * as ModelApi from '@/api/bpm/model'
import { allSchemas } from './definition.data'
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import { DICT_TYPE } from '@/utils/dict' const { push } = useRouter() // 路由
const bpmnXML = ref(null)
const showBpmnOpen = ref(false)
const bpmnControlForm = ref({
prefix: 'flowable'
})
// const message = useMessage() // 消息弹窗
const router = useRouter() // 路由
const { query } = useRoute() // 查询参数 const { query } = useRoute() // 查询参数
// ========== 列表相关 ========== const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
key: query.key 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 formDetailVisible = ref(false)
const formDetailPreview = ref({ const formDetailPreview = ref({
rule: [], rule: [],
...@@ -99,32 +145,25 @@ const handleFormDetail = async (row) => { ...@@ -99,32 +145,25 @@ const handleFormDetail = async (row) => {
// 弹窗打开 // 弹窗打开
formDetailVisible.value = true formDetailVisible.value = true
} else { } else {
await router.push({ await push({
path: row.formCustomCreatePath path: row.formCustomCreatePath
}) })
} }
} }
// 流程图的详情按钮操作 /** 流程图的详情按钮操作 */
const handleBpmnDetail = (row) => { const bpmnDetailVisible = ref(false)
// TODO 芋艿:流程组件开发中 const bpmnXML = ref(null)
console.log(row) const bpmnControlForm = ref({
DefinitionApi.getProcessDefinitionBpmnXMLApi(row).then((response) => { prefix: 'flowable'
console.log(response, 'response') })
bpmnXML.value = response const handleBpmnDetail = async (row) => {
// 弹窗打开 bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
showBpmnOpen.value = true bpmnDetailVisible.value = true
})
// message.success('流程组件开发中,预计 2 月底完成')
} }
// 点击任务分配按钮 /** 初始化 **/
const handleAssignRule = (row) => { onMounted(() => {
router.push({ getList()
name: 'BpmTaskAssignRuleList', })
query: {
processDefinitionId: row.id
}
})
}
</script> </script>
<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>
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)
<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>
<template> <template>
<ContentWrap> <content-wrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<!-- 操作:新增 --> class="-mb-15px"
<template #toolbar_buttons> :model="queryParams"
<XButton ref="queryFormRef"
type="primary" :inline="true"
preIcon="ep:zoom-in" label-width="68px"
:title="t('action.add')" >
v-hasPermi="['system:post:create']" <el-form-item label="表单名" prop="name">
@click="handleCreate()" <el-input
v-model="queryParams.name"
placeholder="请输入表单名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</template> </el-form-item>
<template #actionbtns_default="{ row }"> <el-form-item>
<!-- 操作:修改 --> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<XTextButton <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
preIcon="ep:edit" <el-button type="primary" @click="openForm()" v-hasPermi="['bpm:form:create']">
:title="t('action.edit')" <Icon icon="ep:plus" class="mr-5px" /> 新增
v-hasPermi="['bpm:form:update']" </el-button>
@click="handleUpdate(row.id)" </el-form-item>
/> </el-form>
<!-- 操作:详情 --> </content-wrap>
<XTextButton
preIcon="ep:view" <!-- 列表 -->
:title="t('action.detail')" <content-wrap>
v-hasPermi="['bpm:form:query']" <el-table v-loading="loading" :data="list">
@click="handleDetail(row.id)" <el-table-column label="编号" align="center" prop="id" />
/> <el-table-column label="表单名" align="center" prop="name" />
<!-- 操作:删除 --> <el-table-column label="状态" align="center" prop="status">
<XTextButton <template #default="scope">
preIcon="ep:delete" <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
:title="t('action.del')" </template>
v-hasPermi="['bpm:form:delete']" </el-table-column>
@click="deleteData(row.id)" <el-table-column label="备注" align="center" prop="remark" />
/> <el-table-column
</template> label="创建时间"
</XTable> align="center"
<!-- 表单详情的弹窗 --> prop="createTime"
<XModal v-model="detailOpen" width="800" title="表单详情"> :formatter="dateFormatter"
<form-create :rule="detailPreview.rule" :option="detailPreview.option" v-if="detailOpen" /> />
</XModal> <el-table-column label="操作" align="center">
</ContentWrap> <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> </template>
<script setup lang="ts" name="BpmForm"> <script setup lang="ts" name="Form">
// 业务相关的 import import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as FormApi from '@/api/bpm/form' import * as FormApi from '@/api/bpm/form'
import { allSchemas } from './form.data'
// 表单详情相关的变量和 import
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由 const { push } = useRouter() // 路由
// 列表相关的变量 const loading = ref(true) // 列表的加载中
const [registerTable, { deleteData }] = useXTable({ const total = ref(0) // 列表的总页数
allSchemas: allSchemas, const list = ref([]) // 列表的数据
getListApi: FormApi.getFormPageApi, const queryParams = reactive({
deleteApi: FormApi.deleteFormApi pageNo: 1,
pageSize: 10,
name: null
}) })
const queryFormRef = ref() // 搜索的表单
// 新增操作 /** 查询参数列表 */
const handleCreate = () => { const getList = async () => {
push({ loading.value = true
name: 'bpmFormEditor' try {
}) const data = await FormApi.getFormPage(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 handleUpdate = async (rowId: number) => { const openForm = (id?: number) => {
await push({ push({
name: 'bpmFormEditor', name: 'bpmFormEditor',
query: { query: {
id: rowId id
} }
}) })
} }
// 详情操作 /** 删除按钮操作 */
const detailOpen = ref(false) const handleDelete = async (id: number) => {
const detailPreview = ref({ try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FormApi.deleteForm(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 详情操作 */
const detailVisible = ref(false)
const detailData = ref({
rule: [], rule: [],
option: {} option: {}
}) })
const handleDetail = async (rowId: number) => { const openDetail = async (rowId: number) => {
// 设置表单 // 设置表单
const data = await FormApi.getFormApi(rowId) const data = await FormApi.getForm(rowId)
setConfAndFields2(detailPreview, data.conf, data.fields) setConfAndFields2(detailData, data.conf, data.fields)
// 弹窗打开 // 弹窗打开
detailOpen.value = true detailVisible.value = true
} }
/** 初始化 **/
onMounted(() => {
getList()
})
</script> </script>
<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>
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)
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<template #toolbar_buttons> class="-mb-15px"
<!-- 操作:新增 --> :model="queryParams"
<XButton ref="queryFormRef"
type="primary" :inline="true"
preIcon="ep:zoom-in" label-width="68px"
:title="t('action.add')" >
v-hasPermi="['bpm:user-group:create']" <el-form-item label="组名" prop="name">
@click="handleCreate()" <el-input
/> v-model="queryParams.name"
</template> placeholder="请输入组名"
<template #memberUserIds_default="{ row }"> clearable
<span v-for="userId in row.memberUserIds" :key="userId"> @keyup.enter="handleQuery"
{{ getUserNickname(userId) }} &nbsp; class="!w-240px"
</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)"
/> />
<!-- 操作:删除 --> </el-form-item>
<XTextButton <el-form-item label="状态" prop="status">
preIcon="ep:delete" <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
:title="t('action.del')" <el-option
v-hasPermi="['bpm:user-group:delete']" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@click="deleteData(row.id)" :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"
/> />
</template> </el-form-item>
</XTable> <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="['bpm:user-group:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap> </ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle" :mask-closable="false"> <!-- 列表 -->
<!-- 对话框(添加 / 修改) --> <ContentWrap>
<Form <el-table v-loading="loading" :data="list">
v-if="['create', 'update'].includes(actionType)" <el-table-column label="编号" align="center" prop="id" />
:schema="allSchemas.formSchema" <el-table-column label="组名" align="center" prop="name" />
:rules="rules" <el-table-column label="描述" align="center" prop="description" />
ref="formRef" <el-table-column label="成员" align="center">
> <template #default="scope">
<template #memberUserIds="form"> <span v-for="userId in scope.row.memberUserIds" :key="userId" class="pr-5px">
<el-select v-model="form.memberUserIds" multiple> {{ userList.find((user) => user.id === userId)?.nickname }}
<el-option v-for="item in users" :key="item.id" :label="item.nickname" :value="item.id" /> </span>
</el-select> </template>
</template> </el-table-column>
</Form> <el-table-column label="状态" align="center" prop="status">
<!-- 对话框(详情) --> <template #default="scope">
<Descriptions <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
v-if="actionType === 'detail'" </template>
:schema="allSchemas.detailSchema" </el-table-column>
:data="detailData" <el-table-column
> label="创建时间"
<template #memberUserIds="{ row }"> align="center"
<span v-for="userId in row.memberUserIds" :key="userId"> prop="createTime"
{{ getUserNickname(userId) }} &nbsp; :formatter="dateFormatter"
</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm"
/> />
<!-- 按钮:关闭 --> <el-table-column label="操作" align="center">
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> <template #default="scope">
</template> <el-button
</XModal> 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> </template>
<script setup lang="ts"> <script setup lang="ts" name="UserGroup">
// 业务相关的 import import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as UserGroupApi from '@/api/bpm/userGroup' import * as UserGroupApi from '@/api/bpm/userGroup'
import { getListSimpleUsersApi, UserVO } from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { allSchemas, rules } from './group.data' import UserGroupForm from './UserGroupForm.vue'
import { FormExpose } from '@/components/Form'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
// 列表相关的变量 const { t } = useI18n() // 国际化
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas, const loading = ref(true) // 列表的加载中
getListApi: UserGroupApi.getUserGroupPageApi, const total = ref(0) // 列表的总页数
deleteApi: UserGroupApi.deleteUserGroupApi const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
status: null,
createTime: []
}) })
// 用户列表 const queryFormRef = ref() // 搜索的表单
const users = ref<UserVO[]>([]) const userList = ref([]) // 用户列表
const getUserNickname = (userId) => { /** 查询参数列表 */
for (const user of users.value) { const getList = async () => {
if (user.id === userId) { loading.value = true
return user.nickname 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 handleCreate = () => { const handleQuery = () => {
setDialogTile('create') queryParams.pageNo = 1
getList()
} }
// 修改操作 /** 重置按钮操作 */
const handleUpdate = async (rowId: number) => { const resetQuery = () => {
setDialogTile('update') queryFormRef.value.resetFields()
// 设置数据 handleQuery()
const res = await UserGroupApi.getUserGroupApi(rowId)
unref(formRef)?.setValues(res)
} }
// 详情操作 /** 添加/修改操作 */
const handleDetail = async (rowId: number) => { const formRef = ref()
setDialogTile('detail') const openForm = (type: string, id?: number) => {
detailData.value = await UserGroupApi.getUserGroupApi(rowId) formRef.value.open(type, id)
} }
// 提交按钮 /** 删除按钮操作 */
const submitForm = async () => { const handleDelete = async (id: number) => {
const elForm = unref(formRef)?.getElFormRef() try {
if (!elForm) return // 删除的二次确认
elForm.validate(async (valid) => { await message.delConfirm()
if (valid) { // 发起删除
actionLoading.value = true await UserGroupApi.deleteUserGroup(id)
// 提交请求 message.success(t('common.delSuccess'))
try { // 刷新列表
const data = unref(formRef)?.formModel as UserGroupApi.UserGroupVO await getList()
if (actionType.value === 'create') { } catch {}
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(() => { onMounted(async () => {
getListSimpleUsersApi().then((data) => { await getList()
users.value = data // 加载用户列表
}) userList.value = await UserApi.getSimpleUserList()
}) })
</script> </script>
<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>
<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>
<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>
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<template #toolbar_buttons> class="-mb-15px"
<!-- 操作:新增 --> :model="queryParams"
<XButton ref="queryFormRef"
type="primary" :inline="true"
preIcon="ep:zoom-in" label-width="68px"
title="新建流程" >
v-hasPermi="['bpm:model:create']" <el-form-item label="流程标识" prop="key">
@click="handleCreate" <el-input
/> v-model="queryParams.key"
<!-- 操作:导入 --> placeholder="请输入流程标识"
<XButton clearable
type="warning" @keyup.enter="handleQuery"
preIcon="ep:upload" class="!w-240px"
:title="'导入流程'"
@click="handleImport"
style="margin-left: 10px"
/>
</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)"
/> />
<!-- 操作:删除 --> </el-form-item>
<XTextButton <el-form-item label="流程名称" prop="name">
preIcon="ep:delete" <el-input
:title="t('action.del')" v-model="queryParams.name"
v-hasPermi="['bpm:model:delete']" placeholder="请输入流程名称"
@click="handleDelete(row.id)" clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</template> </el-form-item>
</XTable> <el-form-item label="流程分类" prop="category">
<el-select
<!-- 对话框(添加 / 修改流程) --> v-model="queryParams.category"
<XModal v-model="dialogVisible" :title="dialogTitle" width="600"> placeholder="请选择流程分类"
<el-form clearable
:loading="dialogLoading" class="!w-240px"
el-form >
ref="saveFormRef" <el-option
:model="saveForm" v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
:rules="rules" :key="dict.value"
label-width="110px" :label="dict.label"
> :value="dict.value"
<el-form-item label="流程标识" prop="key">
<el-input
v-model="saveForm.key"
placeholder="请输入流标标识"
style="width: 330px"
:disabled="!!saveForm.id"
/> />
<el-tooltip </el-select>
v-if="!saveForm.id" </el-form-item>
class="item" <el-form-item>
effect="light" <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
content="新建后,流程标识不可修改!" <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
placement="top" <el-button
> type="primary"
<i style="padding-left: 5px" class="el-icon-question"></i> plain
</el-tooltip> @click="openForm('create')"
<el-tooltip v-hasPermi="['bpm:model:create']"
v-else >
class="item" <Icon icon="ep:plus" class="mr-5px" /> 新建流程
effect="light" </el-button>
content="流程标识不可修改!" <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
placement="top" <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)"
> >
<i style="padding-left: 5px" class="el-icon-question"></i> <span>{{ scope.row.formName }}</span>
</el-tooltip> </el-button>
</el-form-item> <el-button
<el-form-item label="流程名称" prop="name"> v-else-if="scope.row.formType === 20"
<el-input type="text"
v-model="saveForm.name" @click="handleFormDetail(scope.row)"
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 <span>{{ scope.row.formCustomCreatePath }}</span>
v-for="dict in getDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" </el-button>
:key="dict.value" <label v-else>暂无表单</label>
:label="dict.label" </template>
:value="dict.value" </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)"
/> />
</el-select> </template>
</el-form-item> </el-table-column>
<el-form-item label="流程描述" prop="description"> <el-table-column label="部署时间" align="center" prop="deploymentTime" width="180">
<el-input type="textarea" v-model="saveForm.description" clearable /> <template #default="scope">
</el-form-item> <span v-if="scope.row.processDefinition">
<div v-if="saveForm.id"> {{ formatDate(scope.row.processDefinition.deploymentTime) }}
<el-form-item label="表单类型" prop="formType"> </span>
<el-radio-group v-model="saveForm.formType"> </template>
<el-radio </el-table-column>
v-for="dict in getDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" </el-table-column>
:key="parseInt(dict.value)" <el-table-column label="操作" align="center" width="240" fixed="right">
:label="parseInt(dict.value)" <template #default="scope">
> <el-button
{{ dict.label }} link
</el-radio> type="primary"
</el-radio-group> @click="openForm('update', scope.row.id)"
</el-form-item> v-hasPermi="['bpm:model:update']"
<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" </el-button>
placeholder="请输入表单提交路由" <el-button
style="width: 330px" link
/> type="primary"
<el-tooltip @click="handleDesign(scope.row)"
class="item" v-hasPermi="['bpm:model:update']"
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" </el-button>
placeholder="请输入表单查看路由" <el-button
style="width: 330px" link
/> type="primary"
<el-tooltip @click="handleAssignRule(scope.row)"
class="item" v-hasPermi="['bpm:task-assign-rule:query']"
effect="light" >
content="自定义表单的查看路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/view" 分配规则
placement="top" </el-button>
> <el-button
<i style="padding-left: 5px" class="el-icon-question"></i> link
</el-tooltip> type="primary"
</el-form-item> @click="handleDeploy(scope.row)"
</div> v-hasPermi="['bpm:model:deploy']"
</el-form> >
<template #footer> 发布流程
<!-- 按钮:保存 --> </el-button>
<XButton <el-button
type="primary" link
:loading="dialogLoading" type="primary"
@click="submitForm" v-hasPermi="['bpm:process-definition:query']"
:title="t('action.save')" @click="handleDefinitionList(scope.row)"
/> >
<!-- 按钮:关闭 --> 流程定义
<XButton </el-button>
:loading="dialogLoading" <el-button
@click="dialogVisible = false" link
:title="t('dialog.close')" type="danger"
/> @click="handleDelete(scope.row.id)"
</template> v-hasPermi="['bpm:model:delete']"
</XModal> >
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 导入流程 --> <!-- 表单弹窗:添加/修改流程 -->
<XModal v-model="importDialogVisible" width="400" title="导入流程"> <ModelForm ref="formRef" @success="getList" />
<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"
>
<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"> <ModelImportForm ref="importFormRef" @success="getList" />
<form-create
:rule="formDetailPreview.rule"
:option="formDetailPreview.option"
v-if="formDetailVisible"
/>
</XModal>
<!-- 流程模型图的预览 --> <!-- 弹窗:表单详情 -->
<XModal title="流程图" v-model="showBpmnOpen" width="80%" height="90%"> <Dialog title="表单详情" v-model="formDetailVisible" width="800">
<my-process-viewer <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
key="designer" </Dialog>
v-model="bpmnXML"
:value="bpmnXML"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</XModal>
</ContentWrap>
</template>
<script setup lang="ts"> <!-- 弹窗:流程模型图的预览 -->
// 全局相关的 import <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
import { DICT_TYPE, getDictOptions } from '@/utils/dict' <my-process-viewer
import { FormInstance, UploadInstance } from 'element-plus' key="designer"
v-model="bpmnXML"
:value="bpmnXML"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</Dialog>
</template>
// 业务相关的 import <script setup lang="ts" name="Form">
import { getAccessToken, getTenantId } from '@/utils/auth' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as FormApi from '@/api/bpm/form' import { dateFormatter, formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model' 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' import { setConfAndFields2 } from '@/utils/formCreate'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const router = useRouter() // 路由 const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const showBpmnOpen = ref(false) const loading = ref(true) // 列表的加载中
const bpmnXML = ref(null) const total = ref(0) // 列表的总页数
const bpmnControlForm = ref({ const list = ref([]) // 列表的数据
prefix: 'flowable' const queryParams = reactive({
}) pageNo: 1,
// ========== 列表相关 ========== pageSize: 10,
const [registerTable, { reload }] = useXTable({ key: undefined,
allSchemas: allSchemas, name: undefined,
getListApi: ModelApi.getModelPageApi category: undefined
}) })
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) => { const handleDesign = (row) => {
console.log(row, '设计流程') push({
router.push({
name: 'modelEditor', name: 'modelEditor',
query: { query: {
modelId: row.id modelId: row.id
...@@ -355,9 +326,32 @@ const handleDesign = (row) => { ...@@ -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) => { const handleDefinitionList = (row) => {
router.push({ push({
name: 'BpmProcessDefinitionList', name: 'BpmProcessDefinitionList',
query: { query: {
key: row.key key: row.key
...@@ -365,7 +359,7 @@ const handleDefinitionList = (row) => { ...@@ -365,7 +359,7 @@ const handleDefinitionList = (row) => {
}) })
} }
// 流程表单的详情按钮操作 /** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false) const formDetailVisible = ref(false)
const formDetailPreview = ref({ const formDetailPreview = ref({
rule: [], rule: [],
...@@ -374,222 +368,31 @@ const formDetailPreview = ref({ ...@@ -374,222 +368,31 @@ const formDetailPreview = ref({
const handleFormDetail = async (row) => { const handleFormDetail = async (row) => {
if (row.formType == 10) { if (row.formType == 10) {
// 设置表单 // 设置表单
const data = await FormApi.getFormApi(row.formId) const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields) setConfAndFields2(formDetailPreview, data.conf, data.fields)
// 弹窗打开 // 弹窗打开
formDetailVisible.value = true formDetailVisible.value = true
} else { } else {
await router.push({ await push({
path: row.formCustomCreatePath path: row.formCustomCreatePath
}) })
} }
} }
// 流程图的详情按钮操作 /** 流程图的详情按钮操作 */
const handleBpmnDetail = (row) => { const bpmnDetailVisible = ref(false)
// TODO 芋艿:流程组件开发中 const bpmnXML = ref(null)
console.log(row) const bpmnControlForm = ref({
ModelApi.getModelApi(row).then((response) => { prefix: 'flowable'
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 handleBpmnDetail = async (row) => {
// 导入流程弹窗显示 const data = await ModelApi.getModel(row.id)
const handleImport = () => { bpmnXML.value = data.bpmnXml || ''
importDialogVisible.value = true bpmnDetailVisible.value = true
}
// 文件数超出提示
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
// 上传错误提示
const excelUploadError = (): void => {
message.error('导入流程失败,请您重新上传!')
} }
// 提交文件上传 /** 初始化 **/
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(() => { onMounted(() => {
// 获得流程表单的下拉框的数据 getList()
FormApi.getSimpleFormsApi().then((data) => {
forms.value = data
})
}) })
</script> </script>
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)
<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({ ...@@ -72,7 +72,7 @@ const [registerTable] = useXTable({
params: { params: {
suspensionState: 1 suspensionState: 1
}, },
getListApi: DefinitionApi.getProcessDefinitionListApi, getListApi: DefinitionApi.getProcessDefinitionList,
isList: true isList: true
}) })
...@@ -99,7 +99,7 @@ const handleSelect = async (row) => { ...@@ -99,7 +99,7 @@ const handleSelect = async (row) => {
setConfAndFields2(detailForm, row.formConf, row.formFields) setConfAndFields2(detailForm, row.formConf, row.formFields)
// 加载流程图 // 加载流程图
DefinitionApi.getProcessDefinitionBpmnXMLApi(row.id).then((response) => { DefinitionApi.getProcessDefinitionBpmnXML(row.id).then((response) => {
bpmnXML.value = response bpmnXML.value = response
}) })
// 情况二:业务表单 // 情况二:业务表单
......
...@@ -112,13 +112,13 @@ ...@@ -112,13 +112,13 @@
</label> </label>
<label style="font-weight: normal" v-if="item.createTime">创建时间:</label> <label style="font-weight: normal" v-if="item.createTime">创建时间:</label>
<label style="color: #8a909c; font-weight: normal"> <label style="color: #8a909c; font-weight: normal">
{{ dayjs(item?.createTime).format('YYYY-MM-DD HH:mm:ss') }} {{ parseTime(item?.createTime) }}
</label> </label>
<label v-if="item.endTime" style="margin-left: 30px; font-weight: normal"> <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
审批时间: 审批时间:
</label> </label>
<label v-if="item.endTime" style="color: #8a909c; font-weight: normal"> <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>
<label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal"> <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
耗时: 耗时:
...@@ -192,7 +192,7 @@ ...@@ -192,7 +192,7 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import { parseTime } from '@/utils/formatTime'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition' import * as DefinitionApi from '@/api/bpm/definition'
...@@ -378,7 +378,7 @@ onMounted(() => { ...@@ -378,7 +378,7 @@ onMounted(() => {
// 加载详情 // 加载详情
getDetail() getDetail()
// 加载用户的列表 // 加载用户的列表
UserApi.getListSimpleUsersApi().then((data) => { UserApi.getSimpleUserList().then((data) => {
userOptions.value.push(...data) userOptions.value.push(...data)
}) })
}) })
...@@ -411,7 +411,7 @@ const getDetail = () => { ...@@ -411,7 +411,7 @@ const getDetail = () => {
} }
// 加载流程图 // 加载流程图
DefinitionApi.getProcessDefinitionBpmnXMLApi(processDefinition.id).then((data) => { DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id).then((data) => {
bpmnXML.value = data bpmnXML.value = data
}) })
......
<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>
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <el-table v-loading="loading" :data="list">
<XTable @register="registerTable" ref="xGrid"> <el-table-column label="任务名" align="center" prop="taskDefinitionName" />
<template #options_default="{ row }"> <el-table-column label="任务标识" align="center" prop="taskDefinitionKey" />
<span :key="option" v-for="option in row.options"> <el-table-column label="规则类型" align="center" prop="type">
<el-tag> <template #default="scope">
{{ getAssignRuleOptionName(row.type, option) }} <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> </el-tag>
&nbsp; </template>
</span> </el-table-column>
</template> <el-table-column v-if="queryParams.modelId" label="操作" align="center">
<!-- 操作 --> <template #default="scope">
<template #actionbtns_default="{ row }" v-if="modelId"> <el-button
<!-- 操作:修改 --> link
<XTextButton type="primary"
preIcon="ep:edit" @click="openForm(scope.row)"
:title="t('action.edit')" v-hasPermi="['bpm:task-assign-rule:update']"
v-hasPermi="['bpm:task-assign-rule:update']" >
@click="handleUpdate(row)" 修改
/> </el-button>
</template> </template>
</XTable> </el-table-column>
</el-table>
<!-- 添加/修改弹窗 -->
<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>
</ContentWrap> </ContentWrap>
<!-- 添加/修改弹窗 -->
<TaskAssignRuleForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="TaskAssignRule"> <script setup lang="ts" name="TaskAssignRule">
// 全局相关的 import import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { FormInstance } from 'element-plus'
// 业务相关的 import
import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule' import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
import { listSimpleRolesApi } from '@/api/system/role' import * as RoleApi from '@/api/system/role'
import { listSimplePostsApi } from '@/api/system/post' import * as DeptApi from '@/api/system/dept'
import { getListSimpleUsersApi } from '@/api/system/user' import * as PostApi from '@/api/system/post'
import { listSimpleUserGroupsApi } from '@/api/bpm/userGroup' import * as UserApi from '@/api/system/user'
import { listSimpleDeptApi } from '@/api/system/dept' import * as UserGroupApi from '@/api/bpm/userGroup'
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import TaskAssignRuleForm from './TaskAssignRuleForm.vue'
import { handleTree, defaultProps } from '@/utils/tree' const { query } = useRoute() // 查询参数
import { allSchemas, rules, idShowActionClick } from './taskAssignRule.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const { query } = useRoute()
const xGrid = ref()
// ========== 列表相关 ==========
const roleOptions = ref() // 角色列表 const loading = ref(true) // 列表的加载中
const deptOptions = ref() // 部门列表 const list = 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 queryParams = reactive({ const queryParams = reactive({
modelId: modelId, modelId: query.modelId, // 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置
processDefinitionId: processDefinitionId processDefinitionId: query.processDefinitionId // 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置
})
const [registerTable, { reload }] = useXTable({
allSchemas: allSchemas,
params: queryParams,
getListApi: TaskAssignRuleApi.getTaskAssignRuleList,
isList: true
}) })
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) => { const getAssignRuleOptionName = (type, option) => {
if (type === 10) { if (type === 10) {
for (const roleOption of roleOptions.value) { for (const roleOption of roleOptions.value) {
...@@ -223,136 +110,24 @@ const getAssignRuleOptionName = (type, option) => { ...@@ -223,136 +110,24 @@ const getAssignRuleOptionName = (type, option) => {
return '未知(' + option + ')' return '未知(' + option + ')'
} }
// ========== 修改相关 ========== /** 添加/修改操作 */
const formRef = ref()
// 修改任务责任表单 const openForm = (row: TaskAssignRuleApi.TaskAssignVO) => {
const actionLoading = ref(false) // 遮罩层 formRef.value.open(queryParams.modelId, row)
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 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 = [] roleOptions.value = await RoleApi.getSimpleRoleList()
listSimpleRolesApi().then((data) => {
roleOptions.value.push(...data)
})
// 获得部门列表 // 获得部门列表
deptOptions.value = [] deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = []
listSimpleDeptApi().then((data) => {
deptOptions.value.push(...data)
deptTreeOptions.value.push(...handleTree(data, 'id'))
})
// 获得岗位列表 // 获得岗位列表
postOptions.value = [] postOptions.value = await PostApi.getSimplePostList()
listSimplePostsApi().then((data) => {
postOptions.value.push(...data)
})
// 获得用户列表 // 获得用户列表
userOptions.value = [] userOptions.value = await UserApi.getSimpleUserList()
getListSimpleUsersApi().then((data) => {
userOptions.value.push(...data)
})
// 获得用户组列表 // 获得用户组列表
userGroupOptions.value = [] userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
listSimpleUserGroupsApi().then((data) => {
userGroupOptions.value.push(...data)
})
if (!isShow) {
setTimeout(() => {
xGrid.value.Ref.hideColumn('actionbtns')
}, 100)
}
}) })
</script> </script>
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)
<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>
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)
<template> <template>
<ContentWrap> <content-wrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<template #duration_default="{ row }"> class="-mb-15px"
<span>{{ row.duration + 'ms' }}</span> :model="queryParams"
</template> ref="queryFormRef"
<template #resultCode_default="{ row }"> :inline="true"
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span> label-width="68px"
</template> >
<template #actionbtns_default="{ row }"> <el-form-item label="用户编号" prop="userId">
<!-- 操作:详情 --> <el-input
<XTextButton v-model="queryParams.userId"
preIcon="ep:view" placeholder="请输入用户编号"
:title="t('action.detail')" clearable
v-hasPermi="['infra:api-access-log:query']" @keyup.enter="handleQuery"
@click="handleDetail(row)" class="!w-240px"
/> />
</template> </el-form-item>
</XTable> <el-form-item label="用户类型" prop="userType">
</ContentWrap> <el-select
<XModal v-model="dialogVisible" :title="dialogTitle"> v-model="queryParams.userType"
<!-- 对话框(详情) --> placeholder="请选择用户类型"
<Descriptions :schema="allSchemas.detailSchema" :data="detailData"> clearable
<template #duration="{ row }"> class="!w-240px"
<span>{{ row.duration + 'ms' }}</span> >
</template> <el-option
<template #resultCode="{ row }"> v-for="dict in getDictOptions(DICT_TYPE.USER_TYPE)"
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span> :key="parseInt(dict.value)"
</template> :label="dict.label"
</Descriptions> :value="parseInt(dict.value)"
<!-- 操作按钮 --> />
<template #footer> </el-select>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" /> </el-form-item>
</template> <el-form-item label="应用名" prop="applicationName">
</XModal> <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> </template>
<script setup lang="ts" name="ApiAccessLog"> <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 * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
import ApiAccessLogDetail from './ApiAccessLogDetail.vue'
const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗
// 列表相关的变量 const loading = ref(true) // 列表的加载中
const [registerTable] = useXTable({ const total = ref(0) // 列表的总页数
allSchemas: allSchemas, const list = ref([]) // 列表的数据
topActionSlots: false, const queryParams = reactive({
getListApi: ApiAccessLogApi.getApiAccessLogPageApi pageNo: 1,
pageSize: 10,
userId: null,
userType: null,
applicationName: null,
requestUrl: null,
duration: null,
resultCode: null,
beginTime: []
}) })
// ========== 详情相关 ========== const queryFormRef = ref() // 搜索的表单
const detailData = ref() // 详情 Ref const exportLoading = ref(false) // 导出的加载中
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题 /** 查询列表 */
const getList = async () => {
// 详情操作 loading.value = true
const handleDetail = (row: ApiAccessLogApi.ApiAccessLogVO) => { try {
// 设置数据 const data = await ApiAccessLogApi.getApiAccessLogPage(queryParams)
detailData.value = row list.value = data.list
dialogTitle.value = t('action.detail') total.value = data.total
dialogVisible.value = true } 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> </script>
<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>
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)
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<!-- 操作:导出 --> class="-mb-15px"
<template #toolbar_buttons> :model="queryParams"
<XButton ref="queryFormRef"
type="warning" :inline="true"
preIcon="ep:download" label-width="68px"
:title="t('action.export')" >
@click="exportList('错误数据.xls')" <el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</template> </el-form-item>
<template #duration_default="{ row }"> <el-form-item label="用户类型" prop="userType">
<span>{{ row.duration + 'ms' }}</span> <el-select
</template> v-model="queryParams.userType"
<template #resultCode_default="{ row }"> placeholder="请选择用户类型"
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span> clearable
</template> class="!w-240px"
<template #actionbtns_default="{ row }"> >
<!-- 操作:详情 --> <el-option
<XTextButton v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
preIcon="ep:view" :key="dict.value"
:title="t('action.detail')" :label="dict.label"
v-hasPermi="['infra:api-access-log:query']" :value="dict.value"
@click="handleDetail(row)" />
</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 </el-form-item>
preIcon="ep:cpu" <el-form-item label="异常时间" prop="exceptionTime">
title="已处理" <el-date-picker
v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" v-model="queryParams.exceptionTime"
v-hasPermi="['infra:api-error-log:update-status']" value-format="yyyy-MM-dd HH:mm:ss"
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.DONE, '已处理')" type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/> />
<XTextButton </el-form-item>
preIcon="ep:mute-notification" <el-form-item label="处理状态" prop="processStatus">
title="已忽略" <el-select
v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" v-model="queryParams.processStatus"
v-hasPermi="['infra:api-error-log:update-status']" placeholder="请选择处理状态"
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.IGNORE, '已忽略')" clearable
/> class="!w-240px"
</template> >
</XTable> <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>
<!-- 列表 -->
<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> </ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) --> <!-- 表单弹窗:详情 -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailData" /> <ApiErrorLogDetail ref="detailRef" />
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template> </template>
<script setup lang="ts" name="ApiErrorLog"> <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 * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
import ApiErrorLogDetail from './ApiErrorLogDetail.vue'
import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants' import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗
const message = useMessage()
// ========== 列表相关 ========== const loading = ref(true) // 列表的加载中
const [registerTable, { reload, exportList }] = useXTable({ const total = ref(0) // 列表的总页数
allSchemas: allSchemas, const list = ref([]) // 列表的数据
getListApi: ApiErrorLogApi.getApiErrorLogPageApi, const queryParams = reactive({
exportListApi: ApiErrorLogApi.exportApiErrorLogApi pageNo: 1,
pageSize: 10,
userId: null,
userType: null,
applicationName: null,
requestUrl: null,
processStatus: null,
exceptionTime: []
}) })
// ========== 详情相关 ========== const queryFormRef = ref() // 搜索的表单
const detailData = ref() // 详情 Ref const exportLoading = ref(false) // 导出的加载中
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题 /** 查询参数列表 */
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 handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => { const handleQuery = () => {
// 设置数据 queryParams.pageNo = 1
detailData.value = row getList()
dialogTitle.value = t('action.detail')
dialogVisible.value = true
} }
// 异常处理操作 /** 重置按钮操作 */
const handleProcessClick = ( const resetQuery = () => {
row: ApiErrorLogApi.ApiErrorLogVO, queryFormRef.value.resetFields()
processSttatus: number, handleQuery()
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 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> </script>
...@@ -3,77 +3,99 @@ ...@@ -3,77 +3,99 @@
<el-row> <el-row>
<el-col> <el-col>
<div class="mb-2 float-right"> <div class="mb-2 float-right">
<el-button size="small" @click="setJson"> 导入JSON</el-button> <el-button size="small" type="primary" @click="showJson">生成 JSON</el-button>
<el-button size="small" @click="setOption"> 导入Options</el-button> <el-button size="small" type="success" @click="showOption">生成O ptions</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="danger" @click="showTemplate">生成组件</el-button> <el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
<!-- <el-button size="small" @click="changeLocale">中英切换</el-button> -->
</div> </div>
</el-col> </el-col>
<!-- 表单设计器 -->
<el-col> <el-col>
<fc-designer ref="designer" height="780px" /> <fc-designer ref="designer" height="780px" />
</el-col> </el-col>
</el-row> </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> </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> </template>
<script setup lang="ts" name="Build"> <script setup lang="ts" name="Build">
import formCreate from '@form-create/element-ui' import formCreate from '@form-create/element-ui'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息
const { t } = useI18n() const designer = ref() // 表单设计器
const message = useMessage() const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const designer = ref() const formType = ref(-1) // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件
const formData = ref('') // 表单数据
const dialogVisible = ref(false)
const dialogTitle = ref('')
const err = ref(false)
const type = ref(-1)
const formValue = ref('')
/** 打开弹窗 */
const openModel = (title: string) => { const openModel = (title: string) => {
dialogVisible.value = true dialogVisible.value = true
dialogTitle.value = title dialogTitle.value = title
} }
const setJson = () => { /** 生成 JSON */
openModel('导入JSON--未实现')
}
const setOption = () => {
openModel('导入Options--未实现')
}
const showJson = () => { const showJson = () => {
openModel('生成JSON') openModel('生成 JSON')
type.value = 0 formType.value = 0
formValue.value = designer.value.getRule() formData.value = designer.value.getRule()
} }
/** 生成 Options */
const showOption = () => { const showOption = () => {
openModel('生成Options') openModel('生成 Options')
type.value = 1 formType.value = 1
formValue.value = designer.value.getOption() formData.value = designer.value.getOption()
} }
/** 生成组件 */
const showTemplate = () => { const showTemplate = () => {
openModel('生成组件') openModel('生成组件')
type.value = 2 formType.value = 2
formValue.value = makeTemplate() 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) => { const copy = async (text: string) => {
...@@ -87,31 +109,4 @@ 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> </script>
...@@ -6,7 +6,7 @@ import { useForm } from '@/hooks/web/useForm' ...@@ -6,7 +6,7 @@ import { useForm } from '@/hooks/web/useForm'
import { FormSchema } from '@/types/form' import { FormSchema } from '@/types/form'
import { CodegenTableVO } from '@/api/infra/codegen/types' import { CodegenTableVO } from '@/api/infra/codegen/types'
import { getIntDictOptions } from '@/utils/dict' import { getIntDictOptions } from '@/utils/dict'
import { listSimpleMenusApi } from '@/api/system/menu' import { getSimpleMenusList } from '@/api/system/menu'
import { handleTree, defaultProps } from '@/utils/tree' import { handleTree, defaultProps } from '@/utils/tree'
import { PropType } from 'vue' import { PropType } from 'vue'
...@@ -21,7 +21,7 @@ const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_T ...@@ -21,7 +21,7 @@ const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_T
const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE) const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
const menuOptions = ref<any>([]) // 树形结构 const menuOptions = ref<any>([]) // 树形结构
const getTree = async () => { const getTree = async () => {
const res = await listSimpleMenusApi() const res = await getSimpleMenusList()
menuOptions.value = handleTree(res) menuOptions.value = handleTree(res)
} }
......
...@@ -41,10 +41,8 @@ ...@@ -41,10 +41,8 @@
<vxe-column field="comment" title="表描述" /> <vxe-column field="comment" title="表描述" />
</vxe-table> </vxe-table>
<template #footer> <template #footer>
<div class="dialog-footer"> <XButton type="primary" :title="t('action.import')" @click="handleImportTable()" />
<XButton type="primary" :title="t('action.import')" @click="handleImportTable()" /> <XButton :title="t('dialog.close')" @click="handleClose()" />
<XButton :title="t('dialog.close')" @click="handleClose()" />
</div>
</template> </template>
</XModal> </XModal>
</template> </template>
...@@ -52,7 +50,7 @@ ...@@ -52,7 +50,7 @@
import { VxeTableInstance } from 'vxe-table' import { VxeTableInstance } from 'vxe-table'
import type { DatabaseTableVO } from '@/api/infra/codegen/types' import type { DatabaseTableVO } from '@/api/infra/codegen/types'
import { getSchemaTableListApi, createCodegenListApi } from '@/api/infra/codegen' 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 { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
...@@ -63,13 +61,13 @@ const dbLoading = ref(true) ...@@ -63,13 +61,13 @@ const dbLoading = ref(true)
const queryParams = reactive({ const queryParams = reactive({
name: undefined, name: undefined,
comment: undefined, comment: undefined,
dataSourceConfigId: 0 dataSourceConfigId: 0 as number | undefined
}) })
const dataSourceConfigs = ref<DataSourceConfigVO[]>([]) const dataSourceConfigs = ref<DataSourceConfigVO[]>([])
const show = async () => { const show = async () => {
const res = await getDataSourceConfigListApi() const res = await getDataSourceConfigList()
dataSourceConfigs.value = res dataSourceConfigs.value = res
queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id as number
visible.value = true visible.value = true
await getList() await getList()
} }
......
...@@ -35,10 +35,8 @@ ...@@ -35,10 +35,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -21,10 +21,8 @@ ...@@ -21,10 +21,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -23,10 +23,8 @@ ...@@ -23,10 +23,8 @@
</template> </template>
</el-upload> </el-upload>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitFileForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitFileForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -93,10 +93,8 @@ ...@@ -93,10 +93,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -12,11 +12,7 @@ ...@@ -12,11 +12,7 @@
/> />
</template> </template>
<template #beginTime_default="{ row }"> <template #beginTime_default="{ row }">
<span>{{ <span>{{ parseTime(row.beginTime) + ' ~ ' + parseTime(row.endTime) }}</span>
dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') +
' ~ ' +
dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template> </template>
<template #duration_default="{ row }"> <template #duration_default="{ row }">
<span>{{ row.duration + ' 毫秒' }}</span> <span>{{ row.duration + ' 毫秒' }}</span>
...@@ -48,7 +44,7 @@ ...@@ -48,7 +44,7 @@
</XModal> </XModal>
</template> </template>
<script setup lang="ts" name="JobLog"> <script setup lang="ts" name="JobLog">
import dayjs from 'dayjs' import { parseTime } from '@/utils/formatTime'
import * as JobLogApi from '@/api/infra/jobLog' import * as JobLogApi from '@/api/infra/jobLog'
import { allSchemas } from './jobLog.data' import { allSchemas } from './jobLog.data'
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<li v-for="item in getList" class="mt-2" :key="item.time"> <li v-for="item in getList" class="mt-2" :key="item.time">
<div class="flex items-center"> <div class="flex items-center">
<span class="mr-2 text-primary font-medium">收到消息:</span> <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>
<div> <div>
{{ item.res }} {{ item.res }}
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import { parseTime } from '@/utils/formatTime'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useWebSocket } from '@vueuse/core' import { useWebSocket } from '@vueuse/core'
......
<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>
<template> <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> </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>
<!--
【微信消息 - 定位】
-->
<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>
.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;
}
/* 来自 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;
}
}
<!--
- 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>
<!--
【微信消息 - 音乐】
-->
<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>
<!--
- 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>
<!--&lt;!&ndash;-->
<!-- - Copyright (C) 2018-2019-->
<!-- - All rights reserved, Designed By www.joolun.com-->
<!-- 芋道源码:-->
<!-- ① 移除多余的 rep 为前缀的变量,让 message 消息更简单-->
<!-- ② 代码优化,补充注释,提升阅读性-->
<!-- ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入-->
<!-- ④ 支持发送【视频】消息时,支持新建视频-->
<!--&ndash;&gt;-->
<!--<template>-->
<!-- <el-tabs type="border-card" v-model="objData.type" @tab-click="handleClick">-->
<!-- &lt;!&ndash; 类型 1:文本 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 类型 2:图片 &ndash;&gt;-->
<!-- <el-tab-pane name="image">-->
<!-- <span slot="label"><i class="el-icon-picture"></i> 图片</span>-->
<!-- <el-row>-->
<!-- &lt;!&ndash; 情况一:已经选择好素材、或者上传好图片 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 情况二:未做完上述操作 &ndash;&gt;-->
<!-- <div v-else>-->
<!-- <el-row style="text-align: center">-->
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;-->
<!-- <el-col :span="12" class="col-select">-->
<!-- <el-button type="success" @click="openMaterial">-->
<!-- 素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;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>-->
<!-- &lt;!&ndash; 文件上传 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 类型 3:语音 &ndash;&gt;-->
<!-- <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">-->
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;-->
<!-- <el-col :span="12" class="col-select">-->
<!-- <el-button type="success" @click="openMaterial">-->
<!-- 素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;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>-->
<!-- &lt;!&ndash; 文件上传 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 类型 4:视频 &ndash;&gt;-->
<!-- <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">-->
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;-->
<!-- <el-col :span="12">-->
<!-- <el-button type="success" @click="openMaterial">-->
<!-- 素材库选择<i class="el-icon-circle-check el-icon&#45;&#45;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>-->
<!-- &lt;!&ndash; 文件上传 &ndash;&gt;-->
<!-- <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&#45;&#45;right"></i-->
<!-- ></el-button>-->
<!-- </el-upload>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-row>-->
<!-- </el-tab-pane>-->
<!-- &lt;!&ndash; 类型 5:图文 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 选择素材 &ndash;&gt;-->
<!-- <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&#45;&#45;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>-->
<!-- &lt;!&ndash; 类型 6:音乐 &ndash;&gt;-->
<!-- <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>-->
<!--
- 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>
<!--
- 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>
<template> <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> </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>
<template> <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> </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>
<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>
<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 @@ ...@@ -15,10 +15,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
<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>
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)
<template> <template>
<!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- 列表 --> <el-form
<XTable ref="xGrid" @register="registerTable" show-overflow> class="-mb-15px"
<template #toolbar_buttons> :model="queryParams"
<!-- 操作:新增 --> ref="queryFormRef"
<XButton :inline="true"
type="primary" label-width="68px"
preIcon="ep:zoom-in" >
:title="t('action.add')" <el-form-item label="部门名称" prop="title">
v-hasPermi="['system:dept:create']" <el-input
@click="handleCreate()" v-model="queryParams.name"
/> placeholder="请输入部门名称"
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" /> clearable
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" /> class="!w-240px"
</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> </el-form-item>
</XTable> <el-form-item label="部门状态" prop="status">
</ContentWrap> <el-select
<!-- 添加或修改菜单对话框 --> v-model="queryParams.status"
<XModal id="deptModel" v-model="dialogVisible" :title="dialogTitle"> placeholder="请选择不么你状态"
<!-- 对话框(添加 / 修改) --> clearable
<Form ref="formRef" :schema="allSchemas.formSchema" :rules="rules"> class="!w-240px"
<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-option <el-option
v-for="item in userOption" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="item.id" :key="dict.value"
:label="item.nickname" :label="dict.label"
:value="item.id" :value="dict.value"
/> />
</el-select> </el-select>
</template> </el-form-item>
</Form> <el-form-item>
<template #footer> <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>
<XButton <el-button
v-if="['create', 'update'].includes(actionType)" type="primary"
type="primary" plain
:loading="actionLoading" @click="openForm('create')"
@click="submitForm()" v-hasPermi="['system:dept:create']"
:title="t('action.save')" >
<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"
/> />
<!-- 按钮:关闭 --> <el-table-column label="操作" align="center" class-name="fixed-width">
<XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" /> <template #default="scope">
</template> <el-button
</XModal> 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> </template>
<script setup lang="ts" name="Dept"> <script setup lang="ts" name="Dept">
import { handleTree, defaultProps } from '@/utils/tree' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import type { FormExpose } from '@/components/Form' import { dateFormatter } from '@/utils/formatTime'
import { allSchemas, rules } from './dept.data' import { handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept' import * as DeptApi from '@/api/system/dept'
import { getListSimpleUsersApi, UserVO } from '@/api/system/user' import DeptForm from './DeptForm.vue'
import * as UserApi from '@/api/system/user'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
// 列表相关的变量 const { t } = useI18n() // 国际化
const xGrid = ref<any>() // 列表 Grid Ref
const treeConfig = {
transform: true,
rowField: 'id',
parentField: 'parentId',
expandAll: true
}
// 弹窗相关的变量 const loading = ref(true) // 列表的加载中
const dialogVisible = ref(false) // 是否显示弹出层 const list = ref() // 列表的数据
const dialogTitle = ref('edit') // 弹出层标题 const queryParams = reactive({
const actionType = ref('') // 操作按钮的类型 title: '',
const actionLoading = ref(false) // 遮罩层 name: undefined,
const formRef = ref<FormExpose>() // 表单 Ref status: undefined,
const deptOptions = ref() // 树形结构 pageNo: 1,
const userOption = ref<UserVO[]>([]) pageSize: 100
})
const queryFormRef = ref() // 搜索的表单
const isExpandAll = ref(true) // 是否展开,默认全部展开
const refreshTable = ref(true) // 重新渲染表格状态
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
const getUserList = async () => { /** 查询部门列表 */
const res = await getListSimpleUsersApi() const getList = async () => {
userOption.value = res loading.value = true
} try {
// 获取下拉框[上级]的数据 const data = await DeptApi.getDeptPageApi(queryParams)
const getTree = async () => { list.value = handleTree(data)
deptOptions.value = [] } finally {
const res = await DeptApi.listSimpleDeptApi() loading.value = false
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 setDialogTile = (type: string) => { const toggleExpandAll = () => {
dialogTitle.value = t('action.' + type) refreshTable.value = false
actionType.value = type isExpandAll.value = !isExpandAll.value
dialogVisible.value = true console.log(isExpandAll.value)
nextTick(() => {
refreshTable.value = true
})
} }
// 新增操作 /** 搜索按钮操作 */
const handleCreate = async () => { const handleQuery = () => {
setDialogTile('create') getList()
} }
// 修改操作 /** 重置按钮操作 */
const handleUpdate = async (rowId: number) => { const resetQuery = () => {
setDialogTile('update') queryParams.pageNo = 1
// 设置数据 queryFormRef.value.resetFields()
const res = await DeptApi.getDeptApi(rowId) handleQuery()
await nextTick()
unref(formRef)?.setValues(res)
} }
// 提交新增/修改的表单 /** 添加/修改操作 */
const submitForm = async () => { const formRef = ref()
const elForm = unref(formRef)?.getElFormRef() const openForm = (type: string, id?: number) => {
if (!elForm) return formRef.value.open(type, id)
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 userNicknameFormat = (row) => { /** 删除按钮操作 */
if (!row || !row.leaderUserId) { const handleDelete = async (id: number) => {
return '未设置' try {
} // 删除的二次确认
for (const user of userOption.value) { await message.delConfirm()
if (row.leaderUserId === user.id) { // 发起删除
return user.nickname await DeptApi.deleteDeptApi(id)
} message.success(t('common.delSuccess'))
} // 刷新列表
return '未知【' + row.leaderUserId + '】' await getList()
} catch {}
} }
// ========== 初始化 ========== /** 初始化 **/
onMounted(async () => { onMounted(async () => {
await getUserList() await getList()
await getTree() // 获取用户列表
userList.value = await UserApi.getSimpleUserList()
}) })
</script> </script>
...@@ -51,10 +51,8 @@ ...@@ -51,10 +51,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -33,10 +33,8 @@ ...@@ -33,10 +33,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -21,10 +21,8 @@ ...@@ -21,10 +21,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
width="180" width="180"
:formatter="dateFormatter" :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"> <template #default="scope">
<el-button <el-button
link link
......
<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>
<template> <template>
<ContentWrap> <content-wrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<!-- 操作:导出 --> class="-mb-15px"
<template #toolbar_buttons> :model="queryParams"
<XButton ref="queryFormRef"
type="warning" :inline="true"
preIcon="ep:download" label-width="68px"
:title="t('action.export')" >
@click="exportList('登录列表.xls')" <el-form-item label="用户名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
</template> </el-form-item>
<template #actionbtns_default="{ row }"> <el-form-item label="登录地址" prop="userIp">
<!-- 操作:详情 --> <el-input
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" /> v-model="queryParams.userIp"
</template> placeholder="请输入登录地址"
</XTable> clearable
</ContentWrap> @keyup.enter="handleQuery"
<!-- 弹窗 --> class="!w-240px"
<XModal id="postModel" v-model="dialogVisible" :title="dialogTitle"> />
<!-- 表单:详情 --> </el-form-item>
<Descriptions :schema="allSchemas.detailSchema" :data="detailData" /> <el-form-item label="登录日期" prop="createTime">
<template #footer> <el-date-picker
<!-- 按钮:关闭 --> v-model="queryParams.createTime"
<XButton :title="t('dialog.close')" @click="dialogVisible = false" /> value-format="YYYY-MM-DD HH:mm:ss"
</template> type="daterange"
</XModal> 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> </template>
<script setup lang="ts" name="Loginlog"> <script setup lang="ts" name="LoginLog">
// 业务相关的 import import { DICT_TYPE } from '@/utils/dict'
import { allSchemas } from './loginLog.data' import { dateFormatter } from '@/utils/formatTime'
import { getLoginLogPageApi, exportLoginLogApi, LoginLogVO } from '@/api/system/loginLog' import download from '@/utils/download'
import * as LoginLogApi from '@/api/system/loginLog'
const { t } = useI18n() // 国际化 import LoginLogDetail from './LoginLogDetail.vue'
// 列表相关的变量 const message = useMessage() // 消息弹窗
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas, const loading = ref(true) // 列表的加载中
getListApi: getLoginLogPageApi, const total = ref(0) // 列表的总页数
exportListApi: exportLoginLogApi const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
username: undefined,
userIp: undefined,
createTime: []
}) })
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询参数列表 */
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 detailData = ref() // 详情 Ref const handleQuery = () => {
const dialogVisible = ref(false) // 是否显示弹出层 queryParams.pageNo = 1
const dialogTitle = ref(t('action.detail')) // 弹出层标题 getList()
// 详情
const handleDetail = async (row: LoginLogVO) => {
// 设置数据
detailData.value = row
dialogVisible.value = true
} }
/** 重置按钮操作 */
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> </script>
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 @@ ...@@ -26,10 +26,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
......
<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>
<template> <template>
<!-- 搜索工作栏 -->
<ContentWrap> <ContentWrap>
<!-- 列表 -->
<XTable ref="xGrid" @register="registerTable" show-overflow>
<template #toolbar_buttons>
<!-- 操作:新增 -->
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:menu:create']"
@click="handleCreate()"
/>
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
</template>
<template #name_default="{ row }">
<Icon :icon="row.icon" />
<span class="ml-3">{{ row.name }}</span>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作:修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:menu:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作:删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:menu:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<!-- 添加或修改菜单对话框 -->
<XModal id="menuModel" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<el-form <el-form
ref="formRef" class="-mb-15px"
:model="menuForm" :model="queryParams"
:rules="rules" ref="queryFormRef"
label-width="100px" :inline="true"
label-position="right" label-width="68px"
> >
<el-form-item label="上级菜单"> <el-form-item label="菜单名称" prop="name">
<el-tree-select <el-input
node-key="id" v-model="queryParams.name"
v-model="menuForm.parentId" placeholder="请输入菜单名称"
:props="defaultProps" clearable
:data="menuOptions" @keyup.enter="handleQuery"
:default-expanded-keys="[0]" class="!w-240px"
check-strictly
/> />
</el-form-item> </el-form-item>
<el-col :span="16"> <el-form-item label="状态" prop="status">
<el-form-item label="菜单名称" prop="name"> <el-select
<el-input v-model="menuForm.name" placeholder="请输入菜单名称" clearable /> v-model="queryParams.status"
</el-form-item> placeholder="请选择菜单状态"
</el-col> clearable
<el-form-item label="菜单类型" prop="type"> class="!w-240px"
<el-radio-group v-model="menuForm.type"> >
<el-radio-button <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.label" :key="dict.value"
:label="dict.value" :label="dict.label"
> :value="dict.value"
{{ dict.label }} />
</el-radio-button> </el-select>
</el-radio-group> </el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:menu:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="danger" plain @click="toggleExpandAll">
<Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
</el-button>
</el-form-item> </el-form-item>
<template v-if="menuForm.type !== 3">
<el-form-item label="菜单图标">
<IconSelect v-model="menuForm.icon" clearable />
</el-form-item>
<el-col :span="16">
<el-form-item label="路由地址" prop="path">
<template #label>
<Tooltip
titel="路由地址"
message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
/>
</template>
<el-input v-model="menuForm.path" placeholder="请输入路由地址" clearable />
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type === 2">
<el-col :span="16">
<el-form-item label="组件地址" prop="component">
<el-input
v-model="menuForm.component"
placeholder="例如说:system/user/index"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="组件名字" prop="componentName">
<el-input v-model="menuForm.componentName" placeholder="例如说:SystemUser" clearable />
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type !== 1">
<el-col :span="16">
<el-form-item label="权限标识" prop="permission">
<template #label>
<Tooltip
titel="权限标识"
message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
/>
</template>
<el-input v-model="menuForm.permission" placeholder="请输入权限标识" clearable />
</el-form-item>
</el-col>
</template>
<el-col :span="16">
<el-form-item label="显示排序" prop="sort">
<el-input-number v-model="menuForm.sort" controls-position="right" :min="0" clearable />
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="菜单状态" prop="status">
<el-radio-group v-model="menuForm.status">
<el-radio
border
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.label"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<template v-if="menuForm.type !== 3">
<el-col :span="16">
<el-form-item label="显示状态" prop="visible">
<template #label>
<Tooltip
titel="显示状态"
message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问"
/>
</template>
<el-radio-group v-model="menuForm.visible">
<el-radio border key="true" :label="true">显示</el-radio>
<el-radio border key="false" :label="false">隐藏</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type !== 3">
<el-col :span="16">
<el-form-item label="总是显示" prop="alwaysShow">
<template #label>
<Tooltip
titel="总是显示"
message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
/>
</template>
<el-radio-group v-model="menuForm.alwaysShow">
<el-radio border key="true" :label="true">总是</el-radio>
<el-radio border key="false" :label="false">不是</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type === 2">
<el-col :span="16">
<el-form-item label="缓存状态" prop="keepAlive">
<template #label>
<Tooltip
titel="缓存状态"
message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
/>
</template>
<el-radio-group v-model="menuForm.keepAlive">
<el-radio border key="true" :label="true">缓存</el-radio>
<el-radio border key="false" :label="false">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</template>
</el-form> </el-form>
<template #footer> </ContentWrap>
<!-- 按钮:保存 -->
<XButton <!-- 列表 -->
v-if="['create', 'update'].includes(actionType)" <ContentWrap>
type="primary" <el-table
:loading="actionLoading" v-loading="loading"
@click="submitForm()" :data="list"
:title="t('action.save')" row-key="id"
/> v-if="refreshTable"
<!-- 按钮:关闭 --> :default-expand-all="isExpandAll"
<XButton :loading="actionLoading" @click="dialogVisible = false" :title="t('dialog.close')" /> >
</template> <el-table-column prop="name" label="菜单名称" :show-overflow-tooltip="true" width="250" />
</XModal> <el-table-column prop="icon" label="图标" align="center" width="100">
<template #default="scope">
<Icon :icon="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="60" />
<el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true" />
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true" />
<el-table-column prop="componentName" label="组件名称" :show-overflow-tooltip="true" />
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:menu:update']"
>
修改
</el-button>
<el-button
link
type="primary"
@click="openForm('create', undefined, scope.row.id)"
v-hasPermi="['system:menu:create']"
>
新增
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:menu:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<MenuForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="Menu"> <script setup lang="ts" name="Menu">
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { FormInstance } from 'element-plus'
// 业务相关的 import
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants' import { handleTree } from '@/utils/tree'
import { handleTree, defaultProps } from '@/utils/tree'
import * as MenuApi from '@/api/system/menu' import * as MenuApi from '@/api/system/menu'
import { allSchemas, rules } from './menu.data' import MenuForm from './MenuForm.vue'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { wsCache } = useCache()
const xGrid = ref<any>(null) const loading = ref(true) // 列表的加载中
const list = ref<any>([]) // 列表的数据
// 列表相关的变量 const queryParams = reactive({
const treeConfig = { name: undefined,
transform: true, status: undefined
rowField: 'id',
parentField: 'parentId',
expandAll: false
}
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
treeConfig: treeConfig,
getListApi: MenuApi.getMenuListApi,
deleteApi: MenuApi.deleteMenuApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 遮罩层
// 新增和修改的表单值
const formRef = ref<FormInstance>()
const menuForm = ref<MenuApi.MenuVO>({
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: 1,
parentId: 0,
path: '',
icon: '',
component: '',
componentName: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
alwaysShow: true,
createTime: new Date()
}) })
const queryFormRef = ref() // 搜索的表单
const isExpandAll = ref(false) // 是否展开,默认全部折叠
const refreshTable = ref(true) // 重新渲染表格状态
// ========== 下拉框[上级菜单] ========== /** 查询参数列表 */
const menuOptions = ref<any[]>([]) // 树形结构 const getList = async () => {
// 获取下拉框[上级菜单]的数据 loading.value = true
const getTree = async () => { try {
menuOptions.value = [] const data = await MenuApi.getMenuList(queryParams)
const res = await MenuApi.listSimpleMenusApi() list.value = handleTree(data)
let menu: Tree = { id: 0, name: '主类目', children: [] } } finally {
menu.children = handleTree(res) loading.value = false
menuOptions.value.push(menu) }
} }
// ========== 新增/修改 ========== /** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
// 设置标题 /** 重置按钮操作 */
const setDialogTile = async (type: string) => { const resetQuery = () => {
await getTree() queryFormRef.value.resetFields()
dialogTitle.value = t('action.' + type) handleQuery()
actionType.value = type
dialogVisible.value = true
} }
// 新增操作 /** 添加/修改操作 */
const handleCreate = () => { const formRef = ref()
setDialogTile('create') const openForm = (type: string, id?: number, parentId?: number) => {
// 重置表单 formRef.value.open(type, id, parentId)
formRef.value?.resetFields()
menuForm.value = {
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: 1,
parentId: 0,
path: '',
icon: '',
component: '',
componentName: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
alwaysShow: true,
createTime: new Date()
}
} }
// 修改操作 /** 展开/折叠操作 */
const handleUpdate = async (rowId: number) => { const toggleExpandAll = () => {
await setDialogTile('update') refreshTable.value = false
// 设置数据 isExpandAll.value = !isExpandAll.value
const res = await MenuApi.getMenuApi(rowId) nextTick(() => {
menuForm.value = res refreshTable.value = true
// TODO 芋艿:这块要优化下,部分字段未重置,无法修改 })
menuForm.value.componentName = res.componentName || ''
menuForm.value.alwaysShow = res.alwaysShow !== undefined ? res.alwaysShow : true
} }
// 提交新增/修改的表单 /** 删除按钮操作 */
const submitForm = async () => { const handleDelete = async (id: number) => {
actionLoading.value = true
// 提交请求
try { try {
if ( // 删除的二次确认
menuForm.value.type === SystemMenuTypeEnum.DIR || await message.delConfirm()
menuForm.value.type === SystemMenuTypeEnum.MENU // 发起删除
) { await MenuApi.deleteMenu(id)
if (!isExternal(menuForm.value.path)) { message.success(t('common.delSuccess'))
if (menuForm.value.parentId === 0 && menuForm.value.path.charAt(0) !== '/') { // 刷新列表
message.error('路径必须以 / 开头') await getList()
return } catch {}
} else if (menuForm.value.parentId !== 0 && menuForm.value.path.charAt(0) === '/') {
message.error('路径不能以 / 开头')
return
}
}
}
if (actionType.value === 'create') {
await MenuApi.createMenuApi(menuForm.value)
message.success(t('common.createSuccess'))
} else {
await MenuApi.updateMenuApi(menuForm.value)
message.success(t('common.updateSuccess'))
}
} finally {
dialogVisible.value = false
actionLoading.value = false
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
// 操作成功,重新加载列表
await reload()
}
} }
// 判断 path 是不是外部的 HTTP 等链接 /** 初始化 **/
const isExternal = (path: string) => { onMounted(() => {
return /^(https?:|mailto:|tel:)/.test(path) getList()
} })
</script> </script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 新增和修改的表单校验
export const rules = reactive({
name: [required],
sort: [required],
path: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: null,
action: true,
columns: [
{
title: '上级菜单',
field: 'parentId',
isTable: false
},
{
title: '菜单名称',
field: 'name',
isSearch: true,
table: {
treeNode: true,
align: 'left',
width: '200px',
slots: {
default: 'name_default'
}
}
},
{
title: '菜单类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_MENU_TYPE
},
{
title: '路由地址',
field: 'path'
},
{
title: '组件路径',
field: 'component'
},
{
title: '组件名字',
field: 'componentName'
},
{
title: '权限标识',
field: 'permission'
},
{
title: '排序',
field: 'sort'
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isTable: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
...@@ -38,17 +38,14 @@ ...@@ -38,17 +38,14 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import * as NoticeApi from '@/api/system/notice' import * as NoticeApi from '@/api/system/notice'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
......
...@@ -119,7 +119,7 @@ import { FormExpose } from '@/components/Form' ...@@ -119,7 +119,7 @@ import { FormExpose } from '@/components/Form'
// 业务相关的 import // 业务相关的 import
import { rules, allSchemas } from './template.data' import { rules, allSchemas } from './template.data'
import * as NotifyTemplateApi from '@/api/system/notify/template' import * as NotifyTemplateApi from '@/api/system/notify/template'
import { getListSimpleUsersApi, UserVO } from '@/api/system/user' import { getSimpleUserList, UserVO } from '@/api/system/user'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
...@@ -244,7 +244,7 @@ const sendTest = async () => { ...@@ -244,7 +244,7 @@ const sendTest = async () => {
// ========== 初始化 ========== // ========== 初始化 ==========
onMounted(() => { onMounted(() => {
getListSimpleUsersApi().then((data) => { getSimpleUserList().then((data) => {
userOption.value = data userOption.value = data
}) })
}) })
......
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
const authorizedGrantOptions = getStrDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)
// 表单校验
export const rules = reactive({
clientId: [required],
secret: [required],
name: [required],
status: [required],
accessTokenValiditySeconds: [required],
refreshTokenValiditySeconds: [required],
redirectUris: [required],
authorizedGrantTypes: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'clientId',
primaryType: null,
action: true,
columns: [
{
title: '客户端端号',
field: 'clientId'
},
{
title: '客户端密钥',
field: 'secret'
},
{
title: '应用名',
field: 'name',
isSearch: true
},
{
title: '应用图标',
field: 'logo',
table: {
cellRender: {
name: 'XImg'
}
},
form: {
component: 'UploadImg'
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '访问令牌的有效期',
field: 'accessTokenValiditySeconds',
form: {
component: 'InputNumber'
},
table: {
slots: {
default: 'accessTokenValiditySeconds_default'
}
}
},
{
title: '刷新令牌的有效期',
field: 'refreshTokenValiditySeconds',
form: {
component: 'InputNumber'
},
table: {
slots: {
default: 'refreshTokenValiditySeconds_default'
}
}
},
{
title: '授权类型',
field: 'authorizedGrantTypes',
table: {
width: 400,
slots: {
default: 'authorizedGrantTypes_default'
}
},
form: {
component: 'Select',
componentProps: {
options: authorizedGrantOptions,
multiple: true,
filterable: true
}
}
},
{
title: '授权范围',
field: 'scopes',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '自动授权范围',
field: 'autoApproveScopes',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '可重定向的 URI 地址',
field: 'redirectUris',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '权限',
field: 'authorities',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '资源',
field: 'resourceIds',
isTable: false,
form: {
component: 'Select',
componentProps: {
options: [],
multiple: true,
filterable: true,
allowCreate: true,
defaultFirstOption: true
}
}
},
{
title: '附加信息',
field: 'additionalInformation',
isTable: false,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
<template>
<Dialog :title="modelTitle" v-model="modelVisible" width="800">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="客户端编号" prop="secret">
<el-input v-model="formData.clientId" placeholder="请输入客户端编号" />
</el-form-item>
<el-form-item label="客户端密钥" prop="secret">
<el-input v-model="formData.secret" placeholder="请输入客户端密钥" />
</el-form-item>
<el-form-item label="应用名" prop="name">
<el-input v-model="formData.name" placeholder="请输入应用名" />
</el-form-item>
<el-form-item label="应用图标">
<UploadImg v-model="formData.logo" :limit="1" />
</el-form-item>
<el-form-item label="应用描述">
<el-input type="textarea" v-model="formData.description" placeholder="请输入应用名" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="parseInt(dict.value)"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="访问令牌的有效期" prop="accessTokenValiditySeconds">
<el-input-number v-model="formData.accessTokenValiditySeconds" placeholder="单位:秒" />
</el-form-item>
<el-form-item label="刷新令牌的有效期" prop="refreshTokenValiditySeconds">
<el-input-number v-model="formData.refreshTokenValiditySeconds" placeholder="单位:秒" />
</el-form-item>
<el-form-item label="授权类型" prop="authorizedGrantTypes">
<el-select
v-model="formData.authorizedGrantTypes"
multiple
filterable
placeholder="请输入授权类型"
style="width: 500px"
>
<el-option
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="授权范围" prop="scopes">
<el-select
v-model="formData.scopes"
multiple
filterable
allow-create
placeholder="请输入授权范围"
style="width: 500px"
>
<el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" />
</el-select>
</el-form-item>
<el-form-item label="自动授权范围" prop="autoApproveScopes">
<el-select
v-model="formData.autoApproveScopes"
multiple
filterable
placeholder="请输入授权范围"
style="width: 500px"
>
<el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" />
</el-select>
</el-form-item>
<el-form-item label="可重定向的 URI 地址" prop="redirectUris">
<el-select
v-model="formData.redirectUris"
multiple
filterable
allow-create
placeholder="请输入可重定向的 URI 地址"
style="width: 500px"
>
<el-option
v-for="redirectUri in formData.redirectUris"
:key="redirectUri"
:label="redirectUri"
:value="redirectUri"
/>
</el-select>
</el-form-item>
<el-form-item label="权限" prop="authorities">
<el-select
v-model="formData.authorities"
multiple
filterable
allow-create
placeholder="请输入权限"
style="width: 500px"
>
<el-option
v-for="authority in formData.authorities"
:key="authority"
:label="authority"
:value="authority"
/>
</el-select>
</el-form-item>
<el-form-item label="资源" prop="resourceIds">
<el-select
v-model="formData.resourceIds"
multiple
filterable
allow-create
placeholder="请输入资源"
style="width: 500px"
>
<el-option
v-for="resourceId in formData.resourceIds"
:key="resourceId"
:label="resourceId"
:value="resourceId"
/>
</el-select>
</el-form-item>
<el-form-item label="附加信息" prop="additionalInformation">
<el-input
type="textarea"
v-model="formData.additionalInformation"
placeholder="请输入附加信息,JSON 格式数据"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import * as ClientApi from '@/api/system/oauth2/client'
import UploadImg from '@/components/UploadFile'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
clientId: undefined,
secret: undefined,
name: undefined,
logo: undefined,
description: undefined,
status: DICT_TYPE.COMMON_STATUS,
accessTokenValiditySeconds: 30 * 60,
refreshTokenValiditySeconds: 30 * 24 * 60,
redirectUris: [],
authorizedGrantTypes: [],
scopes: [],
autoApproveScopes: [],
authorities: [],
resourceIds: [],
additionalInformation: undefined
})
const formRules = reactive({
clientId: [{ required: true, message: '客户端编号不能为空', trigger: 'blur' }],
secret: [{ required: true, message: '客户端密钥不能为空', trigger: 'blur' }],
name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
logo: [{ required: true, message: '应用图标不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
accessTokenValiditySeconds: [
{ required: true, message: '访问令牌的有效期不能为空', trigger: 'blur' }
],
refreshTokenValiditySeconds: [
{ required: true, message: '刷新令牌的有效期不能为空', trigger: 'blur' }
],
redirectUris: [{ required: true, message: '可重定向的 URI 地址不能为空', trigger: 'blur' }],
authorizedGrantTypes: [{ required: true, message: '授权类型不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const openModal = async (type: string, id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await ClientApi.getOAuth2ClientApi(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as ClientApi.OAuth2ClientVO
if (formType.value === 'create') {
await ClientApi.createOAuth2ClientApi(data)
message.success(t('common.createSuccess'))
} else {
await ClientApi.updateOAuth2ClientApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
clientId: undefined,
secret: undefined,
name: undefined,
logo: undefined,
description: undefined,
status: DICT_TYPE.COMMON_STATUS,
accessTokenValiditySeconds: 30 * 60,
refreshTokenValiditySeconds: 30 * 24 * 60,
redirectUris: [],
authorizedGrantTypes: [],
scopes: [],
autoApproveScopes: [],
authorities: [],
resourceIds: [],
additionalInformation: undefined
}
formRef.value?.resetFields()
}
</script>
...@@ -133,7 +133,6 @@ ...@@ -133,7 +133,6 @@
import type { FormExpose } from '@/components/Form' import type { FormExpose } from '@/components/Form'
// 业务相关的 import // 业务相关的 import
import * as ClientApi from '@/api/system/oauth2/client' import * as ClientApi from '@/api/system/oauth2/client'
import { rules, allSchemas } from './client.data'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
{{ detailData.javaMethodArgs }} {{ detailData.javaMethodArgs }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="操作时间"> <el-descriptions-item label="操作时间">
{{ formatDate(detailData.startTime, 'YYYY-MM-DD HH:mm:ss') }} {{ formatDate(detailData.startTime) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="执行时长">{{ detailData.duration }} ms</el-descriptions-item> <el-descriptions-item label="执行时长">{{ detailData.duration }} ms</el-descriptions-item>
<el-descriptions-item label="操作结果"> <el-descriptions-item label="操作结果">
......
<template>
<Dialog :title="modelTitle" v-model="modelVisible" width="800">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="80px"
v-loading="formLoading"
>
<el-form-item label="岗位标题" prop="name">
<el-input v-model="formData.name" placeholder="请输入岗位标题" />
</el-form-item>
<el-form-item label="岗位编码" prop="code">
<el-input :model-value="formData.code" placeholder="请输入岗位编码" height="150px" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" clearable>
<el-option
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="请输备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import * as PostApi from '@/api/system/post'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: '',
code: '',
sort: undefined,
status: CommonStatusEnum.ENABLE,
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const openModal = async (type: string, id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await PostApi.getPost(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as PostApi.PostVO
if (formType.value === 'create') {
await PostApi.createPost(data)
message.success(t('common.createSuccess'))
} else {
await PostApi.updatePost(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
code: '',
sort: undefined,
status: CommonStatusEnum.ENABLE,
remark: ''
}
formRef.value?.resetFields()
}
</script>
<template>
<!-- 弹窗 -->
<XModal :title="modelTitle" :loading="modelLoading" v-model="modelVisible">
<!-- 表单:添加/修改 -->
<Form
ref="formRef"
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
/>
<!-- 表单:详情 -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<template #footer>
<!-- 按钮:保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮:关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="modelVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts">
import type { FormExpose } from '@/components/Form'
import * as PostApi from '@/api/system/post'
import { rules, allSchemas } from './post.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 弹窗相关的变量
const modelVisible = ref(false) // 是否显示弹出层
const modelTitle = ref('') // 弹出层标题
const modelLoading = ref(false) // 弹出层loading
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 打开弹窗
const openModal = async (type: string, id?: number) => {
modelVisible.value = true
modelLoading.value = true
modelTitle.value = t('action.' + type)
actionType.value = type
// 设置数据
if (id) {
const res = await PostApi.getPostApi(id)
if (type === 'update') {
unref(formRef)?.setValues(res)
} else if (type === 'detail') {
detailData.value = res
}
}
modelLoading.value = false
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
// 提交新增/修改的表单
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 提交请求
actionLoading.value = true
try {
const data = unref(formRef)?.formModel as PostApi.PostVO
if (actionType.value === 'create') {
await PostApi.createPostApi(data)
message.success(t('common.createSuccess'))
} else {
await PostApi.updatePostApi(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
emit('success')
} finally {
actionLoading.value = false
}
}
</script>
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<template #toolbar_buttons> class="-mb-15px"
<!-- 操作:新增 --> :model="queryParams"
<XButton ref="queryFormRef"
type="primary" :inline="true"
preIcon="ep:zoom-in" label-width="68px"
:title="t('action.add')" >
v-hasPermi="['system:post:create']" <el-form-item label="岗位名称" prop="name">
@click="openModal('create')" <el-input
v-model="queryParams.name"
placeholder="请输入岗位名称"
clearable
@keyup.enter="handleQuery"
/> />
<!-- 操作:导出 --> </el-form-item>
<XButton <el-form-item label="岗位编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入岗位编码"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary" type="primary"
@click="openModal('create')"
v-hasPermi="['system:notice:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain plain
preIcon="ep:download" @click="handleExport"
:title="t('action.export')" :loading="exportLoading"
v-hasPermi="['system:post:export']" v-hasPermi="['infra:config:export']"
@click="exportList('岗位列表.xls')" >
/> <Icon icon="ep:download" class="mr-5px" /> 导出
</template> </el-button>
<template #actionbtns_default="{ row }"> </el-form-item>
<!-- 操作:修改 --> </el-form>
<XTextButton </ContentWrap>
preIcon="ep:edit"
:title="t('action.edit')" <!-- 列表 -->
v-hasPermi="['system:post:update']" <ContentWrap>
@click="openModal('update', row?.id)" <el-table v-loading="loading" :data="list" align="center">
/> <el-table-column label="岗位编号" align="center" prop="id" />
<!-- 操作:详情 --> <el-table-column label="岗位名称" align="center" prop="name" />
<XTextButton <el-table-column label="岗位编码" align="center" prop="code" />
preIcon="ep:view" <el-table-column label="岗位顺序" align="center" prop="sort" />
:title="t('action.detail')" <el-table-column label="岗位备注" align="center" prop="remark" />
v-hasPermi="['system:post:query']" <el-table-column label="状态" align="center" prop="status">
@click="openModal('detail', row?.id)" <template #default="scope">
/> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
<!-- 操作:删除 --> </template>
<XTextButton </el-table-column>
preIcon="ep:delete" <el-table-column
:title="t('action.delete')" label="创建时间"
v-hasPermi="['system:post:delete']" align="center"
@click="deleteData(row?.id)" prop="createTime"
/> width="180"
</template> :formatter="dateFormatter"
</XTable> />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openModal('update', scope.row.id)"
v-hasPermi="['system:post:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:post:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap> </ContentWrap>
<!-- 表单弹窗:添加/修改/详情 -->
<PostForm ref="modalRef" @success="reload()" /> <!-- 表单弹窗:添加/修改 -->
<PostForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="Post"> <script setup lang="tsx">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as PostApi from '@/api/system/post' import * as PostApi from '@/api/system/post'
import { allSchemas } from './post.data' import PostForm from './PostForm.vue'
import PostForm from './form.vue'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
// 列表相关的变量 const loading = ref(true) // 列表的加载中
const [registerTable, { reload, deleteData, exportList }] = useXTable({ const total = ref(0) // 列表的总页数
allSchemas: allSchemas, // 列表配置 const list = ref([]) // 列表的数据
getListApi: PostApi.getPostPageApi, // 加载列表的 API const queryParams = reactive({
deleteApi: PostApi.deletePostApi, // 删除数据的 API pageNo: 1,
exportListApi: PostApi.exportPostApi // 导出数据的 API pageSize: 10,
code: '',
name: '',
status: undefined
}) })
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
// 表单相关的变量 /** 查询岗位列表 */
const modalRef = ref() const getList = async () => {
loading.value = true
try {
const data = await PostApi.getPostPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openModal = (type: string, id?: number) => { const openModal = (type: string, id?: number) => {
modalRef.value.openModal(type, id) formRef.value.openModal(type, id)
} }
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await PostApi.deletePost(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await PostApi.exportPost(queryParams)
download.excel(data, '岗位列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script> </script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required]
})
// 增删改查 CrudSchema 配置
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'id',
primaryTitle: '岗位编号',
action: true,
columns: [
{
title: '岗位名称',
field: 'name',
isSearch: true
},
{
title: '岗位编码',
field: 'code',
isSearch: true
},
{
title: '岗位顺序',
field: 'sort',
form: {
component: 'InputNumber'
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '备注',
field: 'remark',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
table: {
width: 180
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
<template>
<Dialog :title="dialogScopeTitle" v-model="dialogScopeVisible" width="800">
<el-form
ref="menuPermissionFormRef"
:model="dataScopeForm"
:inline="true"
label-width="80px"
v-loading="formLoading"
>
<el-form-item label="角色名称">
<el-tag>{{ dataScopeForm.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ dataScopeForm.code }}</el-tag>
</el-form-item>
<!-- 分配角色的数据权限对话框 -->
<el-form-item label="权限范围" v-if="actionScopeType === 'data'">
<el-select v-model="dataScopeForm.dataScope">
<el-option
v-for="item in dataScopeDictDatas"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 分配角色的菜单权限对话框 -->
<el-row>
<el-col :span="24">
<el-form-item
label="权限范围"
v-if="
actionScopeType === 'menu' ||
dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
"
style="display: flex"
>
<el-card class="card" shadow="never">
<template #header>
父子联动(选中父节点,自动选择子节点):
<el-switch
v-model="checkStrictly"
inline-prompt
active-text="是"
inactive-text="否"
/>
全选/全不选:
<el-switch
v-model="treeNodeAll"
inline-prompt
active-text="是"
inactive-text="否"
@change="handleCheckedTreeNodeAll()"
/>
</template>
<el-tree
ref="treeRef"
node-key="id"
show-checkbox
:check-strictly="!checkStrictly"
:props="defaultProps"
:data="dataScopeForm"
empty-text="加载中,请稍后"
/>
</el-card>
</el-form-item> </el-col
></el-row>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button
:title="t('action.save')"
:loading="actionLoading"
@click="submitScope()"
type="primary"
:disabled="formLoading"
>
保存
</el-button>
<el-button
:loading="actionLoading"
:title="t('dialog.close')"
@click="dialogScopeVisible = false"
>取 消</el-button
>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as RoleApi from '@/api/system/role'
import type { ElTree } from 'element-plus'
import type { FormExpose } from '@/components/Form'
import { handleTree, defaultProps } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as MenuApi from '@/api/system/menu'
import * as DeptApi from '@/api/system/dept'
import * as PermissionApi from '@/api/system/permission'
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const menuPermissionFormRef = ref<FormExpose>() // 表单 Ref
const { t } = useI18n() // 国际化
const dialogScopeTitle = ref('菜单权限')
const dataScopeDictDatas = ref()
const message = useMessage() // 消息弹窗
const actionScopeType = ref('')
// 选项
const checkStrictly = ref(true)
const treeNodeAll = ref(false)
const dialogScopeVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const treeOptions = ref<any[]>([]) // 菜单树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
// ========== 数据权限 ==========
const dataScopeForm = reactive({
id: 0,
name: '',
code: '',
dataScope: 0,
checkList: []
})
/** 打开弹窗 */
const openModal = async (type: string, row: RoleApi.RoleVO) => {
dataScopeForm.id = row.id
dataScopeForm.name = row.name
dataScopeForm.code = row.code
actionScopeType.value = type
dialogScopeVisible.value = true
if (type === 'menu') {
const menuRes = await MenuApi.getSimpleMenusList()
treeOptions.value = handleTree(menuRes)
const role = await PermissionApi.listRoleMenusApi(row.id)
if (role) {
role?.forEach((item: any) => {
unref(treeRef)?.setChecked(item, true, false)
})
}
} else if (type === 'data') {
const deptRes = await DeptApi.getSimpleDeptList()
treeOptions.value = handleTree(deptRes)
const role = await RoleApi.getRole(row.id)
dataScopeForm.dataScope = role.dataScope
if (role.dataScopeDeptIds) {
role.dataScopeDeptIds?.forEach((item: any) => {
unref(treeRef)?.setChecked(item, true, false)
})
}
}
}
// 保存权限
const submitScope = async () => {
if ('data' === actionScopeType.value) {
const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({
roleId: dataScopeForm.id,
dataScope: dataScopeForm.dataScope,
dataScopeDeptIds:
dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
? []
: (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>)
})
await PermissionApi.assignRoleDataScopeApi(data.value)
} else if ('menu' === actionScopeType.value) {
const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({
roleId: dataScopeForm.id,
menuIds: [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
]
})
await PermissionApi.assignRoleMenuApi(data.value)
}
message.success(t('common.updateSuccess'))
dialogScopeVisible.value = false
}
// 全选/全不选
const handleCheckedTreeNodeAll = () => {
treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : [])
}
const init = () => {
dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)
}
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗
// ========== 初始化 ==========
onMounted(() => {
init()
})
</script>
<style scoped>
.card {
width: 100%;
max-height: 400px;
overflow-y: scroll;
}
</style>
<template>
<Dialog :title="modelTitle" v-model="modelVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="80px"
v-loading="formLoading"
>
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色标识" prop="code">
<el-input :model-value="formData.code" placeholder="请输入角色标识" height="150px" />
</el-form-item>
<el-form-item label="显示顺序" prop="sort">
<el-input :model-value="formData.sort" placeholder="请输入显示顺序" height="150px" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" clearable>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="请输备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import * as RoleApi from '@/api/system/role'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: '',
code: '',
sort: undefined,
status: CommonStatusEnum.ENABLE,
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await RoleApi.getRole(id)
} finally {
formLoading.value = false
}
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
code: '',
sort: undefined,
status: CommonStatusEnum.ENABLE,
remark: ''
}
formRef.value?.resetFields()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as RoleApi.RoleVO
if (formType.value === 'create') {
await RoleApi.createRole(data)
message.success(t('common.createSuccess'))
} else {
await RoleApi.updateRole(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
</script>
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 列表 --> <!-- 搜索工作栏 -->
<XTable @register="registerTable"> <el-form
<!-- 操作:新增 --> class="-mb-15px"
<template #toolbar_buttons> :model="queryParams"
<XButton ref="queryFormRef"
type="primary" :inline="true"
preIcon="ep:zoom-in" label-width="68px"
:title="t('action.add')" >
v-hasPermi="['system:role:create']" <el-form-item label="角色名称" prop="name">
@click="handleCreate()" <el-input
/> v-model="queryParams.name"
</template> placeholder="请输入角色名称"
<template #actionbtns_default="{ row }"> clearable
<!-- 操作:编辑 --> @keyup.enter="handleQuery"
<XTextButton class="!w-240px"
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:role:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作:详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:role:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作:菜单权限 -->
<XTextButton
preIcon="ep:basketball"
title="菜单权限"
v-hasPermi="['system:permission:assign-role-menu']"
@click="handleScope('menu', row)"
/>
<!-- 操作:数据权限 -->
<XTextButton
preIcon="ep:coin"
title="数据权限"
v-hasPermi="['system:permission:assign-role-data-scope']"
@click="handleScope('data', row)"
/>
<!-- 操作:删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:role:delete']"
@click="deleteData(row.id)"
/> />
</template>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
<XModal v-model="dialogScopeVisible" :title="dialogScopeTitle">
<el-form :model="dataScopeForm" label-width="140px" :inline="true">
<el-form-item label="角色名称">
<el-tag>{{ dataScopeForm.name }}</el-tag>
</el-form-item> </el-form-item>
<el-form-item label="角色标识"> <el-form-item label="角色标识" prop="code">
<el-tag>{{ dataScopeForm.code }}</el-tag> <el-input
v-model="queryParams.code"
placeholder="请输入角色标识"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> </el-form-item>
<!-- 分配角色的数据权限对话框 --> <el-form-item label="状态" prop="status">
<el-form-item label="权限范围" v-if="actionScopeType === 'data'"> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-select v-model="dataScopeForm.dataScope">
<el-option <el-option
v-for="item in dataScopeDictDatas" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="item.value" :key="dict.value"
:label="item.label" :label="dict.label"
:value="item.value" :value="dict.value"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- 分配角色的菜单权限对话框 --> <el-form-item label="创建时间" prop="createTime">
<el-row> <el-date-picker
<el-col :span="24"> v-model="queryParams.createTime"
<el-form-item value-format="YYYY-MM-DD HH:mm:ss"
label="权限范围" type="daterange"
v-if=" start-placeholder="开始日期"
actionScopeType === 'menu' || end-placeholder="结束日期"
dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
" class="!w-240px"
style="display: flex" />
> </el-form-item>
<el-card class="card" shadow="never"> <el-form-item>
<template #header> <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-switch <el-button
v-model="checkStrictly" type="primary"
inline-prompt plain
active-text="是" @click="openForm('create')"
inactive-text="否" v-hasPermi="['system:role:create']"
/> >
全选/全不选: <Icon icon="ep:plus" class="mr-5px" /> 新增
<el-switch </el-button>
v-model="treeNodeAll" <el-button
inline-prompt type="success"
active-text="是" plain
inactive-text="否" @click="handleExport"
@change="handleCheckedTreeNodeAll()" :loading="exportLoading"
/> v-hasPermi="['system:role:export']"
</template> >
<el-tree <Icon icon="ep:download" class="mr-5px" /> 导出
ref="treeRef" </el-button>
node-key="id" </el-form-item>
show-checkbox
:check-strictly="!checkStrictly"
:props="defaultProps"
:data="treeOptions"
empty-text="加载中,请稍后"
/>
</el-card>
</el-form-item> </el-col
></el-row>
</el-form> </el-form>
<!-- 操作按钮 --> </ContentWrap>
<template #footer>
<XButton <!-- 列表 -->
type="primary" <ContentWrap>
:title="t('action.save')" <el-table v-loading="loading" :data="list">
:loading="actionLoading" <el-table-column label="角色编号" align="center" prop="id" />
@click="submitScope()" <el-table-column label="角色名称" align="center" prop="name" />
/> <el-table-column label="角色类型" align="center" prop="type" />
<XButton <el-table-column label="角色标识" align="center" prop="code" />
:loading="actionLoading" <el-table-column label="显示顺序" align="center" prop="sort" />
:title="t('dialog.close')" <el-table-column label="备注" align="center" prop="remark" />
@click="dialogScopeVisible = false" <el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/> />
</template> <el-table-column :width="300" label="操作" align="center">
</XModal> <template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:role:update']"
>
编辑
</el-button>
<el-button
link
type="primary"
preIcon="ep:basketball"
title="菜单权限"
v-hasPermi="['system:permission:assign-role-menu']"
@click="handleScope('menu', scope.row)"
>
菜单权限
</el-button>
<el-button
link
type="primary"
preIcon="ep:coin"
title="数据权限"
v-hasPermi="['system:permission:assign-role-data-scope']"
@click="handleScope('data', scope.row)"
>
数据权限
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:role:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<RoleForm ref="formRef" @success="getList" />
<!-- 表单弹窗:菜单权限 -->
<MenuPermissionForm ref="menuPermissionFormRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="Role"> <script setup lang="tsx">
import type { ElTree } from 'element-plus'
import type { FormExpose } from '@/components/Form'
import { handleTree, defaultProps } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { rules, allSchemas } from './role.data'
import * as RoleApi from '@/api/system/role' import * as RoleApi from '@/api/system/role'
import { listSimpleMenusApi } from '@/api/system/menu' import RoleForm from './RoleForm.vue'
import { listSimpleDeptApi } from '@/api/system/dept' import MenuPermissionForm from './MenuPermissionForm.vue'
import * as PermissionApi from '@/api/system/permission' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
const { t } = useI18n() // 国际化 import download from '@/utils/download'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
// 列表相关的变量 const { t } = useI18n() // 国际化
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: RoleApi.getRolePageApi,
deleteApi: RoleApi.deleteRoleApi
})
// ========== CRUD 相关 ========== const loading = ref(true) // 列表的加载中
const actionLoading = ref(false) // 遮罩层 const total = ref(0) // 列表的总页数
const actionType = ref('') // 操作按钮的类型 const list = ref([]) // 列表的数据
const dialogVisible = ref(false) // 是否显示弹出层 const queryParams = reactive({
const dialogTitle = ref('edit') // 弹出层标题 pageNo: 1,
const formRef = ref<FormExpose>() // 表单 Ref pageSize: 10,
const detailData = ref() // 详情 Ref code: '',
name: '',
status: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
// 设置标题 /** 查询角色列表 */
const setDialogTile = (type: string) => { const getList = async () => {
dialogTitle.value = t('action.' + type) loading.value = true
actionType.value = type try {
dialogVisible.value = true const data = await RoleApi.getRolePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
} }
// 新增操作 /** 搜索按钮操作 */
const handleCreate = () => { const handleQuery = () => {
setDialogTile('create') queryParams.pageNo = 1
getList()
} }
// 修改操作 /** 重置按钮操作 */
const handleUpdate = async (rowId: number) => { const resetQuery = () => {
setDialogTile('update') queryFormRef.value.resetFields()
// 设置数据 handleQuery()
const res = await RoleApi.getRoleApi(rowId)
unref(formRef)?.setValues(res)
} }
// 详情操作 /** 添加/修改操作 */
const handleDetail = async (rowId: number) => { const formRef = ref()
setDialogTile('detail') const openForm = (type: string, id?: number) => {
// 设置数据 formRef.value.open(type, id)
const res = await RoleApi.getRoleApi(rowId)
detailData.value = res
} }
// 提交按钮 /** 数据权限操作 */
const submitForm = async () => { const menuPermissionFormRef = ref()
const elForm = unref(formRef)?.getElFormRef() const handleScope = async (type: string, row: RoleApi.RoleVO) => {
if (!elForm) return menuPermissionFormRef.value.openForm(type, row)
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as RoleApi.RoleVO
if (actionType.value === 'create') {
await RoleApi.createRoleApi(data)
message.success(t('common.createSuccess'))
} else {
await RoleApi.updateRoleApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
} }
// ========== 数据权限 ========== /** 删除按钮操作 */
const dataScopeForm = reactive({ const handleDelete = async (id: number) => {
id: 0, try {
name: '', // 删除的二次确认
code: '', await message.delConfirm()
dataScope: 0, // 发起删除
checkList: [] await RoleApi.deleteRole(id)
}) message.success(t('common.delSuccess'))
const treeOptions = ref<any[]>([]) // 菜单树形结构 // 刷新列表
const treeRef = ref<InstanceType<typeof ElTree>>() await getList()
const dialogScopeVisible = ref(false) } catch {}
const dialogScopeTitle = ref('数据权限')
const actionScopeType = ref('')
const dataScopeDictDatas = ref()
// 选项
const checkStrictly = ref(true)
const treeNodeAll = ref(false)
// 全选/全不选
const handleCheckedTreeNodeAll = () => {
treeRef.value!.setCheckedNodes(treeNodeAll.value ? treeOptions.value : [])
} }
// 权限操作
const handleScope = async (type: string, row: RoleApi.RoleVO) => { /** 导出按钮操作 */
dataScopeForm.id = row.id const handleExport = async () => {
dataScopeForm.name = row.name try {
dataScopeForm.code = row.code // 导出的二次确认
actionScopeType.value = type await message.exportConfirm()
dialogScopeVisible.value = true // 发起导出
if (type === 'menu') { exportLoading.value = true
const menuRes = await listSimpleMenusApi() const data = await RoleApi.exportRole(queryParams)
treeOptions.value = handleTree(menuRes) download.excel(data, '角色列表.xls')
const role = await PermissionApi.listRoleMenusApi(row.id) } catch {
if (role) { } finally {
role?.forEach((item: any) => { exportLoading.value = false
unref(treeRef)?.setChecked(item, true, false)
})
}
} else if (type === 'data') {
const deptRes = await listSimpleDeptApi()
treeOptions.value = handleTree(deptRes)
const role = await RoleApi.getRoleApi(row.id)
dataScopeForm.dataScope = role.dataScope
if (role.dataScopeDeptIds) {
role.dataScopeDeptIds?.forEach((item: any) => {
unref(treeRef)?.setChecked(item, true, false)
})
}
}
}
// 保存权限
const submitScope = async () => {
if ('data' === actionScopeType.value) {
const data = ref<PermissionApi.PermissionAssignRoleDataScopeReqVO>({
roleId: dataScopeForm.id,
dataScope: dataScopeForm.dataScope,
dataScopeDeptIds:
dataScopeForm.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
? []
: (treeRef.value!.getCheckedKeys(false) as unknown as Array<number>)
})
await PermissionApi.assignRoleDataScopeApi(data.value)
} else if ('menu' === actionScopeType.value) {
const data = ref<PermissionApi.PermissionAssignRoleMenuReqVO>({
roleId: dataScopeForm.id,
menuIds: [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
]
})
await PermissionApi.assignRoleMenuApi(data.value)
} }
message.success(t('common.updateSuccess'))
dialogScopeVisible.value = false
} }
const init = () => {
dataScopeDictDatas.value = getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE) /** 初始化 **/
}
// ========== 初始化 ==========
onMounted(() => { onMounted(() => {
init() getList()
}) })
</script> </script>
<style scoped>
.card {
width: 100%;
max-height: 400px;
overflow-y: scroll;
}
</style>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
// primaryKey: 'id',
// primaryTitle: '角色编号',
// primaryType: 'seq',
action: true,
actionWidth: '400px',
columns: [
{
title: '角色编号',
field: 'id',
table: {
width: 200
}
},
{
title: '角色名称',
field: 'name',
isSearch: true
},
{
title: '角色类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_ROLE_TYPE,
dictClass: 'number',
isForm: false
},
{
title: '角色标识',
field: 'code',
isSearch: true
},
{
title: '显示顺序',
field: 'sort'
},
{
title: t('form.remark'),
field: 'remark',
isTable: false,
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
...@@ -33,23 +33,20 @@ ...@@ -33,23 +33,20 @@
placeholder="请选择文章标签" placeholder="请选择文章标签"
style="width: 380px" style="width: 380px"
> >
<el-option v-for="tag in tags" :key="tag" :label="tag" :value="tag" /> <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> <el-button @click="modelVisible = false">取 消</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts" name="SensitiveWordForm">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SensitiveWordApi from '@/api/system/sensitiveWord' import * as SensitiveWordApi from '@/api/system/sensitiveWord'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
...@@ -69,10 +66,10 @@ const formRules = reactive({ ...@@ -69,10 +66,10 @@ const formRules = reactive({
tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }] tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // 表单 Ref const formRef = ref() // 表单 Ref
const tags = ref([]) // todo @blue-syd:在 openModal 里加载下 const tagList = ref([]) // 标签数组
/** 打开弹窗 */ /** 打开弹窗 */
const openModal = async (type: string, id?: number) => { const open = async (type: string, id?: number) => {
modelVisible.value = true modelVisible.value = true
modelTitle.value = t('action.' + type) modelTitle.value = t('action.' + type)
formType.value = type formType.value = type
...@@ -81,14 +78,15 @@ const openModal = async (type: string, id?: number) => { ...@@ -81,14 +78,15 @@ const openModal = async (type: string, id?: number) => {
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await SensitiveWordApi.getSensitiveWordApi(id) formData.value = await SensitiveWordApi.getSensitiveWord(id)
console.log(formData.value)
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
} }
// 获得 Tag 标签列表
tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
} }
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
...@@ -102,10 +100,10 @@ const submitForm = async () => { ...@@ -102,10 +100,10 @@ const submitForm = async () => {
try { try {
const data = formData.value as unknown as SensitiveWordApi.SensitiveWordVO const data = formData.value as unknown as SensitiveWordApi.SensitiveWordVO
if (formType.value === 'create') { if (formType.value === 'create') {
await SensitiveWordApi.createSensitiveWordApi(data) // TODO @blue-syd:去掉 API 后缀 await SensitiveWordApi.createSensitiveWord(data)
message.success(t('common.createSuccess')) message.success(t('common.createSuccess'))
} else { } else {
await SensitiveWordApi.updateSensitiveWordApi(data) // TODO @blue-syd:去掉 API 后缀 await SensitiveWordApi.updateSensitiveWord(data)
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
} }
modelVisible.value = false modelVisible.value = false
......
<template>
<Dialog title="检测敏感词" v-model="modelVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="80px"
v-loading="formLoading"
>
<el-form-item label="文本" prop="text">
<el-input type="textarea" v-model="formData.text" placeholder="请输入测试文本" />
</el-form-item>
<el-form-item label="标签" prop="tags">
<el-select
v-model="formData.tags"
multiple
filterable
allow-create
placeholder="请选择标签"
style="width: 380px"
>
<el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="submitForm" type="primary" :disabled="formLoading">检 测</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({
text: '',
tags: []
})
const formRules = reactive({
text: [{ required: true, message: '测试文本不能为空', trigger: 'blur' }],
tags: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const tagList = ref([]) // 标签数组
/** 打开弹窗 */
const open = async () => {
modelVisible.value = true
resetForm()
// 获得 Tag 标签列表
tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const form = formData.value as unknown as SensitiveWordApi.SensitiveWordTestReqVO
const data = await SensitiveWordApi.validateText(form)
if (data.length === 0) {
message.success('不包含敏感词!')
return
}
message.warning('包含敏感词:' + data.join(', '))
modelVisible.value = false
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
text: '',
tags: []
}
formRef.value?.resetFields()
}
</script>
<template> <template>
<!-- 搜索 --> <!-- 搜索工作栏 -->
<content-wrap> <ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true"> <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="敏感词" prop="name"> <el-form-item label="敏感词" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
placeholder="请输入敏感词" placeholder="请输入敏感词"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="标签" prop="tag"> <el-form-item label="标签" prop="tag">
...@@ -16,17 +23,19 @@ ...@@ -16,17 +23,19 @@
placeholder="请选择标签" placeholder="请选择标签"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px"
> >
<el-option v-for="tag in tags" :key="tag" :label="tag" :value="tag" /> <el-option v-for="tag in tagList" :key="tag" :label="tag" :value="tag" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable> <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable>
<el-option <el-option
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="parseInt(dict.value)" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="parseInt(dict.value)" :value="dict.value"
class="!w-240px"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
...@@ -38,6 +47,7 @@ ...@@ -38,6 +47,7 @@
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
...@@ -45,7 +55,8 @@ ...@@ -45,7 +55,8 @@
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button <el-button
type="primary" type="primary"
@click="openModal('create')" plain
@click="openForm('create')"
v-hasPermi="['system:sensitive-word:create']" v-hasPermi="['system:sensitive-word:create']"
> >
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
...@@ -59,12 +70,15 @@ ...@@ -59,12 +70,15 @@
> >
<Icon icon="ep:download" class="mr-5px" /> 导出 <Icon icon="ep:download" class="mr-5px" /> 导出
</el-button> </el-button>
<el-button type="warning" plain @click="openTestForm">
<Icon icon="ep:document-checked" class="mr-5px" /> 测试
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</content-wrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<content-wrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" /> <el-table-column label="编号" align="center" prop="id" />
<el-table-column label="敏感词" align="center" prop="name" /> <el-table-column label="敏感词" align="center" prop="name" />
...@@ -77,15 +91,13 @@ ...@@ -77,15 +91,13 @@
<el-table-column label="标签" align="center" prop="tags"> <el-table-column label="标签" align="center" prop="tags">
<template #default="scope"> <template #default="scope">
<el-tag <el-tag
:disable-transitions="true"
:key="index"
v-for="(tag, index) in scope.row.tags"
:index="index"
class="mr-5px" class="mr-5px"
v-for="tag in scope.row.tags"
:key="tag"
:disable-transitions="true"
> >
{{ tag }} {{ tag }}
</el-tag> </el-tag>
&nbsp; &nbsp;
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
...@@ -100,7 +112,7 @@ ...@@ -100,7 +112,7 @@
<el-button <el-button
link link
type="primary" type="primary"
@click="openModal('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['infra:config:update']" v-hasPermi="['infra:config:update']"
> >
编辑 编辑
...@@ -123,17 +135,21 @@ ...@@ -123,17 +135,21 @@
v-model:limit="queryParams.pageSize" v-model:limit="queryParams.pageSize"
@pagination="getList" @pagination="getList"
/> />
</content-wrap> </ContentWrap>
<!-- 表单弹窗:添加/修改 --> <!-- 表单弹窗:添加/修改 -->
<config-form ref="modalRef" @success="getList" /> <SensitiveWordForm ref="formRef" @success="getList" />
<!-- 表单弹窗:测试敏感词 -->
<SensitiveWordTestForm ref="testFormRef" />
</template> </template>
<script setup lang="ts" name="SensitiveWord"> <script setup lang="ts" name="SensitiveWord">
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import * as SensitiveWordApi from '@/api/system/sensitiveWord' import * as SensitiveWordApi from '@/api/system/sensitiveWord'
import ConfigForm from './form.vue' // TODO @blue-syd:组件名不对 import SensitiveWordForm from './SensitiveWordForm.vue'
import SensitiveWordTestForm from './SensitiveWordTestForm.vue'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
...@@ -150,13 +166,13 @@ const queryParams = reactive({ ...@@ -150,13 +166,13 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const tags = ref([]) const tagList = ref([]) // 标签数组
/** 查询参数列表 */ /** 查询参数列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
const data = await SensitiveWordApi.getSensitiveWordPageApi(queryParams) // TODO @blue-syd:去掉 API 后缀哈 const data = await SensitiveWordApi.getSensitiveWordPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
} finally { } finally {
...@@ -177,12 +193,16 @@ const resetQuery = () => { ...@@ -177,12 +193,16 @@ const resetQuery = () => {
} }
/** 添加/修改操作 */ /** 添加/修改操作 */
const modalRef = ref() const formRef = ref()
const openModal = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {
modalRef.value.openModal(type, id) formRef.value.open(type, id)
} }
// TODO @blue-syd:还少一个【测试】按钮的功能,参见 http://dashboard.yudao.iocoder.cn/system/sensitive-word /** 测试敏感词按钮操作 */
const testFormRef = ref()
const openTestForm = () => {
testFormRef.value.open()
}
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
...@@ -190,7 +210,7 @@ const handleDelete = async (id: number) => { ...@@ -190,7 +210,7 @@ const handleDelete = async (id: number) => {
// 删除的二次确认 // 删除的二次确认
await message.delConfirm() await message.delConfirm()
// 发起删除 // 发起删除
await SensitiveWordApi.deleteSensitiveWordApi(id) await SensitiveWordApi.deleteSensitiveWord(id)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// 刷新列表 // 刷新列表
await getList() await getList()
...@@ -204,7 +224,7 @@ const handleExport = async () => { ...@@ -204,7 +224,7 @@ const handleExport = async () => {
await message.exportConfirm() await message.exportConfirm()
// 发起导出 // 发起导出
exportLoading.value = true exportLoading.value = true
const data = await SensitiveWordApi.exportSensitiveWordApi(queryParams) // TODO @blue-syd:去掉 API 后缀哈 const data = await SensitiveWordApi.exportSensitiveWord(queryParams)
download.excel(data, '敏感词.xls') download.excel(data, '敏感词.xls')
} catch { } catch {
} finally { } finally {
...@@ -212,14 +232,10 @@ const handleExport = async () => { ...@@ -212,14 +232,10 @@ const handleExport = async () => {
} }
} }
/** 获得 Tag 标签列表 */
const getTags = async () => {
tags.value = await SensitiveWordApi.getSensitiveWordTagsApi() // TODO @blue-syd:去掉 API 后缀哈
}
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(async () => {
getTags() await getList()
getList() // 获得 Tag 标签列表
tagList.value = await SensitiveWordApi.getSensitiveWordTagList()
}) })
</script> </script>
<template> <template>
<Dialog :title="modelTitle" v-model="modelVisible"> <Dialog :title="modelTitle" v-model="modelVisible">
<el-form ref="formRef" :model="form" :rules="rules" label-width="130px" v-loading="formLoading"> <el-form
<el-form-item label="短信签名" prop="signature"> ref="formRef"
<el-input v-model="form.signature" placeholder="请输入短信签名" /> :model="formData"
</el-form-item> :rules="formRules"
<el-form-item label="渠道编码" prop="code"> label-width="130px"
<el-select v-model="form.code" placeholder="请选择渠道编码" clearable> v-loading="formLoading"
<el-option >
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)" <el-form-item label="短信签名" prop="signature">
:key="dict.value" <el-input v-model="formData.signature" placeholder="请输入短信签名" />
:label="dict.label" </el-form-item>
:value="dict.value" <el-form-item label="渠道编码" prop="code">
/> <el-select v-model="formData.code" placeholder="请选择渠道编码" clearable>
</el-select> <el-option
</el-form-item> v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)"
<el-form-item label="启用状态"> :key="dict.value"
<el-radio-group v-model="form.status"> :label="dict.label"
<el-radio :value="dict.value"
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" />
:key="dict.value" </el-select>
:label="parseInt(dict.value)" </el-form-item>
>{{ dict.label }}</el-radio <el-form-item label="启用状态">
> <el-radio-group v-model="formData.status">
</el-radio-group> <el-radio
</el-form-item> v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
<el-form-item label="备注" prop="remark"> :key="dict.value"
<el-input v-model="form.remark" placeholder="请输入备注" /> :label="dict.value"
</el-form-item> >
<el-form-item label="短信 API 的账号" prop="apiKey"> {{ dict.label }}
<el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" /> </el-radio>
</el-form-item> </el-radio-group>
<el-form-item label="短信 API 的密钥" prop="apiSecret"> </el-form-item>
<el-input v-model="form.apiSecret" placeholder="请输入短信 API 的密钥" /> <el-form-item label="备注" prop="remark">
</el-form-item> <el-input v-model="formData.remark" placeholder="请输入备注" />
<el-form-item label="短信发送回调 URL" prop="callbackUrl"> </el-form-item>
<el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" /> <el-form-item label="短信 API 的账号" prop="apiKey">
</el-form-item> <el-input v-model="formData.apiKey" placeholder="请输入短信 API 的账号" />
</el-form> </el-form-item>
<template #footer> <el-form-item label="短信 API 的密钥" prop="apiSecret">
<div class="dialog-footer"> <el-input v-model="formData.apiSecret" placeholder="请输入短信 API 的密钥" />
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> </el-form-item>
<el-button @click="modelVisible = false">取 消</el-button> <el-form-item label="短信发送回调 URL" prop="callbackUrl">
</div> <el-input v-model="formData.callbackUrl" placeholder="请输入短信发送回调 URL" />
</template> </el-form-item>
</Dialog> </el-form>
</template> <template #footer>
<script setup lang="ts"> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
import { DICT_TYPE, getDictOptions } from '@/utils/dict' <el-button @click="modelVisible = false">取 消</el-button>
import * as SmsChannelApi from '@/api/system/sms/smsChannel' </template>
</Dialog>
const { t } = useI18n() // 国际化 </template>
const message = useMessage() // 消息弹窗 <script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
const modelVisible = ref(false) // 弹窗的是否展示 import * as SmsChannelApi from '@/api/system/sms/smsChannel'
const modelTitle = ref('') // 弹窗的标题 import { CommonStatusEnum } from '@/utils/constants'
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const { t } = useI18n() // 国际化
const formType = ref('') // 表单的类型:create - 新增;update - 修改 const message = useMessage() // 消息弹窗
const form = ref({
id: undefined, const modelVisible = ref(false) // 弹窗的是否展示
signature: '', const modelTitle = ref('') // 弹窗的标题
code: '', const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
status: '', const formType = ref('') // 表单的类型:create - 新增;update - 修改
remark: '', const formData = ref({
apiKey: '', id: undefined,
apiSecret: '', signature: '',
callbackUrl: '' code: '',
}) status: CommonStatusEnum.ENABLE,
const rules = reactive({ remark: '',
signature: [{ required: true, message: '短信签名不能为空', trigger: 'blur' }], apiKey: '',
code: [{ required: true, message: '渠道编码不能为空', trigger: 'blur' }], apiSecret: '',
status: [{ required: true, message: '启用状态不能为空', trigger: 'blur' }], callbackUrl: ''
apiKey: [{ required: true, message: '短信 API 的账号不能为空', trigger: 'blur' }] })
}) const formRules = reactive({
const formRef = ref() // 表单 Ref signature: [{ required: true, message: '短信签名不能为空', trigger: 'blur' }],
code: [{ required: true, message: '渠道编码不能为空', trigger: 'blur' }],
/** 打开弹窗 */ status: [{ required: true, message: '启用状态不能为空', trigger: 'blur' }],
const openModal = async (type: string, id?: number) => { apiKey: [{ required: true, message: '短信 API 的账号不能为空', trigger: 'blur' }]
modelVisible.value = true })
modelTitle.value = t('action.' + type) const formRef = ref() // 表单 Ref
formType.value = type
resetForm() /** 打开弹窗 */
// 修改时,设置数据 const open = async (type: string, id?: number) => {
if (id) { modelVisible.value = true
formLoading.value = true modelTitle.value = t('action.' + type)
try { formType.value = type
form.value = await SmsChannelApi.getSmsChannelApi(id) resetForm()
console.log(form) // 修改时,设置数据
} finally { if (id) {
formLoading.value = false formLoading.value = true
} try {
} formData.value = await SmsChannelApi.getSmsChannel(id)
} console.log(formData)
} finally {
defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 formLoading.value = false
}
/** 提交表单 */ }
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 }
const submitForm = async () => { defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 校验表单
if (!formRef) return /** 提交表单 */
const valid = await formRef.value.validate() const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
if (!valid) return const submitForm = async () => {
// 提交请求 // 校验表单
formLoading.value = true if (!formRef) return
try { const valid = await formRef.value.validate()
const data = unref(formRef)?.formModel as SmsChannelApi.SmsChannelVO if (!valid) return
if (formType.value === 'create') { // 提交请求
await SmsChannelApi.createSmsChannelApi(data) formLoading.value = true
message.success(t('common.createSuccess')) try {
} else { const data = unref(formRef)?.formModel as SmsChannelApi.SmsChannelVO
await SmsChannelApi.updateSmsChannelApi(data) if (formType.value === 'create') {
message.success(t('common.updateSuccess')) await SmsChannelApi.createSmsChannel(data)
} message.success(t('common.createSuccess'))
modelVisible.value = false } else {
// 发送操作成功的事件 await SmsChannelApi.updateSmsChannel(data)
emit('success') message.success(t('common.updateSuccess'))
} finally { }
formLoading.value = false modelVisible.value = false
} // 发送操作成功的事件
} emit('success')
} finally {
/** 重置表单 */ formLoading.value = false
const resetForm = () => { }
form.value = { }
id: undefined,
signature: '', /** 重置表单 */
code: '', const resetForm = () => {
status: '', formData.value = {
remark: '', id: undefined,
apiKey: '', signature: '',
apiSecret: '', code: '',
callbackUrl: '' status: CommonStatusEnum.ENABLE,
} remark: '',
formRef.value?.resetFields() apiKey: '',
} apiSecret: '',
</script> callbackUrl: ''
}
formRef.value?.resetFields()
}
</script>
<template> <template>
<ContentWrap> <ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="短信签名" prop="signature"> <el-form-item label="短信签名" prop="signature">
<el-input <el-input
v-model="queryParams.signature" v-model="queryParams.signature"
...@@ -12,10 +18,10 @@ ...@@ -12,10 +18,10 @@
<el-form-item label="启用状态" prop="status"> <el-form-item label="启用状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable> <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable>
<el-option <el-option
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="parseInt(dict.value)" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="parseInt(dict.value)" :value="dict.value"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
...@@ -34,24 +40,17 @@ ...@@ -34,24 +40,17 @@
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button <el-button
type="primary" type="primary"
@click="openModal('create')" @click="openForm('create')"
v-hasPermi="['system:sms-channel:create']" v-hasPermi="['system:sms-channel:create']"
> >
<Icon icon="ep:plus" class="mr-5px" /> 新增</el-button <Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
> >
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:sms-channel:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" align="center"> <el-table v-loading="loading" :data="list" align="center">
<el-table-column label="编号" align="center" prop="id" /> <el-table-column label="编号" align="center" prop="id" />
<el-table-column label="短信签名" align="center" prop="signature" /> <el-table-column label="短信签名" align="center" prop="signature" />
...@@ -71,18 +70,21 @@ ...@@ -71,18 +70,21 @@
align="center" align="center"
prop="apiKey" prop="apiKey"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
width="180"
/> />
<el-table-column <el-table-column
label="短信 API 的密钥" label="短信 API 的密钥"
align="center" align="center"
prop="apiSecret" prop="apiSecret"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
width="180"
/> />
<el-table-column <el-table-column
label="短信发送回调 URL" label="短信发送回调 URL"
align="center" align="center"
prop="callbackUrl" prop="callbackUrl"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
width="180"
/> />
<el-table-column <el-table-column
label="创建时间" label="创建时间"
...@@ -96,7 +98,7 @@ ...@@ -96,7 +98,7 @@
<el-button <el-button
link link
type="primary" type="primary"
@click="openModal('update', scope.row.id)" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:sms-channel:update']" v-hasPermi="['system:sms-channel:update']"
> >
编辑 编辑
...@@ -120,35 +122,22 @@ ...@@ -120,35 +122,22 @@
@pagination="getList" @pagination="getList"
/> />
</ContentWrap> </ContentWrap>
<!-- 表单弹窗:添加/修改 --> <!-- 表单弹窗:添加/修改 -->
<SmsChannelForm ref="modalRef" @success="getList" /> <SmsChannelForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="SmsChannel"> <script setup lang="ts" name="SmsChannel">
// 业务相关的 import import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
//格式化时间
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
//字典 import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import SmsChannelForm from './SmsChannelForm.vue'
//表单弹窗:添加/修改
import SmsChannelForm from './form.vue'
//下载
// import download from '@/utils/download'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
// 列表的加载中 const loading = ref(false) // 列表的加载中
const loading = ref(true) const total = ref(0) // 列表的总页数
//搜索的表单 const list = ref([]) // 列表的数据
const queryFormRef = ref() const queryFormRef = ref() // 搜索的表单
// 列表的总页数
const total = ref(0)
// 列表的数据
const list = ref([])
//导出的加载中
const exportLoading = ref(false)
//查询参数
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
...@@ -160,9 +149,8 @@ const queryParams = reactive({ ...@@ -160,9 +149,8 @@ const queryParams = reactive({
/** 查询参数列表 */ /** 查询参数列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
// 执行查询
try { try {
const data = await SmsChannelApi.getSmsChannelPageApi(queryParams) const data = await SmsChannelApi.getSmsChannelPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
} finally { } finally {
...@@ -183,26 +171,9 @@ const resetQuery = () => { ...@@ -183,26 +171,9 @@ const resetQuery = () => {
} }
/** 添加/修改操作 */ /** 添加/修改操作 */
const modalRef = ref() const formRef = ref()
const openModal = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {
modalRef.value.openModal(type, id) formRef.value.open(type, id)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
await message.info('该功能目前不支持')
//导出功能先不考虑
// const data = await SmsChannelApi.exportSmsChanelApi(queryParams)
// download.excel(data, '短信渠道.xls')
} catch {
} finally {
exportLoading.value = false
}
} }
/** 删除按钮操作 */ /** 删除按钮操作 */
...@@ -211,7 +182,7 @@ const handleDelete = async (id: number) => { ...@@ -211,7 +182,7 @@ const handleDelete = async (id: number) => {
// 删除的二次确认 // 删除的二次确认
await message.delConfirm() await message.delConfirm()
// 发起删除 // 发起删除
await SmsChannelApi.deleteSmsChannelApi(id) await SmsChannelApi.deleteSmsChannel(id)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// 刷新列表 // 刷新列表
await getList() await getList()
......
<template>
<Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="500" width="800">
<el-descriptions border :column="1">
<el-descriptions-item label="日志主键" min-width="120">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="短信渠道">
{{ channelList.find((channel) => channel.id === detailData.channelId)?.signature }}
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="detailData.channelCode" />
</el-descriptions-item>
<el-descriptions-item label="短信模板">
{{ detailData.templateId }} | {{ detailData.templateCode }}
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="detailData.templateType" />
</el-descriptions-item>
<el-descriptions-item label="API 的模板编号">
{{ detailData.apiTemplateId }}
</el-descriptions-item>
<el-descriptions-item label="用户信息">
{{ detailData.mobile }}
<span v-if="detailData.userType && detailData.userId">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
({{ detailData.userId }})
</span>
</el-descriptions-item>
<el-descriptions-item label="短信内容">
{{ detailData.templateContent }}
</el-descriptions-item>
<el-descriptions-item label="短信参数">
{{ detailData.templateParams }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(detailData.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="发送状态">
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="detailData.sendStatus" />
</el-descriptions-item>
<el-descriptions-item label="发送时间">
{{ formatDate(detailData.sendTime) }}
</el-descriptions-item>
<el-descriptions-item label="发送结果">
{{ detailData.sendCode }} | {{ detailData.sendMsg }}
</el-descriptions-item>
<el-descriptions-item label="API 发送结果">
{{ detailData.apiSendCode }} | {{ detailData.apiSendMsg }}
</el-descriptions-item>
<el-descriptions-item label="API 短信编号">
{{ detailData.apiSerialNo }}
</el-descriptions-item>
<el-descriptions-item label="API 请求编号">
{{ detailData.apiRequestId }}
</el-descriptions-item>
<el-descriptions-item label="API 接收状态">
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="detailData.receiveStatus" />
{{ formatDate(detailData.receiveTime) }}
</el-descriptions-item>
<el-descriptions-item label="API 接收结果">
{{ detailData.apiReceiveCode }} | {{ detailData.apiReceiveMsg }}
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as SmsLogApi from '@/api/system/sms/smsLog'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
const modelVisible = ref(false) // 弹窗的是否展示
const detailLoading = ref(false) // 表单的加载中
const detailData = ref() // 详情数据
const channelList = ref([]) // 短信渠道列表
/** 打开弹窗 */
const open = async (data: SmsLogApi.SmsLogVO) => {
modelVisible.value = true
// 设置数据
detailLoading.value = true
try {
detailData.value = data
} finally {
detailLoading.value = false
}
// 加载渠道列表
channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
</script>
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="100px"
>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="短信渠道" prop="channelId">
<el-select
v-model="queryParams.channelId"
placeholder="请选择短信渠道"
clearable
class="!w-240px"
>
<el-option
v-for="channel in channelList"
:key="channel.id"
:value="channel.id"
:label="
channel.signature +
`【 ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}】`
"
/>
</el-select>
</el-form-item>
<el-form-item label="模板编号" prop="templateId">
<el-input
v-model="queryParams.templateId"
placeholder="请输入模板编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="发送状态" prop="sendStatus">
<el-select
v-model="queryParams.sendStatus"
placeholder="请选择发送状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="发送时间" prop="sendTime">
<el-date-picker
v-model="queryParams.sendTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="接收状态" prop="receiveStatus">
<el-select
v-model="queryParams.receiveStatus"
placeholder="请选择接收状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="接收时间" prop="receiveTime">
<el-date-picker
v-model="queryParams.receiveTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:sms-log:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="手机号" align="center" prop="mobile" width="120">
<template #default="scope">
<div>{{ scope.row.mobile }}</div>
<div v-if="scope.row.userType && scope.row.userId">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
{{ '(' + scope.row.userId + ')' }}
</div>
</template>
</el-table-column>
<el-table-column label="短信内容" align="center" prop="templateContent" width="300" />
<el-table-column label="发送状态" align="center" width="180">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="scope.row.sendStatus" />
<div>{{ formatDate(scope.row.sendTime) }}</div>
</template>
</el-table-column>
<el-table-column label="接收状态" align="center" width="180">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="scope.row.receiveStatus" />
<div>{{ formatDate(scope.row.receiveTime) }}</div>
</template>
</el-table-column>
<el-table-column label="短信渠道" align="center" width="120">
<template #default="scope">
<div>
{{ channelList.find((channel) => channel.id === scope.row.channelId)?.signature }}
</div>
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.channelCode" />
</template>
</el-table-column>
<el-table-column label="模板编号" align="center" prop="templateId" />
<el-table-column label="短信类型" align="center" prop="templateType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="scope.row.templateType" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" class-name="fixed-width">
<template #default="scope">
<el-button
link
type="primary"
@click="openDetail(scope.row)"
v-hasPermi="['system:sms-log:query']"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:详情 -->
<SmsLogDetail ref="detailRef" />
</template>
<script setup lang="ts" name="smsLog">
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import * as SmsLogApi from '@/api/system/sms/smsLog'
import SmsLogDetail from './SmsLogDetail.vue'
const message = useMessage() // 消息弹窗
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
channelId: null,
templateId: null,
mobile: '',
sendStatus: null,
receiveStatus: null,
sendTime: [],
receiveTime: []
})
const exportLoading = ref(false) // 导出的加载中
const channelList = ref([]) // 短信渠道列表
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SmsLogApi.getSmsLogPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await SmsLogApi.exportSmsLog(queryParams)
download.excel(data, '短信日志.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 详情操作 */
const detailRef = ref()
const openDetail = (data: SmsLogApi.SmsLogVO) => {
detailRef.value.open(data)
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 加载渠道列表
channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
})
</script>
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作:导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="exportList('短信日志.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</XTable>
</ContentWrap>
<XModal id="smsLog" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<XButton :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="SmsLog">
import { allSchemas } from './sms.log.data'
import * as SmsLoglApi from '@/api/system/sms/smsLog'
const { t } = useI18n() // 国际化
// 列表相关的变量
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsLoglApi.getSmsLogPageApi,
exportListApi: SmsLoglApi.exportSmsLogApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref(t('action.detail')) // 弹出层标题
const actionType = ref('') // 操作按钮的类型
// ========== 详情相关 ==========
const detailData = ref() // 详情 Ref
const handleDetail = (row: SmsLoglApi.SmsLogVO) => {
// 设置数据
actionType.value = 'detail'
detailData.value = row
dialogVisible.value = true
}
</script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
const { t } = useI18n() // 国际化
const authorizedGrantOptions = getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'id',
primaryTitle: '日志编号',
action: true,
columns: [
{
title: '手机号',
field: 'mobile',
isSearch: true
},
{
title: '短信内容',
field: 'templateContent'
},
{
title: '模板编号',
field: 'templateId',
isSearch: true
},
{
title: '短信渠道',
field: 'channelId',
// dictType: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE,
// dictClass: 'number',
isSearch: true,
// table: {
// component: 'Select',
componentProps: {
options: authorizedGrantOptions
// multiple: false,
// filterable: true
}
// }
},
{
title: '发送状态',
field: 'sendStatus',
dictType: DICT_TYPE.SYSTEM_SMS_SEND_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '发送时间',
field: 'sendTime',
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: '短信类型',
field: 'templateType',
dictType: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
dictClass: 'number',
isSearch: true
},
{
title: '接收状态',
field: 'receiveStatus',
dictType: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: '接收时间',
field: 'receiveTime',
formatter: 'formatDate',
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate'
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
<template>
<ContentWrap>
<!-- 列表 -->
<XTable @register="registerTable">
<!-- 操作:新增 -->
<template #toolbar_buttons>
<XButton
type="primary"
preIcon="ep:zoom-in"
:title="t('action.add')"
v-hasPermi="['system:sms-channel:create']"
@click="handleCreate()"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton
preIcon="ep:cpu"
:title="t('action.test')"
v-hasPermi="['system:sms-template:send-sms']"
@click="handleSendSms(row)"
/>
<!-- 操作:修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:sms-template:update']"
@click="handleUpdate(row.id)"
/>
<!-- 操作:详情 -->
<XTextButton
preIcon="ep:view"
:title="t('action.detail')"
v-hasPermi="['system:sms-template:query']"
@click="handleDetail(row.id)"
/>
<!-- 操作:删除 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sms-template:delete']"
@click="deleteData(row.id)"
/>
</template>
</XTable>
</ContentWrap>
<XModal id="smsTemplate" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailData"
/>
<!-- 操作按钮 -->
<template #footer>
<!-- 按钮:保存 -->
<XButton
v-if="['create', 'update'].includes(actionType)"
type="primary"
:title="t('action.save')"
:loading="actionLoading"
@click="submitForm()"
/>
<!-- 按钮:关闭 -->
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" />
</template>
</XModal>
<XModal id="sendTest" v-model="sendVisible" title="测试">
<el-form :model="sendSmsForm" :rules="sendSmsRules" label-width="200px" label-position="top">
<el-form-item label="模板内容" prop="content">
<el-input
v-model="sendSmsForm.content"
type="textarea"
placeholder="请输入模板内容"
readonly
/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item
v-for="param in sendSmsForm.params"
:key="param"
:label="'参数 {' + param + '}'"
:prop="'templateParams.' + param"
>
<el-input
v-model="sendSmsForm.templateParams[param]"
:placeholder="'请输入 ' + param + ' 参数'"
/>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<XButton
type="primary"
:title="t('action.test')"
:loading="actionLoading"
@click="sendSmsTest()"
/>
<XButton :title="t('dialog.close')" @click="sendVisible = false" />
</template>
</XModal>
</template>
<script setup lang="ts" name="SmsTemplate">
import type { FormExpose } from '@/components/Form'
// 业务相关的 import
import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
import { rules, allSchemas } from './sms.template.data'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsTemplateApi.getSmsTemplatePageApi,
deleteApi: SmsTemplateApi.deleteSmsTemplateApi
})
// 弹窗相关的变量
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionType = ref('') // 操作按钮的类型
const actionLoading = ref(false) // 按钮 Loading
const formRef = ref<FormExpose>() // 表单 Ref
const detailData = ref() // 详情 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
// 设置数据
const res = await SmsTemplateApi.getSmsTemplateApi(rowId)
unref(formRef)?.setValues(res)
}
// 详情操作
const handleDetail = async (rowId: number) => {
setDialogTile('detail')
// 设置数据
const res = await SmsTemplateApi.getSmsTemplateApi(rowId)
detailData.value = res
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as SmsTemplateApi.SmsTemplateVO
if (actionType.value === 'create') {
await SmsTemplateApi.createSmsTemplateApi(data)
message.success(t('common.createSuccess'))
} else {
await SmsTemplateApi.updateSmsTemplateApi(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
}
// ========== 测试相关 ==========
const sendSmsForm = ref({
content: '',
params: {},
mobile: '',
templateCode: '',
templateParams: {}
})
const sendSmsRules = ref({
mobile: [{ required: true, message: '手机不能为空', trigger: 'blur' }],
templateCode: [{ required: true, message: '模版编号不能为空', trigger: 'blur' }],
templateParams: {}
})
const sendVisible = ref(false)
const handleSendSms = (row: any) => {
sendSmsForm.value.content = row.content
sendSmsForm.value.params = row.params
sendSmsForm.value.templateCode = row.code
sendSmsForm.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = undefined
return obj
}, {})
sendSmsRules.value.templateParams = row.params.reduce(function (obj, item) {
obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'change' }
return obj
}, {})
sendVisible.value = true
}
const sendSmsTest = async () => {
const data: SmsTemplateApi.SendSmsReqVO = {
mobile: sendSmsForm.value.mobile,
templateCode: sendSmsForm.value.templateCode,
templateParams: sendSmsForm.value.templateParams as unknown as Map<string, Object>
}
const res = await SmsTemplateApi.sendSmsApi(data)
if (res) {
message.success('提交发送成功!发送结果,见发送日志编号:' + res)
}
sendVisible.value = false
}
</script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
import * as smsApi from '@/api/system/sms/smsChannel'
const { t } = useI18n() // 国际化
const tenantPackageOption = []
const getTenantPackageOptions = async () => {
const res = await smsApi.getSimpleSmsChannels()
console.log(res, 'resresres')
res.forEach((tenantPackage: TenantPackageVO) => {
tenantPackageOption.push({
key: tenantPackage.id,
value: tenantPackage.id,
label: tenantPackage.signature
})
})
}
getTenantPackageOptions()
// 表单校验
export const rules = reactive({
type: [required],
status: [required],
code: [required],
name: [required],
content: [required],
apiTemplateId: [required],
channelId: [required]
})
// CrudSchema
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'id',
primaryTitle: '模板编号',
action: true,
actionWidth: '280',
columns: [
{
title: '短信渠道编码',
field: 'channelId',
isSearch: false,
isForm: true,
isTable: false,
form: {
component: 'Select',
componentProps: {
options: tenantPackageOption
}
}
},
{
title: '模板编码',
field: 'code',
isSearch: true
},
{
title: '模板名称',
field: 'name',
isSearch: true
},
{
title: '模板内容',
field: 'content'
},
{
title: '短信 API 的模板编号',
field: 'apiTemplateId',
isSearch: true
},
{
title: '短信类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
dictClass: 'number',
isSearch: true,
table: {
width: 80
}
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
table: {
width: 80
}
},
{
title: t('form.remark'),
field: 'remark',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
<template>
<Dialog :title="modelTitle" v-model="modelVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="140px"
v-loading="formLoading"
>
<el-form-item label="短信渠道编号" prop="channelId">
<el-select v-model="formData.channelId" placeholder="请选择短信渠道编号">
<el-option
v-for="channel in channelList"
:key="channel.id"
:value="channel.id"
:label="
channel.signature +
`【 ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}】`
"
/>
</el-select>
</el-form-item>
<el-form-item label="短信类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择短信类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="模板编号" prop="code">
<el-input v-model="formData.code" placeholder="请输入模板编号" />
</el-form-item>
<el-form-item label="模板名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入模板名称" />
</el-form-item>
<el-form-item label="模板内容" prop="content">
<el-input type="textarea" v-model="formData.content" placeholder="请输入模板内容" />
</el-form-item>
<el-form-item label="开启状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="parseInt(dict.value as string)"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="短信 API 模板编号" prop="apiTemplateId">
<el-input v-model="formData.apiTemplateId" placeholder="请输入短信 API 的模板编号" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import { CommonStatusEnum } from '@/utils/constants'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型
const formData = ref<SmsTemplateApi.SmsTemplateVO>({
id: null,
type: null,
status: CommonStatusEnum.ENABLE,
code: '',
name: '',
content: '',
remark: '',
apiTemplateId: '',
channelId: null
})
const formRules = reactive({
type: [{ required: true, message: '短信类型不能为空', trigger: 'change' }],
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }],
code: [{ required: true, message: '模板编码不能为空', trigger: 'blur' }],
name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
content: [{ required: true, message: '模板内容不能为空', trigger: 'blur' }],
apiTemplateId: [{ required: true, message: '短信 API 的模板编号不能为空', trigger: 'blur' }],
channelId: [{ required: true, message: '短信渠道编号不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
const channelList = ref<SmsChannelApi.SmsChannelVO[]>([]) // 短信渠道列表
const open = async (type: string, id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await SmsTemplateApi.getSmsTemplate(id)
} finally {
formLoading.value = false
}
}
// 加载渠道列表
channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
formLoading.value = true
try {
const data = formData.value as SmsTemplateApi.SmsTemplateVO
if (formType.value === 'create') {
await SmsTemplateApi.createSmsTemplate(data)
message.success(t('common.createSuccess'))
} else {
await SmsTemplateApi.updateSmsTemplate(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: null,
type: null,
status: CommonStatusEnum.ENABLE,
code: '',
name: '',
content: '',
remark: '',
apiTemplateId: '',
channelId: null
}
formRef.value?.resetFields()
}
</script>
<template>
<Dialog title="测试" v-model="modelVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="140px"
v-loading="formLoading"
>
<el-form-item label="模板内容" prop="content">
<el-input
v-model="formData.content"
type="textarea"
placeholder="请输入模板内容"
readonly
/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item
v-for="param in formData.params"
:key="param"
:label="'参数 {' + param + '}'"
:prop="'templateParams.' + param"
>
<el-input
v-model="formData.templateParams[param]"
:placeholder="'请输入 ' + param + ' 参数'"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
// 发送短信表单相关
const formData = ref({
content: '',
params: {},
mobile: '',
templateCode: '',
templateParams: new Map()
})
const formRules = reactive({
mobile: [{ required: true, message: '手机不能为空', trigger: 'blur' }],
templateCode: [{ required: true, message: '模版编码不能为空', trigger: 'blur' }],
templateParams: {}
})
const formRef = ref() // 表单 Ref
const open = async (id: number) => {
modelVisible.value = true
resetForm()
// 设置数据
formLoading.value = true
try {
const data = await SmsTemplateApi.getSmsTemplate(id)
// 设置动态表单
formData.value.content = data.content
formData.value.params = data.params
formData.value.templateCode = data.code
formData.value.templateParams = data.params.reduce((obj, item) => {
obj[item] = '' // 给每个动态属性赋值,避免无法读取
return obj
}, {})
formRules.templateParams = data.params.reduce((obj, item) => {
obj[item] = { required: true, message: '参数 ' + item + ' 不能为空', trigger: 'blur' }
return obj
}, {})
} finally {
formLoading.value = false
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as SmsTemplateApi.SendSmsReqVO
const logId = await SmsTemplateApi.sendSms(data)
if (logId) {
message.success('提交发送成功!发送结果,见发送日志编号:' + logId)
}
modelVisible.value = false
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: '',
params: {},
mobile: '',
templateCode: '',
templateParams: new Map()
}
formRef.value?.resetFields()
}
</script>
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="150px"
>
<el-form-item label="短信类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择短信类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="开启状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择开启状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="模板编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入模板编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="短信 API 的模板编号" prop="apiTemplateId">
<el-input
v-model="queryParams.apiTemplateId"
placeholder="请输入短信 API 的模板编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="短信渠道" prop="channelId">
<el-select
v-model="queryParams.channelId"
placeholder="请选择短信渠道"
clearable
class="!w-240px"
>
<el-option
v-for="channel in channelList"
:key="channel.id"
:value="channel.id"
:label="
channel.signature +
`【 ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}】`
"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
style="width: 240px"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:sms-template:create']"
>
<Icon icon="ep:plus" class="mr-5px" />新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:sms-template:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" align="center">
<el-table-column
label="模板编码"
align="center"
prop="code"
width="120"
:show-overflow-tooltip="true"
/>
<el-table-column
label="模板名称"
align="center"
prop="name"
width="120"
:show-overflow-tooltip="true"
/>
<el-table-column
label="模板内容"
align="center"
prop="content"
width="200"
:show-overflow-tooltip="true"
/>
<el-table-column label="短信类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column
label="短信 API 的模板编号"
align="center"
prop="apiTemplateId"
width="200"
:show-overflow-tooltip="true"
/>
<el-table-column label="短信渠道" align="center" width="120">
<template #default="scope">
<div>
{{ channelList.find((channel) => channel.id === scope.row.channelId)?.signature }}
</div>
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.channelCode" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" width="210" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:sms-template:update']"
>
修改
</el-button>
<el-button
link
type="primary"
@click="openSendForm(scope.row.id)"
v-hasPermi="['system:sms-template:send-sms']"
>
测试
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:sms-template:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<SmsTemplateForm ref="formRef" @success="getList" />
<!-- 表单弹窗:测试发送 -->
<SmsTemplateSendForm ref="sendFormRef" />
</template>
<script setup lang="ts" name="SmsTemplate">
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import download from '@/utils/download'
import SmsTemplateForm from './SmsTemplateForm.vue'
import SmsTemplateSendForm from './SmsTemplateSendForm.vue'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: null,
status: null,
code: '',
content: '',
apiTemplateId: '',
channelId: null,
createTime: []
})
const exportLoading = ref(false) // 导出的加载中
const channelList = ref<SmsChannelApi.SmsChannelVO[]>([]) // 短信渠道列表
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SmsTemplateApi.getSmsTemplatePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 发送短信按钮 */
const sendFormRef = ref()
const openSendForm = (id: number) => {
sendFormRef.value.open(id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsTemplateApi.deleteSmsTemplate(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await SmsTemplateApi.exportSmsTemplate(queryParams)
download.excel(data, '短信模板.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 加载渠道列表
channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
})
</script>
<template>
<Dialog :title="modelTitle" v-model="modelVisible" width="50%">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="80px"
v-loading="formLoading"
>
<el-form-item label="租户名" prop="name">
<el-input v-model="formData.name" placeholder="请输入租户名" />
</el-form-item>
<el-form-item label="租户套餐" prop="packageId">
<el-select v-model="formData.packageId" placeholder="请选择租户套餐" clearable>
<el-option
v-for="item in packageList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系手机" prop="contactMobile">
<el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
</el-form-item>
<el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名称" />
</el-form-item>
<el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
<el-input
v-model="formData.password"
placeholder="请输入用户密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="账号额度" prop="accountCount">
<el-input-number
v-model="formData.accountCount"
placeholder="请输入账号额度"
controls-position="right"
:min="0"
/>
</el-form-item>
<el-form-item label="过期时间" prop="expireTime">
<el-date-picker
clearable
v-model="formData.expireTime"
type="date"
value-format="x"
placeholder="请选择过期时间"
/>
</el-form-item>
<el-form-item label="绑定域名" prop="domain">
<el-input v-model="formData.domain" placeholder="请输入绑定域名" />
</el-form-item>
<el-form-item label="租户状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="modelVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as TenantApi from '@/api/system/tenant'
import { CommonStatusEnum } from '@/utils/constants'
import * as TenantPackageApi from '@/api/system/tenantPackage'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const modelVisible = ref(false) // 弹窗的是否展示
const modelTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: undefined,
packageId: undefined,
contactName: undefined,
contactMobile: undefined,
accountCount: undefined,
expireTime: undefined,
domain: undefined,
status: CommonStatusEnum.ENABLE
})
const formRules = reactive({
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }],
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
domain: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const packageList = ref([]) // 租户套餐
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
modelVisible.value = true
modelTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await TenantApi.getTenant(id)
} finally {
formLoading.value = false
}
}
// 加载套餐列表
packageList.value = await TenantPackageApi.getTenantPackageList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TenantApi.TenantVO
if (formType.value === 'create') {
await TenantApi.createTenant(data)
message.success(t('common.createSuccess'))
} else {
await TenantApi.updateTenant(data)
message.success(t('common.updateSuccess'))
}
modelVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
packageId: undefined,
contactName: undefined,
contactMobile: undefined,
accountCount: undefined,
expireTime: undefined,
domain: undefined,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
</script>
<template> <template>
<!-- 搜索 -->
<ContentWrap> <ContentWrap>
<!-- 列表 --> <el-form
<XTable @register="registerTable"> class="-mb-15px"
<template #toolbar_buttons> :model="queryParams"
<!-- 操作:新增 --> ref="queryFormRef"
<XButton :inline="true"
type="primary" label-width="68px"
preIcon="ep:zoom-in" >
:title="t('action.add')" <el-form-item label="租户名" prop="name">
v-hasPermi="['system:tenant:create']" <el-input
@click="handleCreate()" v-model="queryParams.name"
/> placeholder="请输入租户名"
<XButton clearable
type="warning" @keyup.enter="handleQuery"
preIcon="ep:download" class="!w-240px"
:title="t('action.export')"
v-hasPermi="['system:tenant:export']"
@click="exportList('租户列表.xls')"
/> />
</template> </el-form-item>
<template #accountCount_default="{ row }"> <el-form-item label="联系人" prop="contactName">
<el-tag> {{ row.accountCount }} </el-tag> <el-input
</template> v-model="queryParams.contactName"
<template #packageId_default="{ row }"> placeholder="请输入联系人"
<el-tag v-if="row.packageId === 0" type="danger">系统租户</el-tag> clearable
<el-tag v-else type="success"> {{ getPackageName(row.packageId) }} </el-tag> @keyup.enter="handleQuery"
</template> class="!w-240px"
<template #actionbtns_default="{ row }">
<!-- 操作:修改 -->
<XTextButton
preIcon="ep:edit"
:title="t('action.edit')"
v-hasPermi="['system:tenant:update']"
@click="handleUpdate(row.id)"
/> />
<!-- 操作:详情 --> </el-form-item>
<XTextButton <el-form-item label="联系手机" prop="contactMobile">
preIcon="ep:view" <el-input
:title="t('action.detail')" v-model="queryParams.contactMobile"
v-hasPermi="['system:tenant:update']" placeholder="请输入联系手机"
@click="handleDetail(row.id)" clearable
@keyup.enter="handleQuery"
class="!w-240px"
/> />
<!-- 操作:删除 --> </el-form-item>
<XTextButton <el-form-item label="租户状态" prop="status">
preIcon="ep:delete" <el-select
:title="t('action.del')" v-model="queryParams.status"
v-hasPermi="['system:tenant:delete']" placeholder="请选择租户状态"
@click="deleteData(row.id)" 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"
/> />
</template> </el-form-item>
</XTable>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['system:tenant:create']">
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:tenant:export']"
>
<Icon icon="ep:download" class="mr-5px" />
导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap> </ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) --> <!-- 列表 -->
<Form <ContentWrap>
v-if="['create', 'update'].includes(actionType)" <el-table v-loading="loading" :data="list" align="center">
:schema="allSchemas.formSchema" <el-table-column label="租户编号" align="center" prop="id" />
:rules="rules" <el-table-column label="租户名" align="center" prop="name" />
ref="formRef" <el-table-column label="租户套餐" align="center" prop="packageId">
/> <template #default="scope">
<!-- 对话框(详情) --> <el-tag v-if="scope.row.packageId === 0" type="danger">系统租户</el-tag>
<Descriptions <template v-else v-for="item in packageList">
v-if="actionType === 'detail'" <el-tag type="success" :key="item.id" v-if="item.id === scope.row.packageId"
:schema="allSchemas.detailSchema" >{{ item.name }}
:data="detailData" </el-tag>
> </template>
<template #packageId="{ row }"> </template>
<el-tag v-if="row.packageId === 0" type="danger">系统租户</el-tag> </el-table-column>
<el-tag v-else type="success"> {{ getPackageName(row.packageId) }} </el-tag> <el-table-column label="联系人" align="center" prop="contactName" />
</template> <el-table-column label="联系手机" align="center" prop="contactMobile" />
</Descriptions> <el-table-column label="账号额度" align="center" prop="accountCount">
<!-- 操作按钮 --> <template #default="scope">
<template #footer> <el-tag>{{ scope.row.accountCount }}</el-tag>
<!-- 按钮:保存 --> </template>
<XButton </el-table-column>
v-if="['create', 'update'].includes(actionType)" <el-table-column
type="primary" label="过期时间"
:title="t('action.save')" align="center"
:loading="actionLoading" prop="expireTime"
@click="submitForm()" width="180"
:formatter="dateFormatter"
/>
<el-table-column label="绑定域名" align="center" prop="domain" width="180" />
<el-table-column label="租户状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/> />
<!-- 按钮:关闭 --> <el-table-column label="操作" align="center" min-width="110" fixed="right">
<XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> <template #default="scope">
</template> <el-button
</XModal> link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<TenantForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts" name="Tenant"> <script setup lang="ts" name="Tenant">
import type { FormExpose } from '@/components/Form' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as TenantApi from '@/api/system/tenant' import * as TenantApi from '@/api/system/tenant'
import { rules, allSchemas, tenantPackageOption } from './tenant.data' import * as TenantPackageApi from '@/api/system/tenantPackage'
import TenantForm from './form.vue'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
// 列表相关的变量 const { t } = useI18n() // 国际化
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas, const loading = ref(true) // 列表的加载中
getListApi: TenantApi.getTenantPageApi, const total = ref(0) // 列表的总页数
deleteApi: TenantApi.deleteTenantApi, const list = ref([]) // 列表的数据
exportListApi: TenantApi.exportTenantApi const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
contactName: undefined,
contactMobile: undefined,
status: undefined,
createTime: []
}) })
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const packageList = ref([]) //租户套餐列表
const actionLoading = ref(false) // 遮罩层 /** 查询参数列表 */
const actionType = ref('') // 操作按钮的类型 const getList = async () => {
const dialogVisible = ref(false) // 是否显示弹出层 loading.value = true
const dialogTitle = ref('edit') // 弹出层标题 try {
const formRef = ref<FormExpose>() // 表单 Ref const data = await TenantApi.getTenantPage(queryParams)
const detailData = ref() // 详情 Ref list.value = data.list
const getPackageName = (packageId: number) => { total.value = data.total
for (let item of tenantPackageOption) { } finally {
if (item.value === packageId) { loading.value = false
return item.label
}
} }
return '未知套餐'
} }
// 设置标题 /** 搜索按钮操作 */
const setDialogTile = (type: string) => { const handleQuery = () => {
dialogTitle.value = t('action.' + type) queryParams.pageNo = 1
actionType.value = type getList()
dialogVisible.value = true
} }
// 新增操作 /** 重置按钮操作 */
const handleCreate = async () => { const resetQuery = () => {
// 重置表单 queryFormRef.value.resetFields()
setDialogTile('create') handleQuery()
await nextTick()
console.log(allSchemas.formSchema, 'allSchemas.formSchema')
if (allSchemas.formSchema[4].field !== 'username') {
unref(formRef)?.addSchema(
{
field: 'username',
label: '用户名称',
component: 'Input'
},
0
)
unref(formRef)?.addSchema(
{
field: 'password',
label: '用户密码',
component: 'InputPassword'
},
1
)
}
} }
// 修改操作 /** 添加/修改操作 */
const handleUpdate = async (rowId: number) => { const formRef = ref()
setDialogTile('update') const openForm = (type: string, id?: number) => {
await nextTick() formRef.value.open(type, id)
unref(formRef)?.delSchema('username')
unref(formRef)?.delSchema('password')
// 设置数据
const res = await TenantApi.getTenantApi(rowId)
unref(formRef)?.setValues(res)
} }
// 详情操作 /** 删除按钮操作 */
const handleDetail = async (rowId: number) => { const handleDelete = async (id: number) => {
// 设置数据 try {
const res = await TenantApi.getTenantApi(rowId) // 删除的二次确认
detailData.value = res await message.delConfirm()
setDialogTile('detail') // 发起删除
await TenantApi.deleteTenant(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
} }
// 提交按钮 /** 导出按钮操作 */
const submitForm = async () => { const handleExport = async () => {
const elForm = unref(formRef)?.getElFormRef() try {
if (!elForm) return // 导出的二次确认
elForm.validate(async (valid) => { await message.exportConfirm()
if (valid) { // 发起导出
actionLoading.value = true exportLoading.value = true
// 提交请求 const data = await TenantApi.exportTenant(queryParams)
try { download.excel(data, '租户列表.xls')
const data = unref(formRef)?.formModel as TenantApi.TenantVO } catch {
if (actionType.value === 'create') { } finally {
await TenantApi.createTenantApi(data) exportLoading.value = false
message.success(t('common.createSuccess')) }
} else {
await TenantApi.updateTenantApi(data)
message.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
} finally {
actionLoading.value = false
// 刷新列表
await reload()
}
}
})
} }
/** 初始化 **/
onMounted(async () => {
await getList()
packageList.value = await TenantPackageApi.getTenantPackageList()
})
</script> </script>
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
import { getTenantPackageList, TenantPackageVO } from '@/api/system/tenantPackage'
import { ComponentOptions } from '@/types/components'
const { t } = useI18n() // 国际化
export const tenantPackageOption: ComponentOptions[] = []
const getTenantPackageOptions = async () => {
const res = await getTenantPackageList()
res.forEach((tenantPackage: TenantPackageVO) => {
tenantPackageOption.push({
key: tenantPackage.id,
value: tenantPackage.id,
label: tenantPackage.name
})
})
return tenantPackageOption
}
getTenantPackageOptions()
const validateName = (rule: any, value: any, callback: any) => {
const reg = /^[a-zA-Z0-9]{4,30}$/
if (value === '') {
callback(new Error('请输入用户名称'))
} else {
console.log(reg.test(rule), 'reg.test(rule)')
if (!reg.test(value)) {
callback(new Error('用户名称由 数字、字母 组成'))
} else {
callback()
}
}
}
const validateMobile = (rule: any, value: any, callback: any) => {
const reg = /^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/
if (value === '') {
callback(new Error('请输入联系手机'))
} else {
if (!reg.test(value)) {
callback(new Error('请输入正确的手机号'))
} else {
callback()
}
}
}
// 表单校验
export const rules = reactive({
name: [required],
packageId: [required],
contactName: [required],
contactMobile: [
required,
{
validator: validateMobile,
trigger: 'blur'
}
],
accountCount: [required],
expireTime: [required],
username: [
required,
{
min: 4,
max: 30,
trigger: 'blur',
message: '用户名称长度为 4-30 个字符'
},
{ validator: validateName, trigger: 'blur' }
],
password: [
required,
{
min: 4,
max: 16,
trigger: 'blur',
message: '密码长度为 4-16 位'
}
],
domain: [required],
status: [required]
})
// CrudSchema.
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryTitle: '租户编号',
primaryType: 'id',
action: true,
columns: [
{
title: '租户名称',
field: 'name',
isSearch: true
},
{
title: '租户套餐',
field: 'packageId',
table: {
slots: {
default: 'packageId_default'
}
},
form: {
component: 'Select',
componentProps: {
options: tenantPackageOption
}
}
},
{
title: '联系人',
field: 'contactName',
isSearch: true
},
{
title: '联系手机',
field: 'contactMobile',
isSearch: true
},
{
title: '用户名称',
field: 'username',
isTable: false,
isDetail: false
},
{
title: '用户密码',
field: 'password',
isTable: false,
isDetail: false,
form: {
component: 'InputPassword'
}
},
{
title: '账号额度',
field: 'accountCount',
table: {
slots: {
default: 'accountCount_default'
}
},
form: {
component: 'InputNumber'
}
},
{
title: '过期时间',
field: 'expireTime',
formatter: 'formatDate',
form: {
component: 'DatePicker',
componentProps: {
type: 'datetime',
valueFormat: 'x'
}
}
},
{
title: '绑定域名',
field: 'domain'
},
{
title: '租户状态',
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true
},
{
title: t('table.createTime'),
field: 'createTime',
formatter: 'formatDate',
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
...@@ -71,7 +71,7 @@ import type { ElTree } from 'element-plus' ...@@ -71,7 +71,7 @@ import type { ElTree } from 'element-plus'
// 业务相关的 import // 业务相关的 import
import { rules, allSchemas } from './tenantPackage.data' import { rules, allSchemas } from './tenantPackage.data'
import * as TenantPackageApi from '@/api/system/tenantPackage' import * as TenantPackageApi from '@/api/system/tenantPackage'
import { listSimpleMenusApi } from '@/api/system/menu' import { getSimpleMenusList } from '@/api/system/menu'
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
...@@ -102,7 +102,7 @@ const validateCategory = (rule: any, value: any, callback: any) => { ...@@ -102,7 +102,7 @@ const validateCategory = (rule: any, value: any, callback: any) => {
rules.menuIds = [{ required: true, validator: validateCategory, trigger: 'blur' }] rules.menuIds = [{ required: true, validator: validateCategory, trigger: 'blur' }]
const getTree = async () => { const getTree = async () => {
const res = await listSimpleMenusApi() const res = await getSimpleMenusList()
menuOptions.value = handleTree(res) menuOptions.value = handleTree(res)
} }
......
<template>
<!-- 添加或修改参数配置对话框 -->
<el-dialog
:title="title"
:modelValue="modelValue"
width="600px"
append-to-body
@close="closeDialog"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入用户昵称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
node-key="id"
v-model="formData.deptId"
:data="deptOptions"
:props="defaultProps"
check-strictly
placeholder="请选择归属部门"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
<el-input
v-model="formData.password"
placeholder="请输入用户密码"
type="password"
show-password
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="formData.sex" placeholder="请选择">
<el-option
v-for="dict in sexDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="formData.postIds" multiple placeholder="请选择">
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { PostVO } from '@/api/system/post'
import { createUserApi, updateUserApi } from '@/api/system/user'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { defaultProps } from '@/utils/tree'
import { ElForm, FormItemRule } from 'element-plus'
import { Arrayable } from 'element-plus/es/utils'
type Form = InstanceType<typeof ElForm>
interface Props {
deptOptions?: Tree[]
postOptions?: PostVO[] //岗位列表
modelValue: boolean
formInitValue?: Recordable & Partial<typeof initParams>
}
const props = withDefaults(defineProps<Props>(), {
deptOptions: () => [],
postOptions: () => [],
modelValue: false,
formInitValue: () => ({})
})
const emits = defineEmits(['update:modelValue', 'success'])
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 弹出层标题
const title = computed(() => {
return formData.value?.id ? '修改用户' : '添加用户'
})
// 性别字典
const sexDictDatas = getDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
// 表单初始化参数
const initParams = {
nickname: '',
deptId: '',
mobile: '',
email: '',
id: undefined,
username: '',
password: '',
sex: 1,
postIds: [],
remark: '',
status: '0',
roleIds: []
}
// 校验规则
const rules = {
username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
nickname: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }],
email: [
{
type: 'email',
message: "'请输入正确的邮箱地址",
trigger: ['blur', 'change']
}
],
mobile: [
{
pattern: /^(?:(?:\+|00)86)?1(?:3[\d]|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8[\d]|9[189])\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
]
} as Partial<Record<string, Arrayable<FormItemRule>>>
const formRef = ref<Form | null>()
const formData = ref<Recordable>({ ...initParams })
watch(
() => props.formInitValue,
(val) => {
formData.value = { ...val }
},
{ deep: true }
)
const resetForm = () => {
let form = formRef?.value
if (!form) return
formData.value = { ...initParams }
form && (form as Form).resetFields()
}
const closeDialog = () => {
emits('update:modelValue', false)
}
// 操作成功
const operateOk = () => {
emits('success', true)
closeDialog()
}
const submitForm = () => {
let form = formRef.value as Form
form.validate(async (valid) => {
let data = formData.value
if (valid) {
try {
if (data?.id !== undefined) {
await updateUserApi(data)
message.success(t('common.updateSuccess'))
operateOk()
} else {
await createUserApi(data)
message.success(t('common.createSuccess'))
operateOk()
}
} catch (err) {
console.error(err)
}
}
})
}
const cancel = () => {
closeDialog()
}
defineExpose({
resetForm
})
</script>
<template>
<el-dialog
:title="upload.title"
:modelValue="modelValue"
width="400px"
append-to-body
@close="closeDialog"
>
<el-upload
ref="uploadRef"
accept=".xlsx, .xls"
:limit="1"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-exceed="handleExceed"
:on-error="excelUploadError"
:auto-upload="false"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" /> 是否更新已经存在的用户数据
</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link
type="primary"
:underline="false"
style="font-size: 12px; vertical-align: baseline"
@click="importTemplate"
>下载模板</el-link
>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { importUserTemplateApi } from '@/api/system/user'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
interface Props {
modelValue: boolean
}
// const props =
withDefaults(defineProps<Props>(), {
modelValue: false
})
const emits = defineEmits(['update:modelValue', 'success'])
const message = useMessage() // 消息弹窗
const uploadRef = ref()
// 用户导入参数
const upload = reactive({
// // 是否显示弹出层(用户导入)
// open: false,
// 弹出层标题(用户导入)
title: '用户导入',
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
},
// 上传的地址
url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import'
})
// 文件上传中处理
const handleFileUploadProgress = () => {
upload.isUploading = true
}
// 文件上传成功处理
const handleFileSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
return
}
upload.isUploading = false
uploadRef.value?.clearFiles()
// 拼接提示语
const data = response.data
let text = '上传成功数量:' + data.createUsernames.length + ';'
for (let username of data.createUsernames) {
text += '< ' + username + ' >'
}
text += '更新成功数量:' + data.updateUsernames.length + ';'
for (const username of data.updateUsernames) {
text += '< ' + username + ' >'
}
text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';'
for (const username in data.failureUsernames) {
text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
}
message.alert(text)
emits('success')
closeDialog()
}
// 文件数超出提示
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
// 上传错误提示
const excelUploadError = (): void => {
message.error('导入数据失败,请您重新上传!')
}
/** 下载模板操作 */
const importTemplate = async () => {
try {
const res = await importUserTemplateApi()
download.excel(res, '用户导入模版.xls')
} catch (error) {
console.error(error)
}
}
/* 弹框按钮操作 */
// 点击取消
const cancel = () => {
closeDialog()
}
// 关闭弹窗
const closeDialog = () => {
emits('update:modelValue', false)
}
// 提交上传文件
const submitFileForm = () => {
uploadRef.value?.submit()
}
</script>
<template>
<el-dialog title="分配角色" :modelValue="show" width="500px" append-to-body @close="closeDialog">
<el-form :model="formData" label-width="80px" ref="formRef">
<el-form-item label="用户名称">
<el-input v-model="formData.username" :disabled="true" />
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="formData.nickname" :disabled="true" />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="formData.roleIds" multiple placeholder="请选择">
<el-option
v-for="item in roleOptions"
:key="parseInt(item.id)"
:label="item.name"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submit">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { assignUserRoleApi, PermissionAssignUserRoleReqVO } from '@/api/system/permission'
interface Props {
show: boolean
roleOptions: any[]
formInitValue?: Recordable & Partial<typeof initParams>
}
const props = withDefaults(defineProps<Props>(), {
show: false,
roleOptions: () => [],
formInitValue: () => ({})
})
const emits = defineEmits(['update:show', 'success'])
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 表单初始化参数
const initParams = {
nickname: '',
id: 0,
username: '',
roleIds: [] as number[]
}
const formData = ref<Recordable>({ ...initParams })
watch(
() => props.formInitValue,
(val) => {
formData.value = { ...val }
},
{ deep: true }
)
/* 弹框按钮操作 */
// 点击取消
const cancel = () => {
closeDialog()
}
// 关闭弹窗
const closeDialog = () => {
emits('update:show', false)
}
// 提交
const submit = async () => {
const data = ref<PermissionAssignUserRoleReqVO>({
userId: formData.value.id,
roleIds: formData.value.roleIds
})
try {
await assignUserRoleApi(data.value)
message.success(t('common.updateSuccess'))
emits('success', true)
closeDialog()
} catch (error) {
console.error(error)
}
}
</script>
<style></style>
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 搜索工作栏 --> <content-wrap>
<el-row :gutter="20"> <!-- 搜索工作栏 -->
<!--部门数据--> <el-row :gutter="20">
<el-col :span="4" :xs="24"> <!--部门数据-->
<div class="head-container"> <el-col :span="4" :xs="24">
<el-input <div class="head-container">
v-model="deptName"
placeholder="请输入部门名称"
clearable
size="small"
style="margin-bottom: 20px"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
</div>
<div class="head-container">
<el-tree
:data="deptOptions"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="treeRef"
node-key="id"
default-expand-all
highlight-current
@node-click="handleDeptNodeClick"
/>
</div>
</el-col>
<!--用户数据-->
<el-col :span="20" :xs="24">
<el-form
:model="queryParams"
ref="queryForm"
size="small"
:inline="true"
v-show="showSearch"
label-width="68px"
>
<el-form-item label="用户名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input <el-input
v-model="queryParams.mobile" v-model="deptName"
placeholder="请输入手机号码" placeholder="请输入部门名称"
clearable clearable
style="width: 240px" style="margin-bottom: 20px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
style="width: 240px"
> >
<el-option <template #prefix>
v-for="dict in statusDictDatas" <Icon icon="ep:search" />
:key="parseInt(dict.value)" </template>
:label="dict.label" </el-input>
:value="parseInt(dict.value)" </div>
/> <div class="head-container">
</el-select> <el-tree
</el-form-item> :data="deptOptions"
<el-form-item label="创建时间" prop="createTime"> :props="defaultProps"
<el-date-picker :expand-on-click-node="false"
v-model="queryParams.createTime" :filter-node-method="filterNode"
style="width: 240px" ref="treeRef"
value-format="yyyy-MM-dd HH:mm:ss" node-key="id"
type="daterange" default-expand-all
range-separator="-" highlight-current
start-placeholder="开始日期" @node-click="handleDeptNodeClick"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
/> />
</el-form-item> </div>
<el-form-item> </el-col>
<el-button type="primary" @click="handleQuery"><Icon icon="ep:search" />搜索</el-button> <!--用户数据-->
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button> <el-col :span="20" :xs="24">
</el-form-item> <el-form
</el-form> :model="queryParams"
ref="queryFormRef"
<el-row :gutter="10" class="mb8"> :inline="true"
<el-col :span="1.5"> v-show="showSearch"
<el-button label-width="68px"
type="primary"
plain
size="small"
@click="handleAdd"
v-hasPermi="['system:user:create']"
><Icon icon="ep:plus" />新增</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
size="small"
@click="handleImport"
v-hasPermi="['system:user:import']"
><Icon icon="ep:upload" />导入</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
size="small"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:user:export']"
><Icon icon="ep:download" />导出</el-button
>
</el-col>
<!-- <right-toolbar
:showSearch.sync="showSearch"
@queryTable="getList"
:columns="columns"
></right-toolbar> -->
</el-row>
<el-table v-loading="loading" :data="userList">
<el-table-column
label="用户编号"
align="center"
key="id"
prop="id"
v-if="columns[0].visible"
/>
<el-table-column
label="用户名称"
align="center"
key="username"
prop="username"
v-if="columns[1].visible"
:show-overflow-tooltip="true"
/>
<el-table-column
label="用户昵称"
align="center"
key="nickname"
prop="nickname"
v-if="columns[2].visible"
:show-overflow-tooltip="true"
/>
<el-table-column
label="部门"
align="center"
key="deptName"
prop="dept.name"
v-if="columns[3].visible"
:show-overflow-tooltip="true"
/>
<el-table-column
label="手机号码"
align="center"
key="mobile"
prop="mobile"
v-if="columns[4].visible"
width="120"
/>
<el-table-column label="状态" key="status" v-if="columns[5].visible" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
v-if="columns[6].visible"
width="160"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="160"
class-name="small-padding fixed-width"
> >
<template #default="scope"> <el-form-item label="用户名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号码"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in statusDictDatas"
:key="parseInt(dict.value)"
:label="dict.label"
:value="parseInt(dict.value)"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
style="width: 240px"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"
><Icon icon="ep:search" />搜索</el-button
>
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb-8px">
<el-col :span="1.5">
<el-button
type="primary"
plain
size="small"
@click="handleAdd"
v-hasPermi="['system:user:create']"
><Icon icon="ep:plus" />新增</el-button
>
</el-col>
<el-col :span="1.5">
<el-button <el-button
type="info"
size="small" size="small"
type="text" @click="handleImport"
@click="handleUpdate(scope.row)" v-hasPermi="['system:user:import']"
v-hasPermi="['system:user:update']" ><Icon icon="ep:upload" />导入</el-button
><Icon icon="ep:edit" />修改</el-button
> >
<el-dropdown </el-col>
@command="(command) => handleCommand(command, scope.$index, scope.row)" <el-col :span="1.5">
v-hasPermi="[ <el-button
'system:user:delete', type="warning"
'system:user:update-password', size="small"
'system:permission:assign-user-role' @click="handleExport"
]" :loading="exportLoading"
v-hasPermi="['system:user:export']"
><Icon icon="ep:download" />导出</el-button
> >
<el-button size="small" type="text"><Icon icon="ep:d-arrow-right" />更多</el-button> </el-col>
<template #dropdown> </el-row>
<el-dropdown-menu> <el-table v-loading="loading" :data="userList">
<el-dropdown-item <el-table-column
command="handleDelete" label="用户编号"
v-if="scope.row.id !== 1" align="center"
size="small" key="id"
type="text" prop="id"
v-hasPermi="['system:user:delete']" v-if="columns[0].visible"
><Icon icon="ep:delete" />删除</el-dropdown-item />
> <el-table-column
<el-dropdown-item label="用户名称"
command="handleResetPwd" align="center"
size="small" key="username"
type="text" prop="username"
v-hasPermi="['system:user:update-password']" v-if="columns[1].visible"
><Icon icon="ep:key" />重置密码</el-dropdown-item :show-overflow-tooltip="true"
> />
<el-dropdown-item <el-table-column
command="handleRole" label="用户昵称"
size="small" align="center"
type="text" key="nickname"
v-hasPermi="['system:permission:assign-user-role']" prop="nickname"
><Icon icon="ep:circle-check" />分配角色</el-dropdown-item v-if="columns[2].visible"
> :show-overflow-tooltip="true"
</el-dropdown-menu> />
</template> <el-table-column
</el-dropdown> label="部门"
</template> align="center"
</el-table-column> key="deptName"
</el-table> prop="dept.name"
<pagination v-if="columns[3].visible"
v-show="total > 0" :show-overflow-tooltip="true"
:total="total" />
v-model:page="queryParams.pageNo" <el-table-column
v-model:limit="queryParams.pageSize" label="手机号码"
@pagination="getList" align="center"
/> key="mobile"
</el-col> prop="mobile"
</el-row> v-if="columns[4].visible"
width="120"
/>
<el-table-column label="状态" key="status" v-if="columns[5].visible" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
v-if="columns[6].visible"
width="160"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="160"
class-name="small-padding fixed-width"
>
<template #default="scope">
<div class="flex justify-center items-center">
<el-button
type="primary"
link
@click="handleUpdate(scope.row)"
v-hasPermi="['system:user:update']"
><Icon icon="ep:edit" />修改</el-button
>
<el-dropdown
@command="(command) => handleCommand(command, scope.$index, scope.row)"
v-hasPermi="[
'system:user:delete',
'system:user:update-password',
'system:permission:assign-user-role'
]"
>
<el-button type="primary" link><Icon icon="ep:d-arrow-right" />更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<!-- div包住避免控制台报错:Runtime directive used on component with non-element root node -->
<div v-if="scope.row.id !== 1" v-hasPermi="['system:user:delete']">
<el-dropdown-item command="handleDelete" type="text"
><Icon icon="ep:delete" />删除</el-dropdown-item
>
</div>
<div v-hasPermi="['system:user:update-password']">
<el-dropdown-item command="handleResetPwd" type="text"
><Icon icon="ep:key" />重置密码</el-dropdown-item
></div
>
<div v-hasPermi="['system:permission:assign-user-role']">
<el-dropdown-item command="handleRole" type="text"
><Icon icon="ep:circle-check" />分配角色</el-dropdown-item
></div
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-col>
</el-row>
</content-wrap>
<!-- 添加或修改用户对话框 -->
<AddForm
ref="addEditFormRef"
v-model="showAddDialog"
:dept-options="deptOptions"
:post-options="postOptions"
:form-init-value="addFormInitValue"
@success="getList"
/>
<!-- 用户导入对话框 -->
<ImportForm v-model="importDialogVisible" @success="getList" />
<!-- 分配角色 -->
<RoleForm
ref="roleFormRef"
v-model:show="roleDialogVisible"
:role-options="roleOptions"
:form-init-value="userRole"
@success="getList"
/>
</div> </div>
</template> </template>
<script setup lang="ts" name="User"> <script setup lang="ts" name="User">
import type { ElTree } from 'element-plus' import type { ElTree } from 'element-plus'
import { handleTree, defaultProps } from '@/utils/tree' import { handleTree, defaultProps } from '@/utils/tree'
import { listSimpleDeptApi } from '@/api/system/dept' // 原vue3版本api方法都是Api结尾觉得见名知义,个人觉得这个可以形成规范
import { listSimplePostsApi, PostVO } from '@/api/system/post' import { getSimpleDeptList as getSimpleDeptListApi } from '@/api/system/dept'
import { getSimplePostList as getSimplePostListApi, PostVO } from '@/api/system/post'
import { DICT_TYPE, getDictOptions } from '@/utils/dict' import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { UserVO } from '@/api/system/user'
import { import {
// createUserApi, deleteUserApi,
// updateUserStatusApi, exportUserApi,
// deleteUserApi, resetUserPwdApi,
// exportUserApi, updateUserStatusApi,
// getUserApi, UserVO
// importUserTemplateApi,
getUserPageApi
// resetUserPwdApid,
// updateUserApi
} from '@/api/system/user' } from '@/api/system/user'
import { parseTime } from './utils'
import AddForm from './AddForm.vue'
import ImportForm from './ImportForm.vue'
import RoleForm from './RoleForm.vue'
import { getUserApi, getUserPageApi } from '@/api/system/user'
import { getSimpleRoleList as getSimpleRoleListApi } from '@/api/system/role'
import { listUserRolesApi } from '@/api/system/permission'
import { CommonStatusEnum } from '@/utils/constants'
import download from '@/utils/download'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
...@@ -283,16 +302,24 @@ const queryParams = reactive({ ...@@ -283,16 +302,24 @@ const queryParams = reactive({
createTime: [] createTime: []
}) })
const showSearch = ref(true) const showSearch = ref(true)
// 数据字典 const showAddDialog = ref(false)
// 数据字典-
const statusDictDatas = getDictOptions(DICT_TYPE.COMMON_STATUS) const statusDictDatas = getDictOptions(DICT_TYPE.COMMON_STATUS)
// const sexDictDatas = getDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
// ========== 创建部门树结构 ========== // ========== 创建部门树结构 ==========
const deptName = ref('') const deptName = ref('')
watch(
() => deptName.value,
(val) => {
treeRef.value?.filter(val)
}
)
const deptOptions = ref<Tree[]>([]) // 树形结构 const deptOptions = ref<Tree[]>([]) // 树形结构
const treeRef = ref<InstanceType<typeof ElTree>>() const treeRef = ref<InstanceType<typeof ElTree>>()
const getTree = async () => { const getTree = async () => {
const res = await listSimpleDeptApi() const res = await getSimpleDeptListApi()
deptOptions.value = []
deptOptions.value.push(...handleTree(res)) deptOptions.value.push(...handleTree(res))
} }
const filterNode = (value: string, data: Tree) => { const filterNode = (value: string, data: Tree) => {
...@@ -301,13 +328,13 @@ const filterNode = (value: string, data: Tree) => { ...@@ -301,13 +328,13 @@ const filterNode = (value: string, data: Tree) => {
} }
const handleDeptNodeClick = async (row: { [key: string]: any }) => { const handleDeptNodeClick = async (row: { [key: string]: any }) => {
queryParams.deptId = row.id queryParams.deptId = row.id
// await reload()
getList() getList()
} }
// 获取岗位列表 // 获取岗位列表
const postOptions = ref<PostVO[]>([]) //岗位列表 const postOptions = ref<PostVO[]>([]) //岗位列表
const getPostOptions = async () => { const getPostOptions = async () => {
const res = await listSimplePostsApi() const res = await getSimplePostListApi()
postOptions.value.push(...res) postOptions.value.push(...res)
} }
// 用户列表 // 用户列表
...@@ -323,6 +350,7 @@ const columns = ref([ ...@@ -323,6 +350,7 @@ const columns = ref([
{ key: 5, label: `状态`, visible: true }, { key: 5, label: `状态`, visible: true },
{ key: 6, label: `创建时间`, visible: true } { key: 6, label: `创建时间`, visible: true }
]) ])
/* 查询列表 */
const getList = () => { const getList = () => {
loading.value = true loading.value = true
getUserPageApi(queryParams).then((response) => { getUserPageApi(queryParams).then((response) => {
...@@ -331,15 +359,169 @@ const getList = () => { ...@@ -331,15 +359,169 @@ const getList = () => {
loading.value = false loading.value = false
}) })
} }
const handleQuery = () => {} /** 搜索按钮操作 */
const resetQuery = () => {} const handleQuery = () => {
const handleAdd = () => {} queryParams.pageNo = 1
const handleImport = () => {} getList()
}
/** 重置按钮操作 */
const queryFormRef = ref()
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
// 添加或编辑
const addEditFormRef = ref()
// 添加用户
const handleAdd = () => {
addEditFormRef?.value.resetForm()
// 获得下拉数据
getTree()
// 打开表单,并设置初始化
showAddDialog.value = true
}
// 用户导入
const handleImport = () => {
importDialogVisible.value = true
}
// 用户导出
const exportLoading = ref(false) const exportLoading = ref(false)
const handleExport = () => {} const handleExport = () => {
const handleStatusChange = () => {} message
const handleUpdate = () => {} .confirm('是否确认导出所有用户数据项?')
const handleCommand = () => {} .then(() => {
// 处理查询参数
let params = { ...queryParams }
params.pageNo = 1
params.pageSize = 99999
exportLoading.value = true
return exportUserApi(params)
})
.then((response) => {
download.excel(response, '用户数据.xls')
})
.catch(() => {})
.finally(() => {
exportLoading.value = false
})
}
// 操作分发
const handleCommand = (command: string, index: number, row: UserVO) => {
console.log(index)
switch (command) {
case 'handleUpdate':
handleUpdate(row) //修改客户信息
break
case 'handleDelete':
handleDelete(row) //红号变更
break
case 'handleResetPwd':
handleResetPwd(row)
break
case 'handleRole':
handleRole(row)
break
default:
break
}
}
// 用户状态修改
const handleStatusChange = (row: UserVO) => {
let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
message
.confirm('确认要"' + text + '""' + row.username + '"用户吗?', t('common.reminder'))
.then(function () {
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.ENABLE : CommonStatusEnum.DISABLE
return updateUserStatusApi(row.id, row.status)
})
.then(() => {
message.success(text + '成功')
// 刷新列表
getList()
})
.catch(() => {
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
})
}
// 具体数据单行操作
const addFormInitValue = ref<Recordable>({})
/** 修改按钮操作 */
const handleUpdate = (row: UserVO) => {
addEditFormRef.value?.resetForm()
getTree()
const id = row.id
getUserApi(id).then((response) => {
addFormInitValue.value = response
showAddDialog.value = true
})
}
// 删除用户
const handleDelete = (row: UserVO) => {
const ids = row.id
message
.confirm('是否确认删除用户编号为"' + ids + '"的数据项?')
.then(() => {
return deleteUserApi(ids)
})
.then(() => {
getList()
message.success('删除成功')
})
.catch(() => {})
}
// 重置密码
const handleResetPwd = (row: UserVO) => {
message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => {
resetUserPwdApi(row.id, value)
.then(() => {
message.success('修改成功,新密码是:' + value)
})
.catch((e) => {
console.error(e)
})
})
}
// 分配角色
const roleDialogVisible = ref(false)
const roleOptions = ref()
const userRole = reactive({
id: 0,
username: '',
nickname: '',
roleIds: []
})
const handleRole = async (row: UserVO) => {
addEditFormRef.value?.resetForm()
userRole.id = row.id
userRole.username = row.username
userRole.nickname = row.nickname
// 获得角色列表
const roleOpt = await getSimpleRoleListApi()
roleOptions.value = [...roleOpt]
// 获得角色拥有的菜单集合
const roles = await listUserRolesApi(row.id)
userRole.roleIds = roles
roleDialogVisible.value = true
}
/* 用户导入 */
const importDialogVisible = ref(false)
// ========== 初始化 ========== // ========== 初始化 ==========
onMounted(async () => { onMounted(async () => {
getList() getList()
......
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
// 国际化
const { t } = useI18n()
const validateMobile = (rule: any, value: any, callback: any) => {
const reg = /^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/
if (value === '') {
callback(new Error('请输入联系手机'))
} else {
if (!reg.test(value)) {
callback(new Error('请输入正确的手机号'))
} else {
callback()
}
}
}
// 表单校验
export const rules = reactive({
username: [required],
nickname: [required],
password: [required],
deptId: [required],
email: [
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' },
{
type: 'email',
message: t('profile.rules.truemail'),
trigger: ['blur', 'change']
}
],
status: [required],
mobile: [
required,
{
len: 11,
trigger: 'blur',
message: '请输入正确的手机号码'
},
{ validator: validateMobile, trigger: 'blur' }
]
})
// crudSchemas
const crudSchemas = reactive<VxeCrudSchema>({
primaryKey: 'id',
primaryType: 'id',
primaryTitle: '用户编号',
action: true,
actionWidth: '200px',
columns: [
{
title: '用户账号',
field: 'username',
isSearch: true
},
{
title: '用户密码',
field: 'password',
isDetail: false,
isTable: false,
form: {
component: 'InputPassword'
}
},
{
title: '用户' + t('profile.user.sex'),
field: 'sex',
dictType: DICT_TYPE.SYSTEM_USER_SEX,
dictClass: 'number',
table: { show: false }
},
{
title: '用户昵称',
field: 'nickname'
},
{
title: '用户邮箱',
field: 'email'
},
{
title: '手机号码',
field: 'mobile',
isSearch: true
},
{
title: '部门',
field: 'deptId',
isTable: false
},
{
title: '岗位',
field: 'postIds',
isTable: false
},
{
title: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
dictClass: 'number',
isSearch: true,
table: {
slots: {
default: 'status_default'
}
}
},
{
title: '最后登录时间',
field: 'loginDate',
formatter: 'formatDate',
isForm: false
},
{
title: '最后登录IP',
field: 'loginIp',
isTable: false,
isForm: false
},
{
title: t('form.remark'),
field: 'remark',
isTable: false
},
{
title: t('common.createTime'),
field: 'createTime',
formatter: 'formatDate',
isTable: false,
isForm: false,
search: {
show: true,
itemRender: {
name: 'XDataTimePicker'
}
}
}
]
})
export const { allSchemas } = useVxeCrudSchemas(crudSchemas)
export const parseTime = (time) => {
if (!time) {
return null
}
const format = '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time
.replace(new RegExp(/-/gm), '/')
.replace('T', ' ')
.replace(new RegExp(/\.[\d]{3}/gm), '')
}
if (typeof time === 'number' && time.toString().length === 10) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
"vite-plugin-svg-icons/client", "vite-plugin-svg-icons/client",
"@form-create/element-ui/types" "@form-create/element-ui/types"
], ],
"outDir": "target", // 请保留这个属性,防止tsconfig.json文件报错
"typeRoots": ["./node_modules/@types/", "./types"] "typeRoots": ["./node_modules/@types/", "./types"]
}, },
"include": [ "include": [
...@@ -40,5 +41,5 @@ ...@@ -40,5 +41,5 @@
"src/types/auto-imports.d.ts", "src/types/auto-imports.d.ts",
"src/types/auto-components.d.ts" "src/types/auto-components.d.ts"
], ],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "target", "node_modules"]
} }
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Icon: typeof import('../components/Icon/src/Icon.vue')['default'] Icon: typeof import('@/components/Icon')['Icon']
DictTag: typeof import('@/components/DictTag')['DictTag']
} }
} }
......
export {} export {}
declare global { declare global {
declare interface Fn<T = any> { interface Fn<T = any> {
(...arg: T[]): T (...arg: T[]): T
} }
declare type Nullable<T> = T | null type Nullable<T> = T | null
declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T> type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>
declare type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T> type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>
declare type ComponentRef<T> = InstanceType<T> type ComponentRef<T> = InstanceType<T>
declare type LocaleType = 'zh-CN' | 'en' type LocaleType = 'zh-CN' | 'en'
declare type AxiosHeaders = type AxiosHeaders =
| 'application/json' | 'application/json'
| 'application/x-www-form-urlencoded' | 'application/x-www-form-urlencoded'
| 'multipart/form-data' | 'multipart/form-data'
declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT' type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT'
declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
declare interface AxiosConfig { interface AxiosConfig {
params?: any params?: any
data?: any data?: any
url?: string url?: string
...@@ -32,17 +32,17 @@ declare global { ...@@ -32,17 +32,17 @@ declare global {
responseType?: AxiosResponseType responseType?: AxiosResponseType
} }
declare interface IResponse<T = any> { interface IResponse<T = any> {
code: string code: string
data: T extends any ? T : T & any data: T extends any ? T : T & any
} }
declare interface PageParam { interface PageParam {
pageSize?: number pageSize?: number
pageNo?: number pageNo?: number
} }
declare interface Tree { interface Tree {
id: number id: number
name: string name: string
children?: Tree[] | any[] children?: Tree[] | any[]
......
...@@ -54,7 +54,7 @@ type Component<T = any> = ...@@ -54,7 +54,7 @@ type Component<T = any> =
| (() => Promise<T>) | (() => Promise<T>)
declare global { declare global {
declare interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string name: string
meta: RouteMeta meta: RouteMeta
component?: Component | string component?: Component | string
...@@ -64,7 +64,7 @@ declare global { ...@@ -64,7 +64,7 @@ declare global {
keepAlive?: boolean keepAlive?: boolean
} }
declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
icon: any icon: any
name: string name: string
meta: RouteMeta meta: RouteMeta
......
...@@ -42,7 +42,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => { ...@@ -42,7 +42,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
// }, // },
}, },
// 项目使用的vite插件。 单独提取到build/vite/plugin中管理 // 项目使用的vite插件。 单独提取到build/vite/plugin中管理
plugins: createVitePlugins(env.VITE_APP_TITLE), plugins: createVitePlugins(),
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment