9 Commits

Author SHA1 Message Date
guanj
dc3b3cfc74 全局下拉框添加filterable属性 2026-07-15 09:21:35 +08:00
guanj
04c5eb7185 调整退出问题 2026-07-14 14:49:10 +08:00
guanj
d0d62ab0af 修改菜单树无法展开问题 2026-07-14 14:39:59 +08:00
guanj
b3d671a93e 调整前端两套互不连通的 HTTP 请求层并存、两套布局框架并存、两套 Pinia 实例并存问题 2026-07-10 14:52:29 +08:00
guanj
888b8d1bf4 修改点击列问题 2026-07-10 08:57:37 +08:00
guanj
ee9cd057b8 优化菜单切换导致浏览器刷新问题 2026-07-09 15:40:56 +08:00
guanj
8bca4191f2 调整代码 2026-07-09 15:26:01 +08:00
guanj
69e6a1f93f 调整 2026-07-09 14:41:56 +08:00
guanj
71df1ac4be 修改冲突 2026-07-09 13:59:02 +08:00
258 changed files with 10802 additions and 15458 deletions

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData, requestDownload } from '@/utils/request'
// BPM 流程表达式 VO // BPM 流程表达式 VO
export interface ProcessExpressionVO { export interface ProcessExpressionVO {
@@ -12,31 +12,36 @@ export interface ProcessExpressionVO {
export const ProcessExpressionApi = { export const ProcessExpressionApi = {
// 查询BPM 流程表达式分页 // 查询BPM 流程表达式分页
getProcessExpressionPage: async (params: any) => { getProcessExpressionPage: async (params: any) => {
return await request.get({ url: `/bpm/process-expression/page`, params }) return await requestData({ url: '/bpm/process-expression/page', method: 'GET', params })
}, },
// 查询BPM 流程表达式详情 // 查询BPM 流程表达式详情
getProcessExpression: async (id: number) => { getProcessExpression: async (id: number) => {
return await request.get({ url: `/bpm/process-expression/get?id=` + id }) return await requestData({ url: '/bpm/process-expression/get?id=' + id, method: 'GET' })
}, },
// 新增BPM 流程表达式 // 新增BPM 流程表达式
createProcessExpression: async (data: ProcessExpressionVO) => { createProcessExpression: async (data: ProcessExpressionVO) => {
return await request.post({ url: `/bpm/process-expression/create`, data }) return await requestData({ url: '/bpm/process-expression/create', method: 'POST', data })
}, },
// 修改BPM 流程表达式 // 修改BPM 流程表达式
updateProcessExpression: async (data: ProcessExpressionVO) => { updateProcessExpression: async (data: ProcessExpressionVO) => {
return await request.put({ url: `/bpm/process-expression/update`, data }) return await requestData({ url: '/bpm/process-expression/update', method: 'PUT', data })
}, },
// 删除BPM 流程表达式 // 删除BPM 流程表达式
deleteProcessExpression: async (id: number) => { deleteProcessExpression: async (id: number) => {
return await request.delete({ url: `/bpm/process-expression/delete?id=` + id }) return await requestData({ url: '/bpm/process-expression/delete?id=' + id, method: 'DELETE' })
}, },
// 导出BPM 流程表达式 Excel // 导出BPM 流程表达式 Excel
exportProcessExpression: async (params) => { exportProcessExpression: async (params) => {
return await request.download({ url: `/bpm/process-expression/export-excel`, params }) return await requestDownload({
url: '/bpm/process-expression/export-excel',
method: 'GET',
params,
responseType: 'blob'
})
} }
} }

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
// BPM 流程监听器 VO // BPM 流程监听器 VO
export interface ProcessListenerVO { export interface ProcessListenerVO {
@@ -15,26 +15,26 @@ export interface ProcessListenerVO {
export const ProcessListenerApi = { export const ProcessListenerApi = {
// 查询流程监听器分页 // 查询流程监听器分页
getProcessListenerPage: async (params: any) => { getProcessListenerPage: async (params: any) => {
return await request.get({ url: `/bpm/process-listener/page`, params }) return await requestData({ url: '/bpm/process-listener/page', method: 'GET', params })
}, },
// 查询流程监听器详情 // 查询流程监听器详情
getProcessListener: async (id: number) => { getProcessListener: async (id: number) => {
return await request.get({ url: `/bpm/process-listener/get?id=` + id }) return await requestData({ url: '/bpm/process-listener/get?id=' + id, method: 'GET' })
}, },
// 新增流程监听器 // 新增流程监听器
createProcessListener: async (data: ProcessListenerVO) => { createProcessListener: async (data: ProcessListenerVO) => {
return await request.post({ url: `/bpm/process-listener/create`, data }) return await requestData({ url: '/bpm/process-listener/create', method: 'POST', data })
}, },
// 修改流程监听器 // 修改流程监听器
updateProcessListener: async (data: ProcessListenerVO) => { updateProcessListener: async (data: ProcessListenerVO) => {
return await request.put({ url: `/bpm/process-listener/update`, data }) return await requestData({ url: '/bpm/process-listener/update', method: 'PUT', data })
}, },
// 删除流程监听器 // 删除流程监听器
deleteProcessListener: async (id: number) => { deleteProcessListener: async (id: number) => {
return await request.delete({ url: `/bpm/process-listener/delete?id=` + id }) return await requestData({ url: '/bpm/process-listener/delete?id=' + id, method: 'DELETE' })
} }
} }

View File

@@ -1,47 +1,41 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
export type UserGroupVO = { export type UserGroupVO = {
id: number id: number
name: string name: string
description: string description: string
userIds: number[] userIds: number[]
status: number status: number
remark: string remark: string
createTime: string createTime: string
} }
// 创建用户组 // 创建用户组
export const createUserGroup = async (data: UserGroupVO) => { export const createUserGroup = async (data: UserGroupVO) => {
return await request.post({ return await requestData({ url: '/bpm/user-group/create', method: 'POST', data })
url: '/bpm/user-group/create', }
data: data
}) // 更新用户组
} export const updateUserGroup = async (data: UserGroupVO) => {
return await requestData({ url: '/bpm/user-group/update', method: 'PUT', data })
// 更新用户组 }
export const updateUserGroup = async (data: UserGroupVO) => {
return await request.put({ // 删除用户组
url: '/bpm/user-group/update', export const deleteUserGroup = async (id: number) => {
data: data return await requestData({ url: '/bpm/user-group/delete?id=' + id, method: 'DELETE' })
}) }
}
// 获得用户组
// 删除用户组 export const getUserGroup = async (id: number) => {
export const deleteUserGroup = async (id: number) => { return await requestData({ url: '/bpm/user-group/get?id=' + id, method: 'GET' })
return await request.delete({ url: '/bpm/user-group/delete?id=' + id }) }
}
// 获得用户组分页
// 获得用户组 export const getUserGroupPage = async (params) => {
export const getUserGroup = async (id: number) => { return await requestData({ url: '/bpm/user-group/page', method: 'GET', params })
return await request.get({ url: '/bpm/user-group/get?id=' + id }) }
}
// 获取用户组精简信息列表
// 获得用户组分页 export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
export const getUserGroupPage = async (params) => { return await requestData({ url: '/bpm/user-group/simple-list', method: 'GET' })
return await request.get({ url: '/bpm/user-group/page', params }) }
}
// 获取用户组精简信息列表
export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
return await request.get({ url: '/bpm/user-group/simple-list' })
}

View File

@@ -1,49 +1,49 @@
import request from '@/config/axios' import { requestData, requestDownload } from '@/utils/request'
export type DictDataVO = { export type DictDataVO = {
id: number | undefined id: number | undefined
sort: number | undefined sort: number | undefined
label: string label: string
value: string value: string
dictType: string dictType: string
status: number status: number
colorType: string colorType: string
cssClass: string cssClass: string
remark: string remark: string
createTime: Date createTime: Date
} }
// 查询字典数据(精简)列表 // 查询字典数据(精简)列表
export const getSimpleDictDataList = () => { export const getSimpleDictDataList = () => {
return request.get({ url: '/system/dict-data/simple-list' }) return requestData({ url: '/system/dict-data/simple-list', method: 'GET' })
} }
// 查询字典数据列表 // 查询字典数据列表
export const getDictDataPage = (params: PageParam) => { export const getDictDataPage = (params: PageParam) => {
return request.get({ url: '/system/dict-data/page', params }) return requestData({ url: '/system/dict-data/page', method: 'GET', params })
} }
// 查询字典数据详情 // 查询字典数据详情
export const getDictData = (id: number) => { export const getDictData = (id: number) => {
return request.get({ url: '/system/dict-data/get?id=' + id }) return requestData({ url: '/system/dict-data/get?id=' + id, method: 'GET' })
} }
// 新增字典数据 // 新增字典数据
export const createDictData = (data: DictDataVO) => { export const createDictData = (data: DictDataVO) => {
return request.post({ url: '/system/dict-data/create', data }) return requestData({ url: '/system/dict-data/create', method: 'POST', data })
} }
// 修改字典数据 // 修改字典数据
export const updateDictData = (data: DictDataVO) => { export const updateDictData = (data: DictDataVO) => {
return request.put({ url: '/system/dict-data/update', data }) return requestData({ url: '/system/dict-data/update', method: 'PUT', data })
} }
// 删除字典数据 // 删除字典数据
export const deleteDictData = (id: number) => { export const deleteDictData = (id: number) => {
return request.delete({ url: '/system/dict-data/delete?id=' + id }) return requestData({ url: '/system/dict-data/delete?id=' + id, method: 'DELETE' })
} }
// 导出字典类型数据 // 导出字典类型数据
export const exportDictData = (params) => { export const exportDictData = (params) => {
return request.download({ url: '/system/dict-data/export', params }) return requestDownload({ url: '/system/dict-data/export', method: 'GET', params, responseType: 'blob' })
} }

View File

@@ -1,44 +1,45 @@
import request from '@/config/axios' import { requestData, requestDownload } from '@/utils/request'
export type DictTypeVO = { export type DictTypeVO = {
id: number | undefined id: number | undefined
name: string name: string
type: string type: string
status: number status: number
remark: string remark: string
createTime: Date createTime: Date
} }
// 查询字典(精简)列表 // 查询字典(精简)列表
export const getSimpleDictTypeList = () => { export const getSimpleDictTypeList = () => {
return request.get({ url: '/system/dict-type/list-all-simple' }) return requestData({ url: '/system/dict-type/list-all-simple', method: 'GET' })
} }
// 查询字典列表 // 查询字典列表
export const getDictTypePage = (params: PageParam) => { export const getDictTypePage = (params: PageParam) => {
return request.get({ url: '/system/dict-type/page', params }) return requestData({ url: '/system/dict-type/page', method: 'GET', params })
} }
// 查询字典详情 // 查询字典详情
export const getDictType = (id: number) => { export const getDictType = (id: number) => {
return request.get({ url: '/system/dict-type/get?id=' + id }) return requestData({ url: '/system/dict-type/get?id=' + id, method: 'GET' })
} }
// 新增字典 // 新增字典
export const createDictType = (data: DictTypeVO) => { export const createDictType = (data: DictTypeVO) => {
return request.post({ url: '/system/dict-type/create', data }) return requestData({ url: '/system/dict-type/create', method: 'POST', data })
} }
// 修改字典 // 修改字典
export const updateDictType = (data: DictTypeVO) => { export const updateDictType = (data: DictTypeVO) => {
return request.put({ url: '/system/dict-type/update', data }) return requestData({ url: '/system/dict-type/update', method: 'PUT', data })
} }
// 删除字典 // 删除字典
export const deleteDictType = (id: number) => { export const deleteDictType = (id: number) => {
return request.delete({ url: '/system/dict-type/delete?id=' + id }) return requestData({ url: '/system/dict-type/delete?id=' + id, method: 'DELETE' })
} }
// 导出字典类型
export const exportDictType = (params) => { // 导出字典类型
return request.download({ url: '/system/dict-type/export', params }) export const exportDictType = (params) => {
} return requestDownload({ url: '/system/dict-type/export', method: 'GET', params, responseType: 'blob' })
}

View File

@@ -1,49 +1,50 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
import qs from 'qs' import qs from 'qs'
export interface NotifyMessageVO { export interface NotifyMessageVO {
id: number id: number
userId: number userId: number
userType: number userType: number
templateId: number templateId: number
templateCode: string templateCode: string
templateNickname: string templateNickname: string
templateContent: string templateContent: string
templateType: number templateType: number
templateParams: string templateParams: string
readStatus: boolean readStatus: boolean
readTime: Date readTime: Date
createTime: Date createTime: Date
} }
// 查询站内信消息列表 // 查询站内信消息列表
export const getNotifyMessagePage = async (params: PageParam) => { export const getNotifyMessagePage = async (params: any) => {
return await request.get({ url: '/system/notify-message/page', params }) return await requestData({ url: '/system/notify-message/page', method: 'GET', params })
} }
// 获得我的站内信分页 // 获得我的站内信分页
export const getMyNotifyMessagePage = async (params: PageParam) => { export const getMyNotifyMessagePage = async (params: any) => {
return await request.get({ url: '/system/notify-message/my-page', params }) return await requestData({ url: '/system/notify-message/my-page', method: 'GET', params })
} }
// 批量标记已读 // 批量标记已读
export const updateNotifyMessageRead = async (ids) => { export const updateNotifyMessageRead = async (ids: any) => {
return await request.put({ return await requestData({
url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }) url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }),
}) method: 'PUT'
} })
}
// 标记所有站内信为已读
export const updateAllNotifyMessageRead = async () => { // 标记所有站内信为已读
return await request.put({ url: '/system/notify-message/update-all-read' }) export const updateAllNotifyMessageRead = async () => {
} return await requestData({ url: '/system/notify-message/update-all-read', method: 'PUT' })
}
// 获取当前用户的最新站内信列表
export const getUnreadNotifyMessageList = async () => { // 获取当前用户的最新站内信列表
return await request.get({ url: '/system/notify-message/get-unread-list' }) export const getUnreadNotifyMessageList = async () => {
} return await requestData({ url: '/system/notify-message/get-unread-list', method: 'GET' })
}
// 获得当前用户的未读站内信数量
export const getUnreadNotifyMessageCount = async () => { // 获得当前用户的未读站内信数量
return await request.get({ url: '/system/notify-message/get-unread-count' }) export const getUnreadNotifyMessageCount = async () => {
} return await requestData({ url: '/system/notify-message/get-unread-count', method: 'GET' })
}

View File

@@ -1,49 +1,49 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
export interface NotifyTemplateVO { export interface NotifyTemplateVO {
id?: number id?: number
name: string name: string
nickname: string nickname: string
code: string code: string
content: string content: string
type?: number type?: number
params: string params: string
status: number status: number
remark: string remark: string
} }
export interface NotifySendReqVO { export interface NotifySendReqVO {
userId: number | null userId: number | null
templateCode: string templateCode: string
templateParams: Map<String, Object> templateParams: Map<String, Object>
} }
// 查询站内信模板列表 // 查询站内信模板列表
export const getNotifyTemplatePage = async (params: PageParam) => { export const getNotifyTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-template/page', params }) return await requestData({ url: '/system/notify-template/page', method: 'GET', params })
} }
// 查询站内信模板详情 // 查询站内信模板详情
export const getNotifyTemplate = async (id: number) => { export const getNotifyTemplate = async (id: number) => {
return await request.get({ url: '/system/notify-template/get?id=' + id }) return await requestData({ url: '/system/notify-template/get?id=' + id, method: 'GET' })
} }
// 新增站内信模板 // 新增站内信模板
export const createNotifyTemplate = async (data: NotifyTemplateVO) => { export const createNotifyTemplate = async (data: NotifyTemplateVO) => {
return await request.post({ url: '/system/notify-template/create', data }) return await requestData({ url: '/system/notify-template/create', method: 'POST', data })
} }
// 修改站内信模板 // 修改站内信模板
export const updateNotifyTemplate = async (data: NotifyTemplateVO) => { export const updateNotifyTemplate = async (data: NotifyTemplateVO) => {
return await request.put({ url: '/system/notify-template/update', data }) return await requestData({ url: '/system/notify-template/update', method: 'PUT', data })
} }
// 删除站内信模板 // 删除站内信模板
export const deleteNotifyTemplate = async (id: number) => { export const deleteNotifyTemplate = async (id: number) => {
return await request.delete({ url: '/system/notify-template/delete?id=' + id }) return await requestData({ url: '/system/notify-template/delete?id=' + id, method: 'DELETE' })
} }
// 发送站内信 // 发送站内信
export const sendNotify = (data: NotifySendReqVO) => { export const sendNotify = (data: NotifySendReqVO) => {
return request.post({ url: '/system/notify-template/send-notify', data }) return requestData({ url: '/system/notify-template/send-notify', method: 'POST', data })
} }

View File

@@ -1,46 +1,46 @@
<!-- 数据字典 Select 选择器 --> <!-- 数据字典 Select 选择器 -->
<template> <template>
<el-select class="w-1/1" v-bind="attrs"> <el-select class="w-1/1" v-bind="attrs" filterable>
<template v-if="valueType === 'int'"> <template v-if="valueType === 'int'">
<el-option <el-option
v-for="(dict, index) in getIntDictOptions(dictType)" v-for="(dict, index) in getIntDictOptions(dictType)"
:key="index" :key="index"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />
</template> </template>
<template v-if="valueType === 'str'"> <template v-if="valueType === 'str'">
<el-option <el-option
v-for="(dict, index) in getStrDictOptions(dictType)" v-for="(dict, index) in getStrDictOptions(dictType)"
:key="index" :key="index"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />
</template> </template>
<template v-if="valueType === 'bool'"> <template v-if="valueType === 'bool'">
<el-option <el-option
v-for="(dict, index) in getBoolDictOptions(dictType)" v-for="(dict, index) in getBoolDictOptions(dictType)"
:key="index" :key="index"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
/> />
</template> </template>
</el-select> </el-select>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict' import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
// 接受父组件参数 // 接受父组件参数
interface Props { interface Props {
dictType: string // 字典类型 dictType: string // 字典类型
valueType: string // 字典值类型 valueType: string // 字典值类型
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
dictType: '', dictType: '',
valueType: 'str' valueType: 'str'
}) })
const attrs = useAttrs() const attrs = useAttrs()
defineOptions({ name: 'DictSelect' }) defineOptions({ name: 'DictSelect' })
</script> </script>

View File

@@ -103,7 +103,7 @@ const highlightDiagram = async () => {
let findProcessTask = false //是否已经高亮了进行中的任务 let findProcessTask = false //是否已经高亮了进行中的任务
//进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据 //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
let removeTaskDefinitionKeyList = [] let removeTaskDefinitionKeyList = []
// debugger //
//芋道这里取值是rootElements[0].flowElements //芋道这里取值是rootElements[0].flowElements
bpmnModeler.getDefinitions().rootElements[1].flowElements?.forEach((n: any) => { bpmnModeler.getDefinitions().rootElements[1].flowElements?.forEach((n: any) => {
let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动 let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
@@ -135,7 +135,6 @@ const highlightDiagram = async () => {
// 处理 outgoing 出线 // 处理 outgoing 出线
const outgoing = getActivityOutgoing(activity) const outgoing = getActivityOutgoing(activity)
outgoing?.forEach((nn: any) => { outgoing?.forEach((nn: any) => {
// debugger
let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id) let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
// 如果目标活动存在则根据该活动是否结束进行【bpmn:SequenceFlow】连线的高亮设置 // 如果目标活动存在则根据该活动是否结束进行【bpmn:SequenceFlow】连线的高亮设置
if (targetActivity) { if (targetActivity) {

View File

@@ -2,14 +2,14 @@
<div class="panel-tab__content"> <div class="panel-tab__content">
<el-form :model="flowConditionForm" label-width="90px" size="small"> <el-form :model="flowConditionForm" label-width="90px" size="small">
<el-form-item label="流转类型"> <el-form-item label="流转类型">
<el-select v-model="flowConditionForm.type" @change="updateFlowType"> <el-select v-model="flowConditionForm.type" @change="updateFlowType" filterable>
<el-option label="普通流转路径" value="normal" /> <el-option label="普通流转路径" value="normal" />
<el-option label="默认流转路径" value="default" /> <el-option label="默认流转路径" value="default" />
<el-option label="条件流转路径" value="condition" /> <el-option label="条件流转路径" value="condition" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="条件格式" v-if="flowConditionForm.type === 'condition'" key="condition"> <el-form-item label="条件格式" v-if="flowConditionForm.type === 'condition'" key="condition">
<el-select v-model="flowConditionForm.conditionType"> <el-select v-model="flowConditionForm.conditionType" filterable>
<el-option label="表达式" value="expression" /> <el-option label="表达式" value="expression" />
<el-option label="脚本" value="script" /> <el-option label="脚本" value="script" />
</el-select> </el-select>
@@ -33,7 +33,7 @@
<el-input v-model="flowConditionForm.language" clearable @change="updateFlowCondition" /> <el-input v-model="flowConditionForm.language" clearable @change="updateFlowCondition" />
</el-form-item> </el-form-item>
<el-form-item label="脚本类型" key="scriptType"> <el-form-item label="脚本类型" key="scriptType">
<el-select v-model="flowConditionForm.scriptType"> <el-select v-model="flowConditionForm.scriptType" filterable>
<el-option label="内联脚本" value="inlineScript" /> <el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" /> <el-option label="外部脚本" value="externalScript" />
</el-select> </el-select>

View File

@@ -1,481 +1,481 @@
<template> <template>
<div class="panel-tab__content"> <div class="panel-tab__content">
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="流程表单"> <el-form-item label="流程表单">
<!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />--> <!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
<el-select v-model="formKey" clearable @change="updateElementFormKey"> <el-select v-model="formKey" clearable @change="updateElementFormKey" filterable>
<el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- <el-form-item label="业务标识">--> <!-- <el-form-item label="业务标识">-->
<!-- <el-select v-model="businessKey" @change="updateElementBusinessKey">--> <!-- <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
<!-- <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />--> <!-- <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
<!-- <el-option label="无" value="" />--> <!-- <el-option label="无" value="" />-->
<!-- </el-select>--> <!-- </el-select>-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
</el-form> </el-form>
<!--字段列表--> <!--字段列表-->
<!-- <div class="element-property list-property">--> <!-- <div class="element-property list-property">-->
<!-- <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>--> <!-- <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>-->
<!-- <el-table :data="fieldList" max-height="240" fit border>--> <!-- <el-table :data="fieldList" max-height="240" fit border>-->
<!-- <el-table-column label="序号" type="index" width="50px" />--> <!-- <el-table-column label="序号" type="index" width="50px" />-->
<!-- <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />--> <!-- <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />-->
<!-- <el-table-column--> <!-- <el-table-column-->
<!-- label="字段类型"--> <!-- label="字段类型"-->
<!-- prop="type"--> <!-- prop="type"-->
<!-- min-width="80px"--> <!-- min-width="80px"-->
<!-- :formatter="(row) => fieldType[row.type] || row.type"--> <!-- :formatter="(row) => fieldType[row.type] || row.type"-->
<!-- show-overflow-tooltip--> <!-- show-overflow-tooltip-->
<!-- />--> <!-- />-->
<!-- <el-table-column--> <!-- <el-table-column-->
<!-- label="默认值"--> <!-- label="默认值"-->
<!-- prop="defaultValue"--> <!-- prop="defaultValue"-->
<!-- min-width="80px"--> <!-- min-width="80px"-->
<!-- show-overflow-tooltip--> <!-- show-overflow-tooltip-->
<!-- />--> <!-- />-->
<!-- <el-table-column label="操作" width="90px">--> <!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">--> <!-- <template #default="scope">-->
<!-- <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"--> <!-- <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
<!-- >编辑</el-button--> <!-- >编辑</el-button-->
<!-- >--> <!-- >-->
<!-- <el-divider direction="vertical" />--> <!-- <el-divider direction="vertical" />-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- style="color: #ff4d4f"--> <!-- style="color: #ff4d4f"-->
<!-- @click="removeField(scope, scope.$index)"--> <!-- @click="removeField(scope, scope.$index)"-->
<!-- >移除</el-button--> <!-- >移除</el-button-->
<!-- >--> <!-- >-->
<!-- </template>--> <!-- </template>-->
<!-- </el-table-column>--> <!-- </el-table-column>-->
<!-- </el-table>--> <!-- </el-table>-->
<!-- </div>--> <!-- </div>-->
<!-- <div class="element-drawer__button">--> <!-- <div class="element-drawer__button">-->
<!-- <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />--> <!-- <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />-->
<!-- </div>--> <!-- </div>-->
<!--字段配置侧边栏--> <!--字段配置侧边栏-->
<!-- <el-drawer--> <!-- <el-drawer-->
<!-- v-model="fieldModelVisible"--> <!-- v-model="fieldModelVisible"-->
<!-- title="字段配置"--> <!-- title="字段配置"-->
<!-- :size="`${width}px`"--> <!-- :size="`${width}px`"-->
<!-- append-to-body--> <!-- append-to-body-->
<!-- destroy-on-close--> <!-- destroy-on-close-->
<!-- >--> <!-- >-->
<!-- <el-form :model="formFieldForm" label-width="90px">--> <!-- <el-form :model="formFieldForm" label-width="90px">-->
<!-- <el-form-item label="字段ID">--> <!-- <el-form-item label="字段ID">-->
<!-- <el-input v-model="formFieldForm.id" clearable />--> <!-- <el-input v-model="formFieldForm.id" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="类型">--> <!-- <el-form-item label="类型">-->
<!-- <el-select--> <!-- <el-select-->
<!-- v-model="formFieldForm.typeType"--> <!-- v-model="formFieldForm.typeType"-->
<!-- placeholder="请选择字段类型"--> <!-- placeholder="请选择字段类型"-->
<!-- clearable--> <!-- clearable-->
<!-- @change="changeFieldTypeType"--> <!-- @change="changeFieldTypeType"-->
<!-- >--> <!-- >-->
<!-- <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />--> <!-- <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
<!-- </el-select>--> <!-- </el-select>-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">--> <!-- <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
<!-- <el-input v-model="formFieldForm.type" clearable />--> <!-- <el-input v-model="formFieldForm.type" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="名称">--> <!-- <el-form-item label="名称">-->
<!-- <el-input v-model="formFieldForm.label" clearable />--> <!-- <el-input v-model="formFieldForm.label" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">--> <!-- <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
<!-- <el-input v-model="formFieldForm.datePattern" clearable />--> <!-- <el-input v-model="formFieldForm.datePattern" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="默认值">--> <!-- <el-form-item label="默认值">-->
<!-- <el-input v-model="formFieldForm.defaultValue" clearable />--> <!-- <el-input v-model="formFieldForm.defaultValue" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- </el-form>--> <!-- </el-form>-->
<!-- &lt;!&ndash; 枚举值设置 &ndash;&gt;--> <!-- &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
<!-- <template v-if="formFieldForm.type === 'enum'">--> <!-- <template v-if="formFieldForm.type === 'enum'">-->
<!-- <el-divider key="enum-divider" />--> <!-- <el-divider key="enum-divider" />-->
<!-- <p class="listener-filed__title" key="enum-title">--> <!-- <p class="listener-filed__title" key="enum-title">-->
<!-- <span><Icon icon="ep:menu" />枚举值列表</span>--> <!-- <span><Icon icon="ep:menu" />枚举值列表</span>-->
<!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"--> <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
<!-- >添加枚举值</el-button--> <!-- >添加枚举值</el-button-->
<!-- >--> <!-- >-->
<!-- </p>--> <!-- </p>-->
<!-- <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>--> <!-- <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
<!-- <el-table-column label="序号" width="50px" type="index" />--> <!-- <el-table-column label="序号" width="50px" type="index" />-->
<!-- <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />--> <!-- <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />--> <!-- <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="操作" width="90px">--> <!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">--> <!-- <template #default="scope">-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"--> <!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
<!-- >编辑</el-button--> <!-- >编辑</el-button-->
<!-- >--> <!-- >-->
<!-- <el-divider direction="vertical" />--> <!-- <el-divider direction="vertical" />-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- style="color: #ff4d4f"--> <!-- style="color: #ff4d4f"-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"--> <!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
<!-- >移除</el-button--> <!-- >移除</el-button-->
<!-- >--> <!-- >-->
<!-- </template>--> <!-- </template>-->
<!-- </el-table-column>--> <!-- </el-table-column>-->
<!-- </el-table>--> <!-- </el-table>-->
<!-- </template>--> <!-- </template>-->
<!-- &lt;!&ndash; 校验规则 &ndash;&gt;--> <!-- &lt;!&ndash; 校验规则 &ndash;&gt;-->
<!-- <el-divider key="validation-divider" />--> <!-- <el-divider key="validation-divider" />-->
<!-- <p class="listener-filed__title" key="validation-title">--> <!-- <p class="listener-filed__title" key="validation-title">-->
<!-- <span><Icon icon="ep:menu" />约束条件列表</span>--> <!-- <span><Icon icon="ep:menu" />约束条件列表</span>-->
<!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"--> <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
<!-- >添加约束</el-button--> <!-- >添加约束</el-button-->
<!-- >--> <!-- >-->
<!-- </p>--> <!-- </p>-->
<!-- <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>--> <!-- <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
<!-- <el-table-column label="序号" width="50px" type="index" />--> <!-- <el-table-column label="序号" width="50px" type="index" />-->
<!-- <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />--> <!-- <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />--> <!-- <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="操作" width="90px">--> <!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">--> <!-- <template #default="scope">-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"--> <!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
<!-- >编辑</el-button--> <!-- >编辑</el-button-->
<!-- >--> <!-- >-->
<!-- <el-divider direction="vertical" />--> <!-- <el-divider direction="vertical" />-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- style="color: #ff4d4f"--> <!-- style="color: #ff4d4f"-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"--> <!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
<!-- >移除</el-button--> <!-- >移除</el-button-->
<!-- >--> <!-- >-->
<!-- </template>--> <!-- </template>-->
<!-- </el-table-column>--> <!-- </el-table-column>-->
<!-- </el-table>--> <!-- </el-table>-->
<!-- &lt;!&ndash; 表单属性 &ndash;&gt;--> <!-- &lt;!&ndash; 表单属性 &ndash;&gt;-->
<!-- <el-divider key="property-divider" />--> <!-- <el-divider key="property-divider" />-->
<!-- <p class="listener-filed__title" key="property-title">--> <!-- <p class="listener-filed__title" key="property-title">-->
<!-- <span><Icon icon="ep:menu" />字段属性列表</span>--> <!-- <span><Icon icon="ep:menu" />字段属性列表</span>-->
<!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"--> <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
<!-- >添加属性</el-button--> <!-- >添加属性</el-button-->
<!-- >--> <!-- >-->
<!-- </p>--> <!-- </p>-->
<!-- <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>--> <!-- <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
<!-- <el-table-column label="序号" width="50px" type="index" />--> <!-- <el-table-column label="序号" width="50px" type="index" />-->
<!-- <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />--> <!-- <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />--> <!-- <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />-->
<!-- <el-table-column label="操作" width="90px">--> <!-- <el-table-column label="操作" width="90px">-->
<!-- <template #default="scope">--> <!-- <template #default="scope">-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"--> <!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
<!-- >编辑</el-button--> <!-- >编辑</el-button-->
<!-- >--> <!-- >-->
<!-- <el-divider direction="vertical" />--> <!-- <el-divider direction="vertical" />-->
<!-- <el-button--> <!-- <el-button-->
<!-- type="primary"--> <!-- type="primary"-->
<!-- link--> <!-- link-->
<!-- style="color: #ff4d4f"--> <!-- style="color: #ff4d4f"-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"--> <!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
<!-- >移除</el-button--> <!-- >移除</el-button-->
<!-- >--> <!-- >-->
<!-- </template>--> <!-- </template>-->
<!-- </el-table-column>--> <!-- </el-table-column>-->
<!-- </el-table>--> <!-- </el-table>-->
<!-- &lt;!&ndash; 底部按钮 &ndash;&gt;--> <!-- &lt;!&ndash; 底部按钮 &ndash;&gt;-->
<!-- <div class="element-drawer__button">--> <!-- <div class="element-drawer__button">-->
<!-- <el-button> </el-button>--> <!-- <el-button> </el-button>-->
<!-- <el-button type="primary" @click="saveField"> </el-button>--> <!-- <el-button type="primary" @click="saveField"> </el-button>-->
<!-- </div>--> <!-- </div>-->
<!-- </el-drawer>--> <!-- </el-drawer>-->
<!-- <el-dialog--> <!-- <el-dialog-->
<!-- v-model="fieldOptionModelVisible"--> <!-- v-model="fieldOptionModelVisible"-->
<!-- :title="optionModelTitle"--> <!-- :title="optionModelTitle"-->
<!-- width="600px"--> <!-- width="600px"-->
<!-- append-to-body--> <!-- append-to-body-->
<!-- destroy-on-close--> <!-- destroy-on-close-->
<!-- >--> <!-- >-->
<!-- <el-form :model="fieldOptionForm" label-width="96px">--> <!-- <el-form :model="fieldOptionForm" label-width="96px">-->
<!-- <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">--> <!-- <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
<!-- <el-input v-model="fieldOptionForm.id" clearable />--> <!-- <el-input v-model="fieldOptionForm.id" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">--> <!-- <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
<!-- <el-input v-model="fieldOptionForm.name" clearable />--> <!-- <el-input v-model="fieldOptionForm.name" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">--> <!-- <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
<!-- <el-input v-model="fieldOptionForm.config" clearable />--> <!-- <el-input v-model="fieldOptionForm.config" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">--> <!-- <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
<!-- <el-input v-model="fieldOptionForm.value" clearable />--> <!-- <el-input v-model="fieldOptionForm.value" clearable />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- </el-form>--> <!-- </el-form>-->
<!-- <template #footer>--> <!-- <template #footer>-->
<!-- <el-button @click="fieldOptionModelVisible = false"> </el-button>--> <!-- <el-button @click="fieldOptionModelVisible = false"> </el-button>-->
<!-- <el-button type="primary" @click="saveFieldOption"> </el-button>--> <!-- <el-button type="primary" @click="saveFieldOption"> </el-button>-->
<!-- </template>--> <!-- </template>-->
<!-- </el-dialog>--> <!-- </el-dialog>-->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getFormSimpleList } from '@/api/bpm-boot/form' import { getFormSimpleList } from '@/api/bpm-boot/form'
defineOptions({ name: 'ElementForm' }) defineOptions({ name: 'ElementForm' })
const props = defineProps({ const props = defineProps({
id: String, id: String,
type: String type: String
}) })
const prefix = inject('prefix') const prefix = inject('prefix')
const width = inject('width') const width = inject('width')
const formKey = ref('') const formKey = ref('')
const businessKey = ref('') const businessKey = ref('')
const optionModelTitle = ref('') const optionModelTitle = ref('')
const fieldList = ref<any[]>([]) const fieldList = ref<any[]>([])
const formFieldForm = ref<any>({}) const formFieldForm = ref<any>({})
const fieldType = ref({ const fieldType = ref({
long: '长整型', long: '长整型',
string: '字符串', string: '字符串',
boolean: '布尔类', boolean: '布尔类',
date: '日期类', date: '日期类',
enum: '枚举类', enum: '枚举类',
custom: '自定义类型' custom: '自定义类型'
}) })
const formFieldIndex = ref(-1) // 编辑中的字段, -1 为新增 const formFieldIndex = ref(-1) // 编辑中的字段, -1 为新增
const formFieldOptionIndex = ref(-1) // 编辑中的字段配置项, -1 为新增 const formFieldOptionIndex = ref(-1) // 编辑中的字段配置项, -1 为新增
const fieldModelVisible = ref(false) const fieldModelVisible = ref(false)
const fieldOptionModelVisible = ref(false) const fieldOptionModelVisible = ref(false)
const fieldOptionForm = ref<any>({}) // 当前激活的字段配置项数据 const fieldOptionForm = ref<any>({}) // 当前激活的字段配置项数据
const fieldOptionType = ref('') // 当前激活的字段配置项弹窗 类型 const fieldOptionType = ref('') // 当前激活的字段配置项弹窗 类型
const fieldEnumList = ref<any[]>([]) // 枚举值列表 const fieldEnumList = ref<any[]>([]) // 枚举值列表
const fieldConstraintsList = ref<any[]>([]) // 约束条件列表 const fieldConstraintsList = ref<any[]>([]) // 约束条件列表
const fieldPropertiesList = ref<any[]>([]) // 绑定属性列表 const fieldPropertiesList = ref<any[]>([]) // 绑定属性列表
const bpmnELement = ref() const bpmnELement = ref()
const elExtensionElements = ref() const elExtensionElements = ref()
const formData = ref() const formData = ref()
const otherExtensions = ref() const otherExtensions = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances const bpmnInstances = () => (window as any)?.bpmnInstances
const resetFormList = () => { const resetFormList = () => {
bpmnELement.value = bpmnInstances().bpmnElement bpmnELement.value = bpmnInstances().bpmnElement
formKey.value = bpmnELement.value.businessObject.formKey formKey.value = bpmnELement.value.businessObject.formKey
// if (formKey.value?.length > 0) { // if (formKey.value?.length > 0) {
// formKey.value = parseInt(formKey.value) // formKey.value = parseInt(formKey.value)
// } // }
// 获取元素扩展属性 或者 创建扩展属性 // 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value = elExtensionElements.value =
bpmnELement.value.businessObject.get('extensionElements') || bpmnELement.value.businessObject.get('extensionElements') ||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
// 获取元素表单配置 或者 创建新的表单配置 // 获取元素表单配置 或者 创建新的表单配置
formData.value = formData.value =
elExtensionElements.value.values.filter((ex) => ex.$type === `${prefix}:FormData`)?.[0] || elExtensionElements.value.values.filter((ex) => ex.$type === `${prefix}:FormData`)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] }) bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] })
// 业务标识 businessKey 绑定在 formData 中 // 业务标识 businessKey 绑定在 formData 中
businessKey.value = formData.value.businessKey businessKey.value = formData.value.businessKey
// 保留剩余扩展元素,便于后面更新该元素对应属性 // 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value = elExtensionElements.value.values.filter( otherExtensions.value = elExtensionElements.value.values.filter(
(ex) => ex.$type !== `${prefix}:FormData` (ex) => ex.$type !== `${prefix}:FormData`
) )
// 复制原始值,填充表格 // 复制原始值,填充表格
fieldList.value = JSON.parse(JSON.stringify(formData.value.fields || [])) fieldList.value = JSON.parse(JSON.stringify(formData.value.fields || []))
// 更新元素扩展属性,避免后续报错 // 更新元素扩展属性,避免后续报错
updateElementExtensions() updateElementExtensions()
} }
const updateElementFormKey = () => { const updateElementFormKey = () => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
formKey: formKey.value formKey: formKey.value
}) })
} }
const updateElementBusinessKey = () => { const updateElementBusinessKey = () => {
bpmnInstances().modeling.updateModdleProperties(toRaw(bpmnELement.value), formData.value, { bpmnInstances().modeling.updateModdleProperties(toRaw(bpmnELement.value), formData.value, {
businessKey: businessKey.value businessKey: businessKey.value
}) })
} }
// 根据类型调整字段type // 根据类型调整字段type
const changeFieldTypeType = (type) => { const changeFieldTypeType = (type) => {
// this.$set(this.formFieldForm, "type", type === "custom" ? "" : type); // this.$set(this.formFieldForm, "type", type === "custom" ? "" : type);
formFieldForm.value['type'] = type === 'custom' ? '' : type formFieldForm.value['type'] = type === 'custom' ? '' : type
} }
// 打开字段详情侧边栏 // 打开字段详情侧边栏
const openFieldForm = (field, index) => { const openFieldForm = (field, index) => {
formFieldIndex.value = index formFieldIndex.value = index
if (index !== -1) { if (index !== -1) {
const FieldObject = formData.value.fields[index] const FieldObject = formData.value.fields[index]
formFieldForm.value = JSON.parse(JSON.stringify(field)) formFieldForm.value = JSON.parse(JSON.stringify(field))
// 设置自定义类型 // 设置自定义类型
// this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type); // this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
formFieldForm.value['typeType'] = !fieldType.value[field.type] ? 'custom' : field.type formFieldForm.value['typeType'] = !fieldType.value[field.type] ? 'custom' : field.type
// 初始化枚举值列表 // 初始化枚举值列表
field.type === 'enum' && field.type === 'enum' &&
(fieldEnumList.value = JSON.parse(JSON.stringify(FieldObject?.values || []))) (fieldEnumList.value = JSON.parse(JSON.stringify(FieldObject?.values || [])))
// 初始化约束条件列表 // 初始化约束条件列表
fieldConstraintsList.value = JSON.parse( fieldConstraintsList.value = JSON.parse(
JSON.stringify(FieldObject?.validation?.constraints || []) JSON.stringify(FieldObject?.validation?.constraints || [])
) )
// 初始化自定义属性列表 // 初始化自定义属性列表
fieldPropertiesList.value = JSON.parse(JSON.stringify(FieldObject?.properties?.values || [])) fieldPropertiesList.value = JSON.parse(JSON.stringify(FieldObject?.properties?.values || []))
} else { } else {
formFieldForm.value = {} formFieldForm.value = {}
// 初始化枚举值列表 // 初始化枚举值列表
fieldEnumList.value = [] fieldEnumList.value = []
// 初始化约束条件列表 // 初始化约束条件列表
fieldConstraintsList.value = [] fieldConstraintsList.value = []
// 初始化自定义属性列表 // 初始化自定义属性列表
fieldPropertiesList.value = [] fieldPropertiesList.value = []
} }
fieldModelVisible.value = true fieldModelVisible.value = true
} }
// 打开字段 某个 配置项 弹窗 // 打开字段 某个 配置项 弹窗
const openFieldOptionForm = (option, index, type) => { const openFieldOptionForm = (option, index, type) => {
fieldOptionModelVisible.value = true fieldOptionModelVisible.value = true
fieldOptionType.value = type fieldOptionType.value = type
formFieldOptionIndex.value = index formFieldOptionIndex.value = index
if (type === 'property') { if (type === 'property') {
fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {} fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
return (optionModelTitle.value = '属性配置') return (optionModelTitle.value = '属性配置')
} }
if (type === 'enum') { if (type === 'enum') {
fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {} fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
return (optionModelTitle.value = '枚举值配置') return (optionModelTitle.value = '枚举值配置')
} }
fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {} fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
return (optionModelTitle.value = '约束条件配置') return (optionModelTitle.value = '约束条件配置')
} }
// 保存字段 某个 配置项 // 保存字段 某个 配置项
const saveFieldOption = () => { const saveFieldOption = () => {
if (formFieldOptionIndex.value === -1) { if (formFieldOptionIndex.value === -1) {
if (fieldOptionType.value === 'property') { if (fieldOptionType.value === 'property') {
fieldPropertiesList.value.push(fieldOptionForm.value) fieldPropertiesList.value.push(fieldOptionForm.value)
} }
if (fieldOptionType.value === 'constraint') { if (fieldOptionType.value === 'constraint') {
fieldConstraintsList.value.push(fieldOptionForm.value) fieldConstraintsList.value.push(fieldOptionForm.value)
} }
if (fieldOptionType.value === 'enum') { if (fieldOptionType.value === 'enum') {
fieldEnumList.value.push(fieldOptionForm.value) fieldEnumList.value.push(fieldOptionForm.value)
} }
} else { } else {
fieldOptionType.value === 'property' && fieldOptionType.value === 'property' &&
fieldPropertiesList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value) fieldPropertiesList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
fieldOptionType.value === 'constraint' && fieldOptionType.value === 'constraint' &&
fieldConstraintsList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value) fieldConstraintsList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
fieldOptionType.value === 'enum' && fieldOptionType.value === 'enum' &&
fieldEnumList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value) fieldEnumList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
} }
fieldOptionModelVisible.value = false fieldOptionModelVisible.value = false
fieldOptionForm.value = {} fieldOptionForm.value = {}
} }
// 保存字段配置 // 保存字段配置
const saveField = () => { const saveField = () => {
const { id, type, label, defaultValue, datePattern } = formFieldForm.value const { id, type, label, defaultValue, datePattern } = formFieldForm.value
const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, { id, type, label }) const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, { id, type, label })
defaultValue && (Field.defaultValue = defaultValue) defaultValue && (Field.defaultValue = defaultValue)
datePattern && (Field.datePattern = datePattern) datePattern && (Field.datePattern = datePattern)
// 构建属性 // 构建属性
if (fieldPropertiesList.value && fieldPropertiesList.value.length) { if (fieldPropertiesList.value && fieldPropertiesList.value.length) {
const fieldPropertyList = fieldPropertiesList.value.map((fp) => { const fieldPropertyList = fieldPropertiesList.value.map((fp) => {
return bpmnInstances().moddle.create(`${prefix}:Property`, { return bpmnInstances().moddle.create(`${prefix}:Property`, {
id: fp.id, id: fp.id,
value: fp.value value: fp.value
}) })
}) })
Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, { Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, {
values: fieldPropertyList values: fieldPropertyList
}) })
} }
// 构建校验规则 // 构建校验规则
if (fieldConstraintsList.value && fieldConstraintsList.value.length) { if (fieldConstraintsList.value && fieldConstraintsList.value.length) {
const fieldConstraintList = fieldConstraintsList.value.map((fc) => { const fieldConstraintList = fieldConstraintsList.value.map((fc) => {
return bpmnInstances().moddle.create(`${prefix}:Constraint`, { return bpmnInstances().moddle.create(`${prefix}:Constraint`, {
name: fc.name, name: fc.name,
config: fc.config config: fc.config
}) })
}) })
Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, { Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, {
constraints: fieldConstraintList constraints: fieldConstraintList
}) })
} }
// 构建枚举值 // 构建枚举值
if (fieldEnumList.value && fieldEnumList.value.length) { if (fieldEnumList.value && fieldEnumList.value.length) {
Field.values = fieldEnumList.value.map((fe) => { Field.values = fieldEnumList.value.map((fe) => {
return bpmnInstances().moddle.create(`${prefix}:Value`, { name: fe.name, id: fe.id }) return bpmnInstances().moddle.create(`${prefix}:Value`, { name: fe.name, id: fe.id })
}) })
} }
// 更新数组 与 表单配置实例 // 更新数组 与 表单配置实例
if (formFieldIndex.value === -1) { if (formFieldIndex.value === -1) {
fieldList.value.push(formFieldForm.value) fieldList.value.push(formFieldForm.value)
formData.value.fields.push(Field) formData.value.fields.push(Field)
} else { } else {
fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value) fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value)
formData.value.fields.splice(formFieldIndex.value, 1, Field) formData.value.fields.splice(formFieldIndex.value, 1, Field)
} }
updateElementExtensions() updateElementExtensions()
fieldModelVisible.value = false fieldModelVisible.value = false
} }
// 移除某个 字段的 配置项 // 移除某个 字段的 配置项
const removeFieldOptionItem = (option, index, type) => { const removeFieldOptionItem = (option, index, type) => {
// console.log(option, 'option') // console.log(option, 'option')
if (type === 'property') { if (type === 'property') {
fieldPropertiesList.value.splice(index, 1) fieldPropertiesList.value.splice(index, 1)
return return
} }
if (type === 'enum') { if (type === 'enum') {
fieldEnumList.value.splice(index, 1) fieldEnumList.value.splice(index, 1)
return return
} }
fieldConstraintsList.value.splice(index, 1) fieldConstraintsList.value.splice(index, 1)
} }
// 移除 字段 // 移除 字段
const removeField = (field, index) => { const removeField = (field, index) => {
console.log(field, 'field') console.log(field, 'field')
fieldList.value.splice(index, 1) fieldList.value.splice(index, 1)
formData.value.fields.splice(index, 1) formData.value.fields.splice(index, 1)
updateElementExtensions() updateElementExtensions()
} }
const updateElementExtensions = () => { const updateElementExtensions = () => {
// 更新回扩展元素 // 更新回扩展元素
const newElExtensionElements = bpmnInstances().moddle.create(`bpmn:ExtensionElements`, { const newElExtensionElements = bpmnInstances().moddle.create(`bpmn:ExtensionElements`, {
values: otherExtensions.value.concat(formData.value) values: otherExtensions.value.concat(formData.value)
}) })
// 更新到元素上 // 更新到元素上
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
extensionElements: newElExtensionElements extensionElements: newElExtensionElements
}) })
} }
const formList = ref([]) // 流程表单的下拉框的数据 const formList = ref([]) // 流程表单的下拉框的数据
onMounted(async () => { onMounted(async () => {
await getFormSimpleList().then(res => { await getFormSimpleList().then(res => {
formList.value = res.data formList.value = res.data
}) })
}) })
watch( watch(
() => props.id, () => props.id,
(val) => { (val) => {
val && val &&
val.length && val.length &&
nextTick(() => { nextTick(() => {
resetFormList() resetFormList()
}) })
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>

View File

@@ -1,463 +1,459 @@
<template> <template>
<div class='panel-tab__content'> <div class='panel-tab__content'>
<el-table :data='elementListenersList' size='small' border> <el-table :data='elementListenersList' size='small' border>
<el-table-column label='序号' width='80px' type='index' /> <el-table-column label='序号' width='80px' type='index' />
<el-table-column label='事件类型' min-width='100px' prop='event' /> <el-table-column label='事件类型' min-width='100px' prop='event' />
<el-table-column <el-table-column
label='监听器类型' label='监听器类型'
min-width='100px' min-width='100px'
show-overflow-tooltip show-overflow-tooltip
:formatter='(row) => listenerTypeObject[row.listenerType]' :formatter='(row) => listenerTypeObject[row.listenerType]'
/> />
<el-table-column label='操作' width='100px'> <el-table-column label='操作' width='100px'>
<template #default='scope'> <template #default='scope'>
<el-button size='small' link @click='openListenerForm(scope.row, scope.$index)' <el-button size='small' link @click='openListenerForm(scope.row, scope.$index)'
>编辑 >编辑
</el-button </el-button
> >
<el-divider direction='vertical' /> <el-divider direction='vertical' />
<el-button size='small' link style='color: #ff4d4f' @click='removeListener(scope.$index)' <el-button size='small' link style='color: #ff4d4f' @click='removeListener(scope.$index)'
>移除 >移除
</el-button </el-button
> >
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class='element-drawer__button'> <div class='element-drawer__button'>
<XButton <XButton
type="primary" type="primary"
preIcon="ep:plus" preIcon="ep:plus"
title="添加监听器" title="添加监听器"
size="small" size="small"
@click="openListenerForm(null)" @click="openListenerForm(null)"
/> />
<!-- <Plus type='primary'--> <!-- <Plus type='primary'-->
<!-- title='添加监听器'--> <!-- title='添加监听器'-->
<!-- size='small'--> <!-- size='small'-->
<!-- @click='openListenerForm(null)' />--> <!-- @click='openListenerForm(null)' />-->
<XButton <XButton
type="success" type="success"
preIcon="ep:select" preIcon="ep:select"
title="选择监听器" title="选择监听器"
size="small" size="small"
@click="openProcessListenerDialog" @click="openProcessListenerDialog"
/> />
<!-- <Select--> <!-- <Select-->
<!-- type='success'--> <!-- type='success'-->
<!-- title='选择监听器'--> <!-- title='选择监听器'-->
<!-- size='small'--> <!-- size='small'-->
<!-- @click='openProcessListenerDialog'--> <!-- @click='openProcessListenerDialog'-->
<!-- />--> <!-- />-->
</div> </div>
<!-- 监听器 编辑/创建 部分 --> <!-- 监听器 编辑/创建 部分 -->
<el-drawer <el-drawer
v-model='listenerFormModelVisible' v-model='listenerFormModelVisible'
title='执行监听器' title='执行监听器'
:size='`${width}px`' :size='`${width}px`'
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<el-form :model='listenerForm' label-width='96px' ref='listenerFormRef'> <el-form :model='listenerForm' label-width='96px' ref='listenerFormRef'>
<el-form-item <el-form-item
label='事件类型' label='事件类型'
prop='event' prop='event'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model='listenerForm.event'> <el-select v-model='listenerForm.event' filterable>
<el-option label='start' value='start' /> <el-option label='start' value='start' />
<el-option label='end' value='end' /> <el-option label='end' value='end' />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label='监听器类型' label='监听器类型'
prop='listenerType' prop='listenerType'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model='listenerForm.listenerType'> <el-select v-model='listenerForm.listenerType' filterable>
<el-option <el-option
v-for='i in Object.keys(listenerTypeObject)' v-for='i in Object.keys(listenerTypeObject)'
:key='i' :key='i'
:label='listenerTypeObject[i]' :label='listenerTypeObject[i]'
:value='i' :value='i'
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerForm.listenerType === 'classListener'" v-if="listenerForm.listenerType === 'classListener'"
label='Java类' label='Java类'
prop='class' prop='class'
key='listener-class' key='listener-class'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-input v-model='listenerForm.class' clearable /> <el-input v-model='listenerForm.class' clearable />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerForm.listenerType === 'expressionListener'" v-if="listenerForm.listenerType === 'expressionListener'"
label='表达式' label='表达式'
prop='expression' prop='expression'
key='listener-expression' key='listener-expression'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-input v-model='listenerForm.expression' clearable /> <el-input v-model='listenerForm.expression' clearable />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerForm.listenerType === 'delegateExpressionListener'" v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label='代理表达式' label='代理表达式'
prop='delegateExpression' prop='delegateExpression'
key='listener-delegate' key='listener-delegate'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-input v-model='listenerForm.delegateExpression' clearable /> <el-input v-model='listenerForm.delegateExpression' clearable />
</el-form-item> </el-form-item>
<template v-if="listenerForm.listenerType === 'scriptListener'"> <template v-if="listenerForm.listenerType === 'scriptListener'">
<el-form-item <el-form-item
label='脚本格式' label='脚本格式'
prop='scriptFormat' prop='scriptFormat'
key='listener-script-format' key='listener-script-format'
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
> >
<el-input v-model='listenerForm.scriptFormat' clearable /> <el-input v-model='listenerForm.scriptFormat' clearable />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label='脚本类型' label='脚本类型'
prop='scriptType' prop='scriptType'
key='listener-script-type' key='listener-script-type'
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
> >
<el-select v-model='listenerForm.scriptType'> <el-select v-model='listenerForm.scriptType' filterable>
<el-option label='内联脚本' value='inlineScript' /> <el-option label='内联脚本' value='inlineScript' />
<el-option label='外部脚本' value='externalScript' /> <el-option label='外部脚本' value='externalScript' />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerForm.scriptType === 'inlineScript'" v-if="listenerForm.scriptType === 'inlineScript'"
label='脚本内容' label='脚本内容'
prop='value' prop='value'
key='listener-script' key='listener-script'
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
> >
<el-input v-model='listenerForm.value' clearable /> <el-input v-model='listenerForm.value' clearable />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerForm.scriptType === 'externalScript'" v-if="listenerForm.scriptType === 'externalScript'"
label='资源地址' label='资源地址'
prop='resource' prop='resource'
key='listener-resource' key='listener-resource'
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
> >
<el-input v-model='listenerForm.resource' clearable /> <el-input v-model='listenerForm.resource' clearable />
</el-form-item> </el-form-item>
</template> </template>
</el-form> </el-form>
<el-divider /> <el-divider />
<p class='listener-filed__title'> <p class='listener-filed__title'>
<span><Menu />注入字段</span> <span><Menu />注入字段</span>
<XButton type='primary' @click='openListenerFieldForm(null)' title='添加字段' /> <XButton type='primary' @click='openListenerFieldForm(null)' title='添加字段' />
</p> </p>
<el-table <el-table
:data='fieldsListOfListener' :data='fieldsListOfListener'
size='small' size='small'
max-height='240' max-height='240'
fit fit
border border
style='flex: none' style='flex: none'
> >
<el-table-column label='序号' width='80px' type='index' /> <el-table-column label='序号' width='80px' type='index' />
<el-table-column label='字段名称' min-width='100px' prop='name' /> <el-table-column label='字段名称' min-width='100px' prop='name' />
<el-table-column <el-table-column
label='字段类型' label='字段类型'
min-width='80px' min-width='80px'
show-overflow-tooltip show-overflow-tooltip
:formatter='(row) => fieldTypeObject[row.fieldType]' :formatter='(row) => fieldTypeObject[row.fieldType]'
/> />
<el-table-column <el-table-column
label='字段值/表达式' label='字段值/表达式'
min-width='100px' min-width='100px'
show-overflow-tooltip show-overflow-tooltip
:formatter='(row) => row.string || row.expression' :formatter='(row) => row.string || row.expression'
/> />
<el-table-column label='操作' width='130px'> <el-table-column label='操作' width='130px'>
<template #default='scope'> <template #default='scope'>
<el-button size='small' link @click='openListenerFieldForm(scope.row, scope.$index)' <el-button size='small' link @click='openListenerFieldForm(scope.row, scope.$index)'
>编辑 >编辑
</el-button </el-button
> >
<el-divider direction='vertical' /> <el-divider direction='vertical' />
<el-button <el-button
size='small' size='small'
link link
style='color: #ff4d4f' style='color: #ff4d4f'
@click='removeListenerField(scope.$index)' @click='removeListenerField(scope.$index)'
>移除 >移除
</el-button </el-button
> >
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class='element-drawer__button'> <div class='element-drawer__button'>
<el-button @click='listenerFormModelVisible = false'> </el-button> <el-button @click='listenerFormModelVisible = false'> </el-button>
<el-button type='primary' @click='saveListenerConfig'> </el-button> <el-button type='primary' @click='saveListenerConfig'> </el-button>
</div> </div>
</el-drawer> </el-drawer>
<!-- 注入西段 编辑/创建 部分 --> <!-- 注入西段 编辑/创建 部分 -->
<el-dialog <el-dialog
title='字段配置' title='字段配置'
v-model='listenerFieldFormModelVisible' v-model='listenerFieldFormModelVisible'
width='600px' width='600px'
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<el-form <el-form
:model='listenerFieldForm' :model='listenerFieldForm'
label-width='96spx' label-width='96spx'
ref='listenerFieldFormRef' ref='listenerFieldFormRef'
style='height: 136px' style='height: 136px'
> >
<el-form-item <el-form-item
label='字段名称:' label='字段名称:'
prop='name' prop='name'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-input v-model='listenerFieldForm.name' clearable /> <el-input v-model='listenerFieldForm.name' clearable />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label='字段类型:' label='字段类型:'
prop='fieldType' prop='fieldType'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model='listenerFieldForm.fieldType'> <el-select v-model='listenerFieldForm.fieldType' filterable>
<el-option <el-option
v-for='i in Object.keys(fieldTypeObject)' v-for='i in Object.keys(fieldTypeObject)'
:key='i' :key='i'
:label='fieldTypeObject[i]' :label='fieldTypeObject[i]'
:value='i' :value='i'
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerFieldForm.fieldType === 'string'" v-if="listenerFieldForm.fieldType === 'string'"
label='字段值:' label='字段值:'
prop='string' prop='string'
key='field-string' key='field-string'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-input v-model='listenerFieldForm.string' clearable /> <el-input v-model='listenerFieldForm.string' clearable />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="listenerFieldForm.fieldType === 'expression'" v-if="listenerFieldForm.fieldType === 'expression'"
label='表达式:' label='表达式:'
prop='expression' prop='expression'
key='field-expression' key='field-expression'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-input v-model='listenerFieldForm.expression' clearable /> <el-input v-model='listenerFieldForm.expression' clearable />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button size='small' @click='listenerFieldFormModelVisible = false'> </el-button> <el-button size='small' @click='listenerFieldFormModelVisible = false'> </el-button>
<el-button size='small' type='primary' @click='saveListenerFiled'> </el-button> <el-button size='small' type='primary' @click='saveListenerFiled'> </el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
<!-- 选择弹窗 --> <!-- 选择弹窗 -->
<ProcessListenerDialog ref='processListenerDialogRef' @select='selectProcessListener' /> <ProcessListenerDialog ref='processListenerDialogRef' @select='selectProcessListener' />
</template> </template>
<script lang='ts' setup> <script lang='ts' setup>
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import XButton from '@/components/XButton/src/XButton.vue' import XButton from '@/components/XButton/src/XButton.vue'
import { Plus, Select, Menu } from '@element-plus/icons-vue' import { Plus, Select, Menu } from '@element-plus/icons-vue'
import { createListenerObject, updateElementExtensions } from '../../utils' import { createListenerObject, updateElementExtensions } from '../../utils'
import { import {
initListenerType, initListenerType,
initListenerForm, initListenerForm,
listenerType, listenerType,
fieldType, fieldType,
initListenerForm2 initListenerForm2
} from './utilSelf' } from './utilSelf'
import ProcessListenerDialog from './ProcessListenerDialog.vue' import ProcessListenerDialog from './ProcessListenerDialog.vue'
defineOptions({ name: 'ElementListeners' }) defineOptions({ name: 'ElementListeners' })
const props = defineProps({ const props = defineProps({
id: String, id: String,
type: String type: String
}) })
const prefix = inject('prefix') const prefix = inject('prefix')
const width = inject('width') const width = inject('width')
const elementListenersList = ref<any[]>([]) // 监听器列表 const elementListenersList = ref<any[]>([]) // 监听器列表
const listenerForm = ref<any>({}) // 监听器详情表单 const listenerForm = ref<any>({}) // 监听器详情表单
const listenerFormModelVisible = ref(false) // 监听器 编辑 侧边栏显示状态 const listenerFormModelVisible = ref(false) // 监听器 编辑 侧边栏显示状态
const fieldsListOfListener = ref<any[]>([]) const fieldsListOfListener = ref<any[]>([])
const listenerFieldForm = ref<any>({}) // 监听器 注入字段 详情表单 const listenerFieldForm = ref<any>({}) // 监听器 注入字段 详情表单
const listenerFieldFormModelVisible = ref(false) // 监听器 注入字段表单弹窗 显示状态 const listenerFieldFormModelVisible = ref(false) // 监听器 注入字段表单弹窗 显示状态
const editingListenerIndex = ref(-1) // 监听器所在下标,-1 为新增 const editingListenerIndex = ref(-1) // 监听器所在下标,-1 为新增
const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增 const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增
const listenerTypeObject = ref(listenerType) const listenerTypeObject = ref(listenerType)
const fieldTypeObject = ref(fieldType) const fieldTypeObject = ref(fieldType)
const bpmnElement = ref() const bpmnElement = ref()
const otherExtensionList = ref() const otherExtensionList = ref()
const bpmnElementListeners = ref() const bpmnElementListeners = ref()
const listenerFormRef = ref() const listenerFormRef = ref()
const listenerFieldFormRef = ref() const listenerFieldFormRef = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances const bpmnInstances = () => (window as any)?.bpmnInstances
const resetListenersList = () => { const resetListenersList = () => {
bpmnElement.value = bpmnInstances().bpmnElement bpmnElement.value = bpmnInstances().bpmnElement
otherExtensionList.value = [] otherExtensionList.value = []
bpmnElementListeners.value = bpmnElementListeners.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter( bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type === `${prefix}:ExecutionListener` (ex) => ex.$type === `${prefix}:ExecutionListener`
) ?? [] ) ?? []
elementListenersList.value = bpmnElementListeners.value.map((listener) => elementListenersList.value = bpmnElementListeners.value.map((listener) =>
initListenerType(listener) initListenerType(listener)
) )
} }
// 打开 监听器详情 侧边栏 // 打开 监听器详情 侧边栏
const openListenerForm = (listener, index?) => { const openListenerForm = (listener, index?) => {
// debugger if (listener) {
if (listener) { listenerForm.value = initListenerForm(listener)
listenerForm.value = initListenerForm(listener) editingListenerIndex.value = index
editingListenerIndex.value = index } else {
} else { listenerForm.value = {}
listenerForm.value = {} editingListenerIndex.value = -1 // 标记为新增
editingListenerIndex.value = -1 // 标记为新增 }
} if (listener && listener.fields) {
if (listener && listener.fields) { fieldsListOfListener.value = listener.fields.map((field) => ({
fieldsListOfListener.value = listener.fields.map((field) => ({ ...field,
...field, fieldType: field.string ? 'string' : 'expression'
fieldType: field.string ? 'string' : 'expression' }))
})) } else {
} else { fieldsListOfListener.value = []
fieldsListOfListener.value = [] listenerForm.value['fields'] = []
listenerForm.value['fields'] = [] }
} // 打开侧边栏并清楚验证状态
// 打开侧边栏并清楚验证状态 listenerFormModelVisible.value = true
listenerFormModelVisible.value = true nextTick(() => {
nextTick(() => { if (listenerFormRef.value) {
if (listenerFormRef.value) { listenerFormRef.value.clearValidate()
listenerFormRef.value.clearValidate() }
} })
}) }
} // 打开监听器字段编辑弹窗
// 打开监听器字段编辑弹窗 const openListenerFieldForm = (field, index?) => {
const openListenerFieldForm = (field, index?) => { listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {}
listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {} editingListenerFieldIndex.value = field ? index : -1
editingListenerFieldIndex.value = field ? index : -1 listenerFieldFormModelVisible.value = true
listenerFieldFormModelVisible.value = true nextTick(() => {
nextTick(() => { if (listenerFieldFormRef.value) {
if (listenerFieldFormRef.value) { listenerFieldFormRef.value.clearValidate()
listenerFieldFormRef.value.clearValidate() }
} })
}) }
} // 保存监听器注入字段
// 保存监听器注入字段 const saveListenerFiled = async () => {
const saveListenerFiled = async () => { let validateStatus = await listenerFieldFormRef.value.validate()
// debugger if (!validateStatus) return // 验证不通过直接返回
let validateStatus = await listenerFieldFormRef.value.validate() if (editingListenerFieldIndex.value === -1) {
if (!validateStatus) return // 验证不通过直接返回 fieldsListOfListener.value.push(listenerFieldForm.value)
if (editingListenerFieldIndex.value === -1) { listenerForm.value.fields.push(listenerFieldForm.value)
fieldsListOfListener.value.push(listenerFieldForm.value) } else {
listenerForm.value.fields.push(listenerFieldForm.value) fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
} else { listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value) }
listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value) listenerFieldFormModelVisible.value = false
} nextTick(() => {
listenerFieldFormModelVisible.value = false listenerFieldForm.value = {}
nextTick(() => { })
listenerFieldForm.value = {} }
}) // 移除监听器字段
} const removeListenerField = (index) => {
// 移除监听器字段 ElMessageBox.confirm('确认移除该字段吗?', '提示', {
const removeListenerField = (index) => { confirmButtonText: '确 认',
// debugger cancelButtonText: '取 消'
ElMessageBox.confirm('确认移除该字段吗?', '提示', { })
confirmButtonText: '确 认', .then(() => {
cancelButtonText: '取 消' fieldsListOfListener.value.splice(index, 1)
}) listenerForm.value.fields.splice(index, 1)
.then(() => { })
fieldsListOfListener.value.splice(index, 1) .catch(() => console.info('操作取消'))
listenerForm.value.fields.splice(index, 1) }
}) // 移除监听器
.catch(() => console.info('操作取消')) const removeListener = (index) => {
} ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
// 移除监听器 confirmButtonText: '确 认',
const removeListener = (index) => { cancelButtonText: '取 消'
ElMessageBox.confirm('确认移除该监听器吗?', '提示', { })
confirmButtonText: '确 认', .then(() => {
cancelButtonText: '取 消' bpmnElementListeners.value.splice(index, 1)
}) elementListenersList.value.splice(index, 1)
.then(() => { updateElementExtensions(
bpmnElementListeners.value.splice(index, 1) bpmnElement.value,
elementListenersList.value.splice(index, 1) otherExtensionList.value.concat(bpmnElementListeners.value)
updateElementExtensions( )
bpmnElement.value, })
otherExtensionList.value.concat(bpmnElementListeners.value) .catch(() => console.info('操作取消'))
) }
}) // 保存监听器配置
.catch(() => console.info('操作取消')) const saveListenerConfig = async () => {
} let validateStatus = await listenerFormRef.value.validate()
// 保存监听器配置 if (!validateStatus) return // 验证不通过直接返回
const saveListenerConfig = async () => { const listenerObject = createListenerObject(listenerForm.value, false, prefix)
// debugger if (editingListenerIndex.value === -1) {
let validateStatus = await listenerFormRef.value.validate() bpmnElementListeners.value.push(listenerObject)
if (!validateStatus) return // 验证不通过直接返回 elementListenersList.value.push(listenerForm.value)
const listenerObject = createListenerObject(listenerForm.value, false, prefix) } else {
if (editingListenerIndex.value === -1) { bpmnElementListeners.value.splice(editingListenerIndex.value, 1, listenerObject)
bpmnElementListeners.value.push(listenerObject) elementListenersList.value.splice(editingListenerIndex.value, 1, listenerForm.value)
elementListenersList.value.push(listenerForm.value) }
} else { // 保存其他配置
bpmnElementListeners.value.splice(editingListenerIndex.value, 1, listenerObject) otherExtensionList.value =
elementListenersList.value.splice(editingListenerIndex.value, 1, listenerForm.value) bpmnElement.value.businessObject?.extensionElements?.values?.filter(
} (ex) => ex.$type !== `${prefix}:ExecutionListener`
// 保存其他配置 ) ?? []
otherExtensionList.value = updateElementExtensions(
bpmnElement.value.businessObject?.extensionElements?.values?.filter( bpmnElement.value,
(ex) => ex.$type !== `${prefix}:ExecutionListener` otherExtensionList.value.concat(bpmnElementListeners.value)
) ?? [] )
updateElementExtensions( // 4. 隐藏侧边栏
bpmnElement.value, listenerFormModelVisible.value = false
otherExtensionList.value.concat(bpmnElementListeners.value) listenerForm.value = {}
) }
// 4. 隐藏侧边栏
listenerFormModelVisible.value = false // 打开监听器弹窗
listenerForm.value = {} const processListenerDialogRef = ref()
} const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('execution')
// 打开监听器弹窗 }
const processListenerDialogRef = ref() const selectProcessListener = (listener) => {
const openProcessListenerDialog = async () => { const listenerForm = initListenerForm2(listener)
processListenerDialogRef.value.open('execution') const listenerObject = createListenerObject(listenerForm, false, prefix)
} bpmnElementListeners.value.push(listenerObject)
const selectProcessListener = (listener) => { elementListenersList.value.push(listenerForm)
const listenerForm = initListenerForm2(listener)
const listenerObject = createListenerObject(listenerForm, false, prefix) // 保存其他配置
bpmnElementListeners.value.push(listenerObject) otherExtensionList.value =
elementListenersList.value.push(listenerForm) bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex) => ex.$type !== `${prefix}:ExecutionListener`
// 保存其他配置 ) ?? []
otherExtensionList.value = updateElementExtensions(
bpmnElement.value.businessObject?.extensionElements?.values?.filter( bpmnElement.value,
(ex) => ex.$type !== `${prefix}:ExecutionListener` otherExtensionList.value.concat(bpmnElementListeners.value)
) ?? [] )
updateElementExtensions( }
bpmnElement.value,
otherExtensionList.value.concat(bpmnElementListeners.value) watch(
) () => props.id,
} (val) => {
val &&
watch( val.length &&
() => props.id, nextTick(() => {
(val) => { resetListenersList()
val && })
val.length && },
nextTick(() => { { immediate: true }
resetListenersList() )
}) </script>
},
{ immediate: true }
)
</script>

View File

@@ -62,7 +62,7 @@
prop="event" prop="event"
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model="listenerForm.event"> <el-select v-model="listenerForm.event" filterable>
<el-option <el-option
v-for="i in Object.keys(listenerEventTypeObject)" v-for="i in Object.keys(listenerEventTypeObject)"
:key="i" :key="i"
@@ -83,7 +83,7 @@
prop="listenerType" prop="listenerType"
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model="listenerForm.listenerType"> <el-select v-model="listenerForm.listenerType" filterable>
<el-option <el-option
v-for="i in Object.keys(listenerTypeObject)" v-for="i in Object.keys(listenerTypeObject)"
:key="i" :key="i"
@@ -134,7 +134,7 @@
key="listener-script-type" key="listener-script-type"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
> >
<el-select v-model="listenerForm.scriptType"> <el-select v-model="listenerForm.scriptType" filterable>
<el-option label="内联脚本" value="inlineScript" /> <el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" /> <el-option label="外部脚本" value="externalScript" />
</el-select> </el-select>
@@ -161,7 +161,7 @@
<template v-if="listenerForm.event === 'timeout'"> <template v-if="listenerForm.event === 'timeout'">
<el-form-item label="定时器类型" prop="eventDefinitionType" key="eventDefinitionType"> <el-form-item label="定时器类型" prop="eventDefinitionType" key="eventDefinitionType">
<el-select v-model="listenerForm.eventDefinitionType"> <el-select v-model="listenerForm.eventDefinitionType" filterable>
<el-option label="统计时间" value="date" /> <el-option label="统计时间" value="date" />
<el-option label="持续时长" value="duration" /> <el-option label="持续时长" value="duration" />
<el-option label="循环" value="cycle" /> <el-option label="循环" value="cycle" />
@@ -259,7 +259,7 @@
prop="fieldType" prop="fieldType"
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model="listenerFieldForm.fieldType"> <el-select v-model="listenerFieldForm.fieldType" filterable>
<el-option <el-option
v-for="i in Object.keys(fieldTypeObject)" v-for="i in Object.keys(fieldTypeObject)"
:key="i" :key="i"

View File

@@ -1,280 +1,280 @@
<template> <template>
<div class="panel-tab__content"> <div class="panel-tab__content">
<el-form label-width="90px"> <el-form label-width="90px">
<el-form-item label="快捷配置"> <el-form-item label="快捷配置">
<el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button> <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
<el-button size="small" @click="changeConfig('会签')">会签</el-button> <el-button size="small" @click="changeConfig('会签')">会签</el-button>
<el-button size="small" @click="changeConfig('或签')">或签</el-button> <el-button size="small" @click="changeConfig('或签')">或签</el-button>
</el-form-item> </el-form-item>
<el-form-item label="会签类型"> <el-form-item label="会签类型">
<el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType"> <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType" filterable>
<el-option label="并行多重事件" value="ParallelMultiInstance" /> <el-option label="并行多重事件" value="ParallelMultiInstance" />
<el-option label="时序多重事件" value="SequentialMultiInstance" /> <el-option label="时序多重事件" value="SequentialMultiInstance" />
<el-option label="无" value="Null" /> <el-option label="无" value="Null" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<template <template
v-if=" v-if="
loopCharacteristics === 'ParallelMultiInstance' || loopCharacteristics === 'ParallelMultiInstance' ||
loopCharacteristics === 'SequentialMultiInstance' loopCharacteristics === 'SequentialMultiInstance'
" "
> >
<el-form-item label="循环数量" key="loopCardinality"> <el-form-item label="循环数量" key="loopCardinality">
<el-input <el-input
v-model="loopInstanceForm.loopCardinality" v-model="loopInstanceForm.loopCardinality"
clearable clearable
@change="updateLoopCardinality" @change="updateLoopCardinality"
/> />
</el-form-item> </el-form-item>
<el-form-item label="集合" key="collection" v-show="false"> <el-form-item label="集合" key="collection" v-show="false">
<el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" /> <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
</el-form-item> </el-form-item>
<!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none --> <!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
<el-form-item label="元素变量" key="elementVariable" style="display: none"> <el-form-item label="元素变量" key="elementVariable" style="display: none">
<el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" /> <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
</el-form-item> </el-form-item>
<el-form-item label="完成条件" key="completionCondition"> <el-form-item label="完成条件" key="completionCondition">
<el-input <el-input
v-model="loopInstanceForm.completionCondition" v-model="loopInstanceForm.completionCondition"
clearable clearable
@change="updateLoopCondition" @change="updateLoopCondition"
/> />
</el-form-item> </el-form-item>
<!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none --> <!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
<el-form-item label="异步状态" key="async" style="display: none"> <el-form-item label="异步状态" key="async" style="display: none">
<el-checkbox <el-checkbox
v-model="loopInstanceForm.asyncBefore" v-model="loopInstanceForm.asyncBefore"
label="异步前" label="异步前"
@change="updateLoopAsync('asyncBefore')" @change="updateLoopAsync('asyncBefore')"
/> />
<el-checkbox <el-checkbox
v-model="loopInstanceForm.asyncAfter" v-model="loopInstanceForm.asyncAfter"
label="异步后" label="异步后"
@change="updateLoopAsync('asyncAfter')" @change="updateLoopAsync('asyncAfter')"
/> />
<el-checkbox <el-checkbox
v-model="loopInstanceForm.exclusive" v-model="loopInstanceForm.exclusive"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
label="排除" label="排除"
@change="updateLoopAsync('exclusive')" @change="updateLoopAsync('exclusive')"
/> />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
label="重试周期" label="重试周期"
prop="timeCycle" prop="timeCycle"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
key="timeCycle" key="timeCycle"
> >
<el-input v-model="loopInstanceForm.timeCycle" clearable @change="updateLoopTimeCycle" /> <el-input v-model="loopInstanceForm.timeCycle" clearable @change="updateLoopTimeCycle" />
</el-form-item> </el-form-item>
</template> </template>
</el-form> </el-form>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ name: 'ElementMultiInstance' }) defineOptions({ name: 'ElementMultiInstance' })
const props = defineProps({ const props = defineProps({
businessObject: Object, businessObject: Object,
type: String type: String
}) })
const prefix = inject('prefix') const prefix = inject('prefix')
const loopCharacteristics = ref('') const loopCharacteristics = ref('')
//默认配置,用来覆盖原始不存在的选项,避免报错 //默认配置,用来覆盖原始不存在的选项,避免报错
const defaultLoopInstanceForm = ref({ const defaultLoopInstanceForm = ref({
completionCondition: '', completionCondition: '',
loopCardinality: '', loopCardinality: '',
extensionElements: [], extensionElements: [],
asyncAfter: false, asyncAfter: false,
asyncBefore: false, asyncBefore: false,
exclusive: false exclusive: false
}) })
const loopInstanceForm = ref<any>({}) const loopInstanceForm = ref<any>({})
const bpmnElement = ref(null) const bpmnElement = ref(null)
const multiLoopInstance = ref(null) const multiLoopInstance = ref(null)
const bpmnInstances = () => (window as any)?.bpmnInstances const bpmnInstances = () => (window as any)?.bpmnInstances
const getElementLoop = (businessObject) => { const getElementLoop = (businessObject) => {
if (!businessObject.loopCharacteristics) { if (!businessObject.loopCharacteristics) {
loopCharacteristics.value = 'Null' loopCharacteristics.value = 'Null'
loopInstanceForm.value = {} loopInstanceForm.value = {}
return return
} }
if (businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics') { if (businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics') {
loopCharacteristics.value = 'StandardLoop' loopCharacteristics.value = 'StandardLoop'
loopInstanceForm.value = {} loopInstanceForm.value = {}
return return
} }
if (businessObject.loopCharacteristics.isSequential) { if (businessObject.loopCharacteristics.isSequential) {
loopCharacteristics.value = 'SequentialMultiInstance' loopCharacteristics.value = 'SequentialMultiInstance'
} else { } else {
loopCharacteristics.value = 'ParallelMultiInstance' loopCharacteristics.value = 'ParallelMultiInstance'
} }
// 合并配置 // 合并配置
loopInstanceForm.value = { loopInstanceForm.value = {
...defaultLoopInstanceForm.value, ...defaultLoopInstanceForm.value,
...businessObject.loopCharacteristics, ...businessObject.loopCharacteristics,
completionCondition: businessObject.loopCharacteristics?.completionCondition?.body ?? '', completionCondition: businessObject.loopCharacteristics?.completionCondition?.body ?? '',
loopCardinality: businessObject.loopCharacteristics?.loopCardinality?.body ?? '' loopCardinality: businessObject.loopCharacteristics?.loopCardinality?.body ?? ''
} }
// 保留当前元素 businessObject 上的 loopCharacteristics 实例 // 保留当前元素 businessObject 上的 loopCharacteristics 实例
multiLoopInstance.value = bpmnInstances().bpmnElement.businessObject.loopCharacteristics multiLoopInstance.value = bpmnInstances().bpmnElement.businessObject.loopCharacteristics
// 更新表单 // 更新表单
if ( if (
businessObject.loopCharacteristics.extensionElements && businessObject.loopCharacteristics.extensionElements &&
businessObject.loopCharacteristics.extensionElements.values && businessObject.loopCharacteristics.extensionElements.values &&
businessObject.loopCharacteristics.extensionElements.values.length businessObject.loopCharacteristics.extensionElements.values.length
) { ) {
loopInstanceForm.value['timeCycle'] = loopInstanceForm.value['timeCycle'] =
businessObject.loopCharacteristics.extensionElements.values[0].body businessObject.loopCharacteristics.extensionElements.values[0].body
} }
} }
const changeLoopCharacteristicsType = (type) => { const changeLoopCharacteristicsType = (type) => {
// this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置 // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置
// 取消多实例配置 // 取消多实例配置
if (type === 'Null') { if (type === 'Null') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: null loopCharacteristics: null
}) })
return return
} }
// 配置循环 // 配置循环
if (type === 'StandardLoop') { if (type === 'StandardLoop') {
const loopCharacteristicsObject = bpmnInstances().moddle.create( const loopCharacteristicsObject = bpmnInstances().moddle.create(
'bpmn:StandardLoopCharacteristics' 'bpmn:StandardLoopCharacteristics'
) )
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: loopCharacteristicsObject loopCharacteristics: loopCharacteristicsObject
}) })
multiLoopInstance.value = null multiLoopInstance.value = null
return return
} }
// 时序 // 时序
if (type === 'SequentialMultiInstance') { if (type === 'SequentialMultiInstance') {
multiLoopInstance.value = bpmnInstances().moddle.create( multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics', 'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: true } { isSequential: true }
) )
} else { } else {
multiLoopInstance.value = bpmnInstances().moddle.create( multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics', 'bpmn:MultiInstanceLoopCharacteristics',
{ collection: '${coll_userList}' } { collection: '${coll_userList}' }
) )
} }
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value) loopCharacteristics: toRaw(multiLoopInstance.value)
}) })
} }
// 循环基数 // 循环基数
const updateLoopCardinality = (cardinality) => { const updateLoopCardinality = (cardinality) => {
let loopCardinality = null let loopCardinality = null
if (cardinality && cardinality.length) { if (cardinality && cardinality.length) {
loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', { loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: cardinality body: cardinality
}) })
} }
bpmnInstances().modeling.updateModdleProperties( bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value), toRaw(bpmnElement.value),
multiLoopInstance.value, multiLoopInstance.value,
{ {
loopCardinality loopCardinality
} }
) )
} }
// 完成条件 // 完成条件
const updateLoopCondition = (condition) => { const updateLoopCondition = (condition) => {
let completionCondition = null let completionCondition = null
if (condition && condition.length) { if (condition && condition.length) {
completionCondition = bpmnInstances().moddle.create('bpmn:FormalExpression', { completionCondition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: condition body: condition
}) })
} }
bpmnInstances().modeling.updateModdleProperties( bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value), toRaw(bpmnElement.value),
multiLoopInstance.value, multiLoopInstance.value,
{ {
completionCondition completionCondition
} }
) )
} }
// 重试周期 // 重试周期
const updateLoopTimeCycle = (timeCycle) => { const updateLoopTimeCycle = (timeCycle) => {
const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', { const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [ values: [
bpmnInstances().moddle.create(`${prefix}:FailedJobRetryTimeCycle`, { bpmnInstances().moddle.create(`${prefix}:FailedJobRetryTimeCycle`, {
body: timeCycle body: timeCycle
}) })
] ]
}) })
bpmnInstances().modeling.updateModdleProperties( bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value), toRaw(bpmnElement.value),
multiLoopInstance.value, multiLoopInstance.value,
{ {
extensionElements extensionElements
} }
) )
} }
// 直接更新的基础信息 // 直接更新的基础信息
const updateLoopBase = () => { const updateLoopBase = () => {
bpmnInstances().modeling.updateModdleProperties( bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value), toRaw(bpmnElement.value),
multiLoopInstance.value, multiLoopInstance.value,
{ {
collection: loopInstanceForm.value.collection || null, collection: loopInstanceForm.value.collection || null,
elementVariable: loopInstanceForm.value.elementVariable || null elementVariable: loopInstanceForm.value.elementVariable || null
} }
) )
} }
// 各异步状态 // 各异步状态
const updateLoopAsync = (key) => { const updateLoopAsync = (key) => {
const { asyncBefore, asyncAfter } = loopInstanceForm.value const { asyncBefore, asyncAfter } = loopInstanceForm.value
let asyncAttr = Object.create(null) let asyncAttr = Object.create(null)
if (!asyncBefore && !asyncAfter) { if (!asyncBefore && !asyncAfter) {
// this.$set(this.loopInstanceForm, "exclusive", false); // this.$set(this.loopInstanceForm, "exclusive", false);
loopInstanceForm.value['exclusive'] = false loopInstanceForm.value['exclusive'] = false
asyncAttr = { asyncBefore: false, asyncAfter: false, exclusive: false, extensionElements: null } asyncAttr = { asyncBefore: false, asyncAfter: false, exclusive: false, extensionElements: null }
} else { } else {
asyncAttr[key] = loopInstanceForm.value[key] asyncAttr[key] = loopInstanceForm.value[key]
} }
bpmnInstances().modeling.updateModdleProperties( bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value), toRaw(bpmnElement.value),
multiLoopInstance.value, multiLoopInstance.value,
asyncAttr asyncAttr
) )
} }
const changeConfig = (config) => { const changeConfig = (config) => {
if (config === '依次审批') { if (config === '依次审批') {
changeLoopCharacteristicsType('SequentialMultiInstance') changeLoopCharacteristicsType('SequentialMultiInstance')
updateLoopCardinality('1') updateLoopCardinality('1')
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }') updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
} else if (config === '会签') { } else if (config === '会签') {
changeLoopCharacteristicsType('ParallelMultiInstance') changeLoopCharacteristicsType('ParallelMultiInstance')
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }') updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
} else if (config === '或签') { } else if (config === '或签') {
changeLoopCharacteristicsType('ParallelMultiInstance') changeLoopCharacteristicsType('ParallelMultiInstance')
updateLoopCondition('${ nrOfCompletedInstances > 0 }') updateLoopCondition('${ nrOfCompletedInstances > 0 }')
} }
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
multiLoopInstance.value = null multiLoopInstance.value = null
bpmnElement.value = null bpmnElement.value = null
}) })
watch( watch(
() => props.businessObject, () => props.businessObject,
(val) => { (val) => {
bpmnElement.value = bpmnInstances().bpmnElement bpmnElement.value = bpmnInstances().bpmnElement
getElementLoop(val) getElementLoop(val)
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>

View File

@@ -1,125 +1,125 @@
<template> <template>
<div style="margin-top: 16px"> <div style="margin-top: 16px">
<el-form-item label="消息实例"> <el-form-item label="消息实例">
<div <div
style=" style="
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: nowrap; flex-wrap: nowrap;
" "
> >
<el-select v-model="bindMessageId" @change="updateTaskMessage"> <el-select v-model="bindMessageId" @change="updateTaskMessage" filterable>
<el-option <el-option
v-for="key in Object.keys(messageMap)" v-for="key in Object.keys(messageMap)"
:value="key" :value="key"
:label="messageMap[key]" :label="messageMap[key]"
:key="key" :key="key"
/> />
</el-select> </el-select>
<XButton <XButton
type="primary" type="primary"
preIcon="ep:plus" preIcon="ep:plus"
style="margin-left: 8px" style="margin-left: 8px"
@click="openMessageModel" @click="openMessageModel"
/> />
</div> </div>
</el-form-item> </el-form-item>
<el-dialog <el-dialog
v-model="messageModelVisible" v-model="messageModelVisible"
:close-on-click-modal="false" :close-on-click-modal="false"
title="创建新消息" title="创建新消息"
width="400px" width="400px"
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<el-form :model="newMessageForm" size="small" label-width="90px"> <el-form :model="newMessageForm" size="small" label-width="90px">
<el-form-item label="消息ID"> <el-form-item label="消息ID">
<el-input v-model="newMessageForm.id" clearable /> <el-input v-model="newMessageForm.id" clearable />
</el-form-item> </el-form-item>
<el-form-item label="消息名称"> <el-form-item label="消息名称">
<el-input v-model="newMessageForm.name" clearable /> <el-input v-model="newMessageForm.name" clearable />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button size="small" type="primary" @click="createNewMessage"> </el-button> <el-button size="small" type="primary" @click="createNewMessage"> </el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ name: 'ReceiveTask' }) defineOptions({ name: 'ReceiveTask' })
const props = defineProps({ const props = defineProps({
id: String, id: String,
type: String type: String
}) })
const message = useMessage() const message = useMessage()
const bindMessageId = ref('') const bindMessageId = ref('')
const newMessageForm = ref<any>({}) const newMessageForm = ref<any>({})
const messageMap = ref<any>({}) const messageMap = ref<any>({})
const messageModelVisible = ref(false) const messageModelVisible = ref(false)
const bpmnElement = ref<any>() const bpmnElement = ref<any>()
const bpmnMessageRefsMap = ref<any>() const bpmnMessageRefsMap = ref<any>()
const bpmnRootElements = ref<any>() const bpmnRootElements = ref<any>()
const bpmnInstances = () => (window as any).bpmnInstances const bpmnInstances = () => (window as any).bpmnInstances
const getBindMessage = () => { const getBindMessage = () => {
bpmnElement.value = bpmnInstances().bpmnElement bpmnElement.value = bpmnInstances().bpmnElement
bindMessageId.value = bpmnElement.value.businessObject?.messageRef?.id || '-1' bindMessageId.value = bpmnElement.value.businessObject?.messageRef?.id || '-1'
} }
const openMessageModel = () => { const openMessageModel = () => {
messageModelVisible.value = true messageModelVisible.value = true
newMessageForm.value = {} newMessageForm.value = {}
} }
const createNewMessage = () => { const createNewMessage = () => {
if (messageMap.value[newMessageForm.value.id]) { if (messageMap.value[newMessageForm.value.id]) {
message.error('该消息已存在请修改id后重新保存') message.error('该消息已存在请修改id后重新保存')
return return
} }
const newMessage = bpmnInstances().moddle.create('bpmn:Message', newMessageForm.value) const newMessage = bpmnInstances().moddle.create('bpmn:Message', newMessageForm.value)
bpmnRootElements.value.push(newMessage) bpmnRootElements.value.push(newMessage)
messageMap.value[newMessageForm.value.id] = newMessageForm.value.name messageMap.value[newMessageForm.value.id] = newMessageForm.value.name
bpmnMessageRefsMap.value[newMessageForm.value.id] = newMessage bpmnMessageRefsMap.value[newMessageForm.value.id] = newMessage
messageModelVisible.value = false messageModelVisible.value = false
} }
const updateTaskMessage = (messageId) => { const updateTaskMessage = (messageId) => {
if (messageId === '-1') { if (messageId === '-1') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
messageRef: null messageRef: null
}) })
} else { } else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
messageRef: bpmnMessageRefsMap.value[messageId] messageRef: bpmnMessageRefsMap.value[messageId]
}) })
} }
} }
onMounted(() => { onMounted(() => {
bpmnMessageRefsMap.value = Object.create(null) bpmnMessageRefsMap.value = Object.create(null)
bpmnRootElements.value = bpmnInstances().modeler.getDefinitions().rootElements bpmnRootElements.value = bpmnInstances().modeler.getDefinitions().rootElements
bpmnRootElements.value bpmnRootElements.value
.filter((el) => el.$type === 'bpmn:Message') .filter((el) => el.$type === 'bpmn:Message')
.forEach((m) => { .forEach((m) => {
bpmnMessageRefsMap.value[m.id] = m bpmnMessageRefsMap.value[m.id] = m
messageMap.value[m.id] = m.name messageMap.value[m.id] = m.name
}) })
messageMap.value['-1'] = '无' messageMap.value['-1'] = '无'
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
bpmnElement.value = null bpmnElement.value = null
}) })
watch( watch(
() => props.id, () => props.id,
() => { () => {
// bpmnElement.value = bpmnInstances().bpmnElement // bpmnElement.value = bpmnInstances().bpmnElement
nextTick(() => { nextTick(() => {
getBindMessage() getBindMessage()
}) })
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>

View File

@@ -1,99 +1,99 @@
<template> <template>
<div style="margin-top: 16px"> <div style="margin-top: 16px">
<el-form-item label="脚本格式"> <el-form-item label="脚本格式">
<el-input <el-input
v-model="scriptTaskForm.scriptFormat" v-model="scriptTaskForm.scriptFormat"
clearable clearable
@input="updateElementTask()" @input="updateElementTask()"
@change="updateElementTask()" @change="updateElementTask()"
/> />
</el-form-item> </el-form-item>
<el-form-item label="脚本类型"> <el-form-item label="脚本类型">
<el-select v-model="scriptTaskForm.scriptType"> <el-select v-model="scriptTaskForm.scriptType" filterable>
<el-option label="内联脚本" value="inline" /> <el-option label="内联脚本" value="inline" />
<el-option label="外部资源" value="external" /> <el-option label="外部资源" value="external" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="脚本" v-show="scriptTaskForm.scriptType === 'inline'"> <el-form-item label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<el-input <el-input
v-model="scriptTaskForm.script" v-model="scriptTaskForm.script"
type="textarea" type="textarea"
resize="vertical" resize="vertical"
:autosize="{ minRows: 2, maxRows: 4 }" :autosize="{ minRows: 2, maxRows: 4 }"
clearable clearable
@input="updateElementTask()" @input="updateElementTask()"
@change="updateElementTask()" @change="updateElementTask()"
/> />
</el-form-item> </el-form-item>
<el-form-item label="资源地址" v-show="scriptTaskForm.scriptType === 'external'"> <el-form-item label="资源地址" v-show="scriptTaskForm.scriptType === 'external'">
<el-input <el-input
v-model="scriptTaskForm.resource" v-model="scriptTaskForm.resource"
clearable clearable
@input="updateElementTask()" @input="updateElementTask()"
@change="updateElementTask()" @change="updateElementTask()"
/> />
</el-form-item> </el-form-item>
<el-form-item label="结果变量"> <el-form-item label="结果变量">
<el-input <el-input
v-model="scriptTaskForm.resultVariable" v-model="scriptTaskForm.resultVariable"
clearable clearable
@input="updateElementTask()" @input="updateElementTask()"
@change="updateElementTask()" @change="updateElementTask()"
/> />
</el-form-item> </el-form-item>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ name: 'ScriptTask' }) defineOptions({ name: 'ScriptTask' })
const props = defineProps({ const props = defineProps({
id: String, id: String,
type: String type: String
}) })
const defaultTaskForm = ref({ const defaultTaskForm = ref({
scriptFormat: '', scriptFormat: '',
script: '', script: '',
resource: '', resource: '',
resultVariable: '' resultVariable: ''
}) })
const scriptTaskForm = ref<any>({}) const scriptTaskForm = ref<any>({})
const bpmnElement = ref() const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances const bpmnInstances = () => (window as any)?.bpmnInstances
const resetTaskForm = () => { const resetTaskForm = () => {
for (let key in defaultTaskForm.value) { for (let key in defaultTaskForm.value) {
let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key] let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
scriptTaskForm.value[key] = value scriptTaskForm.value[key] = value
} }
scriptTaskForm.value.scriptType = scriptTaskForm.value.script ? 'inline' : 'external' scriptTaskForm.value.scriptType = scriptTaskForm.value.script ? 'inline' : 'external'
} }
const updateElementTask = () => { const updateElementTask = () => {
let taskAttr = Object.create(null) let taskAttr = Object.create(null)
taskAttr.scriptFormat = scriptTaskForm.value.scriptFormat || null taskAttr.scriptFormat = scriptTaskForm.value.scriptFormat || null
taskAttr.resultVariable = scriptTaskForm.value.resultVariable || null taskAttr.resultVariable = scriptTaskForm.value.resultVariable || null
if (scriptTaskForm.value.scriptType === 'inline') { if (scriptTaskForm.value.scriptType === 'inline') {
taskAttr.script = scriptTaskForm.value.script || null taskAttr.script = scriptTaskForm.value.script || null
taskAttr.resource = null taskAttr.resource = null
} else { } else {
taskAttr.resource = scriptTaskForm.value.resource || null taskAttr.resource = scriptTaskForm.value.resource || null
taskAttr.script = null taskAttr.script = null
} }
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr) bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
bpmnElement.value = null bpmnElement.value = null
}) })
watch( watch(
() => props.id, () => props.id,
() => { () => {
bpmnElement.value = bpmnInstances().bpmnElement bpmnElement.value = bpmnInstances().bpmnElement
nextTick(() => { nextTick(() => {
resetTaskForm() resetTaskForm()
}) })
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>

View File

@@ -1,185 +1,188 @@
<template> <template>
<el-form label-width='100px'> <el-form label-width='100px'>
<el-form-item label='规则类型' prop='candidateStrategy'> <el-form-item label='规则类型' prop='candidateStrategy'>
<el-select <el-select
v-model='userTaskForm.candidateStrategy' v-model='userTaskForm.candidateStrategy'
clearable clearable
style='width: 100%' style='width: 100%'
@change='changeCandidateStrategy' @change='changeCandidateStrategy'
> filterable
<el-option >
v-for='dict in RuleTypeList' <el-option
:key='dict.code' v-for='dict in RuleTypeList'
:label='dict.name' :key='dict.code'
:value='dict.code' :label='dict.name'
/> :value='dict.code'
</el-select> />
</el-form-item> </el-select>
<el-form-item </el-form-item>
v-if='userTaskForm.candidateStrategy == 10' <el-form-item
label='指定角色' v-if='userTaskForm.candidateStrategy == 10'
prop='candidateParam' label='指定角色'
> prop='candidateParam'
<el-select >
v-model='userTaskForm.candidateParam' <el-select
clearable v-model='userTaskForm.candidateParam'
multiple clearable
style='width: 100%' multiple
@change='updateElementTask' style='width: 100%'
> @change='updateElementTask'
<el-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' /> filterable
</el-select> >
</el-form-item> <el-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' />
<el-form-item </el-select>
v-if='userTaskForm.candidateStrategy == 20' </el-form-item>
label='指定部门' <el-form-item
prop='candidateParam' v-if='userTaskForm.candidateStrategy == 20'
span='24' label='指定部门'
> prop='candidateParam'
<el-tree-select span='24'
ref='treeRef' >
v-model='userTaskForm.candidateParam' <el-tree-select
:data='deptTreeOptions' ref='treeRef'
:props='defaultProps' v-model='userTaskForm.candidateParam'
empty-text='加载中请稍后' :data='deptTreeOptions'
multiple :props='defaultProps'
node-key='id' empty-text='加载中请稍后'
show-checkbox multiple
@change='updateElementTask' node-key='id'
/> show-checkbox
</el-form-item> @change='updateElementTask'
/>
<el-form-item </el-form-item>
v-if='userTaskForm.candidateStrategy == 30'
label='指定用户' <el-form-item
prop='candidateParam' v-if='userTaskForm.candidateStrategy == 30'
span='24' label='指定用户'
> prop='candidateParam'
<el-select span='24'
v-model='userTaskForm.candidateParam' >
clearable <el-select
multiple v-model='userTaskForm.candidateParam'
style='width: 100%' clearable
@change='updateElementTask' multiple
> style='width: 100%'
<el-option @change='updateElementTask'
v-for='item in userOptions' filterable
:key='item.id' >
:label='item.name' <el-option
:value='item.id' v-for='item in userOptions'
/> :key='item.id'
</el-select> :label='item.name'
</el-form-item> :value='item.id'
/>
</el-form> </el-select>
</template> </el-form-item>
<script lang='ts' setup> </el-form>
import { defaultProps, handleTree } from '@/utils/tree' </template>
import { useDictData } from '@/stores/dictData'
const dictData = useDictData() <script lang='ts' setup>
const RuleTypeList = ref() import { defaultProps, handleTree } from '@/utils/tree'
import { getRoleSimpleList } from '@/api/user-boot/role' import { useDictData } from '@/stores/dictData'
import { getUserSimpleList } from '@/api/user-boot/user' const dictData = useDictData()
const RuleTypeList = ref()
defineOptions({ name: 'UserTask' }) import { getRoleSimpleList } from '@/api/user-boot/role'
const props = defineProps({ import { getUserSimpleList } from '@/api/user-boot/user'
id: String,
type: String defineOptions({ name: 'UserTask' })
}) const props = defineProps({
const userTaskForm = ref({ id: String,
candidateStrategy: '', // 分配规则 type: String
candidateParam: [] // 分配选项 })
}) const userTaskForm = ref({
const bpmnElement = ref() candidateStrategy: '', // 分配规则
const bpmnInstances = () => (window as any)?.bpmnInstances candidateParam: [] // 分配选项
})
export interface optionVO { const bpmnElement = ref()
id: string const bpmnInstances = () => (window as any)?.bpmnInstances
name: string
} export interface optionVO {
id: string
const roleOptions = ref<optionVO[]>([]) // 角色列表 name: string
const userOptions = ref<optionVO[]>([]) // 用户列表 }
const deptTreeOptions = ref() // 部门树
const roleOptions = ref<optionVO[]>([]) // 角色列表
const resetTaskForm = () => { const userOptions = ref<optionVO[]>([]) // 用户列表
const businessObject = bpmnElement.value.businessObject const deptTreeOptions = ref() // 部门树
if (!businessObject) {
return const resetTaskForm = () => {
} const businessObject = bpmnElement.value.businessObject
if (businessObject.candidateStrategy != undefined) { if (!businessObject) {
userTaskForm.value.candidateStrategy = businessObject.candidateStrategy return
} else { }
userTaskForm.value.candidateStrategy = undefined if (businessObject.candidateStrategy != undefined) {
} userTaskForm.value.candidateStrategy = businessObject.candidateStrategy
if (businessObject.candidateParam && businessObject.candidateParam.length > 0) { } else {
if (userTaskForm.value.candidateStrategy === 60) { userTaskForm.value.candidateStrategy = undefined
// 特殊:流程表达式,只有一个 input 输入框 }
userTaskForm.value.candidateParam = [businessObject.candidateParam] if (businessObject.candidateParam && businessObject.candidateParam.length > 0) {
} else { if (userTaskForm.value.candidateStrategy === 60) {
userTaskForm.value.candidateParam = businessObject.candidateParam.split(',') // 特殊:流程表达式,只有一个 input 输入框
} userTaskForm.value.candidateParam = [businessObject.candidateParam]
} else { } else {
userTaskForm.value.candidateParam = [] userTaskForm.value.candidateParam = businessObject.candidateParam.split(',')
} }
} } else {
userTaskForm.value.candidateParam = []
/** 更新 candidateStrategy 字段时,需要清空 candidateParam并触发 bpmn 图更新 */ }
const changeCandidateStrategy = () => { }
userTaskForm.value.candidateParam = []
updateElementTask() /** 更新 candidateStrategy 字段时,需要清空 candidateParam并触发 bpmn 图更新 */
} const changeCandidateStrategy = () => {
userTaskForm.value.candidateParam = []
/** 选中某个 options 时候,更新 bpmn 图 */ updateElementTask()
const updateElementTask = () => { }
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
candidateStrategy: userTaskForm.value.candidateStrategy, /** 选中某个 options 时候,更新 bpmn 图 */
candidateParam: userTaskForm.value.candidateParam.join(',') const updateElementTask = () => {
}) bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
} candidateStrategy: userTaskForm.value.candidateStrategy,
candidateParam: userTaskForm.value.candidateParam.join(',')
// 打开监听器弹窗 })
const processExpressionDialogRef = ref() }
const openProcessExpressionDialog = async () => {
processExpressionDialogRef.value.open() // 打开监听器弹窗
} const processExpressionDialogRef = ref()
const selectProcessExpression = (expression) => { const openProcessExpressionDialog = async () => {
userTaskForm.value.candidateParam = [expression.expression] processExpressionDialogRef.value.open()
} }
const selectProcessExpression = (expression) => {
watch( userTaskForm.value.candidateParam = [expression.expression]
() => props.id, }
() => {
bpmnElement.value = bpmnInstances().bpmnElement watch(
nextTick(() => { () => props.id,
resetTaskForm() () => {
}) bpmnElement.value = bpmnInstances().bpmnElement
}, nextTick(() => {
{ immediate: true } resetTaskForm()
) })
},
{ immediate: true }
onMounted(async () => { )
// 获得角色列表
await getRoleSimpleList().then(res => {
roleOptions.value = res.data onMounted(async () => {
}) // 获得角色列表
await getRoleSimpleList().then(res => {
// 获得用户列表 roleOptions.value = res.data
await getUserSimpleList().then(res => { })
userOptions.value = res.data
}) // 获得用户列表
//传入type的名称 await getUserSimpleList().then(res => {
RuleTypeList.value = dictData.getBasicData('rule_type') userOptions.value = res.data
})
// //传入type的名称
// // 获得部门列表 RuleTypeList.value = dictData.getBasicData('rule_type')
// const deptOptions = await DeptApi.getSimpleDeptList()
// deptTreeOptions.value = handleTree(deptOptions, 'id') //
}) // // 获得部门列表
// const deptOptions = await DeptApi.getSimpleDeptList()
onBeforeUnmount(() => { // deptTreeOptions.value = handleTree(deptOptions, 'id')
bpmnElement.value = null })
})
</script> onBeforeUnmount(() => {
bpmnElement.value = null
})
</script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div :style="{ width: isMinuteData ? '750px' : '610px' }"> <div :style="{ width: isMinuteData ? '750px' : '610px' }">
<el-select v-model="interval" style="min-width: 90px; width: 90px; margin-right: 10px" @change="timeChange"> <el-select v-model="interval" style="min-width: 90px; width: 90px; margin-right: 10px" @change="timeChange" filterable>
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>

View File

@@ -10,7 +10,7 @@ export default defineComponent({
required: true required: true
}, },
size: { size: {
type: String, type: [String, Number],
default: '18px' default: '18px'
}, },
color: { color: {
@@ -21,7 +21,7 @@ export default defineComponent({
setup(props) { setup(props) {
const iconStyle = computed((): CSSProperties => { const iconStyle = computed((): CSSProperties => {
const { size, color } = props const { size, color } = props
let s = `${size.replace('px', '')}px` const s = `${String(size).replace('px', '')}px`
return { return {
fontSize: s, fontSize: s,
color: color color: color

View File

@@ -1,49 +1,49 @@
<template> <template>
<div v-if="isUrl" :style="urlIconStyle" class="url-svg svg-icon icon" /> <div v-if="isUrl" :style="urlIconStyle" class="url-svg svg-icon icon" />
<svg v-else class="svg-icon icon" :style="iconStyle"> <svg v-else class="svg-icon icon" :style="iconStyle">
<use :href="iconName" /> <use :href="iconName" />
</svg> </svg>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, type CSSProperties } from 'vue' import { computed, type CSSProperties } from 'vue'
import { isExternal } from '@/utils/common' import { isExternal } from '@/utils/common'
interface Props { interface Props {
name: string name: string
size: string size: string | number
color: string color: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
name: '', name: '',
size: '18px', size: '18px',
color: '#000000', color: '#000000',
}) })
const s = `${props.size.replace('px', '')}px` const s = `${String(props.size).replace('px', '')}px`
const iconName = computed(() => `#${props.name}`) const iconName = computed(() => `#${props.name}`)
const iconStyle = computed((): CSSProperties => { const iconStyle = computed((): CSSProperties => {
return { return {
color: props.color, color: props.color,
fontSize: s, fontSize: s,
} }
}) })
const isUrl = computed(() => isExternal(props.name)) const isUrl = computed(() => isExternal(props.name))
const urlIconStyle = computed(() => { const urlIconStyle = computed(() => {
return { return {
width: s, width: s,
height: s, height: s,
mask: `url(${props.name}) no-repeat 50% 50%`, mask: `url(${props.name}) no-repeat 50% 50%`,
'-webkit-mask': `url(${props.name}) no-repeat 50% 50%`, '-webkit-mask': `url(${props.name}) no-repeat 50% 50%`,
} }
}) })
</script> </script>
<style scoped> <style scoped>
.svg-icon { .svg-icon {
width: 1em; width: 1em;
height: 1em; height: 1em;
fill: currentColor; fill: currentColor;
overflow: hidden; overflow: hidden;
} }
</style> </style>

View File

@@ -6,11 +6,12 @@ export const defaultAttribute: VxeTableProps = {
border: true, border: true,
stripe: true, stripe: true,
size: 'small', size: 'small',
columnConfig: { resizable: true }, columnConfig: { resizable: true, useKey: true },
rowConfig: { isCurrent: true, isHover: true }, rowConfig: { isCurrent: true, isHover: true, keyField: 'id' },
scrollX: { scrollToLeftOnChange: true }, scrollX: { scrollToLeftOnChange: true },
scrollY: { scrollToTopOnChange: true, enabled: true, gt: 100 }, scrollY: { enabled: false },
customConfig: { enabled: true, allowFixed: false, showFooter: false, immediate: true ,mode:'default'}, // 注意:全局不要默认开启 treeConfig会与 stripe 冲突;树表在页面自行配置
customConfig: { enabled: true, allowFixed: false, showFooter: false, immediate: true, mode: 'default' },
showOverflow: 'tooltip', showOverflow: 'tooltip',
showHeaderOverflow: false showHeaderOverflow: false
} }

View File

@@ -70,7 +70,15 @@
</el-button> </el-button>
</template> </template>
<slot name="operation"></slot> <slot name="operation"></slot>
<el-button class="ml10" v-if="showCustomColumn" :icon="Setting" @click="openCustomColumn">列设置</el-button> <el-button
class="ml10"
v-if="showCustomColumn"
:icon="Setting"
@mousedown="onCustomColumnMousedown"
@click="openCustomColumn"
>
列设置
</el-button>
</div> </div>
<el-form <el-form
:style="showSelect && showUnfoldButton ? headerFormSecondStyleOpen : headerFormSecondStyleClose" :style="showSelect && showUnfoldButton ? headerFormSecondStyleOpen : headerFormSecondStyleClose"
@@ -167,8 +175,26 @@ watch(
}, 500) }, 500)
} }
) )
let customColumnVisibleOnPress = false
const onCustomColumnMousedown = () => {
const table = tableStore?.table?.ref as any
if (!table) return
customColumnVisibleOnPress = table.getCustomVisible?.() ?? !!table.reactData?.customStore?.visible
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = true
}
}
const openCustomColumn = () => { const openCustomColumn = () => {
tableStore?.table?.ref?.openCustom?.() const table = tableStore?.table?.ref as any
if (!table) return
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = false
}
if (customColumnVisibleOnPress) {
table?.closeCustom?.()
} else {
table?.openCustom?.()
}
} }
onMounted(() => { onMounted(() => {
if (props.showCustomColumn && tableStore?.table) { if (props.showCustomColumn && tableStore?.table) {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div :style="{ height: typeof props.height === 'string' ? props.height : tableStore.table.height }"> <div :style="{ height: typeof props.height === 'string' ? props.height : tableStore.table.height }">
<div v-if="showCustomColumn && !tableStore?.table?.customColumnInHeader" class="table-custom-toolbar"> <div v-if="showCustomColumn && !tableStore?.table?.customColumnInHeader" class="table-custom-toolbar">
<el-button icon="el-icon-Setting" @click="openCustomColumn">列设置</el-button> <el-button icon="el-icon-Setting" @mousedown="onCustomColumnMousedown" @click="openCustomColumn">列设置</el-button>
</div> </div>
<vxe-table <vxe-table
ref="tableRef" ref="tableRef"
@@ -75,6 +75,10 @@ import type TableStoreClass from '@/utils/tableStore'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { defaultAttribute } from '@/components/table/defaultAttribute' import { defaultAttribute } from '@/components/table/defaultAttribute'
defineOptions({
inheritAttrs: false
})
const config = useConfig() const config = useConfig()
const tableRef = ref<VxeTableInstance>() const tableRef = ref<VxeTableInstance>()
const tableStore = inject('tableStore') as TableStoreClass const tableStore = inject('tableStore') as TableStoreClass
@@ -135,8 +139,26 @@ const selectChangeEvent: VxeTableEvents.CheckboxChange<any> = ({ checked }) => {
const records = (tableRef.value as VxeTableInstance).getCheckboxRecords() const records = (tableRef.value as VxeTableInstance).getCheckboxRecords()
tableStore.onTableAction('selection-change', records) tableStore.onTableAction('selection-change', records)
} }
let customColumnVisibleOnPress = false
const onCustomColumnMousedown = () => {
const table = tableRef.value as any
if (!table) return
customColumnVisibleOnPress = table.getCustomVisible?.() ?? !!table.reactData?.customStore?.visible
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = true
}
}
const openCustomColumn = () => { const openCustomColumn = () => {
tableRef.value?.openCustom?.() const table = tableRef.value as any
if (!table) return
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = false
}
if (customColumnVisibleOnPress) {
table?.closeCustom?.()
} else {
table?.openCustom?.()
}
} }
const getRef = () => { const getRef = () => {
return tableRef.value return tableRef.value

View File

@@ -1,8 +1,7 @@
<template> <template>
<Tree ref="treRef" :data="tree" style="height: 100%" :width="'100%'" :expanded="expanded" /> <Tree ref="treRef" :data="tree" style="height: 100%" :width="'100%'" :expanded="expanded" />
</template> </template>
<!-- <div class="mb10"> <!-- <div class="mb10">
<el-button type="primary" icon="el-icon-Download" size="small" @click="exportExcelTemplate" :loading="loading">模版下载</el-button> <el-button type="primary" icon="el-icon-Download" size="small" @click="exportExcelTemplate" :loading="loading">模版下载</el-button>
<el-button type="primary" icon="el-icon-Upload" size="small">导入</el-button> <el-button type="primary" icon="el-icon-Upload" size="small">导入</el-button>
<el-button type="primary" icon="el-icon-Download" size="small" @click="ledgerEverywhere" :loading="loading1">导出</el-button> <el-button type="primary" icon="el-icon-Download" size="small" @click="ledgerEverywhere" :loading="loading1">导出</el-button>
@@ -11,7 +10,7 @@
import { ref, nextTick } from 'vue' import { ref, nextTick } from 'vue'
import Tree from './index.vue' import Tree from './index.vue'
import { getTerminalTree,downTerminalTemplate,exportTerminalBase } from '@/api/device-boot/Business' import { getTerminalTree, downTerminalTemplate, exportTerminalBase } from '@/api/device-boot/Business'
import { useConfig } from '@/stores/config' import { useConfig } from '@/stores/config'
const VITE_FLAG = import.meta.env.VITE_NAME == 'jibei' const VITE_FLAG = import.meta.env.VITE_NAME == 'jibei'
defineOptions({ defineOptions({
@@ -88,40 +87,48 @@ const info = (id: any) => {
// }) // })
// }) // })
// } else { // } else {
res.data.forEach((item: any) => { res.data.forEach((item: any) => {
item.icon = 'el-icon-Menu' item.icon = 'el-icon-Menu'
item.plevel = item.level item.color = 'var(--el-color-primary)'
item.level = (item.level + 1) * 100 item.plevel = item.level
item.children.forEach((item2: any) => { item.level = (item.level + 1) * 100
item2.icon = 'el-icon-HomeFilled' item.children.forEach((item2: any) => {
item2.plevel = item2.level item2.icon = 'el-icon-HomeFilled'
item2.level = (item2.level + 1) * 100 item2.color = 'var(--el-color-primary)'
expanded.value.push(item2.id) item2.plevel = item2.level
item2.children.forEach((item3: any) => { item2.level = (item2.level + 1) * 100
item3.icon = 'el-icon-CollectionTag' expanded.value.push(item2.id)
item3.plevel = item3.level item2.children.forEach((item3: any) => {
item3.level = (item3.level + 1) * 100 item3.icon = 'el-icon-CollectionTag'
item3.children.forEach((item4: any) => { item3.color = 'var(--el-color-primary)'
item4.icon = 'el-icon-Flag' item3.plevel = item3.level
item4.plevel = item4.level item3.level = (item3.level + 1) * 100
item4.level = (item4.level + 1) * 100 item3.children.forEach((item4: any) => {
item4.children.forEach((item5: any) => { item4.icon = 'el-icon-Flag'
item5.icon = 'el-icon-OfficeBuilding' item4.color = 'var(--el-color-primary)'
item5.plevel = item5.level item4.plevel = item4.level
item5.level = (item5.level == 7 ? 4 : item5.level + 1) * 100 item4.level = (item4.level + 1) * 100
item5.children.forEach((item6: any) => { item4.children.forEach((item5: any) => {
item6.icon = 'el-icon-Film' item5.icon = 'el-icon-OfficeBuilding'
item6.plevel = item6.level item5.color = 'var(--el-color-primary)'
item6.level = (item6.level + 1) * 100 item5.plevel = item5.level
item6.children.forEach((item7: any) => { item5.level = (item5.level == 7 ? 4 : item5.level + 1) * 100
item7.icon = 'el-icon-Share' item5.children.forEach((item6: any) => {
item7.plevel = item7.level item6.icon =
item7.level = (item7.level + 1) * 100 item6.children?.[0]?.children?.length > 0 ? 'el-icon-Film' : 'el-icon-Share'
item7.children.forEach((item8: any) => { item6.color = 'var(--el-color-primary)'
item8.icon = 'el-icon-Location' item6.plevel = item6.level
item8.plevel = item8.level item6.level = (item6.level + 1) * 100
item8.level = (item8.level + 1) * 100 item6.children.forEach((item7: any) => {
}) item7.icon = item7.children.length > 0 ? 'el-icon-Share' : 'el-icon-Location'
item7.color = 'var(--el-color-primary)'
item7.plevel = item7.level
item7.level = (item7.level + 1) * 100
item7.children.forEach((item8: any) => {
item8.icon = 'el-icon-Location'
item8.color = 'var(--el-color-primary)'
item8.plevel = item8.level
item8.level = (item8.level + 1) * 100
}) })
}) })
}) })
@@ -129,6 +136,7 @@ const info = (id: any) => {
}) })
}) })
}) })
})
// } // }
tree.value = res.data tree.value = res.data
@@ -149,7 +157,7 @@ const info = (id: any) => {
}) })
} }
// 下载模版 // 下载模版
const exportExcelTemplate = async() => { const exportExcelTemplate = async () => {
loading.value = true loading.value = true
downTerminalTemplate().then((res: any) => { downTerminalTemplate().then((res: any) => {
let blob = new Blob([res], { let blob = new Blob([res], {
@@ -168,7 +176,7 @@ const exportExcelTemplate = async() => {
}, 0) }, 0)
} }
// 导出台账 // 导出台账
const ledgerEverywhere = async() => { const ledgerEverywhere = async () => {
loading1.value = true loading1.value = true
exportTerminalBase().then((res: any) => { exportTerminalBase().then((res: any) => {
let blob = new Blob([res], { let blob = new Blob([res], {

View File

@@ -6,6 +6,7 @@
style="min-width: unset; padding: 10px 10px 0" style="min-width: unset; padding: 10px 10px 0"
@change="loadData" @change="loadData"
v-if="props.showSelect" v-if="props.showSelect"
filterable
> >
<el-option <el-option
v-for="item in classificationData" v-for="item in classificationData"

View File

@@ -1,28 +0,0 @@
const config: {
base_url: string
result_code: number | string
default_headers: AxiosHeaders
request_timeout: number
} = {
/**
* api请求基础路径
*/
base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL,
/**
* 接口成功返回状态码
*/
result_code: 200,
/**
* 接口请求超时时间
*/
request_timeout: 30000,
/**
* 默认接口请求类型
* 可选值application/x-www-form-urlencoded multipart/form-data
*/
default_headers: 'application/json'
}
export { config }

View File

@@ -1,6 +0,0 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
default: '系统未知错误,请反馈给管理员'
}

View File

@@ -1,51 +0,0 @@
import { service } from './service'
import { config } from './config'
const { default_headers } = config
const request = (option: any) => {
const { url, method, params, data, headersType, responseType, ...config } = option
return service({
url: url,
method,
params,
data,
...config,
responseType: responseType,
headers: {
'Content-Type': headersType || default_headers
}
})
}
export default {
get: async <T = any>(option: any) => {
const res = await request({ method: 'GET', ...option })
return res.data as unknown as T
},
post: async <T = any>(option: any) => {
const res = await request({ method: 'POST', ...option })
return res.data as unknown as T
},
postOriginal: async (option: any) => {
const res = await request({ method: 'POST', ...option })
return res
},
delete: async <T = any>(option: any) => {
const res = await request({ method: 'DELETE', ...option })
return res.data as unknown as T
},
put: async <T = any>(option: any) => {
const res = await request({ method: 'PUT', ...option })
return res.data as unknown as T
},
download: async <T = any>(option: any) => {
const res = await request({ method: 'GET', responseType: 'blob', ...option })
return res as unknown as Promise<T>
},
upload: async <T = any>(option: any) => {
option.headersType = 'multipart/form-data'
const res = await request({ method: 'POST', ...option })
return res as unknown as Promise<T>
}
}

View File

@@ -1,228 +0,0 @@
import axios, {
AxiosError,
AxiosInstance,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import qs from 'qs'
import { config } from '@/config/axios/config'
import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
import errorCode from './errorCode'
import { deleteUserCache } from '@/hooks/web/useCache'
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
const { result_code, base_url, request_timeout } = config
// 需要忽略的提示。忽略后,自动 Promise.reject('error')
const ignoreMsgs = [
'无效的刷新令牌', // 刷新令牌被删除时,不用提示
'刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401无法跳转到登出界面
]
// 是否显示重新登录
export const isRelogin = { show: false }
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
// 请求队列
let requestList: any[] = []
// 是否正在刷新中
let isRefreshToken = false
// 请求白名单无须token的接口
const whiteList: string[] = ['/login', '/refresh-token']
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: base_url, // api 的 base_url
timeout: request_timeout, // 请求超时时间
withCredentials: false // 禁用 Cookie 等信息
})
// request拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 是否需要设置 token
let isToken = (config!.headers || {}).isToken === false
whiteList.some((v) => {
if (config.url) {
config.url.indexOf(v) > -1
return (isToken = false)
}
})
if (getAccessToken() && !isToken) {
;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
}
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId
}
const params = config.params || {}
const data = config.data || false
if (
config.method?.toUpperCase() === 'POST' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] ===
'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(data)
}
// get参数编码
if (config.method?.toUpperCase() === 'GET' && params) {
config.params = {}
const paramsStr = qs.stringify(params, { allowDots: true })
if (paramsStr) {
config.url = config.url + '?' + paramsStr
}
}
return config
},
(error: AxiosError) => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
async (response: AxiosResponse<any>) => {
let { data } = response
const config = response.config
if (!data) {
// 返回“[HTTP]请求没有返回值”;
throw new Error()
}
const { t } = useI18n()
// 未设置状态码则默认成功状态
// 二进制数据则直接返回,例如说 Excel 导出
if (
response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer'
) {
// 注意:如果导出的响应为 json说明可能失败了不直接返回进行下载
if (response.data.type !== 'application/json') {
return response.data
}
data = await new Response(response.data).json()
}
const code = data.code || result_code
// 获取错误信息
const msg = data.msg || errorCode[code] || errorCode['default']
if (ignoreMsgs.indexOf(msg) !== -1) {
// 如果是忽略的错误码,直接返回 msg 异常
return Promise.reject(msg)
} else if (code === 401) {
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true
// 1. 如果获取不到刷新令牌,则只能执行登出操作
if (!getRefreshToken()) {
return handleAuthorized()
}
// 2. 进行刷新访问令牌
try {
const refreshTokenRes = await refreshToken()
// 2.1 刷新成功,则回放队列的请求 + 当前请求
setToken((await refreshTokenRes).data.data)
config.headers!.Authorization = 'Bearer ' + getAccessToken()
requestList.forEach((cb: any) => {
cb()
})
requestList = []
return service(config)
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb: any) => {
cb()
})
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized()
} finally {
requestList = []
isRefreshToken = false
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(service(config))
})
})
}
} else if (code === 500) {
ElMessage.error(t('sys.api.errMsg500'))
return Promise.reject(new Error(msg))
} else if (code === 901) {
ElMessage.error({
offset: 300,
dangerouslyUseHTMLString: true,
message:
'<div>' +
t('sys.api.errMsg901') +
'</div>' +
'<div> &nbsp; </div>' +
'<div>参考 https://doc.iocoder.cn/ 教程</div>' +
'<div> &nbsp; </div>' +
'<div>5 分钟搭建本地环境</div>'
})
return Promise.reject(new Error(msg))
} else if (code !== 200) {
if (msg === '无效的刷新令牌') {
// hard coding忽略这个提示直接登出
console.log(msg)
} else {
ElNotification.error({ title: msg })
}
return Promise.reject('error')
} else {
return data
}
},
(error: AxiosError) => {
console.log('err' + error) // for debug
let { message } = error
const { t } = useI18n()
if (message === 'Network Error') {
message = t('sys.api.errorMessage')
} else if (message.includes('timeout')) {
message = t('sys.api.apiTimeoutMessage')
} else if (message.includes('Request failed with status code')) {
message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3)
}
ElMessage.error(message)
return Promise.reject(error)
}
)
const refreshToken = async () => {
axios.defaults.headers.common['tenant-id'] = getTenantId()
return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
}
const handleAuthorized = () => {
const { t } = useI18n()
if (!isRelogin.show) {
// 如果已经到重新登录页面则不进行弹窗提示
if (window.location.href.includes('login?redirect=')) {
return
}
isRelogin.show = true
ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
showCancelButton: false,
closeOnClickModal: false,
showClose: false,
confirmButtonText: t('login.relogin'),
type: 'warning'
}).then(() => {
deleteUserCache() // 删除用户缓存
removeToken()
isRelogin.show = false
// 干掉token后再走一次路由让它过router.beforeEach的校验
window.location.href = window.location.href
})
}
return Promise.reject(t('sys.api.timeoutMessage'))
}
export { service }

View File

@@ -1,35 +1,36 @@
import { i18n } from '@/plugins/vueI18n' import { i18n } from '@/plugins/vueI18n'
import { useLocaleStoreWithOut } from '@/stores/modules/locale' import { useLocaleStoreWithOut } from '@/stores/modules/locale'
import { setHtmlPageLang } from '@/plugins/vueI18n/helper' import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
import { loadLocaleMessages } from '@/locales'
const setI18nLanguage = (locale: LocaleType) => {
const localeStore = useLocaleStoreWithOut() const setI18nLanguage = (locale: LocaleType) => {
const localeStore = useLocaleStoreWithOut()
if (i18n.mode === 'legacy') {
i18n.global.locale = locale if (i18n.mode === 'legacy') {
} else { i18n.global.locale = locale
;(i18n.global.locale as any).value = locale } else {
} ;(i18n.global.locale as any).value = locale
localeStore.setCurrentLocale({ }
lang: locale localeStore.setCurrentLocale({
}) lang: locale
setHtmlPageLang(locale) })
} setHtmlPageLang(locale)
}
export const useLocale = () => {
// Switching the language will change the locale of useI18n export const useLocale = () => {
// And submit to configuration modification // Switching the language will change the locale of useI18n
const changeLocale = async (locale: LocaleType) => { // And submit to configuration modification
const globalI18n = i18n.global const changeLocale = async (locale: LocaleType) => {
const globalI18n = i18n.global
const langModule = await import(`../../locales/${locale}.ts`)
const message = await loadLocaleMessages(locale)
globalI18n.setLocaleMessage(locale, langModule.default)
globalI18n.setLocaleMessage(locale, message)
setI18nLanguage(locale)
} setI18nLanguage(locale)
}
return {
changeLocale return {
} changeLocale
} }
}

View File

@@ -1,78 +0,0 @@
<script lang="tsx">
import { computed, defineComponent, unref } from 'vue'
import { useAppStore } from '@/stores/modules/app'
import { Backtop } from '@/components/Backtop'
import { Setting } from '@/layouts/components/Setting'
import { useRenderLayout } from './components/useRenderLayout'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('layout')
const appStore = useAppStore()
// 是否是移动端
const mobile = computed(() => appStore.getMobile)
// 菜单折叠
const collapse = computed(() => appStore.getCollapse)
const layout = computed(() => appStore.getLayout)
const handleClickOutside = () => {
appStore.setCollapse(true)
}
const renderLayout = () => {
switch (unref(layout)) {
case 'classic':
const { renderClassic } = useRenderLayout()
return renderClassic()
case 'topLeft':
const { renderTopLeft } = useRenderLayout()
return renderTopLeft()
case 'top':
const { renderTop } = useRenderLayout()
return renderTop()
case 'cutMenu':
const { renderCutMenu } = useRenderLayout()
return renderCutMenu()
default:
break
}
}
export default defineComponent({
name: 'Layout',
setup() {
return () => (
<section class={[prefixCls, `${prefixCls}__${layout.value}`, 'w-[100%] h-[100%] relative']}>
{mobile.value && !collapse.value ? (
<div
class="absolute left-0 top-0 z-99 h-full w-full bg-[var(--el-color-black)] opacity-30"
onClick={handleClickOutside}
></div>
) : undefined}
{renderLayout()}
<Backtop></Backtop>
<Setting></Setting>
</section>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-layout;
.#{$prefix-cls} {
background-color: var(--app-content-bg-color);
:deep(.el-scrollbar__view) {
height: 100% !important;
}
}
</style>

View File

@@ -1,452 +1,453 @@
<template> <template>
<div class="layout-config-drawer"> <div class="layout-config-drawer">
<el-drawer :model-value="configStore.layout.showDrawer" title="布局配置" size="310px" @close="onCloseDrawer"> <el-drawer :model-value="configStore.layout.showDrawer" title="布局配置" size="310px" @close="onCloseDrawer">
<el-scrollbar class="layout-mode-style-scrollbar"> <el-scrollbar class="layout-mode-style-scrollbar">
<el-form ref="formRef" :model="configStore.layout"> <el-form ref="formRef" :model="configStore.layout">
<div class="layout-mode-styles-box"> <div class="layout-mode-styles-box">
<el-divider border-style="dashed">布局方式</el-divider> <el-divider border-style="dashed">布局方式</el-divider>
<div class="layout-mode-box-style"> <div class="layout-mode-box-style">
<el-row class="layout-mode-box-style-row" :gutter="10"> <el-row class="layout-mode-box-style-row" :gutter="10">
<el-col :span="12"> <el-col :span="12">
<div <div
@click="setLayoutMode('Default')" @click="setLayoutMode('Default')"
class="layout-mode-style default" class="layout-mode-style default"
:class="configStore.layout.layoutMode == 'Default' ? 'active' : ''" :class="configStore.layout.layoutMode == 'Default' ? 'active' : ''"
> >
<div class="layout-mode-style-box"> <div class="layout-mode-style-box">
<div class="layout-mode-style-aside"></div> <div class="layout-mode-style-aside"></div>
<div class="layout-mode-style-container-box"> <div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div> <div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div> <div class="layout-mode-style-container"></div>
</div> </div>
</div> </div>
<div class="layout-mode-style-name">默认</div> <div class="layout-mode-style-name">默认</div>
</div> </div>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<div <div
@click="setLayoutMode('Classic')" @click="setLayoutMode('Classic')"
class="layout-mode-style classic" class="layout-mode-style classic"
:class="configStore.layout.layoutMode == 'Classic' ? 'active' : ''" :class="configStore.layout.layoutMode == 'Classic' ? 'active' : ''"
> >
<div class="layout-mode-style-box"> <div class="layout-mode-style-box">
<div class="layout-mode-style-aside"></div> <div class="layout-mode-style-aside"></div>
<div class="layout-mode-style-container-box"> <div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div> <div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div> <div class="layout-mode-style-container"></div>
</div> </div>
</div> </div>
<div class="layout-mode-style-name">经典</div> <div class="layout-mode-style-name">经典</div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="12"> <el-col :span="12">
<div <div
@click="setLayoutMode('Streamline')" @click="setLayoutMode('Streamline')"
class="layout-mode-style streamline" class="layout-mode-style streamline"
:class="configStore.layout.layoutMode == 'Streamline' ? 'active' : ''" :class="configStore.layout.layoutMode == 'Streamline' ? 'active' : ''"
> >
<div class="layout-mode-style-box"> <div class="layout-mode-style-box">
<div class="layout-mode-style-container-box"> <div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div> <div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div> <div class="layout-mode-style-container"></div>
</div> </div>
</div> </div>
<div class="layout-mode-style-name">单栏</div> <div class="layout-mode-style-name">单栏</div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<el-divider border-style="dashed">全局</el-divider> <el-divider border-style="dashed">全局</el-divider>
<div class="layout-config-global"> <div class="layout-config-global">
<el-form-item label="后台页面切换动画"> <el-form-item label="后台页面切换动画">
<el-select <el-select
@change="onCommitState($event, 'mainAnimation')" @change="onCommitState($event, 'mainAnimation')"
:model-value="configStore.layout.mainAnimation" :model-value="configStore.layout.mainAnimation"
:placeholder="'layouts.Please select an animation name'" :placeholder="'layouts.Please select an animation name'"
> filterable
<el-option label="slide-right" value="slide-right"></el-option> >
<el-option label="slide-left" value="slide-left"></el-option> <el-option label="slide-right" value="slide-right"></el-option>
<el-option label="el-fade-in-linear" value="el-fade-in-linear"></el-option> <el-option label="slide-left" value="slide-left"></el-option>
<el-option label="el-fade-in" value="el-fade-in"></el-option> <el-option label="el-fade-in-linear" value="el-fade-in-linear"></el-option>
<el-option label="el-zoom-in-center" value="el-zoom-in-center"></el-option> <el-option label="el-fade-in" value="el-fade-in"></el-option>
<el-option label="el-zoom-in-top" value="el-zoom-in-top"></el-option> <el-option label="el-zoom-in-center" value="el-zoom-in-center"></el-option>
<el-option label="el-zoom-in-bottom" value="el-zoom-in-bottom"></el-option> <el-option label="el-zoom-in-top" value="el-zoom-in-top"></el-option>
</el-select> <el-option label="el-zoom-in-bottom" value="el-zoom-in-bottom"></el-option>
</el-form-item> </el-select>
<el-form-item label="组件主题色"> </el-form-item>
<el-color-picker <el-form-item label="组件主题色">
@change="onCommitColorState($event, 'elementUiPrimary')" <el-color-picker
:model-value="configStore.getColorVal('elementUiPrimary')" @change="onCommitColorState($event, 'elementUiPrimary')"
/> :model-value="configStore.getColorVal('elementUiPrimary')"
</el-form-item> />
<el-form-item label="表格标题栏背景颜色"> </el-form-item>
<el-color-picker <el-form-item label="表格标题栏背景颜色">
@change="onCommitColorState($event, 'tableHeaderBackground')" <el-color-picker
:model-value="configStore.getColorVal('tableHeaderBackground')" @change="onCommitColorState($event, 'tableHeaderBackground')"
/> :model-value="configStore.getColorVal('tableHeaderBackground')"
</el-form-item> />
<el-form-item label="表格标题栏文字颜色"> </el-form-item>
<el-color-picker <el-form-item label="表格标题栏文字颜色">
@change="onCommitColorState($event, 'tableHeaderColor')" <el-color-picker
:model-value="configStore.getColorVal('tableHeaderColor')" @change="onCommitColorState($event, 'tableHeaderColor')"
/> :model-value="configStore.getColorVal('tableHeaderColor')"
</el-form-item> />
<el-form-item label="表格激活栏颜色"> </el-form-item>
<el-color-picker <el-form-item label="表格激活栏颜色">
@change="onCommitColorState($event, 'tableCurrent')" <el-color-picker
:model-value="configStore.getColorVal('tableCurrent')" @change="onCommitColorState($event, 'tableCurrent')"
/> :model-value="configStore.getColorVal('tableCurrent')"
</el-form-item> />
</div> </el-form-item>
<el-divider border-style="dashed">侧边栏</el-divider> </div>
<div class="layout-config-aside"> <el-divider border-style="dashed">侧边栏</el-divider>
<el-form-item label="侧边菜单栏背景色"> <div class="layout-config-aside">
<el-color-picker <el-form-item label="侧边菜单栏背景色">
@change="onCommitColorState($event, 'menuBackground')" <el-color-picker
:model-value="configStore.getColorVal('menuBackground')" @change="onCommitColorState($event, 'menuBackground')"
/> :model-value="configStore.getColorVal('menuBackground')"
</el-form-item> />
<el-form-item label="侧边菜单文字颜色"> </el-form-item>
<el-color-picker <el-form-item label="侧边菜单文字颜色">
@change="onCommitColorState($event, 'menuColor')" <el-color-picker
:model-value="configStore.getColorVal('menuColor')" @change="onCommitColorState($event, 'menuColor')"
/> :model-value="configStore.getColorVal('menuColor')"
</el-form-item> />
<el-form-item label="侧边菜单激活项背景色"> </el-form-item>
<el-color-picker <el-form-item label="侧边菜单激活项背景色">
@change="onCommitColorState($event, 'menuActiveBackground')" <el-color-picker
:model-value="configStore.getColorVal('menuActiveBackground')" @change="onCommitColorState($event, 'menuActiveBackground')"
/> :model-value="configStore.getColorVal('menuActiveBackground')"
</el-form-item> />
<el-form-item label="侧边菜单激活项文字色"> </el-form-item>
<el-color-picker <el-form-item label="侧边菜单激活项文字色">
@change="onCommitColorState($event, 'menuActiveColor')" <el-color-picker
:model-value="configStore.getColorVal('menuActiveColor')" @change="onCommitColorState($event, 'menuActiveColor')"
/> :model-value="configStore.getColorVal('menuActiveColor')"
</el-form-item> />
<el-form-item label="显示侧边菜单顶栏(LOGO栏)"> </el-form-item>
<el-switch <el-form-item label="显示侧边菜单顶栏(LOGO栏)">
@change="onCommitState($event, 'menuShowTopBar')" <el-switch
:model-value="configStore.layout.menuShowTopBar" @change="onCommitState($event, 'menuShowTopBar')"
></el-switch> :model-value="configStore.layout.menuShowTopBar"
</el-form-item> ></el-switch>
<el-form-item label="侧边菜单顶栏背景色"> </el-form-item>
<el-color-picker <el-form-item label="侧边菜单顶栏背景色">
@change="onCommitColorState($event, 'menuTopBarBackground')" <el-color-picker
:model-value="configStore.getColorVal('menuTopBarBackground')" @change="onCommitColorState($event, 'menuTopBarBackground')"
/> :model-value="configStore.getColorVal('menuTopBarBackground')"
</el-form-item> />
<el-form-item label="侧边菜单宽度(展开时)"> </el-form-item>
<el-input <el-form-item label="侧边菜单宽度(展开时)">
@input="onCommitState($event, 'menuWidth')" <el-input
type="number" @input="onCommitState($event, 'menuWidth')"
:step="10" type="number"
:model-value="configStore.layout.menuWidth" :step="10"
> :model-value="configStore.layout.menuWidth"
<template #append>px</template> >
</el-input> <template #append>px</template>
</el-form-item> </el-input>
<!-- <el-form-item label="侧边菜单默认图标">--> </el-form-item>
<!-- <IconSelector--> <!-- <el-form-item label="侧边菜单默认图标">-->
<!-- @change="onCommitMenuDefaultIcon($event, 'menuDefaultIcon')"--> <!-- <IconSelector-->
<!-- :model-value="configStore.layout.menuDefaultIcon"--> <!-- @change="onCommitMenuDefaultIcon($event, 'menuDefaultIcon')"-->
<!-- />--> <!-- :model-value="configStore.layout.menuDefaultIcon"-->
<!-- </el-form-item>--> <!-- />-->
<!-- <el-form-item label="侧边菜单水平折叠">--> <!-- </el-form-item>-->
<!-- <el-switch--> <!-- <el-form-item label="侧边菜单水平折叠">-->
<!-- @change="onCommitState($event, 'menuCollapse')"--> <!-- <el-switch-->
<!-- :model-value="configStore.layout.menuCollapse"--> <!-- @change="onCommitState($event, 'menuCollapse')"-->
<!-- ></el-switch>--> <!-- :model-value="configStore.layout.menuCollapse"-->
<!-- </el-form-item>--> <!-- ></el-switch>-->
<!-- <el-form-item label="侧边菜单手风琴">--> <!-- </el-form-item>-->
<!-- <el-switch--> <!-- <el-form-item label="侧边菜单手风琴">-->
<!-- @change="onCommitState($event, 'menuUniqueOpened')"--> <!-- <el-switch-->
<!-- :model-value="configStore.layout.menuUniqueOpened"--> <!-- @change="onCommitState($event, 'menuUniqueOpened')"-->
<!-- ></el-switch>--> <!-- :model-value="configStore.layout.menuUniqueOpened"-->
<!-- </el-form-item>--> <!-- ></el-switch>-->
</div> <!-- </el-form-item>-->
</div>
<el-divider border-style="dashed">顶栏</el-divider>
<div class="layout-config-aside"> <el-divider border-style="dashed">顶栏</el-divider>
<el-form-item label="顶栏背景色"> <div class="layout-config-aside">
<el-color-picker <el-form-item label="顶栏背景色">
@change="onCommitColorState($event, 'headerBarBackground')" <el-color-picker
:model-value="configStore.getColorVal('headerBarBackground')" @change="onCommitColorState($event, 'headerBarBackground')"
/> :model-value="configStore.getColorVal('headerBarBackground')"
</el-form-item> />
<el-form-item label="顶栏文字色"> </el-form-item>
<el-color-picker <el-form-item label="顶栏文字色">
@change="onCommitColorState($event, 'headerBarTabColor')" <el-color-picker
:model-value="configStore.getColorVal('headerBarTabColor')" @change="onCommitColorState($event, 'headerBarTabColor')"
/> :model-value="configStore.getColorVal('headerBarTabColor')"
</el-form-item> />
<!-- <el-form-item label="顶栏悬停时背景色">--> </el-form-item>
<!-- <el-color-picker--> <!-- <el-form-item label="顶栏悬停时背景色">-->
<!-- @change="onCommitColorState($event, 'headerBarHoverBackground')"--> <!-- <el-color-picker-->
<!-- :model-value="configStore.getColorVal('headerBarHoverBackground')"--> <!-- @change="onCommitColorState($event, 'headerBarHoverBackground')"-->
<!-- />--> <!-- :model-value="configStore.getColorVal('headerBarHoverBackground')"-->
<!-- </el-form-item>--> <!-- />-->
<!-- <el-form-item label="顶栏菜单激活项背景色">--> <!-- </el-form-item>-->
<!-- <el-color-picker--> <!-- <el-form-item label="顶栏菜单激活项背景色">-->
<!-- @change="onCommitColorState($event, 'headerBarTabActiveBackground')"--> <!-- <el-color-picker-->
<!-- :model-value="configStore.getColorVal('headerBarTabActiveBackground')"--> <!-- @change="onCommitColorState($event, 'headerBarTabActiveBackground')"-->
<!-- />--> <!-- :model-value="configStore.getColorVal('headerBarTabActiveBackground')"-->
<!-- </el-form-item>--> <!-- />-->
<!-- <el-form-item label="顶栏菜单激活项文字色">--> <!-- </el-form-item>-->
<!-- <el-color-picker--> <!-- <el-form-item label="顶栏菜单激活项文字色">-->
<!-- @change="onCommitColorState($event, 'headerBarTabActiveColor')"--> <!-- <el-color-picker-->
<!-- :model-value="configStore.getColorVal('headerBarTabActiveColor')"--> <!-- @change="onCommitColorState($event, 'headerBarTabActiveColor')"-->
<!-- />--> <!-- :model-value="configStore.getColorVal('headerBarTabActiveColor')"-->
<!-- </el-form-item>--> <!-- />-->
</div> <!-- </el-form-item>-->
<el-popconfirm @confirm="restoreDefault" title="确定要恢复全部配置到默认值吗?"> </div>
<template #reference> <el-popconfirm @confirm="restoreDefault" title="确定要恢复全部配置到默认值吗?">
<div class="ba-center"> <template #reference>
<el-button class="w80" type="info">恢复默认</el-button> <div class="ba-center">
</div> <el-button class="w80" type="info">恢复默认</el-button>
</template> </div>
</el-popconfirm> </template>
</div> </el-popconfirm>
</el-form> </div>
</el-scrollbar> </el-form>
</el-drawer> </el-scrollbar>
</div> </el-drawer>
</template> </div>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config' <script setup lang="ts">
import { useNavTabs } from '@/stores/navTabs' import { useConfig } from '@/stores/config'
import { useRouter } from 'vue-router' import { useNavTabs } from '@/stores/navTabs'
import IconSelector from '@/components/baInput/components/iconSelector.vue' import { useRouter } from 'vue-router'
import { STORE_CONFIG } from '@/stores/constant/cacheKey' import IconSelector from '@/components/baInput/components/iconSelector.vue'
import { Local, Session } from '@/utils/storage' import { STORE_CONFIG } from '@/stores/constant/cacheKey'
import type { Layout } from '@/stores/interface' import { Local, Session } from '@/utils/storage'
import type { Layout } from '@/stores/interface'
const configStore = useConfig()
const navTabs = useNavTabs() const configStore = useConfig()
const router = useRouter() const navTabs = useNavTabs()
const router = useRouter()
const onCommitState = (value: any, name: any) => {
configStore.setLayout(name, value) const onCommitState = (value: any, name: any) => {
} configStore.setLayout(name, value)
}
const onCommitColorState = (value: string | null, name: keyof Layout) => {
if (value === null) return const onCommitColorState = (value: string | null, name: keyof Layout) => {
const colors = configStore.layout[name] as string[] if (value === null) return
if (configStore.layout.isDark) { const colors = configStore.layout[name] as string[]
colors[1] = value if (configStore.layout.isDark) {
} else { colors[1] = value
colors[0] = value } else {
} colors[0] = value
configStore.setLayout(name, colors) }
} configStore.setLayout(name, colors)
}
const setLayoutMode = (mode: string) => {
configStore.setLayoutMode(mode) const setLayoutMode = (mode: string) => {
} configStore.setLayoutMode(mode)
}
// 修改默认菜单图标
const onCommitMenuDefaultIcon = (value: any, name: any) => { // 修改默认菜单图标
configStore.setLayout(name, value) const onCommitMenuDefaultIcon = (value: any, name: any) => {
configStore.setLayout(name, value)
const menus = navTabs.state.tabsViewRoutes
navTabs.setTabsViewRoutes([]) const menus = navTabs.state.tabsViewRoutes
setTimeout(() => { navTabs.setTabsViewRoutes([])
navTabs.setTabsViewRoutes(menus) setTimeout(() => {
}, 200) navTabs.setTabsViewRoutes(menus)
} }, 200)
}
const onCloseDrawer = () => {
configStore.setLayout('showDrawer', false) const onCloseDrawer = () => {
} configStore.setLayout('showDrawer', false)
}
const restoreDefault = () => {
Local.remove(STORE_CONFIG) const restoreDefault = () => {
router.go(0) Local.remove(STORE_CONFIG)
} router.go(0)
</script> }
</script>
<style scoped lang="scss">
<style scoped lang="scss">
.layout-config-drawer :deep(.el-input__inner) {
padding: 0 0 0 6px; .layout-config-drawer :deep(.el-input__inner) {
} padding: 0 0 0 6px;
}
.layout-config-drawer :deep(.el-input-group__append) {
padding: 0 10px; .layout-config-drawer :deep(.el-input-group__append) {
} padding: 0 10px;
}
.layout-config-drawer :deep(.el-drawer__header) {
margin-bottom: 0 !important; .layout-config-drawer :deep(.el-drawer__header) {
} margin-bottom: 0 !important;
}
.layout-config-drawer :deep(.el-drawer__body) {
padding: 0; .layout-config-drawer :deep(.el-drawer__body) {
} padding: 0;
}
.layout-mode-styles-box {
padding: 20px; .layout-mode-styles-box {
} padding: 20px;
}
.layout-mode-box-style-row {
margin-bottom: 15px; .layout-mode-box-style-row {
} margin-bottom: 15px;
}
.layout-mode-style {
position: relative; .layout-mode-style {
height: 100px; position: relative;
border: 1px solid var(--el-border-color-light); height: 100px;
border-radius: var(--el-border-radius-small); border: 1px solid var(--el-border-color-light);
border-radius: var(--el-border-radius-small);
&:hover,
&.active { &:hover,
border: 1px solid var(--el-color-primary); &.active {
} border: 1px solid var(--el-color-primary);
}
.layout-mode-style-name {
position: absolute; .layout-mode-style-name {
display: flex; position: absolute;
align-items: center; display: flex;
justify-content: center; align-items: center;
color: var(--el-color-primary-light-5); justify-content: center;
border-radius: 50%; color: var(--el-color-primary-light-5);
height: 50px; border-radius: 50%;
width: 50px; height: 50px;
border: 1px solid var(--el-color-primary-light-3); width: 50px;
} border: 1px solid var(--el-color-primary-light-3);
}
.layout-mode-style-box {
display: flex; .layout-mode-style-box {
align-items: center; display: flex;
justify-content: center; align-items: center;
width: 100%; justify-content: center;
height: 100%; width: 100%;
} height: 100%;
}
&.default {
display: flex; &.default {
align-items: center; display: flex;
justify-content: center; align-items: center;
justify-content: center;
.layout-mode-style-aside {
width: 18%; .layout-mode-style-aside {
height: 90%; width: 18%;
background-color: var(--el-border-color-lighter); height: 90%;
} background-color: var(--el-border-color-lighter);
}
.layout-mode-style-container-box {
width: 68%; .layout-mode-style-container-box {
height: 90%; width: 68%;
margin-left: 4%; height: 90%;
margin-left: 4%;
.layout-mode-style-header {
width: 100%; .layout-mode-style-header {
height: 10%; width: 100%;
background-color: var(--el-border-color-lighter); height: 10%;
} background-color: var(--el-border-color-lighter);
}
.layout-mode-style-container {
width: 100%; .layout-mode-style-container {
height: 85%; width: 100%;
background-color: var(--el-border-color-extra-light); height: 85%;
margin-top: 5%; background-color: var(--el-border-color-extra-light);
} margin-top: 5%;
} }
} }
}
&.classic {
display: flex; &.classic {
align-items: center; display: flex;
justify-content: center; align-items: center;
justify-content: center;
.layout-mode-style-aside {
width: 18%; .layout-mode-style-aside {
height: 100%; width: 18%;
background-color: var(--el-border-color-lighter); height: 100%;
} background-color: var(--el-border-color-lighter);
}
.layout-mode-style-container-box {
width: 82%; .layout-mode-style-container-box {
height: 100%; width: 82%;
height: 100%;
.layout-mode-style-header {
width: 100%; .layout-mode-style-header {
height: 10%; width: 100%;
background-color: var(--el-border-color); height: 10%;
} background-color: var(--el-border-color);
}
.layout-mode-style-container {
width: 100%; .layout-mode-style-container {
height: 90%; width: 100%;
background-color: var(--el-border-color-extra-light); height: 90%;
} background-color: var(--el-border-color-extra-light);
} }
} }
}
&.streamline {
display: flex; &.streamline {
align-items: center; display: flex;
justify-content: center; align-items: center;
justify-content: center;
.layout-mode-style-container-box {
width: 100%; .layout-mode-style-container-box {
height: 100%; width: 100%;
height: 100%;
.layout-mode-style-header {
width: 100%; .layout-mode-style-header {
height: 10%; width: 100%;
background-color: var(--el-border-color); height: 10%;
} background-color: var(--el-border-color);
}
.layout-mode-style-container {
width: 100%; .layout-mode-style-container {
height: 90%; width: 100%;
background-color: var(--el-border-color-extra-light); height: 90%;
} background-color: var(--el-border-color-extra-light);
} }
} }
}
&.double {
display: flex; &.double {
align-items: center; display: flex;
justify-content: center; align-items: center;
justify-content: center;
.layout-mode-style-aside {
width: 18%; .layout-mode-style-aside {
height: 100%; width: 18%;
background-color: var(--el-border-color); height: 100%;
} background-color: var(--el-border-color);
}
.layout-mode-style-container-box {
width: 82%; .layout-mode-style-container-box {
height: 100%; width: 82%;
height: 100%;
.layout-mode-style-header {
width: 100%; .layout-mode-style-header {
height: 10%; width: 100%;
background-color: var(--el-border-color); height: 10%;
} background-color: var(--el-border-color);
}
.layout-mode-style-container {
width: 100%; .layout-mode-style-container {
height: 90%; width: 100%;
background-color: var(--el-border-color-extra-light); height: 90%;
} background-color: var(--el-border-color-extra-light);
} }
} }
} }
}
.w80 {
width: 90%; .w80 {
} width: 90%;
</style> }
</style>

View File

@@ -104,7 +104,10 @@ const handleLogout = async () => {
redirectToCasLogout() redirectToCasLogout()
} else { } else {
// 普通环境直接跳登录页 // 普通环境直接跳登录页
replace('/login') window.location.reload()
setTimeout(() => {
replace('/login')
}, 1000)
} }
} }
} }
@@ -119,8 +122,6 @@ const clearLocalData = () => {
// 清空标签页 // 清空标签页
navTabs.closeTabs() navTabs.closeTabs()
console.log('✅ 本地数据已全部清空')
} }
// CAS 登出跳转 // CAS 登出跳转

View File

@@ -1,128 +1,130 @@
<template> <template>
<div> <div>
<component :is="config.layout.layoutMode"></component> <component :is="config.layout.layoutMode"></component>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from 'vue' import { reactive } from 'vue'
import { useConfig } from '@/stores/config' import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs' import { useNavTabs } from '@/stores/navTabs'
import { useAdminInfo } from '@/stores/adminInfo' import { useAdminInfo } from '@/stores/adminInfo'
import { useDictData } from '@/stores/dictData' import { useDictData } from '@/stores/dictData'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import Default from '@/layouts/admin/container/default.vue' import Default from '@/layouts/admin/container/default.vue'
import Classic from '@/layouts/admin/container/classic.vue' import Classic from '@/layouts/admin/container/classic.vue'
import Streamline from '@/layouts/admin/container/streamline.vue' import Streamline from '@/layouts/admin/container/streamline.vue'
import Double from '@/layouts/admin/container/double.vue' import Double from '@/layouts/admin/container/double.vue'
import { onMounted, onBeforeMount } from 'vue' import { onMounted, onBeforeMount } from 'vue'
import { handleAdminRoute, getFirstRoute, routePush } from '@/utils/router' import { handleAdminRoute, getFirstRoute, routePush } from '@/utils/router'
import router from '@/router/index' import router from '@/router/index'
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import { isEmpty } from 'lodash-es' import { isEmpty } from 'lodash-es'
import { setNavTabsWidth } from '@/utils/layout' import { setNavTabsWidth } from '@/utils/layout'
import { adminBaseRoutePath } from '@/router/static' import { adminBaseRoutePath } from '@/router/static'
import { getRouteMenu, dictDataCache } from '@/api/auth' import { getRouteMenu, dictDataCache } from '@/api/auth'
import { getAreaList, areaSelect } from '@/api/common' import { getAreaList, areaSelect } from '@/api/common'
import { BasicDictData } from '@/stores/interface' import { BasicDictData } from '@/stores/interface'
import { getAllUserSimpleList, getUserById } from '@/api/user-boot/user' import { getAllUserSimpleList, getUserById } from '@/api/user-boot/user'
defineOptions({ defineOptions({
components: { Default, Classic, Streamline, Double } components: { Default, Classic, Streamline, Double }
}) })
const navTabs = useNavTabs() const navTabs = useNavTabs()
const config = useConfig() const config = useConfig()
const route = useRoute() const route = useRoute()
const adminInfo = useAdminInfo() const adminInfo = useAdminInfo()
const dictData = useDictData() const dictData = useDictData()
const state = reactive({ const state = reactive({
autoMenuCollapseLock: false autoMenuCollapseLock: false
}) })
onMounted(() => { onMounted(() => {
// if (!adminInfo.token) return router.push({ name: 'login' }) // if (!adminInfo.token) return router.push({ name: 'login' })
init() init()
setNavTabsWidth() setNavTabsWidth()
useEventListener(window, 'resize', setNavTabsWidth) useEventListener(window, 'resize', setNavTabsWidth)
}) })
onBeforeMount(() => { onBeforeMount(() => {
onAdaptiveLayout() onAdaptiveLayout()
useEventListener(window, 'resize', onAdaptiveLayout) useEventListener(window, 'resize', onAdaptiveLayout)
}) })
const init = async () => { const init = async () => {
await Promise.all([getAreaList(), dictDataCache(), getUserById(), areaSelect(), getAllUserSimpleList()]).then(res => { await Promise.all([getAreaList(), dictDataCache(), getUserById(), areaSelect(), getAllUserSimpleList()]).then(res => {
dictData.state.area = res[0].data dictData.state.area = res[0].data
dictData.state.basic = res[1].data dictData.state.basic = res[1].data
dictData.state.userList = res[4].data dictData.state.userList = res[4].data
adminInfo.dataFill(res[2].data) adminInfo.dataFill(res[2].data)
dictData.state.areaTree = res[3].data dictData.state.areaTree = res[3].data
}) })
/** /**
* 后台初始化请求,获取站点配置,动态路由等信息 * 后台初始化请求,获取站点配置,动态路由等信息
*/ */
getRouteMenu().then((res: any) => { getRouteMenu().then((res: any) => {
const handlerMenu = (data: any) => { const handlerMenu = (data: any) => {
data.forEach((item: any) => { data.forEach((item: any) => {
item.routePath = item.routePath =
item.routePath[0] == '/' ? item.routePath.substring(1, item.routePath.length) : item.routePath item.routePath[0] == '/' ? item.routePath.substring(1, item.routePath.length) : item.routePath
item.path = item.routePath item.path = item.routePath
item.name = item.routePath item.name = item.routePath
item.keepalive = item.routePath item.keepalive = item.routePath
item.component = item.routeName item.component = item.routeName
? item.routeName.indexOf('/src/views/') > -1 ? item.routeName.indexOf('/src/views/') > -1
? item.routeName ? item.routeName
: `/src/views/${item.routeName}/index.vue` : `/src/views/${item.routeName}/index.vue`
: '' : ''
item.type = item.children && item.children.length > 0 ? 'menu_dir' : 'menu' item.type = item.children && item.children.length > 0 ? 'menu_dir' : 'menu'
item.menu_type = item.children && item.children.length > 0 ? null : 'tab' item.menu_type = item.children && item.children.length > 0 ? null : 'tab'
if (item.children) { if (item.children) {
handlerMenu(item.children) handlerMenu(item.children)
} }
}) })
} }
handlerMenu(res.data) handlerMenu(res.data)
handleAdminRoute(res.data) handleAdminRoute(res.data)
if (route.params.to) { if (route.params.to) {
const lastRoute = JSON.parse(route.params.to as string) const lastRoute = JSON.parse(route.params.to as string)
if (lastRoute.path != adminBaseRoutePath) { if (lastRoute.path != adminBaseRoutePath) {
let query = !isEmpty(lastRoute.query) ? lastRoute.query : {} let query = !isEmpty(lastRoute.query) ? lastRoute.query : {}
routePush({ path: lastRoute.path, query: query }) routePush({ path: lastRoute.path, query: query })
return return
} }
} }
// 跳转到第一个菜单 // 仅在 loading 页时跳转到第一个菜单,避免覆盖用户当前页面
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes) if (route.name === 'adminMainLoading') {
if (firstRoute) routePush(firstRoute.path) const firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
}) if (firstRoute) routePush(firstRoute.path)
} }
})
const onAdaptiveLayout = () => { }
let defaultBeforeResizeLayout = {
layoutMode: config.layout.layoutMode, const onAdaptiveLayout = () => {
menuCollapse: config.layout.menuCollapse let defaultBeforeResizeLayout = {
} layoutMode: config.layout.layoutMode,
menuCollapse: config.layout.menuCollapse
const clientWidth = document.body.clientWidth }
if (clientWidth < 1024) {
/** const clientWidth = document.body.clientWidth
* 锁定窗口改变自动调整 menuCollapse if (clientWidth < 1024) {
* 避免已是小窗且打开了菜单栏时,意外的自动关闭菜单栏 /**
*/ * 锁定窗口改变自动调整 menuCollapse
if (!state.autoMenuCollapseLock) { * 避免已是小窗且打开了菜单栏时,意外的自动关闭菜单栏
state.autoMenuCollapseLock = true */
config.setLayout('menuCollapse', true) if (!state.autoMenuCollapseLock) {
} state.autoMenuCollapseLock = true
config.setLayout('shrink', true) config.setLayout('menuCollapse', true)
config.setLayoutMode('Classic') }
} else { config.setLayout('shrink', true)
state.autoMenuCollapseLock = false config.setLayoutMode('Classic')
config.setLayout('menuCollapse', defaultBeforeResizeLayout.menuCollapse) } else {
config.setLayout('shrink', false) state.autoMenuCollapseLock = false
config.setLayoutMode(defaultBeforeResizeLayout.layoutMode) config.setLayout('menuCollapse', defaultBeforeResizeLayout.menuCollapse)
} config.setLayout('shrink', false)
} config.setLayoutMode(defaultBeforeResizeLayout.layoutMode)
</script> }
}
</script>

View File

@@ -1,9 +1,13 @@
<template> <template>
<el-main class="layout-main" :style="mainHeight()"> <el-main class="layout-main" :style="mainHeight()">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component, route: viewRoute }">
<transition :name="config.layout.mainAnimation" mode="out-in"> <transition :name="config.layout.mainAnimation" mode="out-in">
<keep-alive :include="state.keepAliveComponentNameList"> <keep-alive :include="state.keepAliveComponentNameList">
<component :is="Component" :key="state.componentKey" /> <component
:is="Component"
v-if="Component"
:key="state.componentKey || viewRoute.fullPath"
/>
</keep-alive> </keep-alive>
</transition> </transition>
</router-view> </router-view>
@@ -32,7 +36,7 @@ const state: {
componentKey: string componentKey: string
keepAliveComponentNameList: string[] keepAliveComponentNameList: string[]
} = reactive({ } = reactive({
componentKey: route.path, componentKey: '',
keepAliveComponentNameList: [] keepAliveComponentNameList: []
}) })
@@ -51,9 +55,10 @@ onBeforeMount(() => {
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter( state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter(
(name: string) => menu.meta.keepalive !== name (name: string) => menu.meta.keepalive !== name
) )
state.componentKey = '' // 临时换 key 强制卸载,再在 nextTick 恢复为路由 fullPath避免 keep-alive 缓存错乱
state.componentKey = `${menu.fullPath || menu.path}__refresh__${Date.now()}`
nextTick(() => { nextTick(() => {
state.componentKey = menu.path state.componentKey = ''
addKeepAliveComponentName(menu.meta.keepalive as string) addKeepAliveComponentName(menu.meta.keepalive as string)
}) })
}) })
@@ -79,7 +84,6 @@ onMounted(() => {
watch( watch(
() => route.path, () => route.path,
() => { () => {
state.componentKey = route.path
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') { if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive) addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
} }

View File

@@ -1,59 +1,98 @@
<template> <template>
<div> <div>
<div <div
v-loading="true" v-loading="true"
element-loading-background="var(--ba-bg-color-overlay)" element-loading-background="var(--ba-bg-color-overlay)"
element-loading-text="加载中" element-loading-text="加载中"
class="default-main ba-main-loading" class="default-main ba-main-loading"
:style="{ height:'calc(100vh - 170px)'}" :style="{ height: 'calc(100vh - 170px)' }"
></div> ></div>
<div v-if="state.showReload" class="loading-footer mt10"> <div v-if="state.showReload" class="loading-footer mt10">
<el-button @click="refresh" type="warning">重新加载</el-button> <el-button @click="refresh" type="warning">重新加载</el-button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onUnmounted, reactive } from 'vue' import { onMounted, onUnmounted, reactive, watch } from 'vue'
import router from '@/router/index' import { useRoute } from 'vue-router'
import { useNavTabs } from '@/stores/navTabs' import router from '@/router/index'
import { isAdminApp } from '@/utils/common' import { useNavTabs } from '@/stores/navTabs'
import { getFirstRoute, routePush } from '@/utils/router' import { isAdminApp } from '@/utils/common'
let timer: number import { getFirstRoute, routePush } from '@/utils/router'
import { isEmpty } from 'lodash-es'
const navTabs = useNavTabs() import { adminBaseRoutePath } from '@/router/static'
const state = reactive({
maximumWait: 1000 * 6, let timer: number
showReload: false,
}) const route = useRoute()
const navTabs = useNavTabs()
const refresh = () => { const state = reactive({
router.go(0) maximumWait: 1000 * 6,
} showReload: false
// if (isAdminApp() && navTabs.state.tabsViewRoutes.length > 0) { })
// let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
// if (firstRoute) routePush(firstRoute.path) const tryRedirect = () => {
// } if (route.params.to) {
try {
timer = window.setTimeout(() => { const lastRoute = JSON.parse(route.params.to as string)
state.showReload = true if (lastRoute.path && lastRoute.path !== adminBaseRoutePath) {
}, state.maximumWait) const query = !isEmpty(lastRoute.query) ? lastRoute.query : {}
routePush({ path: lastRoute.path, query })
onUnmounted(() => { return true
clearTimeout(timer) }
}) } catch (error) {
</script> console.error(error)
}
<style scoped lang="scss"> }
.ba-main-loading { if (isAdminApp() && navTabs.state.tabsViewRoutes.length > 0) {
height: 300px; const firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
display: flex; if (firstRoute) {
align-items: center; routePush(firstRoute.path)
justify-content: center; return true
} }
.loading-footer { }
display: flex; return false
align-items: center; }
justify-content: center;
} const refresh = () => {
</style> if (!tryRedirect()) {
router.replace(route.fullPath)
}
}
onMounted(() => {
if (tryRedirect()) return
const stop = watch(
() => navTabs.state.tabsViewRoutes.length,
len => {
if (len > 0 && tryRedirect()) {
stop()
}
}
)
})
timer = window.setTimeout(() => {
state.showReload = true
}, state.maximumWait)
onUnmounted(() => {
clearTimeout(timer)
})
</script>
<style scoped lang="scss">
.ba-main-loading {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.loading-footer {
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,72 +0,0 @@
<script lang="ts" setup>
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useAppStore } from '@/stores/modules/app'
import { Footer } from '@/layouts/components/Footer'
defineOptions({ name: 'AppView' })
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const fixedHeader = computed(() => appStore.getFixedHeader)
const footer = computed(() => appStore.getFooter)
const tagsViewStore = useTagsViewStore()
const getCaches = computed((): string[] => {
return tagsViewStore.getCachedViews
})
const tagsView = computed(() => appStore.getTagsView)
//region 无感刷新
const routerAlive = ref(true)
// 无感刷新,防止出现页面闪烁白屏
const reload = () => {
routerAlive.value = false
nextTick(() => (routerAlive.value = true))
}
// 为组件后代提供刷新方法
provide('reload', reload)
//endregion
</script>
<template>
<section
:class="[
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
{
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
(fixedHeader &&
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
footer) ||
(!tagsView && layout === 'top' && footer),
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
tagsView && layout === 'top' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
!fixedHeader && layout === 'classic' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
!fixedHeader && layout === 'topLeft' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
fixedHeader && layout === 'cutMenu' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
!fixedHeader && layout === 'cutMenu' && footer
}
]"
>
<router-view v-if="routerAlive">
<template #default="{ Component, route }">
<keep-alive :include="getCaches">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</template>
</router-view>
</section>
<Footer v-if="footer" />
</template>

View File

@@ -1,3 +0,0 @@
import Breadcrumb from './src/Breadcrumb.vue'
export { Breadcrumb }

View File

@@ -1,130 +0,0 @@
<script lang="tsx">
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
import { useRouter } from 'vue-router'
import { usePermissionStore } from '@/stores/modules/permission'
import { filterBreadcrumb } from './helper'
import { filter, treeToList } from '@/utils/tree'
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
import { Icon } from '@/components/Icon'
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('breadcrumb')
const appStore = useAppStore()
// 面包屑图标
const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon)
export default defineComponent({
name: 'Breadcrumb',
setup() {
const { currentRoute } = useRouter()
const { t } = useI18n()
const levelList = ref<AppRouteRecordRaw[]>([])
const permissionStore = usePermissionStore()
const menuRouters = computed(() => {
const routers = permissionStore.getRouters
return filterBreadcrumb(routers)
})
const getBreadcrumb = () => {
const currentPath = currentRoute.value.matched.slice(-1)[0].path
levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
return node.path === currentPath
})
}
const renderBreadcrumb = () => {
const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
return breadcrumbList.map((v) => {
const disabled = !v.redirect || v.redirect === 'noredirect'
const meta = v.meta as RouteMeta
return (
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
{meta?.icon && breadcrumbIcon.value ? (
<div class="flex items-center">
<Icon icon={meta.icon} class="mr-[2px]" svgClass="inline-block"></Icon>
{t(v?.meta?.title)}
</div>
) : (
t(v?.meta?.title)
)}
</ElBreadcrumbItem>
)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
},
{
immediate: true
}
)
return () => (
<ElBreadcrumb separator="/" class={`${prefixCls} flex items-center h-full ml-[10px]`}>
<TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight">
{renderBreadcrumb()}
</TransitionGroup>
</ElBreadcrumb>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: el-breadcrumb;
.#{$prefix-cls} {
:deep(&__item) {
display: flex;
.#{$prefix-cls}__inner {
display: flex;
align-items: center;
color: var(--top-header-text-color);
&:hover {
color: var(--el-color-primary);
}
}
}
:deep(&__item):not(:last-child) {
.#{$prefix-cls}__inner {
color: var(--top-header-text-color);
&:hover {
color: var(--el-color-primary);
}
}
}
:deep(&__item):last-child {
.#{$prefix-cls}__inner {
display: flex;
align-items: center;
color: var(--el-text-color-placeholder);
&:hover {
color: var(--el-text-color-placeholder);
}
}
}
}
</style>

View File

@@ -1,31 +0,0 @@
import { pathResolve } from '@/utils/routerHelper'
import type { RouteMeta } from 'vue-router'
export const filterBreadcrumb = (
routes: AppRouteRecordRaw[],
parentPath = ''
): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
for (const route of routes) {
const meta = route?.meta as RouteMeta
if (meta.hidden && !meta.canTo) {
continue
}
const data: AppRouteRecordRaw =
!meta.alwaysShow && route.children?.length === 1
? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) }
: { ...route }
data.path = pathResolve(parentPath, data.path)
if (data.children) {
data.children = filterBreadcrumb(data.children, data.path)
}
if (data) {
res.push(data)
}
}
return res
}

View File

@@ -1,3 +0,0 @@
import Collapse from './src/Collapse.vue'
export { Collapse }

View File

@@ -1,36 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { Expand,Fold,ZoomIn,Delete} from '@element-plus/icons-vue'
defineOptions({ name: 'Collapse' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('collapse')
defineProps({
color: propTypes.string.def('')
})
const appStore = useAppStore()
const collapse = computed(() => appStore.getCollapse)
const toggleCollapse = () => {
const collapsed = unref(collapse)
appStore.setCollapse(!collapsed)
}
</script>
<template>
<div :class="prefixCls" @click="toggleCollapse">
<Icon
:color="color"
:icon="collapse ? Expand: Fold"
:size="18"
class="cursor-pointer"
/>
</div>
</template>

View File

@@ -1,10 +0,0 @@
import ContextMenu from './src/ContextMenu.vue'
import { ElDropdown } from 'element-plus'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
export interface ContextMenuExpose {
elDropdownMenuRef: ComponentRef<typeof ElDropdown>
tagItem: RouteLocationNormalizedLoaded
}
export { ContextMenu }

View File

@@ -1,76 +0,0 @@
<script lang="ts" setup>
import { PropType } from 'vue'
import { useDesign } from '@/hooks/web/useDesign'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { contextMenuSchema } from '@/types/contextMenu'
import type { ElDropdown } from 'element-plus'
defineOptions({ name: 'ContextMenu' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('context-menu')
const { t } = useI18n()
const emit = defineEmits(['visibleChange'])
const props = defineProps({
schema: {
type: Array as PropType<contextMenuSchema[]>,
default: () => []
},
trigger: {
type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>,
default: 'contextmenu'
},
tagItem: {
type: Object as PropType<RouteLocationNormalizedLoaded>,
default: () => ({})
}
})
const command = (item: contextMenuSchema) => {
item.command && item.command(item)
}
const visibleChange = (visible: boolean) => {
emit('visibleChange', visible, props.tagItem)
}
const elDropdownMenuRef = ref<ComponentRef<typeof ElDropdown>>()
defineExpose({
elDropdownMenuRef,
tagItem: props.tagItem
})
</script>
<template>
<ElDropdown
ref="elDropdownMenuRef"
:class="prefixCls"
:trigger="trigger"
placement="bottom-start"
popper-class="v-context-menu-popper"
@command="command"
@visible-change="visibleChange"
>
<slot></slot>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
v-for="(item, index) in schema"
:key="`dropdown${index}`"
:command="item"
:disabled="item.disabled"
:divided="item.divided"
>
<Icon :icon="item.icon" />
{{ t(item.label) }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>

View File

@@ -1,3 +0,0 @@
import Footer from './src/Footer.vue'
export { Footer }

View File

@@ -1,24 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
// eslint-disable-next-line vue/no-reserved-component-names
defineOptions({ name: 'Footer' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('footer')
const appStore = useAppStore()
const title = computed(() => appStore.getTitle)
</script>
<template>
<div
:class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
>
<span class="text-14px">Copyright ©2022-{{ title }}</span>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import LocaleDropdown from './src/LocaleDropdown.vue'
export { LocaleDropdown }

View File

@@ -1,52 +0,0 @@
<script lang="ts" setup>
import { useLocaleStore } from '@/stores/modules/locale'
import { useLocale } from '@/hooks/web/useLocale'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'LocaleDropdown' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('locale-dropdown')
defineProps({
color: propTypes.string.def('')
})
const localeStore = useLocaleStore()
const langMap = computed(() => localeStore.getLocaleMap)
const currentLang = computed(() => localeStore.getCurrentLocale)
const setLang = (lang: LocaleType) => {
if (lang === unref(currentLang).lang) return
// 需要重新加载页面让整个语言多初始化
window.location.reload()
localeStore.setCurrentLocale({
lang
})
const { changeLocale } = useLocale()
changeLocale(lang)
}
</script>
<template>
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
<Icon
:class="$attrs.class"
:color="color"
:size="18"
class="cursor-pointer !p-0"
icon="ion:language-sharp"
/>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
{{ item.name }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>

View File

@@ -1,3 +0,0 @@
import Logo from './src/Logo.vue'
export { Logo }

View File

@@ -1,88 +0,0 @@
<script lang="ts" setup>
import { computed, onMounted, ref, unref, watch } from 'vue'
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'Logo' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('logo')
const appStore = useAppStore()
const show = ref(true)
const title = computed(() => appStore.getTitle)
const layout = computed(() => appStore.getLayout)
const collapse = computed(() => appStore.getCollapse)
onMounted(() => {
if (unref(collapse)) show.value = false
})
watch(
() => collapse.value,
(collapse: boolean) => {
if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') {
show.value = true
return
}
if (!collapse) {
setTimeout(() => {
show.value = !collapse
}, 400)
} else {
show.value = !collapse
}
}
)
watch(
() => layout.value,
(layout) => {
if (layout === 'top' || layout === 'cutMenu') {
show.value = true
} else {
if (unref(collapse)) {
show.value = false
} else {
show.value = true
}
}
}
)
</script>
<template>
<div>
<router-link
:class="[
prefixCls,
layout !== 'classic' ? `${prefixCls}__Top` : '',
'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden'
]"
to="/"
>
<img
class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
src="@/assets/imgs/logo.png"
/>
<div
v-if="show"
:class="[
'ml-10px text-16px font-700',
{
'text-[var(--logo-title-text-color)]': layout === 'classic',
'text-[var(--top-header-text-color)]':
layout === 'topLeft' || layout === 'top' || layout === 'cutMenu'
}
]"
>
{{ title }}
</div>
</router-link>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import Menu from './src/Menu.vue'
export { Menu }

View File

@@ -1,257 +0,0 @@
<script lang="tsx">
import { PropType } from 'vue'
import { ElMenu, ElScrollbar } from 'element-plus'
import { useAppStore } from '@/stores/modules/app'
import { usePermissionStore } from '@/stores/modules/permission'
import { useRenderMenuItem } from './components/useRenderMenuItem'
import { isUrl } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { LayoutType } from '@/types/layout'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('menu')
export default defineComponent({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'Menu',
props: {
menuSelect: {
type: Function as PropType<(index: string) => void>,
default: undefined
}
},
setup(props) {
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const { push, currentRoute } = useRouter()
const permissionStore = usePermissionStore()
const menuMode = computed((): 'vertical' | 'horizontal' => {
// 竖
const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
if (vertical.includes(unref(layout))) {
return 'vertical'
} else {
return 'horizontal'
}
})
const routers = computed(() =>
unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
)
const collapse = computed(() => appStore.getCollapse)
const uniqueOpened = computed(() => appStore.getUniqueOpened)
const activeMenu = computed(() => {
const { meta, path } = unref(currentRoute)
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu as string
}
return path
})
const menuSelect = (index: string) => {
if (props.menuSelect) {
props.menuSelect(index)
}
// 自定义事件
if (isUrl(index)) {
window.open(index)
} else {
push(index)
}
}
const renderMenuWrap = () => {
if (unref(layout) === 'top') {
return renderMenu()
} else {
return <ElScrollbar>{renderMenu()}</ElScrollbar>
}
}
const renderMenu = () => {
return (
<ElMenu
defaultActive={unref(activeMenu)}
mode={unref(menuMode)}
collapse={
unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse)
}
uniqueOpened={unref(layout) === 'top' ? false : unref(uniqueOpened)}
backgroundColor="var(--left-menu-bg-color)"
textColor="var(--left-menu-text-color)"
activeTextColor="var(--left-menu-text-active-color)"
onSelect={menuSelect}
>
{{
default: () => {
const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
return renderMenuItem(unref(routers))
}
}}
</ElMenu>
)
}
return () => (
<div
id={prefixCls}
class={[
`${prefixCls} ${prefixCls}__${unref(menuMode)}`,
'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]',
{
'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
}
]}
>
{renderMenuWrap()}
</div>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-menu;
.#{$prefix-cls} {
position: relative;
transition: width var(--transition-time-02);
:deep(.el-menu) {
width: 100% !important;
border-right: none;
// 设置选中时子标题的颜色
.is-active {
& > .el-sub-menu__title {
color: var(--left-menu-text-active-color) !important;
}
}
// 设置子菜单悬停的高亮和背景色
.el-sub-menu__title,
.el-menu-item {
&:hover {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-color) !important;
}
}
// 设置选中时的高亮背景和高亮颜色
.el-menu-item.is-active {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-active-color) !important;
&:hover {
background-color: var(--left-menu-bg-active-color) !important;
}
}
.el-menu-item.is-active {
position: relative;
}
// 设置子菜单的背景颜色
.el-menu {
.el-sub-menu__title,
.el-menu-item:not(.is-active) {
background-color: var(--left-menu-bg-light-color) !important;
}
}
}
// 折叠时的最小宽度
:deep(.el-menu--collapse) {
width: var(--left-menu-min-width);
& > .is-active,
& > .is-active > .el-sub-menu__title {
position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important;
}
}
// 折叠动画的时候,就需要把文字给隐藏掉
:deep(.horizontal-collapse-transition) {
// transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
.#{$prefix-cls}__title {
display: none;
}
}
// 水平菜单
&__horizontal {
height: calc(var(--top-tool-height)) !important;
:deep(.el-menu--horizontal) {
height: calc(var(--top-tool-height));
border-bottom: none;
// 重新设置底部高亮颜色
& > .el-sub-menu.is-active {
.el-sub-menu__title {
border-bottom-color: var(--el-color-primary) !important;
}
}
.el-menu-item.is-active {
position: relative;
&::after {
display: none !important;
}
}
.#{$prefix-cls}__title {
/* stylelint-disable-next-line */
max-height: calc(var(--top-tool-height) - 2px) !important;
/* stylelint-disable-next-line */
line-height: calc(var(--top-tool-height) - 2px);
}
}
}
}
</style>
<style lang="scss">
$prefix-cls: v-menu-popper;
.#{$prefix-cls}--vertical,
.#{$prefix-cls}--horizontal {
// 设置选中时子标题的颜色
.is-active {
& > .el-sub-menu__title {
color: var(--left-menu-text-active-color) !important;
}
}
// 设置子菜单悬停的高亮和背景色
.el-sub-menu__title,
.el-menu-item {
&:hover {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-color) !important;
}
}
// 设置选中时的高亮背景
.el-menu-item.is-active {
position: relative;
background-color: var(--left-menu-bg-active-color) !important;
&:hover {
background-color: var(--left-menu-bg-active-color) !important;
}
}
}
</style>

View File

@@ -1,50 +0,0 @@
import { ElSubMenu, ElMenuItem } from 'element-plus'
import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle'
import { pathResolve } from '@/utils/routerHelper'
const { renderMenuTitle } = useRenderMenuTitle()
export const useRenderMenuItem = () =>
// allRouters: AppRouteRecordRaw[] = [],
{
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers
.filter((v) => !v.meta?.hidden)
.map((v) => {
const meta = v.meta ?? {}
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
return (
<ElSubMenu index={fullPath}>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
})
}
return {
renderMenuItem
}
}

View File

@@ -1,27 +0,0 @@
import type { RouteMeta } from 'vue-router'
import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n'
export const useRenderMenuTitle = () => {
const renderMenuTitle = (meta: RouteMeta) => {
const { t } = useI18n()
const { title = 'Please set title', icon } = meta
return icon ? (
<>
<Icon icon={meta.icon}></Icon>
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
</>
) : (
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
)
}
return {
renderMenuTitle
}
}

View File

@@ -1,54 +0,0 @@
import type { RouteMeta } from 'vue-router'
import { findPath } from '@/utils/tree'
type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
interface HasOneShowingChild {
oneShowingChild?: boolean
onlyOneChild?: OnlyOneChildType
}
export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => {
const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[]
return (menuList || []).map((item) => item.path)
}
export const hasOneShowingChild = (
children: AppRouteRecordRaw[] = [],
parent: AppRouteRecordRaw
): HasOneShowingChild => {
const onlyOneChild = ref<OnlyOneChildType>()
const showingChildren = children.filter((v) => {
const meta = (v.meta ?? {}) as RouteMeta
if (meta.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = v
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return {
oneShowingChild: true,
onlyOneChild: unref(onlyOneChild)
}
}
// Show parent if there are no child router to display
if (!showingChildren.length) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return {
oneShowingChild: true,
onlyOneChild: unref(onlyOneChild)
}
}
return {
oneShowingChild: false,
onlyOneChild: unref(onlyOneChild)
}
}

View File

@@ -1,3 +0,0 @@
import Screenfull from './src/Screenfull.vue'
export { Screenfull }

View File

@@ -1,32 +0,0 @@
<script lang="ts" setup>
import { Icon } from '@/components/Icon'
import { useFullscreen } from '@vueuse/core'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'ScreenFull' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('screenfull')
defineProps({
color: propTypes.string.def('')
})
const { toggle, isFullscreen } = useFullscreen()
const toggleFullscreen = () => {
toggle()
}
</script>
<template>
<div :class="prefixCls" @click="toggleFullscreen">
<Icon
:color="color"
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
:size="18"
/>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import Setting from './src/Setting.vue'
export { Setting }

View File

@@ -1,299 +0,0 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { useClipboard, useCssVar } from '@vueuse/core'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { useDesign } from '@/hooks/web/useDesign'
import { setCssVar, trim } from '@/utils'
import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
import { useAppStore } from '@/stores/modules/app'
import { ThemeSwitch } from '@/layouts/components/ThemeSwitch'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
defineOptions({ name: 'Setting' })
const { t } = useI18n()
const appStore = useAppStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('setting')
const layout = computed(() => appStore.getLayout)
const drawer = ref(false)
// 主题色相关
const systemTheme = ref(appStore.getTheme.elColorPrimary)
const setSystemTheme = (color: string) => {
setCssVar('--el-color-primary', color)
appStore.setTheme({ elColorPrimary: color })
const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
setMenuTheme(trim(unref(leftMenuBgColor)))
}
// 头部主题相关
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
const setHeaderTheme = (color: string) => {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
appStore.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
topHeaderHoverColor: textHoverColor,
topToolBorderColor
})
if (unref(layout) === 'top') {
setMenuTheme(color)
}
}
// 菜单主题相关
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
const setMenuTheme = (color: string) => {
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
// 左侧菜单边框颜色
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
// 左侧菜单背景颜色
leftMenuBgColor: color,
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
// 左侧菜单字体颜色
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo字体颜色
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo边框颜色
logoBorderColor: isDarkColor ? color : '#eee'
}
appStore.setTheme(theme)
appStore.setCssVarTheme()
}
if (layout.value === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
}
// 监听layout变化重置一些主题色
watch(
() => layout.value,
(n) => {
if (n === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
} else {
setMenuTheme(unref(menuTheme))
}
}
)
// 拷贝
const copyConfig = async () => {
const { copy, copied, isSupported } = useClipboard({
source: `
// 面包屑
breadcrumb: ${appStore.getBreadcrumb},
// 面包屑图标
breadcrumbIcon: ${appStore.getBreadcrumbIcon},
// 折叠图标
hamburger: ${appStore.getHamburger},
// 全屏图标
screenfull: ${appStore.getScreenfull},
// 尺寸图标
size: ${appStore.getSize},
// 多语言图标
locale: ${appStore.getLocale},
// 消息图标
message: ${appStore.getMessage},
// 标签页
tagsView: ${appStore.getTagsView},
// 标签页图标
getTagsViewIcon: ${appStore.getTagsViewIcon},
// logo
logo: ${appStore.getLogo},
// 菜单手风琴
uniqueOpened: ${appStore.getUniqueOpened},
// 固定header
fixedHeader: ${appStore.getFixedHeader},
// 页脚
footer: ${appStore.getFooter},
// 灰色模式
greyMode: ${appStore.getGreyMode},
// layout布局
layout: '${appStore.getLayout}',
// 暗黑模式
isDark: ${appStore.getIsDark},
// 组件尺寸
currentSize: '${appStore.getCurrentSize}',
// 主题相关
theme: {
// 主题色
elColorPrimary: '${appStore.getTheme.elColorPrimary}',
// 左侧菜单边框颜色
leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
// 左侧菜单背景颜色
leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
// 左侧菜单字体颜色
leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
// logo字体颜色
logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
// logo边框颜色
logoBorderColor: '${appStore.getTheme.logoBorderColor}',
// 头部背景颜色
topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
// 头部字体颜色
topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
// 头部悬停颜色
topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
// 头部边框颜色
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
}
`
})
if (!isSupported) {
ElMessage.error(t('setting.copyFailed'))
} else {
await copy()
if (unref(copied)) {
ElMessage.success(t('setting.copySuccess'))
}
}
}
// 清空缓存
const clear = () => {
const { wsCache } = useCache()
wsCache.delete(CACHE_KEY.LAYOUT)
wsCache.delete(CACHE_KEY.THEME)
wsCache.delete(CACHE_KEY.IS_DARK)
window.location.reload()
}
</script>
<template>
<div
:class="prefixCls"
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
@click="drawer = true"
>
<Icon color="#fff" icon="ep:setting" />
</div>
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
<template #header>
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
</template>
<div class="text-center">
<!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch />
<!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider>
<LayoutRadioPicker />
<!-- 系统主题 -->
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
<ColorRadioPicker
v-model="systemTheme"
:schema="[
'#409eff',
'#009688',
'#536dfe',
'#ff5c93',
'#ee4f12',
'#0096c7',
'#9c27b0',
'#ff9800'
]"
@change="setSystemTheme"
/>
<!-- 头部主题 -->
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
<ColorRadioPicker
v-model="headerTheme"
:schema="[
'#fff',
'#151515',
'#5172dc',
'#e74c3c',
'#24292e',
'#394664',
'#009688',
'#383f45'
]"
@change="setHeaderTheme"
/>
<!-- 菜单主题 -->
<template v-if="layout !== 'top'">
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</template>
</div>
<!-- 界面显示 -->
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
<InterfaceDisplay />
<ElDivider />
<div>
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
</div>
<div class="mt-5px">
<ElButton class="w-full" type="danger" @click="clear">
{{ t('setting.clearAndReset') }}
</ElButton>
</div>
</ElDrawer>
</template>
<style lang="scss" scoped>
$prefix-cls: v-setting;
.#{$prefix-cls} {
border-radius: 6px 0 0 6px;
}
</style>

View File

@@ -1,67 +0,0 @@
<script lang="ts" setup>
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'ColorRadioPicker' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('color-radio-picker')
const props = defineProps({
schema: {
type: Array as PropType<string[]>,
default: () => []
},
modelValue: propTypes.string.def('')
})
const emit = defineEmits(['update:modelValue', 'change'])
const colorVal = ref(props.modelValue)
watch(
() => props.modelValue,
(val: string) => {
if (val === unref(colorVal)) return
colorVal.value = val
}
)
// 监听
watch(
() => colorVal.value,
(val: string) => {
emit('update:modelValue', val)
emit('change', val)
}
)
</script>
<template>
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
<span
v-for="(item, i) in schema"
:key="`radio-${i}`"
:class="{ 'is-active': colorVal === item }"
:style="{
background: item
}"
class="mb-5px h-20px w-20px cursor-pointer border-2px border-gray-300 rounded-2px border-solid text-center leading-20px"
@click="colorVal = item"
>
<Icon v-if="colorVal === item" :size="16" color="#fff" icon="ep:check" />
</span>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: v-color-radio-picker;
.#{$prefix-cls} {
.is-active {
border-color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,224 +0,0 @@
<script lang="ts" setup>
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { useWatermark } from '@/hooks/web/useWatermark'
import { useAppStore } from '@/stores/modules/app'
defineOptions({ name: 'InterfaceDisplay' })
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const { setWatermark } = useWatermark()
const prefixCls = getPrefixCls('interface-display')
const appStore = useAppStore()
const water = ref()
// 面包屑
const breadcrumb = ref(appStore.getBreadcrumb)
const breadcrumbChange = (show: boolean) => {
appStore.setBreadcrumb(show)
}
// 面包屑图标
const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
const breadcrumbIconChange = (show: boolean) => {
appStore.setBreadcrumbIcon(show)
}
// 折叠图标
const hamburger = ref(appStore.getHamburger)
const hamburgerChange = (show: boolean) => {
appStore.setHamburger(show)
}
// 全屏图标
const screenfull = ref(appStore.getScreenfull)
const screenfullChange = (show: boolean) => {
appStore.setScreenfull(show)
}
// 尺寸图标
const size = ref(appStore.getSize)
const sizeChange = (show: boolean) => {
appStore.setSize(show)
}
// 多语言图标
const locale = ref(appStore.getLocale)
const localeChange = (show: boolean) => {
appStore.setLocale(show)
}
// 消息图标
const message = ref(appStore.getMessage)
const messageChange = (show: boolean) => {
appStore.setMessage(show)
}
// 标签页
const tagsView = ref(appStore.getTagsView)
const tagsViewChange = (show: boolean) => {
// 切换标签栏显示时,同步切换标签栏的高度
setCssVar('--tags-view-height', show ? '35px' : '0px')
appStore.setTagsView(show)
}
// 标签页图标
const tagsViewIcon = ref(appStore.getTagsViewIcon)
const tagsViewIconChange = (show: boolean) => {
appStore.setTagsViewIcon(show)
}
// logo
const logo = ref(appStore.getLogo)
const logoChange = (show: boolean) => {
appStore.setLogo(show)
}
// 菜单手风琴
const uniqueOpened = ref(appStore.getUniqueOpened)
const uniqueOpenedChange = (uniqueOpened: boolean) => {
appStore.setUniqueOpened(uniqueOpened)
}
// 固定头部
const fixedHeader = ref(appStore.getFixedHeader)
const fixedHeaderChange = (show: boolean) => {
appStore.setFixedHeader(show)
}
// 页脚
const footer = ref(appStore.getFooter)
const footerChange = (show: boolean) => {
appStore.setFooter(show)
}
// 灰色模式
const greyMode = ref(appStore.getGreyMode)
const greyModeChange = (show: boolean) => {
appStore.setGreyMode(show)
}
// 固定菜单
const fixedMenu = ref(appStore.getFixedMenu)
const fixedMenuChange = (show: boolean) => {
appStore.setFixedMenu(show)
}
// 设置水印
const setWater = () => {
setWatermark(water.value)
}
const layout = computed(() => appStore.getLayout)
watch(
() => layout.value,
(n) => {
if (n === 'top') {
appStore.setCollapse(false)
}
}
)
</script>
<template>
<div :class="prefixCls">
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.breadcrumb') }}</span>
<ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
<ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
<ElSwitch v-model="hamburger" @change="hamburgerChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
<ElSwitch v-model="screenfull" @change="screenfullChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.sizeIcon') }}</span>
<ElSwitch v-model="size" @change="sizeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.localeIcon') }}</span>
<ElSwitch v-model="locale" @change="localeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.messageIcon') }}</span>
<ElSwitch v-model="message" @change="messageChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsView') }}</span>
<ElSwitch v-model="tagsView" @change="tagsViewChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
<ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.logo') }}</span>
<ElSwitch v-model="logo" @change="logoChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
<ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.fixedHeader') }}</span>
<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.footer') }}</span>
<ElSwitch v-model="footer" @change="footerChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.greyMode') }}</span>
<ElSwitch v-model="greyMode" @change="greyModeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('watermark.watermark') }}</span>
<ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
</div>
</div>
</template>

View File

@@ -1,172 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'LayoutRadioPicker' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('layout-radio-picker')
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
</script>
<template>
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
<div
:class="[
`${prefixCls}__classic`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'classic'
}
]"
@click="appStore.setLayout('classic')"
></div>
<div
:class="[
`${prefixCls}__top-left`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'topLeft'
}
]"
@click="appStore.setLayout('topLeft')"
></div>
<div
:class="[
`${prefixCls}__top`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'top'
}
]"
@click="appStore.setLayout('top')"
></div>
<div
:class="[
`${prefixCls}__cut-menu`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'cutMenu'
}
]"
@click="appStore.setLayout('cutMenu')"
>
<div class="absolute left-[10%] top-0 h-full w-[33%] bg-gray-200"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: v-layout-radio-picker;
.#{$prefix-cls} {
&__classic {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 33%;
height: 100%;
background-color: #273352;
border-radius: 4px 0 0 4px;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 25%;
background-color: #fff;
border-radius: 4px 4px 0;
content: '';
}
}
&__top-left {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 33%;
background-color: #273352;
border-radius: 4px 4px 0 0;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 33%;
height: 100%;
background-color: #fff;
border-radius: 4px 0 0 4px;
content: '';
}
}
&__top {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 33%;
background-color: #273352;
border-radius: 4px 4px 0 0;
content: '';
}
}
&__cut-menu {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 33%;
background-color: #273352;
border-radius: 4px 4px 0 0;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 10%;
height: 100%;
background-color: #fff;
border-radius: 4px 0 0 4px;
content: '';
}
}
.is-acitve {
border-color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,3 +0,0 @@
import SizeDropdown from './src/SizeDropdown.vue'
export { SizeDropdown }

View File

@@ -1,40 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
defineOptions({ name: 'SizeDropdown' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('size-dropdown')
defineProps({
color: propTypes.string.def('')
})
const { t } = useI18n()
const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const setCurrentSize = (size: ElementPlusSize) => {
appStore.setCurrentSize(size)
}
</script>
<template>
<ElDropdown :class="prefixCls" trigger="click" @command="setCurrentSize">
<Icon :color="color" :size="18" class="cursor-pointer" icon="mdi:format-size" />
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">
{{ t(`size.${item}`) }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>

View File

@@ -1,3 +0,0 @@
import TabMenu from './src/TabMenu.vue'
export { TabMenu }

View File

@@ -1,237 +0,0 @@
<script lang="tsx">
import { usePermissionStore } from '@/stores/modules/permission'
import { useAppStore } from '@/stores/modules/app'
import { ElScrollbar } from 'element-plus'
import { Menu } from '@/layouts/components/Menu'
import { pathResolve } from '@/utils/routerHelper'
import { cloneDeep } from 'lodash-es'
import { filterMenusPath, initTabMap, tabPathMap } from './helper'
import { useDesign } from '@/hooks/web/useDesign'
import { isUrl } from '@/utils/is'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('tab-menu')
export default defineComponent({
name: 'TabMenu',
setup() {
const { push, currentRoute } = useRouter()
const { t } = useI18n()
const appStore = useAppStore()
const collapse = computed(() => appStore.getCollapse)
const fixedMenu = computed(() => appStore.getFixedMenu)
const permissionStore = usePermissionStore()
const routers = computed(() => permissionStore.getRouters)
const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
const setCollapse = () => {
appStore.setCollapse(!unref(collapse))
}
onMounted(() => {
if (unref(fixedMenu)) {
const path = `/${unref(currentRoute).path.split('/')[1]}`
const children = unref(tabRouters).find(
(v) =>
(v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
v.path === path
)?.children
tabActive.value = path
if (children) {
permissionStore.setMenuTabRouters(
cloneDeep(children).map((v) => {
v.path = pathResolve(unref(tabActive), v.path)
return v
})
)
}
}
})
watch(
() => routers.value,
(routers: AppRouteRecordRaw[]) => {
initTabMap(routers)
filterMenusPath(routers, routers)
},
{
immediate: true,
deep: true
}
)
const showTitle = ref(true)
watch(
() => collapse.value,
(collapse: boolean) => {
if (!collapse) {
setTimeout(() => {
showTitle.value = !collapse
}, 200)
} else {
showTitle.value = !collapse
}
}
)
// 是否显示菜单
const showMenu = ref(unref(fixedMenu) ? true : false)
// tab高亮
const tabActive = ref('')
// tab点击事件
const tabClick = (item: AppRouteRecordRaw) => {
if (isUrl(item.path)) {
window.open(item.path)
return
}
const newPath = item.children ? item.path : item.path.split('/')[0]
const oldPath = unref(tabActive)
tabActive.value = item.children ? item.path : item.path.split('/')[0]
if (item.children) {
if (newPath === oldPath || !unref(showMenu)) {
showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
}
if (unref(showMenu)) {
permissionStore.setMenuTabRouters(
cloneDeep(item.children).map((v) => {
v.path = pathResolve(unref(tabActive), v.path)
return v
})
)
}
} else {
push(item.path)
permissionStore.setMenuTabRouters([])
showMenu.value = false
}
}
// 设置高亮
const isActive = (currentPath: string) => {
const { path } = unref(currentRoute)
if (tabPathMap[currentPath].includes(path)) {
return true
}
return false
}
const mouseleave = () => {
if (!unref(showMenu) || unref(fixedMenu)) return
showMenu.value = false
}
return () => (
<div
id={`${variables.namespace}-menu`}
class={[
prefixCls,
'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
{
'w-[var(--tab-menu-max-width)]': !unref(collapse),
'w-[var(--tab-menu-min-width)]': unref(collapse)
}
]}
onMouseleave={mouseleave}
>
<ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
<div>
{() => {
return unref(tabRouters).map((v) => {
const item = (
v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)
? v
: {
...(v?.children && v?.children[0]),
path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string)
}
) as AppRouteRecordRaw
return (
<div
class={[
`${prefixCls}__item`,
'text-center text-12px relative py-12px cursor-pointer',
{
'is-active': isActive(v.path)
}
]}
onClick={() => {
tabClick(item)
}}
>
<div>
</div>
{!unref(showTitle) ? undefined : (
<p class="mt-5px break-words px-2px">{t(item.meta?.title)}</p>
)}
</div>
)
})
}}
</div>
</ElScrollbar>
<div
class={[
`${prefixCls}--collapse`,
'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
]}
onClick={setCollapse}
>
</div>
<Menu
class={[
'!absolute top-0 z-11',
{
'!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu),
'!w-0': !unref(showMenu) && !unref(fixedMenu)
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></Menu>
</div>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-tab-menu;
.#{$prefix-cls} {
transition: all var(--transition-time-02);
&__item {
color: var(--left-menu-text-color);
transition: all var(--transition-time-02);
&:hover {
color: var(--left-menu-text-active-color);
// background-color: var(--left-menu-bg-active-color);
}
}
&--collapse {
color: var(--left-menu-text-color);
background-color: var(--left-menu-bg-light-color);
}
.is-active {
color: var(--left-menu-text-active-color);
background-color: var(--left-menu-bg-active-color);
}
}
</style>

View File

@@ -1,51 +0,0 @@
import { getAllParentPath } from '@/layouts/components/Menu/src/helper'
import type { RouteMeta } from 'vue-router'
import { isUrl } from '@/utils/is'
import { cloneDeep } from 'lodash-es'
export type TabMapTypes = {
[key: string]: string[]
}
export const tabPathMap = reactive<TabMapTypes>({})
export const initTabMap = (routes: AppRouteRecordRaw[]) => {
for (const v of routes) {
const meta = (v.meta ?? {}) as RouteMeta
if (!meta?.hidden) {
tabPathMap[v.path] = []
}
}
}
export const filterMenusPath = (
routes: AppRouteRecordRaw[],
allRoutes: AppRouteRecordRaw[]
): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
for (const v of routes) {
let data: Nullable<AppRouteRecordRaw> = null
const meta = (v.meta ?? {}) as RouteMeta
if (!meta.hidden || meta.canTo) {
const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
data = cloneDeep(v)
data.path = fullPath
if (v.children && data) {
data.children = filterMenusPath(v.children, allRoutes)
}
if (data) {
res.push(data)
}
if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
tabPathMap[allParentPath[0]].push(fullPath)
}
}
}
return res
}

View File

@@ -1,3 +0,0 @@
import TagsView from './src/TagsView.vue'
export { TagsView }

View File

@@ -1,585 +0,0 @@
<script lang="ts" setup>
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
import { usePermissionStore } from '@/stores/modules/permission'
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useAppStore } from '@/stores/modules/app'
import { useI18n } from '@/hooks/web/useI18n'
import { filterAffixTags } from './helper'
import { ContextMenu, ContextMenuExpose } from '@/layouts/components/ContextMenu'
import { useDesign } from '@/hooks/web/useDesign'
import { useTemplateRefsList } from '@vueuse/core'
import { ElScrollbar } from 'element-plus'
import { useScrollTo } from '@/hooks/event/useScrollTo'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('tags-view')
const { t } = useI18n()
const { currentRoute, push, replace } = useRouter()
const permissionStore = usePermissionStore()
const routers = computed(() => permissionStore.getRouters)
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.getVisitedViews)
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const appStore = useAppStore()
const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
const isDark = computed(() => appStore.getIsDark)
// 初始化tag
const initTags = () => {
affixTagArr.value = filterAffixTags(unref(routers))
for (const tag of unref(affixTagArr)) {
// Must have tag name
if (tag.name) {
tagsViewStore.addVisitedView(tag)
}
}
}
const selectedTag = ref<RouteLocationNormalizedLoaded>()
// 新增tag
const addTags = () => {
const { name } = unref(currentRoute)
if (name) {
selectedTag.value = unref(currentRoute)
tagsViewStore.addView(unref(currentRoute))
}
return false
}
// 关闭选中的tag
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
if (view?.meta?.affix) return
tagsViewStore.delView(view)
if (isActive(view)) {
toLastView()
}
}
// 关闭全部
const closeAllTags = () => {
tagsViewStore.delAllViews()
toLastView()
}
// 关闭其它
const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 重新加载
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
if (!view) return
tagsViewStore.delCachedView()
const { path, query } = view
await nextTick()
replace({
path: '/redirect' + path,
query: query
})
}
// 关闭左侧
const closeLeftTags = () => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 关闭右侧
const closeRightTags = () => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 跳转到最后一个
const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
push(latestView)
} else {
if (
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
) {
addTags()
return
}
// TODO: You can set another route
push('/')
}
}
// 滚动到选中的tag
const moveToCurrentTag = async () => {
await nextTick()
for (const v of unref(visitedViews)) {
if (v.fullPath === unref(currentRoute).path) {
moveToTarget(v)
if (v.fullPath !== unref(currentRoute).fullPath) {
tagsViewStore.updateVisitedView(unref(currentRoute))
}
break
}
}
}
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
const wrap$ = unref(scrollbarRef)?.wrapRef
let firstTag: Nullable<RouterLinkProps> = null
let lastTag: Nullable<RouterLinkProps> = null
const tagList = unref(tagLinksRefs)
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
// 直接滚动到0的位置
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: 0,
duration: 500
})
start()
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
// 滚动到最后的位置
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
duration: 500
})
start()
} else {
// find preTag and nextTag
const currentIndex: number = tagList.findIndex(
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
)
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
duration: 500
})
start()
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: beforePrevTagOffsetLeft,
duration: 500
})
start()
}
}
}
// 是否是当前tag
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
return route.path === unref(currentRoute).path
}
// 所有右键菜单组件的元素
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
// 右键菜单装填改变的时候
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
if (visible) {
for (const v of unref(itemRefs)) {
const elDropdownMenuRef = v.elDropdownMenuRef
if (tagItem.fullPath !== v.tagItem.fullPath) {
elDropdownMenuRef?.handleClose()
}
}
}
}
// elscroll 实例
const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
// 保存滚动位置
const scrollLeftNumber = ref(0)
const scroll = ({ scrollLeft }) => {
scrollLeftNumber.value = scrollLeft as number
}
// 移动到某个位置
const move = (to: number) => {
const wrap$ = unref(scrollbarRef)?.wrapRef
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: unref(scrollLeftNumber) + to,
duration: 500
})
start()
}
onMounted(() => {
initTags()
addTags()
})
watch(
() => currentRoute.value,
() => {
addTags()
moveToCurrentTag()
}
)
</script>
<template>
<div
:id="prefixCls"
:class="prefixCls"
class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
>
<span
:class="`${prefixCls}__tool ${prefixCls}__tool--first`"
class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
@click="move(-200)"
>
<Icon
icon="ep:d-arrow-left"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<div class="flex-1 overflow-hidden">
<ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
<div class="h-full flex">
<ContextMenu
:ref="itemRefs.set"
:schema="[
{
icon: 'ep:refresh',
label: t('common.reload'),
disabled: selectedTag?.fullPath !== item.fullPath,
command: () => {
refreshSelectedTag(item)
}
},
{
icon: 'ep:close',
label: t('common.closeTab'),
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
command: () => {
closeSelectedTag(item)
}
},
{
divided: true,
icon: 'ep:d-arrow-left',
label: t('common.closeTheLeftTab'),
disabled:
!!visitedViews?.length &&
(item.fullPath === visitedViews[0].fullPath ||
selectedTag?.fullPath !== item.fullPath),
command: () => {
closeLeftTags()
}
},
{
icon: 'ep:d-arrow-right',
label: t('common.closeTheRightTab'),
disabled:
!!visitedViews?.length &&
(item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
selectedTag?.fullPath !== item.fullPath),
command: () => {
closeRightTags()
}
},
{
divided: true,
icon: 'ep:discount',
label: t('common.closeOther'),
disabled: selectedTag?.fullPath !== item.fullPath,
command: () => {
closeOthersTags()
}
},
{
icon: 'ep:minus',
label: t('common.closeAll'),
command: () => {
closeAllTags()
}
}
]"
v-for="item in visitedViews"
:key="item.fullPath"
:tag-item="item"
:class="[
`${prefixCls}__item`,
item?.meta?.affix ? `${prefixCls}__item--affix` : '',
{
'is-active': isActive(item)
}
]"
@visible-change="visibleChange"
>
<div>
<router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
<div
@click="navigate"
class="h-full flex items-center justify-center whitespace-nowrap pl-15px"
>
<Icon
v-if="
item?.matched &&
item?.matched[1] &&
item?.matched[1]?.meta?.icon &&
tagsViewIcon
"
:icon="item?.matched[1]?.meta?.icon"
:size="12"
class="mr-5px"
/>
{{ t(item?.meta?.title as string) }}
<Icon
:class="`${prefixCls}__item--close`"
color="#333"
icon="ep:close"
:size="12"
@click.prevent.stop="closeSelectedTag(item)"
/>
</div>
</router-link>
</div>
</ContextMenu>
</div>
</ElScrollbar>
</div>
<span
:class="`${prefixCls}__tool`"
class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
@click="move(200)"
>
<Icon
icon="ep:d-arrow-right"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<span
:class="`${prefixCls}__tool`"
class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
@click="refreshSelectedTag(selectedTag)"
>
<Icon
icon="ep:refresh-right"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<ContextMenu
trigger="click"
:schema="[
{
icon: 'ep:refresh',
label: t('common.reload'),
command: () => {
refreshSelectedTag(selectedTag)
}
},
{
icon: 'ep:close',
label: t('common.closeTab'),
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
command: () => {
closeSelectedTag(selectedTag!)
}
},
{
divided: true,
icon: 'ep:d-arrow-left',
label: t('common.closeTheLeftTab'),
disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
command: () => {
closeLeftTags()
}
},
{
icon: 'ep:d-arrow-right',
label: t('common.closeTheRightTab'),
disabled:
!!visitedViews?.length &&
selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
command: () => {
closeRightTags()
}
},
{
divided: true,
icon: 'ep:discount',
label: t('common.closeOther'),
command: () => {
closeOthersTags()
}
},
{
icon: 'ep:minus',
label: t('common.closeAll'),
command: () => {
closeAllTags()
}
}
]"
>
<span
:class="`${prefixCls}__tool`"
class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
>
<Icon
icon="ep:menu"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
</ContextMenu>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: v-tags-view;
.#{$prefix-cls} {
:deep(.el-scrollbar__view) {
height: 100%;
}
&__tool {
position: relative;
&::before {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(100% - 1px);
border-left: 1px solid var(--el-border-color);
content: '';
}
&--first {
&::before {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(100% - 1px);
border-right: 1px solid var(--el-border-color);
border-left: none;
content: '';
}
}
}
&__item {
position: relative;
top: 2px;
height: calc(100% - 6px);
padding-right: 25px;
margin-left: 4px;
font-size: 12px;
cursor: pointer;
border: 1px solid #d9d9d9;
border-radius: 2px;
&--close {
position: absolute;
top: 50%;
right: 5px;
display: none;
transform: translate(0, -50%);
}
&:not(.#{$prefix-cls}__item--affix):hover {
.#{$prefix-cls}__item--close {
display: block;
}
}
}
&__item:not(.is-active) {
&:hover {
color: var(--el-color-primary);
}
}
&__item.is-active {
color: var(--el-color-white);
background-color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.#{$prefix-cls}__item--close {
:deep(span) {
color: var(--el-color-white) !important;
}
}
}
}
.dark {
.#{$prefix-cls} {
&__tool {
&--first {
&::after {
display: none;
}
}
}
&__item {
border: 1px solid var(--el-border-color);
}
&__item:not(.is-active) {
&:hover {
color: var(--el-color-primary);
}
}
&__item.is-active {
color: var(--el-color-white);
background-color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.#{$prefix-cls}__item--close {
:deep(span) {
color: var(--el-color-white) !important;
}
}
}
}
}
</style>

View File

@@ -1,21 +0,0 @@
import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router'
import { pathResolve } from '@/utils/routerHelper'
export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
let tags: RouteLocationNormalizedLoaded[] = []
routes.forEach((route) => {
const meta = route.meta as RouteMeta
const tagPath = pathResolve(parentPath, route.path)
if (meta?.affix) {
tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)
}
if (route.children) {
const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
}

View File

@@ -1,3 +0,0 @@
import ThemeSwitch from './src/ThemeSwitch.vue'
export { ThemeSwitch }

View File

@@ -1,46 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { useIcon } from '@/hooks/web/useIcon'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'ThemeSwitch' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('theme-switch')
const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' })
const appStore = useAppStore()
// 初始化获取是否是暗黑主题
const isDark = ref(appStore.getIsDark)
// 设置switch的背景颜色
const blackColor = 'var(--el-color-black)'
const themeChange = (val: boolean) => {
appStore.setIsDark(val)
}
</script>
<template>
<ElSwitch
v-model="isDark"
:active-color="blackColor"
:active-icon="Sun"
:border-color="blackColor"
:class="prefixCls"
:inactive-color="blackColor"
:inactive-icon="CrescentMoon"
inline-prompt
@change="themeChange"
/>
</template>
<style lang="scss" scoped>
:deep(.el-switch__core .el-switch__inner .is-icon) {
overflow: visible;
}
</style>

View File

@@ -1,95 +0,0 @@
<script lang="tsx">
import { defineComponent, computed } from 'vue'
import { Collapse } from '@/layouts/components/Collapse'
import { UserInfo } from '@/layouts/components/UserInfo'
import { Screenfull } from '@/layouts/components/Screenfull'
import { Breadcrumb } from '@/layouts/components/Breadcrumb'
import { SizeDropdown } from '@/layouts/components/SizeDropdown'
import { LocaleDropdown } from '@/layouts/components/LocaleDropdown'
import RouterSearch from '@/components/RouterSearch/index.vue'
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('tool-header')
const appStore = useAppStore()
// 面包屑
const breadcrumb = computed(() => appStore.getBreadcrumb)
// 折叠图标
const hamburger = computed(() => appStore.getHamburger)
// 全屏图标
const screenfull = computed(() => appStore.getScreenfull)
// 搜索图片
const search = computed(() => appStore.search)
// 尺寸图标
const size = computed(() => appStore.getSize)
// 布局
const layout = computed(() => appStore.getLayout)
// 多语言图标
const locale = computed(() => appStore.getLocale)
// 消息图标
const message = computed(() => appStore.getMessage)
export default defineComponent({
name: 'ToolHeader',
setup() {
return () => (
<div
id={`${variables.namespace}-tool-header`}
class={[
prefixCls,
'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between',
'dark:bg-[var(--el-bg-color)]'
]}
>
{layout.value !== 'top' ? (
<div class="h-full flex items-center">
{hamburger.value && layout.value !== 'cutMenu' ? (
<Collapse class="custom-hover" color="var(--top-header-text-color)"></Collapse>
) : undefined}
{breadcrumb.value ? <Breadcrumb class="lt-md:hidden"></Breadcrumb> : undefined}
</div>
) : undefined}
<div class="h-full flex items-center">
{screenfull.value ? (
<Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
) : undefined}
{search.value ? <RouterSearch isModal={false} /> : undefined}
{size.value ? (
<SizeDropdown class="custom-hover" color="var(--top-header-text-color)"></SizeDropdown>
) : undefined}
{locale.value ? (
<LocaleDropdown
class="custom-hover"
color="var(--top-header-text-color)"
></LocaleDropdown>
) : undefined}
{message.value ? (
"undefined"
// <Message class="custom-hover" color="var(--top-header-text-color)"></Message>
) : undefined}
<UserInfo></UserInfo>
</div>
</div>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-tool-header;
.#{$prefix-cls} {
transition: left var(--transition-time-02);
}
</style>

View File

@@ -1,3 +0,0 @@
import UserInfo from './src/UserInfo.vue'
export { UserInfo }

View File

@@ -1,113 +0,0 @@
<script lang='ts' setup>
import { ElMessageBox } from 'element-plus'
import { Tools, Menu, Lock, SwitchButton } from '@element-plus/icons-vue'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useUserStore } from '@/stores/modules/user'
import LockDialog from './components/LockDialog.vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/stores/modules/lock'
defineOptions({ name: 'UserInfo' })
const { t } = useI18n()
const { push, replace } = useRouter()
const userStore = useUserStore()
const tagsViewStore = useTagsViewStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('user-info')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
// 锁定屏幕
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const dialogVisible = ref<boolean>(false)
const lockScreen = () => {
dialogVisible.value = true
}
const loginOut = async () => {
try {
await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
await userStore.loginOut()
tagsViewStore.delAllViews()
replace('/login?redirect=/index')
} catch {
}
}
const toProfile = async () => {
push('/user/profile')
}
const toDocument = () => {
window.open('https://doc.iocoder.cn/')
}
</script>
<template>
<ElDropdown class='custom-hover' :class='prefixCls' trigger='click'>
<div class='flex items-center'>
<ElAvatar :src='avatar' alt='' class='w-[calc(var(--logo-height)-25px)] rounded-[50%]' />
<span class='pl-[5px] text-14px text-[var(--top-header-text-color)] <lg:hidden'>
{{ userName }}
</span>
</div>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>
<Tools />
<div @click='toProfile'>{{ t('common.profile') }}</div>
</ElDropdownItem>
<ElDropdownItem>
<Menu />
<div @click='toDocument'>{{ t('common.document') }}</div>
</ElDropdownItem>
<ElDropdownItem divided>
<Lock />
<div @click='lockScreen'>{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<ElDropdownItem divided @click='loginOut'>
<SwitchButton />
<div>{{ t('common.loginOut') }}</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<LockDialog v-if='dialogVisible' v-model='dialogVisible' />
<teleport to='body'>
<transition name='fade-bottom' mode='out-in'>
<LockPage v-if='getIsLock' />
</transition>
</teleport>
</template>
<style scoped lang='scss'>
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>

View File

@@ -1,98 +0,0 @@
<script setup lang="ts">
import { useValidator } from '@/hooks/web/useValidator'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/stores/modules/lock'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/stores/modules/user'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-dialog')
const { required } = useValidator()
const { t } = useI18n()
const lockStore = useLockStore()
const props = defineProps({
modelValue: {
type: Boolean
}
})
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const emit = defineEmits(['update:modelValue'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const dialogTitle = ref(t('lock.lockScreen'))
const formData = ref({
password: undefined
})
const formRules = reactive({
password: [required()]
})
const formRef = ref() // 表单 Ref
const handleLock = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
dialogVisible.value = false
lockStore.setLockInfo({
...formData.value,
isLock: true
})
}
</script>
<template>
<Dialog
v-model="dialogVisible"
width="500px"
max-height="170px"
:class="prefixCls"
:title="dialogTitle"
>
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">
{{ userName }}
</span>
</div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item :label="t('lock.lockPassword')" prop="password">
<el-input
type="password"
v-model="formData.password"
:placeholder="'请输入' + t('lock.lockPassword')"
clearable
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
</template>
</Dialog>
</template>
<style lang="scss" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
max-width: calc(100vw - 16px);
}
}
</style>

View File

@@ -1,269 +0,0 @@
<script lang="ts" setup>
import { deleteUserCache } from '@/hooks/web/useCache'
import { useLockStore } from '@/stores/modules/lock'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useUserStore } from '@/stores/modules/user'
import avatarImg from '@/assets/imgs/avatar.gif'
const tagsViewStore = useTagsViewStore()
const { replace } = useRouter()
const userStore = useUserStore()
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
// 解锁
async function unLock() {
if (!password.value) {
return
}
let pwd = password.value
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
} finally {
loading.value = false
}
}
// 返回登录
async function goLogin() {
await userStore.loginOut().catch(() => {})
// 登出后清理
deleteUserCache() // 清空用户缓存
tagsViewStore.delAllViews()
// resetRouter() // 重置静态路由表
lockStore.resetLockInfo()
replace('/login')
}
function handleShowForm(show = false) {
showDate.value = show
}
</script>
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<Icon icon="ep:lock" />
<span>{{ t('lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">
{{ userName }}
</span>
</div>
<ElInput
type="password"
:placeholder="t('lock.placeholder')"
class="enter-x"
v-model="password"
/>
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</ElButton>
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</ElButton>
<ElButton
type="primary"
class="mt-2"
size="small"
link
@click="unLock()"
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</ElButton>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: 'v-lock-page';
// Small screen / tablet
$screen-sm: 576px;
// Medium screen / desktop
$screen-md: 768px;
// Large screen / wide desktop
$screen-lg: 992px;
// Extra large screen / full hd
$screen-xl: 1200px;
// Extra extra large screen / large desktop
$screen-2xl: 1600px;
$error-color: #ed6f6f;
.#{$prefix-cls} {
z-index: 3000;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: $screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: $screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: $screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: $screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: $error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>

View File

@@ -1,306 +0,0 @@
import { computed } from 'vue'
import { useAppStore } from '@/stores/modules/app'
import { Menu } from '@/layouts/components/Menu'
import { TabMenu } from '@/layouts/components/TabMenu'
import { TagsView } from '@/layouts/components/TagsView'
import { Logo } from '@/layouts/components/Logo'
import AppView from './AppView.vue'
import ToolHeader from './ToolHeader.vue'
import { ElScrollbar } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('layout')
const appStore = useAppStore()
const pageLoading = computed(() => appStore.getPageLoading)
// 标签页
const tagsView = computed(() => appStore.getTagsView)
// 菜单折叠
const collapse = computed(() => appStore.getCollapse)
// logo
const logo = computed(() => appStore.logo)
// 固定头部
const fixedHeader = computed(() => appStore.getFixedHeader)
// 是否是移动端
const mobile = computed(() => appStore.getMobile)
// 固定菜单
const fixedMenu = computed(() => appStore.getFixedMenu)
export const useRenderLayout = () => {
const renderClassic = () => {
return (
<>
<div
class={[
'absolute top-0 left-0 h-full layout-border__right',
{ '!fixed z-3000': mobile.value }
]}
>
{logo.value ? (
<Logo
class={[
'bg-[var(--left-menu-bg-color)] relative',
{
'!pl-0': mobile.value && collapse.value,
'w-[var(--left-menu-min-width)]': appStore.getCollapse,
'w-[var(--left-menu-max-width)]': !appStore.getCollapse
}
]}
style="transition: all var(--transition-time-02);"
></Logo>
) : undefined}
<Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
</div>
<div
class={[
`${prefixCls}-content`,
'absolute top-0 h-[100%]',
{
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
collapse.value && !mobile.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
!collapse.value && !mobile.value && !mobile.value,
'fixed !w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
fixedHeader.value
}
]}
>
<div
class={[
{
'fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)]':
collapse.value && fixedHeader.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)]':
!collapse.value && fixedHeader.value && !mobile.value,
'!w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ToolHeader
class={[
'bg-[var(--top-header-bg-color)]',
{
'layout-border__bottom': !tagsView.value
}
]}
></ToolHeader>
{tagsView.value ? (
<TagsView class="layout-border__top layout-border__bottom"></TagsView>
) : undefined}
</div>
<AppView></AppView>
</ElScrollbar>
</div>
</>
)
}
const renderTopLeft = () => {
return (
<>
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom dark:bg-[var(--el-bg-color)]">
{logo.value ? <Logo class="custom-hover"></Logo> : undefined}
<ToolHeader class="flex-1"></ToolHeader>
</div>
<div class="absolute left-0 top-[var(--logo-height)+1px] h-[calc(100%-1px-var(--logo-height))] w-full flex">
<Menu class="relative layout-border__right !h-full"></Menu>
<div
class={[
`${prefixCls}-content`,
'h-[100%]',
{
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
collapse.value,
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
!collapse.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
fixedHeader.value && tagsView.value
}
]}
>
{tagsView.value ? (
<TagsView
class={[
'layout-border__bottom absolute',
{
'!fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[calc(var(--logo-height)+1px)]':
collapse.value && fixedHeader.value,
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[calc(var(--logo-height)+1px)]':
!collapse.value && fixedHeader.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></TagsView>
) : undefined}
<AppView></AppView>
</ElScrollbar>
</div>
</div>
</>
)
}
const renderTop = () => {
return (
<>
<div
class={[
'flex items-center justify-between bg-[var(--top-header-bg-color)] relative',
{
'layout-border__bottom': !tagsView.value
}
]}
>
{logo.value ? <Logo class="custom-hover"></Logo> : undefined}
<Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu>
<ToolHeader></ToolHeader>
</div>
<div
class={[
`${prefixCls}-content`,
'w-full',
{
'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value,
'h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': fixedHeader.value
}
]}
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'mt-[var(--tags-view-height)] !pb-[calc(var(--tags-view-height)+var(--app-footer-height))]':
fixedHeader.value,
'pb-[var(--app-footer-height)]': !fixedHeader.value
}
]}
>
{tagsView.value ? (
<TagsView
class={[
'layout-border__bottom layout-border__top relative',
{
'!fixed w-full top-[calc(var(--top-tool-height)+1px)] left-0': fixedHeader.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></TagsView>
) : undefined}
<AppView></AppView>
</ElScrollbar>
</div>
</>
)
}
const renderCutMenu = () => {
return (
<>
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom">
{logo.value ? <Logo class="custom-hover !pr-15px"></Logo> : undefined}
<ToolHeader class="flex-1"></ToolHeader>
</div>
<div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-[calc(100%-2px)] flex">
<TabMenu></TabMenu>
<div
class={[
`${prefixCls}-content`,
'h-[100%]',
{
'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
collapse.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]':
!collapse.value && !fixedMenu.value,
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
collapse.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
!collapse.value && fixedMenu.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
fixedHeader.value && tagsView.value
}
]}
>
{tagsView.value ? (
<TagsView
class={[
'relative layout-border__bottom layout-border__top',
{
'!fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value,
'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value,
'!fixed top-0 !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10':
fixedHeader.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
collapse.value && fixedHeader.value && fixedMenu.value,
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
!collapse.value && fixedHeader.value && fixedMenu.value
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></TagsView>
) : undefined}
<AppView></AppView>
</ElScrollbar>
</div>
</div>
</>
)
}
return {
renderClassic,
renderTopLeft,
renderTop,
renderCutMenu
}
}

11
src/locales/index.ts Normal file
View File

@@ -0,0 +1,11 @@
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>('./*.ts')
export async function loadLocaleMessages(locale: string) {
const path = `./${locale}.ts`
const loader = localeModules[path]
if (!loader) {
throw new Error(`Locale not found: ${locale}`)
}
const mod = await loader()
return mod.default ?? {}
}

View File

@@ -1,7 +1,7 @@
import { createApp, reactive } from 'vue' import { createApp, reactive } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import pinia from '@/stores/index' import { setupStore } from '@/stores/index'
import { registerIcons } from '@/utils/common' import { registerIcons } from '@/utils/common'
import mitt from 'mitt' import mitt from 'mitt'
import VxeUI from 'vxe-pc-ui' import VxeUI from 'vxe-pc-ui'
@@ -25,6 +25,13 @@ import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx'
VXETable.use(VXETablePluginExportXLSX, { VXETable.use(VXETablePluginExportXLSX, {
ExcelJS ExcelJS
}) })
VxeUI.setConfig({
table: {
columnConfig: {
useKey: true
}
}
})
// 全局注册 tooltip 组件 // 全局注册 tooltip 组件
window.XEUtils = XEUtils window.XEUtils = XEUtils
@@ -62,7 +69,7 @@ const setupAll = async () => {
await setupI18n(app) await setupI18n(app)
app.use(router) app.use(router)
app.use(pinia) setupStore(app)
app.use(VxeUI) app.use(VxeUI)
app.use(ElementPlus) app.use(ElementPlus)
app.use(VXETable) app.use(VXETable)

View File

@@ -1,42 +1,42 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import { useLocaleStoreWithOut } from '@/stores/modules/locale' import { useLocaleStoreWithOut } from '@/stores/modules/locale'
import type { I18n, I18nOptions } from 'vue-i18n' import type { I18n, I18nOptions } from 'vue-i18n'
import { setHtmlPageLang } from './helper' import { setHtmlPageLang } from './helper'
import { loadLocaleMessages } from '@/locales'
export let i18n: ReturnType<typeof createI18n>
export let i18n: ReturnType<typeof createI18n>
const createI18nOptions = async (): Promise<I18nOptions> => {
const localeStore = useLocaleStoreWithOut() const createI18nOptions = async (): Promise<I18nOptions> => {
const locale = localeStore.getCurrentLocale const localeStore = useLocaleStoreWithOut()
const localeMap = localeStore.getLocaleMap const locale = localeStore.getCurrentLocale
const defaultLocal = await import(`../../locales/${locale.lang}.ts`) const localeMap = localeStore.getLocaleMap
const message = defaultLocal.default ?? {} const message = await loadLocaleMessages(locale.lang)
setHtmlPageLang(locale.lang) setHtmlPageLang(locale.lang)
localeStore.setCurrentLocale({ localeStore.setCurrentLocale({
lang: locale.lang lang: locale.lang
// elLocale: elLocal // elLocale: elLocal
}) })
return { return {
legacy: false, legacy: false,
locale: locale.lang, locale: locale.lang,
fallbackLocale: locale.lang, fallbackLocale: locale.lang,
messages: { messages: {
[locale.lang]: message [locale.lang]: message
}, },
availableLocales: localeMap.map((v) => v.lang), availableLocales: localeMap.map((v) => v.lang),
sync: true, sync: true,
silentTranslationWarn: true, silentTranslationWarn: true,
missingWarn: false, missingWarn: false,
silentFallbackWarn: true silentFallbackWarn: true
} }
} }
export const setupI18n = async (app: App<Element>) => { export const setupI18n = async (app: App<Element>) => {
const options = await createI18nOptions() const options = await createI18nOptions()
i18n = createI18n(options) as I18n i18n = createI18n(options) as I18n
app.use(i18n) app.use(i18n)
} }

View File

@@ -346,7 +346,7 @@ const staticRoutes: Array<RouteRecordRaw> = [
{ {
// 后台找不到页面了-可能是路由未加载上 // 后台找不到页面了-可能是路由未加载上
path: adminBaseRoutePath + ':path(.*)*', path: adminBaseRoutePath + '/:path(.*)*',
redirect: to => { redirect: to => {
return { return {
name: 'adminMainLoading', name: 'adminMainLoading',

View File

@@ -1,7 +1,15 @@
import type { App } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) pinia.use(piniaPluginPersistedstate)
/** 供 setup 外WithOut使用的同一 Pinia 实例 */
export const store = pinia
export const setupStore = (app: App<Element>) => {
app.use(pinia)
}
export default pinia export default pinia

View File

@@ -1,12 +0,0 @@
import type { App } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => {
app.use(store)
}
export { store }

View File

@@ -1,277 +0,0 @@
import { defineStore } from 'pinia'
import { store } from '../indexs'
import { setCssVar, humpToUnderline } from '@/utils'
import { ElMessage } from 'element-plus'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { ElementPlusSize } from '@/types/elementPlus'
import { LayoutType } from '@/types/layout'
import { ThemeTypes } from '@/types/theme'
const { wsCache } = useCache()
interface AppState {
breadcrumb: boolean
breadcrumbIcon: boolean
collapse: boolean
uniqueOpened: boolean
hamburger: boolean
screenfull: boolean
search: boolean
size: boolean
locale: boolean
message: boolean
tagsView: boolean
tagsViewIcon: boolean
logo: boolean
fixedHeader: boolean
greyMode: boolean
pageLoading: boolean
layout: LayoutType
title: string
userInfo: string
isDark: boolean
currentSize: ElementPlusSize
sizeMap: ElementPlusSize[]
mobile: boolean
footer: boolean
theme: ThemeTypes
fixedMenu: boolean
}
export const useAppStore = defineStore('app', {
state: (): AppState => {
return {
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
sizeMap: ['default', 'large', 'small'],
mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题
pageLoading: false, // 路由跳转loading
breadcrumb: true, // 面包屑
breadcrumbIcon: true, // 面包屑图标
collapse: false, // 折叠菜单
uniqueOpened: true, // 是否只保持一个子菜单的展开
hamburger: true, // 折叠图标
screenfull: true, // 全屏图标
search: true, // 搜索图标
size: true, // 尺寸图标
locale: true, // 多语言图标
message: true, // 消息图标
tagsView: true, // 标签页
tagsViewIcon: true, // 是否显示标签图标
logo: true, // logo
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
currentSize: wsCache.get('default') || 'default', // 组件尺寸
theme: wsCache.get(CACHE_KEY.THEME) || {
// 主题色
elColorPrimary: '#409eff',
// 左侧菜单边框颜色
leftMenuBorderColor: 'inherit',
// 左侧菜单背景颜色
leftMenuBgColor: '#001529',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '#0f2438',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单字体颜色
leftMenuTextColor: '#bfcbd9',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '#fff',
// logo字体颜色
logoTitleTextColor: '#fff',
// logo边框颜色
logoBorderColor: 'inherit',
// 头部背景颜色
topHeaderBgColor: '#fff',
// 头部字体颜色
topHeaderTextColor: 'inherit',
// 头部悬停颜色
topHeaderHoverColor: '#f6f6f6',
// 头部边框颜色
topToolBorderColor: '#eee'
}
}
},
getters: {
getBreadcrumb(): boolean {
return this.breadcrumb
},
getBreadcrumbIcon(): boolean {
return this.breadcrumbIcon
},
getCollapse(): boolean {
return this.collapse
},
getUniqueOpened(): boolean {
return this.uniqueOpened
},
getHamburger(): boolean {
return this.hamburger
},
getScreenfull(): boolean {
return this.screenfull
},
getSize(): boolean {
return this.size
},
getLocale(): boolean {
return this.locale
},
getMessage(): boolean {
return this.message
},
getTagsView(): boolean {
return this.tagsView
},
getTagsViewIcon(): boolean {
return this.tagsViewIcon
},
getLogo(): boolean {
return this.logo
},
getFixedHeader(): boolean {
return this.fixedHeader
},
getGreyMode(): boolean {
return this.greyMode
},
getFixedMenu(): boolean {
return this.fixedMenu
},
getPageLoading(): boolean {
return this.pageLoading
},
getLayout(): LayoutType {
return this.layout
},
getTitle(): string {
return this.title
},
getUserInfo(): string {
return this.userInfo
},
getIsDark(): boolean {
return this.isDark
},
getCurrentSize(): ElementPlusSize {
return this.currentSize
},
getSizeMap(): ElementPlusSize[] {
return this.sizeMap
},
getMobile(): boolean {
return this.mobile
},
getTheme(): ThemeTypes {
return this.theme
},
getFooter(): boolean {
return this.footer
}
},
actions: {
setBreadcrumb(breadcrumb: boolean) {
this.breadcrumb = breadcrumb
},
setBreadcrumbIcon(breadcrumbIcon: boolean) {
this.breadcrumbIcon = breadcrumbIcon
},
setCollapse(collapse: boolean) {
this.collapse = collapse
},
setUniqueOpened(uniqueOpened: boolean) {
this.uniqueOpened = uniqueOpened
},
setHamburger(hamburger: boolean) {
this.hamburger = hamburger
},
setScreenfull(screenfull: boolean) {
this.screenfull = screenfull
},
setSize(size: boolean) {
this.size = size
},
setLocale(locale: boolean) {
this.locale = locale
},
setMessage(message: boolean) {
this.message = message
},
setTagsView(tagsView: boolean) {
this.tagsView = tagsView
},
setTagsViewIcon(tagsViewIcon: boolean) {
this.tagsViewIcon = tagsViewIcon
},
setLogo(logo: boolean) {
this.logo = logo
},
setFixedHeader(fixedHeader: boolean) {
this.fixedHeader = fixedHeader
},
setGreyMode(greyMode: boolean) {
this.greyMode = greyMode
},
setFixedMenu(fixedMenu: boolean) {
wsCache.set('fixedMenu', fixedMenu)
this.fixedMenu = fixedMenu
},
setPageLoading(pageLoading: boolean) {
this.pageLoading = pageLoading
},
setLayout(layout: LayoutType) {
if (this.mobile && layout !== 'classic') {
ElMessage.warning('移动端模式下不支持切换其他布局')
return
}
this.layout = layout
wsCache.set(CACHE_KEY.LAYOUT, this.layout)
},
setTitle(title: string) {
this.title = title
},
setIsDark(isDark: boolean) {
this.isDark = isDark
if (this.isDark) {
document.documentElement.classList.add('dark')
document.documentElement.classList.remove('light')
} else {
document.documentElement.classList.add('light')
document.documentElement.classList.remove('dark')
}
wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
},
setCurrentSize(currentSize: ElementPlusSize) {
this.currentSize = currentSize
wsCache.set('currentSize', this.currentSize)
},
setMobile(mobile: boolean) {
this.mobile = mobile
},
setTheme(theme: ThemeTypes) {
this.theme = Object.assign(this.theme, theme)
wsCache.set(CACHE_KEY.THEME, this.theme)
},
setCssVarTheme() {
for (const key in this.theme) {
setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
}
},
setFooter(footer: boolean) {
this.footer = footer
}
},
persist: false
})
export const useAppStoreWithOut = () => {
return useAppStore(store)
}

View File

@@ -1,105 +0,0 @@
import { defineStore } from 'pinia'
import { store } from '../indexs'
// @ts-ignore
import { DictDataVO } from '@/api/system/dict/types'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache('sessionStorage')
// import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
export interface DictValueType {
value: any
label: string
clorType?: string
cssClass?: string
}
export interface DictTypeType {
dictType: string
dictValue: DictValueType[]
}
export interface DictState {
dictMap: Map<string, any>
isSetDict: boolean
}
export const useDictStore = defineStore('dict', {
state: (): DictState => ({
dictMap: new Map<string, any>(),
isSetDict: false
}),
getters: {
getDictMap(): Recordable {
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
if (dictMap) {
this.dictMap = dictMap
}
return this.dictMap
},
getIsSetDict(): boolean {
return this.isSetDict
}
},
actions: {
async setDictMap() {
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
if (dictMap) {
this.dictMap = dictMap
this.isSetDict = true
} else {
// const res = await getSimpleDictDataList()
// 设置数据
const dictDataMap = new Map<string, any>()
// res.forEach((dictData: DictDataVO) => {
// // 获得 dictType 层级
// const enumValueObj = dictDataMap[dictData.dictType]
// if (!enumValueObj) {
// dictDataMap[dictData.dictType] = []
// }
// // 处理 dictValue 层级
// dictDataMap[dictData.dictType].push({
// value: dictData.value,
// label: dictData.label,
// colorType: dictData.colorType,
// cssClass: dictData.cssClass
// })
// })
this.dictMap = dictDataMap
this.isSetDict = true
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
}
},
getDictByType(type: string) {
console.log("🚀 ~ type:", type)
if (!this.isSetDict) {
this.setDictMap()
}
return this.dictMap[type]
},
async resetDict() {
wsCache.delete(CACHE_KEY.DICT_CACHE)
// const res = await getSimpleDictDataList()
// 设置数据
const dictDataMap = new Map<string, any>()
// res.forEach((dictData: DictDataVO) => {
// // 获得 dictType 层级
// const enumValueObj = dictDataMap[dictData.dictType]
// if (!enumValueObj) {
// dictDataMap[dictData.dictType] = []
// }
// // 处理 dictValue 层级
// dictDataMap[dictData.dictType].push({
// value: dictData.value,
// label: dictData.label,
// colorType: dictData.colorType,
// cssClass: dictData.cssClass
// })
// })
this.dictMap = dictDataMap
this.isSetDict = true
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
}
}
})
export const useDictStoreWithOut = () => {
return useDictStore(store)
}

View File

@@ -1,59 +1,59 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { store } from '../indexs' import { store } from '@/stores/index'
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en' import en from 'element-plus/es/locale/lang/en'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache' import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { LocaleDropdownType } from '@/types/localeDropdown' import { LocaleDropdownType } from '@/types/localeDropdown'
const { wsCache } = useCache() const { wsCache } = useCache()
const elLocaleMap = { const elLocaleMap = {
'zh-CN': zhCn, 'zh-CN': zhCn,
en: en en: en
} }
interface LocaleState { interface LocaleState {
currentLocale: LocaleDropdownType currentLocale: LocaleDropdownType
localeMap: LocaleDropdownType[] localeMap: LocaleDropdownType[]
} }
export const useLocaleStore = defineStore('locales', { export const useLocaleStore = defineStore('locales', {
state: (): LocaleState => { state: (): LocaleState => {
return { return {
currentLocale: { currentLocale: {
lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN', lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN'] elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
}, },
// 多语言 // 多语言
localeMap: [ localeMap: [
{ {
lang: 'zh-CN', lang: 'zh-CN',
name: '简体中文' name: '简体中文'
}, },
{ {
lang: 'en', lang: 'en',
name: 'English' name: 'English'
} }
] ]
} }
}, },
getters: { getters: {
getCurrentLocale(): LocaleDropdownType { getCurrentLocale(): LocaleDropdownType {
return this.currentLocale return this.currentLocale
}, },
getLocaleMap(): LocaleDropdownType[] { getLocaleMap(): LocaleDropdownType[] {
return this.localeMap return this.localeMap
} }
}, },
actions: { actions: {
setCurrentLocale(localeMap: LocaleDropdownType) { setCurrentLocale(localeMap: LocaleDropdownType) {
// this.locale = Object.assign(this.locale, localeMap) // this.locale = Object.assign(this.locale, localeMap)
this.currentLocale.lang = localeMap?.lang this.currentLocale.lang = localeMap?.lang
this.currentLocale.elLocale = elLocaleMap[localeMap?.lang] this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
wsCache.set(CACHE_KEY.LANG, localeMap?.lang) wsCache.set(CACHE_KEY.LANG, localeMap?.lang)
} }
} }
}) })
export const useLocaleStoreWithOut = () => { export const useLocaleStoreWithOut = () => {
return useLocaleStore(store) return useLocaleStore(store)
} }

View File

@@ -1,48 +0,0 @@
import { defineStore } from 'pinia'
import { store } from '@/stores/indexs'
interface lockInfo {
isLock?: boolean
password?: string
}
interface LockState {
lockInfo: lockInfo
}
export const useLockStore = defineStore('lock', {
state: (): LockState => {
return {
lockInfo: {
// isLock: false, // 是否锁定屏幕
// password: '' // 锁屏密码
}
}
},
getters: {
getLockInfo(): lockInfo {
return this.lockInfo
}
},
actions: {
setLockInfo(lockInfo: lockInfo) {
this.lockInfo = lockInfo
},
resetLockInfo() {
this.lockInfo = {}
},
unLock(password: string) {
if (this.lockInfo?.password === password) {
this.resetLockInfo()
return true
} else {
return false
}
}
},
persist: true
})
export const useLockStoreWithOut = () => {
return useLockStore(store)
}

View File

@@ -1,66 +0,0 @@
import { defineStore } from 'pinia'
import { store } from '@/stores/indexs'
import { cloneDeep } from 'lodash-es'
import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache()
export interface PermissionState {
routers: AppRouteRecordRaw[]
addRouters: AppRouteRecordRaw[]
menuTabRouters: AppRouteRecordRaw[]
}
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
routers: [],
addRouters: [],
menuTabRouters: []
}),
getters: {
getRouters(): AppRouteRecordRaw[] {
return this.routers
},
getAddRouters(): AppRouteRecordRaw[] {
return flatMultiLevelRoutes(cloneDeep(this.addRouters))
},
getMenuTabRouters(): AppRouteRecordRaw[] {
return this.menuTabRouters
}
},
actions: {
async generateRoutes(): Promise<unknown> {
return new Promise<void>(async (resolve) => {
// 获得菜单列表它在登录的时候setUserInfoAction 方法中已经进行获取
let res: AppCustomRouteRecordRaw[] = []
if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) {
res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[]
}
const routerMap: AppRouteRecordRaw[] = generateRoute(res)
// 动态路由404一定要放到最后面
this.addRouters = routerMap.concat([
{
path: '/:path(.*)*',
redirect: '/404',
name: '404Page',
meta: {
hidden: true,
breadcrumb: false
}
}
])
// 渲染菜单的所有路由
resolve()
})
},
setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
this.menuTabRouters = routers
}
},
persist: false
})
export const usePermissionStoreWithOut = () => {
return usePermissionStore(store)
}

View File

@@ -1,55 +0,0 @@
import { store } from '../index'
import { defineStore } from 'pinia'
export const useWorkFlowStore = defineStore('simpleWorkflow', {
state: () => ({
tableId: '',
isTried: false,
promoterDrawer: false,
flowPermission1: {},
approverDrawer: false,
approverConfig1: {},
copyerDrawer: false,
copyerConfig1: {},
conditionDrawer: false,
conditionsConfig1: {
conditionNodes: []
}
}),
actions: {
setTableId(payload) {
this.tableId = payload
},
setIsTried(payload) {
this.isTried = payload
},
setPromoter(payload) {
this.promoterDrawer = payload
},
setFlowPermission(payload) {
this.flowPermission1 = payload
},
setApprover(payload) {
this.approverDrawer = payload
},
setApproverConfig(payload) {
this.approverConfig1 = payload
},
setCopyer(payload) {
this.copyerDrawer = payload
},
setCopyerConfig(payload) {
this.copyerConfig1 = payload
},
setCondition(payload) {
this.conditionDrawer = payload
},
setConditionsConfig(payload) {
this.conditionsConfig1 = payload
}
}
})
export const useWorkFlowStoreWithOut = () => {
return useWorkFlowStore(store)
}

View File

@@ -1,141 +0,0 @@
import router from '@/router'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { getRawRoute } from '@/utils/routerHelper'
import { defineStore } from 'pinia'
import { store } from '../indexs'
import { findIndex } from '@/utils'
export interface TagsViewState {
visitedViews: RouteLocationNormalizedLoaded[]
cachedViews: Set<string>
}
export const useTagsViewStore = defineStore('tagsView', {
state: (): TagsViewState => ({
visitedViews: [],
cachedViews: new Set()
}),
getters: {
getVisitedViews(): RouteLocationNormalizedLoaded[] {
return this.visitedViews
},
getCachedViews(): string[] {
return Array.from(this.cachedViews)
}
},
actions: {
// 新增缓存和tag
addView(view: RouteLocationNormalizedLoaded): void {
this.addVisitedView(view)
this.addCachedView()
},
// 新增tag
addVisitedView(view: RouteLocationNormalizedLoaded) {
if (this.visitedViews.some((v) => v.path === view.path)) return
if (view.meta?.noTagsView) return
this.visitedViews.push(
Object.assign({}, view, {
title: view.meta?.title || 'no-name'
})
)
},
// 新增缓存
addCachedView() {
const cacheMap: Set<string> = new Set()
for (const v of this.visitedViews) {
const item = getRawRoute(v)
const needCache = !item.meta?.noCache
if (!needCache) {
continue
}
const name = item.name as string
cacheMap.add(name)
}
if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString())
return
this.cachedViews = cacheMap
},
// 删除某个
delView(view: RouteLocationNormalizedLoaded) {
this.delVisitedView(view)
this.delCachedView()
},
// 删除tag
delVisitedView(view: RouteLocationNormalizedLoaded) {
for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) {
this.visitedViews.splice(i, 1)
break
}
}
},
// 删除缓存
delCachedView() {
const route = router.currentRoute.value
const index = findIndex<string>(this.getCachedViews, (v) => v === route.name)
if (index > -1) {
this.cachedViews.delete(this.getCachedViews[index])
}
},
// 删除所有缓存和tag
delAllViews() {
this.delAllVisitedViews()
this.delCachedView()
},
// 删除所有tag
delAllVisitedViews() {
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = []
},
// 删除其他
delOthersViews(view: RouteLocationNormalizedLoaded) {
this.delOthersVisitedViews(view)
this.addCachedView()
},
// 删除其他tag
delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
this.visitedViews = this.visitedViews.filter((v) => {
return v?.meta?.affix || v.path === view.path
})
},
// 删除左侧
delLeftViews(view: RouteLocationNormalizedLoaded) {
const index = findIndex<RouteLocationNormalizedLoaded>(
this.visitedViews,
(v) => v.path === view.path
)
if (index > -1) {
this.visitedViews = this.visitedViews.filter((v, i) => {
return v?.meta?.affix || v.path === view.path || i > index
})
this.addCachedView()
}
},
// 删除右侧
delRightViews(view: RouteLocationNormalizedLoaded) {
const index = findIndex<RouteLocationNormalizedLoaded>(
this.visitedViews,
(v) => v.path === view.path
)
if (index > -1) {
this.visitedViews = this.visitedViews.filter((v, i) => {
return v?.meta?.affix || v.path === view.path || i < index
})
this.addCachedView()
}
},
updateVisitedView(view: RouteLocationNormalizedLoaded) {
for (let v of this.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
}
},
persist: false
})
export const useTagsViewStoreWithOut = () => {
return useTagsViewStore(store)
}

View File

@@ -1,103 +0,0 @@
import { store } from '@/stores/indexs'
import { defineStore } from 'pinia'
import { getAccessToken, removeToken } from '@/utils/auth'
import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
// import { getInfo, loginOut } from '@/api/login'
const { wsCache } = useCache()
interface UserVO {
id: number
avatar: string
nickname: string
deptId: number
}
interface UserInfoVO {
// USER 缓存
permissions: string[]
roles: string[]
isSetUser: boolean
user: UserVO
}
export const useUserStore = defineStore('admin-user', {
state: (): UserInfoVO => ({
permissions: [],
roles: [],
isSetUser: false,
user: {
id: 0,
avatar: '',
nickname: '',
deptId: 0
}
}),
getters: {
getPermissions(): string[] {
return this.permissions
},
getRoles(): string[] {
return this.roles
},
getIsSetUser(): boolean {
return this.isSetUser
},
getUser(): UserVO {
return this.user
}
},
actions: {
async setUserInfoAction() {
if (!getAccessToken()) {
this.resetState()
return null
}
let userInfo = wsCache.get(CACHE_KEY.USER)
if (!userInfo) {
// userInfo = await getInfo()
}
this.permissions = userInfo.permissions
this.roles = userInfo.roles
this.user = userInfo.user
this.isSetUser = true
wsCache.set(CACHE_KEY.USER, userInfo)
wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus)
},
async setUserAvatarAction(avatar: string) {
const userInfo = wsCache.get(CACHE_KEY.USER)
// NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
this.user.avatar = avatar
userInfo.user.avatar = avatar
wsCache.set(CACHE_KEY.USER, userInfo)
},
async setUserNicknameAction(nickname: string) {
const userInfo = wsCache.get(CACHE_KEY.USER)
// NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
this.user.nickname = nickname
userInfo.user.nickname = nickname
wsCache.set(CACHE_KEY.USER, userInfo)
},
async loginOut() {
// await loginOut()
removeToken()
deleteUserCache() // 删除用户缓存
this.resetState()
},
resetState() {
this.permissions = []
this.roles = []
this.isSetUser = false
this.user = {
id: 0,
avatar: '',
nickname: '',
deptId: 0
}
}
}
})
export const useUserStoreWithOut = () => {
return useUserStore(store)
}

View File

@@ -81,14 +81,17 @@
.vxe-modal--wrapper { .vxe-modal--wrapper {
z-index: 5000 !important; z-index: 5000 !important;
} }
// .vxe-table--body .vxe-body--row:nth-child(even) { /* 斑马纹:新版 vxe 背景画在 column 上,默认条纹色 #fafafa 几乎看不见 */
// background-color: #f9f9f9; :root {
// // background-color: var(--el-color-primary-light-9); --vxe-ui-table-row-striped-background-color: #fafafa;
// } --vxe-ui-table-row-hover-striped-background-color: #eef1f6;
}
// .vxe-table--body .vxe-body--row:nth-child(odd) { .vxe-table--render-default .vxe-body--row.row--stripe > .vxe-body--column {
// background-color: #ffffff; background-color: var(--vxe-ui-table-row-striped-background-color);
// } }
.vxe-table--render-default .vxe-body--row.row--hover.row--stripe > .vxe-body--column {
background-color: var(--vxe-ui-table-row-hover-striped-background-color);
}
.default-theme.splitpanes--vertical > .splitpanes__splitter, .default-theme.splitpanes--vertical > .splitpanes__splitter,
.default-theme .splitpanes--vertical > .splitpanes__splitter { .default-theme .splitpanes--vertical > .splitpanes__splitter {
width: 10px !important; width: 10px !important;

View File

@@ -1,16 +1,39 @@
/** /**
* 数据字典工具类 * 数据字典工具类(统一读取 dictData store
*/ */
import {useDictStoreWithOut} from '@/stores/modules/dict' import { useDictData } from '@/stores/dictData'
import {ElementPlusInfoType} from '@/types/elementPlus' import { store } from '@/stores/index'
import type { BasicDictData } from '@/stores/interface'
import { ElementPlusInfoType } from '@/types/elementPlus'
const dictStore = useDictStoreWithOut() const getDictDataStore = () => useDictData(store)
function toDictCodeVariants(dictType: string): string[] {
const variants = new Set<string>([dictType])
if (dictType.includes('_')) {
variants.add(
dictType
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('_')
)
}
return [...variants]
}
function findBasicDict(dictType: string): BasicDictData | undefined {
const basic = getDictDataStore().state.basic
for (const variant of toDictCodeVariants(dictType)) {
const found = basic.find(
(item) => item.code === variant || item.code.toLowerCase() === variant.toLowerCase()
)
if (found) return found
}
return undefined
}
/** /**
* 获取 dictType 对应的数据字典数组 * 获取 dictType 对应的数据字典数组
*
* @param dictType 数据类型
* @returns {*|Array} 数据字典数组
*/ */
export interface DictDataType { export interface DictDataType {
dictType: string dictType: string
@@ -24,15 +47,20 @@ export interface NumberDictDataType extends DictDataType {
value: number value: number
} }
export const getDictOptions = (dictType: string) => { export const getDictOptions = (dictType: string): DictDataType[] => {
return dictStore.getDictByType(dictType) || [] const basic = findBasicDict(dictType)
if (!basic?.children?.length) return []
return basic.children.map((item) => ({
dictType,
label: item.name,
value: item.code || item.id,
colorType: '' as ElementPlusInfoType | '',
cssClass: ''
}))
} }
export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
// 获得通用的 DictDataType 列表
const dictOptions: DictDataType[] = getDictOptions(dictType) const dictOptions: DictDataType[] = getDictOptions(dictType)
// 转换成 number 类型的 NumberDictDataType 类型
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时el-option 的 key 会告警
const dictOption: NumberDictDataType[] = [] const dictOption: NumberDictDataType[] = []
dictOptions.forEach((dict: DictDataType) => { dictOptions.forEach((dict: DictDataType) => {
dictOption.push({ dictOption.push({
@@ -40,7 +68,6 @@ export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
value: parseInt(dict.value + '') value: parseInt(dict.value + '')
}) })
}) })
console.log("🚀 ~ getIntDictOptions ~ dictOption:", dictOption)
return dictOption return dictOption
} }
@@ -70,9 +97,6 @@ export const getBoolDictOptions = (dictType: string) => {
/** /**
* 获取指定字典类型的指定值对应的字典对象 * 获取指定字典类型的指定值对应的字典对象
* @param dictType 字典类型
* @param value 字典值
* @return DictDataType 字典对象
*/ */
export const getDictObj = (dictType: string, value: any): DictDataType | undefined => { export const getDictObj = (dictType: string, value: any): DictDataType | undefined => {
const dictOptions: DictDataType[] = getDictOptions(dictType) const dictOptions: DictDataType[] = getDictOptions(dictType)
@@ -85,10 +109,6 @@ export const getDictObj = (dictType: string, value: any): DictDataType | undefin
/** /**
* 获得字典数据的文本展示 * 获得字典数据的文本展示
*
* @param dictType 字典类型
* @param value 字典数据的值
* @return 字典名称
*/ */
export const getDictLabel = (dictType: string, value: any): string => { export const getDictLabel = (dictType: string, value: any): string => {
const dictOptions: DictDataType[] = getDictOptions(dictType) const dictOptions: DictDataType[] = getDictOptions(dictType)

View File

@@ -179,7 +179,6 @@ function createAxios<Data = any, T = ApiPromise<Data>>(
return Axios(response.config) return Axios(response.config)
}) })
.catch(err => { .catch(err => {
window.location.reload()
adminInfo.removeToken() adminInfo.removeToken()
router.push({ name: 'login' }) router.push({ name: 'login' })
return Promise.reject(err) return Promise.reject(err)
@@ -243,6 +242,25 @@ function createAxios<Data = any, T = ApiPromise<Data>>(
return Axios(axiosConfig) as T return Axios(axiosConfig) as T
} }
/** 发起请求并直接返回业务 data 字段 */
export async function requestData<T = any>(
axiosConfig: AxiosRequestConfig,
options?: Options,
loading?: LoadingOptions
): Promise<T> {
const res = await createAxios<any, ApiPromise<T>>(axiosConfig, options, loading)
return (res as ApiResponse<T>)?.data as T
}
/** 下载文件,返回原始 axios 响应 */
export function requestDownload(
axiosConfig: AxiosRequestConfig,
options?: Options,
loading?: LoadingOptions
) {
return createAxios(axiosConfig, { ...options, reductDataFormat: false }, loading)
}
export default createAxios export default createAxios
/** /**

View File

@@ -70,6 +70,7 @@ export const onClickMenu = (menu: RouteRecordRaw) => {
switch (menu.meta?.menu_type) { switch (menu.meta?.menu_type) {
case 'iframe': case 'iframe':
case 'tab': case 'tab':
if (router.currentRoute.value.path === menu.path) return
routePush({ path: menu.path }) routePush({ path: menu.path })
break break
case 'link': case 'link':

View File

@@ -1,253 +1,14 @@
import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router' const modules = import.meta.glob('../views/**/*.{vue,tsx}')
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { isUrl } from '@/utils/is' /**
import { cloneDeep, omit } from 'lodash-es' * 注册一个异步组件
import qs from 'qs' * @param componentPath 例:/bpm/oa/leave/detail
*/
const modules = import.meta.glob('../views/**/*.{vue,tsx}') export const registerComponent = (componentPath: string) => {
/** for (const item in modules) {
* 注册一个异步组件 if (item.includes(componentPath)) {
* @param componentPath 例:/bpm/oa/leave/detail // @ts-ignore
*/ return defineAsyncComponent(modules[item])
export const registerComponent = (componentPath: string) => { }
for (const item in modules) { }
if (item.includes(componentPath)) { }
// 使用异步组件的方式来动态加载组件
// @ts-ignore
return defineAsyncComponent(modules[item])
}
}
}
/* Layout */
export const Layout = () => import('@/layouts/Layout.vue')
export const getParentLayout = () => {
return () =>
new Promise((resolve) => {
resolve({
name: 'ParentLayout'
})
})
}
// 按照路由中meta下的rank等级升序来排序路由
export const ascending = (arr: any[]) => {
arr.forEach((v) => {
if (v?.meta?.rank === null) v.meta.rank = undefined
if (v?.meta?.rank === 0) {
if (v.name !== 'home' && v.path !== '/') {
console.warn('rank only the home page can be 0')
}
}
})
return arr.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank
})
}
export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => {
if (!route) return route
const { matched, ...opt } = route
return {
...opt,
matched: (matched
? matched.map((item) => ({
meta: item.meta,
name: item.name,
path: item.path
}))
: undefined) as RouteRecordNormalized[]
}
}
// 后端控制路由生成
export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
const modulesRoutesKeys = Object.keys(modules)
for (const route of routes) {
// 1. 生成 meta 菜单元数据
const meta = {
title: route.name,
icon: route.icon,
hidden: !route.visible,
noCache: !route.keepAlive,
alwaysShow:
route.children &&
route.children.length === 1 &&
(route.alwaysShow !== undefined ? route.alwaysShow : true)
} as any
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
// 此时,我们需要解析参数,并且将参数放到 meta.query 中
// 这样,后续在 Vue 文件中,可以通过 const { currentRoute } = useRouter() 中,通过 meta.query 获取到参数
if (route.component && route.component.indexOf('?') > -1) {
const query = route.component.split('?')[1]
route.component = route.component.split('?')[0]
meta.query = qs.parse(query)
}
// 2. 生成 dataAppRouteRecordRaw
// 路由地址转首字母大写驼峰作为路由名称适配keepAlive
let data: AppRouteRecordRaw = {
path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path,
name:
route.componentName && route.componentName.length > 0
? route.componentName
: toCamelCase(route.path, true),
redirect: route.redirect,
meta: meta
}
//处理顶级非目录路由
if (!route.children && route.parentId == 0 && route.component) {
data.component = Layout
data.meta = {}
data.name = toCamelCase(route.path, true) + 'Parent'
data.redirect = ''
meta.alwaysShow = true
const childrenData: AppRouteRecordRaw = {
path: '',
name:
route.componentName && route.componentName.length > 0
? route.componentName
: toCamelCase(route.path, true),
redirect: route.redirect,
meta: meta
}
const index = route?.component
? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
: modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
childrenData.component = modules[modulesRoutesKeys[index]]
data.children = [childrenData]
} else {
// 目录
if (route.children) {
data.component = Layout
data.redirect = getRedirect(route.path, route.children)
// 外链
} else if (isUrl(route.path)) {
data = {
path: '/external-link',
component: Layout,
meta: {
name: route.name
},
children: [data]
} as AppRouteRecordRaw
// 菜单
} else {
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会根path保持一致
const index = route?.component
? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
: modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
data.component = modules[modulesRoutesKeys[index]]
}
if (route.children) {
data.children = generateRoute(route.children)
}
}
res.push(data as AppRouteRecordRaw)
}
return res
}
export const getRedirect = (parentPath: string, children: AppCustomRouteRecordRaw[]) => {
if (!children || children.length == 0) {
return parentPath
}
const path = generateRoutePath(parentPath, children[0].path)
// 递归子节点
if (children[0].children) return getRedirect(path, children[0].children)
}
const generateRoutePath = (parentPath: string, path: string) => {
if (parentPath.endsWith('/')) {
parentPath = parentPath.slice(0, -1) // 移除默认的 /
}
if (!path.startsWith('/')) {
path = '/' + path
}
return parentPath + path
}
export const pathResolve = (parentPath: string, path: string) => {
if (isUrl(path)) return path
const childPath = path.startsWith('/') || !path ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/\//g, '/')
}
// 路由降级
export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => {
const modules: AppRouteRecordRaw[] = cloneDeep(routes)
for (let index = 0; index < modules.length; index++) {
const route = modules[index]
if (!isMultipleRoute(route)) {
continue
}
promoteRouteLevel(route)
}
return modules
}
// 层级是否大于2
const isMultipleRoute = (route: AppRouteRecordRaw) => {
if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
return false
}
const children = route.children
let flag = false
for (let index = 0; index < children.length; index++) {
const child = children[index]
if (child.children?.length) {
flag = true
break
}
}
return flag
}
// 生成二级路由
const promoteRouteLevel = (route: AppRouteRecordRaw) => {
let router: Router | null = createRouter({
routes: [route as RouteRecordRaw],
history: createWebHashHistory()
})
const routes = router.getRoutes()
addToChildren(routes, route.children || [], route)
router = null
route.children = route.children?.map((item) => omit(item, 'children'))
}
// 添加所有子菜单
const addToChildren = (
routes: RouteRecordNormalized[],
children: AppRouteRecordRaw[],
routeModule: AppRouteRecordRaw
) => {
for (let index = 0; index < children.length; index++) {
const child = children[index]
const route = routes.find((item) => item.name === child.name)
if (!route) {
continue
}
routeModule.children = routeModule.children || []
if (!routeModule.children.find((item) => item.name === route.name)) {
routeModule.children?.push(route as unknown as AppRouteRecordRaw)
}
if (child.children?.length) {
addToChildren(routes, child.children, routeModule)
}
}
}
const toCamelCase = (str: string, upperCaseFirst: boolean) => {
str = (str || '')
.replace(/-(.)/g, function (group1: string) {
return group1.toUpperCase()
})
.replaceAll('-', '')
if (upperCaseFirst && str) {
str = str.charAt(0).toUpperCase() + str.slice(1)
}
return str
}

View File

@@ -1,147 +1,147 @@
<template> <template>
<!-- 新增 --> <!-- 新增 -->
<el-dialog draggable :title="title" v-model="userAdd" width="500px" :before-close="cancel"> <el-dialog draggable :title="title" v-model="userAdd" width="500px" :before-close="cancel">
<el-form :inline="true" ref="formRef" :model="addData" label-width="auto" class="form-one" :rules="rules"> <el-form :inline="true" ref="formRef" :model="addData" label-width="auto" class="form-one" :rules="rules">
<el-form-item label="新能源场站名称:" prop="name"> <el-form-item label="新能源场站名称:" prop="name">
<el-input v-model="addData.name" clearable placeholder="请输入新能源场站名称"></el-input> <el-input v-model="addData.name" clearable placeholder="请输入新能源场站名称"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="干扰源类型:" prop="stationType"> <el-form-item label="干扰源类型:" prop="stationType">
<el-select v-model="addData.stationType" clearable collapse-tags placeholder="请选择干扰源类型"> <el-select v-model="addData.stationType" clearable collapse-tags placeholder="请选择干扰源类型" filterable>
<el-option v-for="item in newEnergy" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in newEnergy" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="电压等级:" prop="scale"> <el-form-item label="电压等级:" prop="scale">
<el-select v-model="addData.scale" clearable collapse-tags placeholder="请选择电压等级"> <el-select v-model="addData.scale" clearable collapse-tags placeholder="请选择电压等级" filterable>
<el-option v-for="item in scaleList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in scaleList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="额定有功功率(kW):" prop="ratedPower"> <el-form-item label="额定有功功率(kW):" prop="ratedPower">
<el-input-number v-model="addData.ratedPower" :min="0" style="width: 100%" /> <el-input-number v-model="addData.ratedPower" :min="0" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item label="经度:" prop="longitude"> <el-form-item label="经度:" prop="longitude">
<el-input v-model="addData.longitude" type="text" placeholder="请输入经度" /> <el-input v-model="addData.longitude" type="text" placeholder="请输入经度" />
</el-form-item> </el-form-item>
<el-form-item label="纬度:" prop="latitude"> <el-form-item label="纬度:" prop="latitude">
<el-input v-model="addData.latitude" type="text" placeholder="请输入纬度" /> <el-input v-model="addData.latitude" type="text" placeholder="请输入纬度" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="cancel">取消</el-button> <el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="config">确定</el-button> <el-button type="primary" @click="config">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useDictData } from '@/stores/dictData' import { useDictData } from '@/stores/dictData'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { editNewStation } from '@/api/device-boot/newEnergy' import { editNewStation } from '@/api/device-boot/newEnergy'
const dictData = useDictData() const dictData = useDictData()
const userAdd = ref(false) const userAdd = ref(false)
const title = ref('') const title = ref('')
const newEnergy = dictData.getBasicData('new_station_type') const newEnergy = dictData.getBasicData('new_station_type')
const scaleList = dictData.getBasicData('Dev_Voltage_Stand') const scaleList = dictData.getBasicData('Dev_Voltage_Stand')
const emit = defineEmits(['onSubmit']) const emit = defineEmits(['onSubmit'])
const rules = reactive({ const rules = reactive({
stationType: [{ required: true, message: '请选择干扰源类型', trigger: 'change' }], stationType: [{ required: true, message: '请选择干扰源类型', trigger: 'change' }],
scale: [{ required: true, message: '请选择电压等级', trigger: 'change' }], scale: [{ required: true, message: '请选择电压等级', trigger: 'change' }],
name: [{ required: true, message: '请输入干扰源用户名称', trigger: 'blur' }], name: [{ required: true, message: '请输入干扰源用户名称', trigger: 'blur' }],
ratedPower: [{ required: true, message: '请输入额定功率', trigger: 'blur' }], ratedPower: [{ required: true, message: '请输入额定功率', trigger: 'blur' }],
longitude: [ longitude: [
{ {
required: true, required: true,
message: '请输入经度', message: '请输入经度',
trigger: 'blur' trigger: 'blur'
}, },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (!longitudeReg.test(value)) { if (!longitudeReg.test(value)) {
callback(new Error('请输入有效的经度')) callback(new Error('请输入有效的经度'))
} else { } else {
callback() callback()
} }
}, },
trigger: 'change' trigger: 'change'
} }
], ],
latitude: [ latitude: [
{ {
required: true, required: true,
message: '请输入纬度', message: '请输入纬度',
trigger: 'blur' trigger: 'blur'
}, },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (!latitudeReg.test(value)) { if (!latitudeReg.test(value)) {
callback(new Error('请输入有效的纬度')) callback(new Error('请输入有效的纬度'))
} else { } else {
callback() callback()
} }
}, },
trigger: 'change' trigger: 'change'
} }
] ]
}) })
// 经度正则表达式 // 经度正则表达式
const longitudeReg = /^[-+]?((180(\.0{1,10})?)|(1[0-7][0-9](\.\d{1,10})?)|([1-9]?\d(\.\d{1,10})?)|(0(\.\d{1,10})?))$/ const longitudeReg = /^[-+]?((180(\.0{1,10})?)|(1[0-7][0-9](\.\d{1,10})?)|([1-9]?\d(\.\d{1,10})?)|(0(\.\d{1,10})?))$/
// 纬度正则表达式 // 纬度正则表达式
const latitudeReg = /^[-+]?((90(\.0{1,10})?)|([1-8]?\d(\.\d{1,10})?)|(0(\.\d{1,10})?))$/ const latitudeReg = /^[-+]?((90(\.0{1,10})?)|([1-8]?\d(\.\d{1,10})?)|(0(\.\d{1,10})?))$/
const addData = ref({ const addData = ref({
scale: '', scale: '',
stationType: '', stationType: '',
name: '', name: '',
ratedPower: 0, ratedPower: 0,
longitude: '', longitude: '',
latitude: '' latitude: ''
}) })
const formRef = ref() const formRef = ref()
// 新增 // 新增
const config = () => { const config = () => {
formRef.value.validate((valid: any) => { formRef.value.validate((valid: any) => {
if (valid) { if (valid) {
editNewStation(addData.value).then((res: any) => { editNewStation(addData.value).then((res: any) => {
ElMessage.success(title.value + '成功!') ElMessage.success(title.value + '成功!')
emit('onSubmit') emit('onSubmit')
cancel() cancel()
}) })
} }
}) })
} }
// 重置 // 重置
const cancel = () => { const cancel = () => {
// formRef.value.resetFields() // formRef.value.resetFields()
userAdd.value = false userAdd.value = false
} }
const open = (row: any) => { const open = (row: any) => {
addData.value = { addData.value = {
scale: '', scale: '',
stationType: '', stationType: '',
name: '', name: '',
ratedPower: 0, ratedPower: 0,
longitude: '', longitude: '',
latitude: '' latitude: ''
} }
title.value = row.title title.value = row.title
if (row.title === '修改') { if (row.title === '修改') {
addData.value = { addData.value = {
id: row.row.id, id: row.row.id,
scale: row.row.scale, scale: row.row.scale,
stationType: row.row.stationType, stationType: row.row.stationType,
name: row.row.name, name: row.row.name,
ratedPower: row.row.ratedPower - 0, ratedPower: row.row.ratedPower - 0,
longitude: row.row.longitude, longitude: row.row.longitude,
latitude: row.row.latitude latitude: row.row.latitude
} }
} }
userAdd.value = true userAdd.value = true
setTimeout(() => { setTimeout(() => {
formRef.value.clearValidate() formRef.value.clearValidate()
}, 20) }, 20)
} }
defineExpose({ open }) defineExpose({ open })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -20,7 +20,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="事件类型:"> <el-form-item label="事件类型:">
<el-select v-model="tableStore.table.params.type" placeholder="事件类型" clearable> <el-select v-model="tableStore.table.params.type" placeholder="事件类型" clearable filterable>
<el-option <el-option
v-for="item in eventType" v-for="item in eventType"
:key="item.id" :key="item.id"
@@ -30,7 +30,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="操作类型:"> <el-form-item label="操作类型:">
<el-select v-model="tableStore.table.params.operateType" placeholder="操作类型" clearable> <el-select v-model="tableStore.table.params.operateType" placeholder="操作类型" clearable filterable>
<el-option <el-option
v-for="item in operationType" v-for="item in operationType"
:key="item.value" :key="item.value"
@@ -40,7 +40,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="操作结果:"> <el-form-item label="操作结果:">
<el-select v-model="tableStore.table.params.result" placeholder="操作结果" clearable> <el-select v-model="tableStore.table.params.result" placeholder="操作结果" clearable filterable>
<el-option <el-option
v-for="item in resultList" v-for="item in resultList"
:key="item.id" :key="item.id"

View File

@@ -13,7 +13,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="事件类型:"> <el-form-item label="事件类型:">
<el-select v-model="tableStore.table.params.type" placeholder="事件类型" clearable> <el-select v-model="tableStore.table.params.type" placeholder="事件类型" clearable filterable>
<el-option <el-option
v-for="item in eventType" v-for="item in eventType"
:key="item.id" :key="item.id"
@@ -23,7 +23,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="操作类型:"> <el-form-item label="操作类型:">
<el-select v-model="tableStore.table.params.operateType" placeholder="操作类型" clearable> <el-select v-model="tableStore.table.params.operateType" placeholder="操作类型" clearable filterable>
<el-option <el-option
v-for="item in operationType" v-for="item in operationType"
:key="item.value" :key="item.value"

Some files were not shown because too many files have changed in this diff Show More