This commit is contained in:
YunaiV
2025-10-01 15:56:31 +08:00
32 changed files with 899 additions and 185 deletions

View File

@@ -115,7 +115,8 @@ const include = [
'@element-plus/icons-vue',
'element-plus/es/components/footer/style/css',
'element-plus/es/components/empty/style/css',
'element-plus/es/components/mention/style/css'
'element-plus/es/components/mention/style/css',
'element-plus/es/components/progress/style/css'
]
const exclude = ['@iconify/json']

View File

@@ -34,6 +34,7 @@
"@vueuse/core": "^10.9.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@wangeditor/plugin-mention": "^1.0.0",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "1.9.0",
@@ -65,6 +66,7 @@
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",
"qs": "^6.12.0",
"snabbdom": "^3.6.2",
"sortablejs": "^1.15.3",
"steady-xml": "^0.1.0",
"url": "^0.11.3",
@@ -74,6 +76,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
"vue3-print-nb": "^0.1.4",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",

View File

@@ -70,7 +70,7 @@ export const ChatMessageApi = {
conversationId,
content,
useContext: enableContext,
webSearch: enableWebSearch,
useSearch: enableWebSearch,
attachmentUrls: attachmentUrls || []
}),
onmessage: onMessage,

View File

@@ -108,3 +108,8 @@ export const getFormFieldsPermission = async (params: any) => {
export const getProcessInstanceBpmnModelView = async (id: string) => {
return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
}
// 获取流程实例打印数据
export const getProcessInstancePrintData = async (id: string) => {
return await request.get({ url: '/bpm/process-instance/get-print-data?processInstanceId=' + id })
}

View File

@@ -1,5 +1,4 @@
import * as FileApi from '@/api/infra/file'
// import CryptoJS from 'crypto-js'
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import axios from 'axios'
@@ -20,7 +19,7 @@ export const useUpload = (directory?: string) => {
// 模式一:前端上传
if (isClientUpload) {
// 1.1 生成文件名称
const fileName = await generateFileName(options.file)
const fileName = options.file.name || options.filename
// 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
@@ -32,7 +31,7 @@ export const useUpload = (directory?: string) => {
})
.then(() => {
// 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, options.file)
createFile(presignedInfo, options.file, fileName)
// 通知成功,数据格式保持与后端上传的返回结果一致
return { data: presignedInfo.url }
})
@@ -64,15 +63,15 @@ export const useUpload = (directory?: string) => {
/**
* 创建文件信息
* @param vo 文件预签名信息
* @param name 文件名称
* @param file 文件
* @param fileName
*/
function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) {
function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile, fileName: string) {
const fileVo = {
configId: vo.configId,
url: vo.url,
path: vo.path,
name: file.name,
name: fileName,
type: file.type,
size: file.size
}
@@ -80,22 +79,6 @@ function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) {
return fileVo
}
/**
* 生成文件名称使用算法SHA256
* @param file 要上传的文件
*/
async function generateFileName(file: UploadRawFile) {
// // 读取文件内容
// const data = await file.arrayBuffer()
// const wordArray = CryptoJS.lib.WordArray.create(data)
// // 计算SHA256
// const sha256 = CryptoJS.SHA256(wordArray).toString()
// // 拼接后缀
// const ext = file.name.substring(file.name.lastIndexOf('.'))
// return `${sha256}${ext}`
return file.name
}
/**
* 上传类型
*/

View File

@@ -42,6 +42,11 @@ import Logger from '@/utils/Logger'
import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
// wangEditor 插件注册
import { setupWangEditorPlugin } from '@/views/bpm/model/form/PrintTemplate'
import print from 'vue3-print-nb' // 打印插件
// 创建实例
const setupAll = async () => {
const app = createApp(App)
@@ -62,10 +67,16 @@ const setupAll = async () => {
setupAuth(app)
setupMountedFocus(app)
// wangEditor 插件注册
setupWangEditorPlugin()
await router.isReady()
app.use(VueDOMPurifyHTML)
// 打印
app.use(print)
app.mount('#app')
}

View File

@@ -78,6 +78,21 @@ const schema = reactive<FormSchema[]>([
}
])
const formRef = ref<FormExpose>() // 表单 Ref
// 监听 userStore 中头像的变化,同步更新表单数据
watch(
() => userStore.getUser.avatar,
(newAvatar) => {
if (newAvatar && formRef.value) {
// 直接更新表单模型中的头像字段
const formModel = formRef.value.formModel
if (formModel) {
formModel.avatar = newAvatar
}
}
}
)
const submit = () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
@@ -87,17 +102,19 @@ const submit = () => {
await updateUserProfile(data)
message.success(t('common.updateSuccess'))
const profile = await init()
userStore.setUserNicknameAction(profile.nickname)
await userStore.setUserNicknameAction(profile.nickname)
// 发送成功事件
emit('success')
}
})
}
const init = async () => {
const res = await getUserProfile()
unref(formRef)?.setValues(res)
return res
}
onMounted(async () => {
await init()
})

View File

@@ -49,18 +49,31 @@
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import UserAvatar from './UserAvatar.vue'
import { useUserStore } from '@/store/modules/user'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
defineOptions({ name: 'ProfileUser' })
const { t } = useI18n()
const userStore = useUserStore()
const userInfo = ref({} as ProfileVO)
const getUserInfo = async () => {
const users = await getUserProfile()
userInfo.value = users
}
// 监听 userStore 中头像的变化,同步更新本地 userInfo
watch(
() => userStore.getUser.avatar,
(newAvatar) => {
if (newAvatar && userInfo.value) {
userInfo.value.avatar = newAvatar
}
}
)
// 暴露刷新方法
defineExpose({
refresh: getUserInfo

View File

@@ -1,21 +0,0 @@
<!-- header -->
<template>
<el-header class="flex flex-row justify-between items-center px-10px whitespace-nowrap text-ellipsis w-full" :style="{ backgroundColor: 'var(--el-bg-color-page)' }">
<div class="text-20px font-bold overflow-hidden max-w-220px" :style="{ color: 'var(--el-text-color-primary)' }">
{{ title }}
</div>
<div class="flex flex-row">
<slot></slot>
</div>
</el-header>
</template>
<script setup lang="ts">
// 设置组件属性
defineProps({
title: {
type: String,
required: true
}
})
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-row flex-wrap relative h-full overflow-auto px-25px pb-140px items-start content-start justify-start"
class="flex flex-row flex-wrap relative h-full overflow-auto pb-140px items-start content-start justify-start"
ref="tabsRef"
@scroll="handleTabsScroll"
>

View File

@@ -1,14 +1,10 @@
<!-- chat 角色仓库 -->
<template>
<el-container
class="role-container absolute w-full h-full m-0 p-0 left-0 right-0 top-0 bottom-0 bg-[var(--el-bg-color)] overflow-hidden flex !flex-col"
>
<el-container class="bg-[var(--el-bg-color)] -mt-25px">
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
<!-- header -->
<RoleHeader title="角色仓库" class="relative" />
<!-- main -->
<el-main class="flex-1 overflow-hidden m-0 !p-0 relative">
<div class="mx-5 mt-5 mb-0 absolute right-0 -top-1.25 z-100">
<div class="mx-3 mt-3 mb-0 absolute right-0 -top-1.25 z-100">
<!-- 搜索按钮 -->
<el-input
:loading="loading"
@@ -30,16 +26,8 @@
</el-button>
</div>
<!-- tabs -->
<el-tabs
v-model="activeTab"
@tab-click="handleTabsClick"
class="relative h-full [&_.el-tabs__nav-scroll]:my-2.5 [&_.el-tabs__nav-scroll]:mx-5"
>
<el-tab-pane
label="我的角色"
name="my-role"
class="flex flex-col h-full overflow-y-auto relative"
>
<el-tabs v-model="activeTab" @tab-click="handleTabsClick" class="relative h-full">
<el-tab-pane label="我的角色" name="my-role" class="flex flex-col h-full overflow-y-auto">
<RoleList
:loading="loading"
:role-list="myRoleList"
@@ -48,12 +36,12 @@
@on-edit="handlerCardEdit"
@on-use="handlerCardUse"
@on-page="handlerCardPage('my')"
class="mt-20px"
class="mt-3"
/>
</el-tab-pane>
<el-tab-pane label="公共角色" name="public-role">
<el-tab-pane label="公共角色" name="public-role" class="!pt-2">
<RoleCategoryList
class="mx-6.75"
class="mx-3"
:category-list="categoryList"
:active="activeCategory"
@on-category-click="handlerCategoryClick"
@@ -64,7 +52,7 @@
@on-edit="handlerCardEdit"
@on-use="handlerCardUse"
@on-page="handlerCardPage('public')"
class="mt-20px"
class="mt-3"
loading
/>
</el-tab-pane>
@@ -75,7 +63,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import RoleHeader from './RoleHeader.vue'
import RoleList from './RoleList.vue'
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
import RoleCategoryList from './RoleCategoryList.vue'
@@ -83,8 +70,11 @@ import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatR
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { Search } from '@element-plus/icons-vue'
import { TabsPaneContext } from 'element-plus'
import { useTagsViewStore } from '@/store/modules/tagsView'
const router = useRouter() // 路由对象
const { currentRoute } = useRouter() // 路由
const { delView } = useTagsViewStore() // 视图操作
// 属性定义
const loading = ref<boolean>(false) // 加载中
@@ -134,7 +124,7 @@ const getPublicRole = async (append?: boolean) => {
name: search.value,
publicStatus: true
}
const { total, list } = await ChatRoleApi.getMyPage(params)
const { list } = await ChatRoleApi.getMyPage(params)
if (append) {
publicRoleList.value.push.apply(publicRoleList.value, list)
} else {
@@ -214,7 +204,8 @@ const handlerCardUse = async (role) => {
const conversationId = await ChatConversationApi.createChatConversationMy(data)
// 2. 跳转页面
await router.push({
delView(unref(currentRoute))
await router.replace({
name: 'AiChat',
query: {
conversationId: conversationId
@@ -233,6 +224,23 @@ onMounted(async () => {
<!-- 覆盖 element plus css -->
<style lang="scss">
.el-tabs__nav-scroll {
margin: 10px 20px;
margin: 2px 8px !important;
}
.el-tabs__header {
margin: 0 !important;
padding: 0 !important;
}
.el-tabs__nav-wrap {
margin-bottom: 0 !important;
}
.el-tabs__content {
padding: 0 !important;
}
.el-tab-pane {
padding: 8px 0 0 0 !important;
}
</style>

View File

@@ -516,7 +516,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
},
(error: any) => {
// 异常提示,并停止流
message.alert(`对话异常! ${error}`)
message.alert(`对话异常`)
stopStream()
// 需要抛出异常,禁止重试
throw error

View File

@@ -1,5 +1,5 @@
<template>
<el-form ref="formRef" :model="modelData" label-width="120px" class="mt-20px">
<el-form ref="formRef" :model="modelData" label-width="130px" class="mt-20px">
<el-form-item class="mb-20px">
<template #label>
<el-text size="large" tag="b">提交人权限</el-text>
@@ -231,7 +231,30 @@
/>
</div>
</el-form-item>
<el-form-item class="mb-20px">
<template #label>
<el-text size="large" tag="b">自定义打印模板</el-text>
</template>
<div class="flex flex-col w-100%">
<div class="flex">
<el-switch
v-model="modelData.printTemplateSetting.enable"
@change="handlePrintTemplateEnableChange"
/>
<el-button
v-if="modelData.printTemplateSetting.enable"
class="ml-80px"
type="primary"
link
@click="handleEditPrintTemplate"
>
编辑模板
</el-button>
</div>
</div>
</el-form-item>
</el-form>
<print-template ref="printTemplateRef" @confirm="confirmPrintTemplate" />
</template>
<script setup lang="ts">
@@ -241,6 +264,7 @@ import * as FormApi from '@/api/bpm/form'
import { parseFormFields } from '@/components/FormCreate/src/utils'
import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
import PrintTemplate from './PrintTemplate/Index.vue'
const modelData = defineModel<any>()
@@ -394,6 +418,7 @@ const formFieldOptions4Summary = computed(() => {
const unParsedFormFields = ref<string[]>([])
/** 暴露给子组件 HttpRequestSetting 使用 */
provide('formFields', unParsedFormFields)
provide('formFieldsObj', formFields)
/** 兼容以前未配置更多设置的流程 */
const initData = () => {
@@ -436,6 +461,11 @@ const initData = () => {
if (modelData.value.allowWithdrawTask) {
modelData.value.allowWithdrawTask = false
}
if (!modelData.value.printTemplateSetting) {
modelData.value.printTemplateSetting = {
enable: false
}
}
}
defineExpose({ initData })
@@ -460,4 +490,21 @@ watch(
},
{ immediate: true }
)
const defaultTemplate =
'<p style="text-align: center;"><span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="流程名称" data-info="%7B%22id%22%3A%22processName%22%7D">@流程名称</span></p><p style="text-align: right;">打印人:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="打印人" data-info="%7B%22id%22%3A%22printUser%22%7D">@打印人</span></p><p style="text-align: right;">流程编号:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="流程编号" data-info="%7B%22id%22%3A%22processNum%22%7D">@流程编号</span> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;打印时间:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="打印时间" data-info="%7B%22id%22%3A%22printTime%22%7D">@打印时间</span></p><table style="width: 100%;"><tbody><tr><td colSpan="1" rowSpan="1" width="auto">发起人</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起人" data-info="%7B%22id%22%3A%22startUser%22%7D">@发起人</span></td><td colSpan="1" rowSpan="1" width="auto">发起时间</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起时间" data-info="%7B%22id%22%3A%22startTime%22%7D">@发起时间</span></td></tr><tr><td colSpan="1" rowSpan="1" width="auto">所属部门</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起人部门" data-info="%7B%22id%22%3A%22startUserDept%22%7D">@发起人部门</span></td><td colSpan="1" rowSpan="1" width="auto">流程状态</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="流程状态" data-info="%7B%22id%22%3A%22processStatus%22%7D">@流程状态</span></td></tr></tbody></table><p><span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>流程记录</span></p>'
const handlePrintTemplateEnableChange = (val: boolean) => {
if (val) {
if (!modelData.value.printTemplateSetting.template) {
modelData.value.printTemplateSetting.template = defaultTemplate
}
}
}
const printTemplateRef = ref()
const handleEditPrintTemplate = () => {
printTemplateRef.value.open(modelData.value.printTemplateSetting.template)
}
const confirmPrintTemplate = (template: any) => {
modelData.value.printTemplateSetting.template = template
}
</script>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { IDomEditor } from '@wangeditor/editor'
import MentionModal from './MentionModal.vue'
const emit = defineEmits(['confirm'])
// @mention 相关
const isShowModal = ref(false)
const showModal = () => {
isShowModal.value = true
}
const hideModal = () => {
isShowModal.value = false
}
const insertMention = (id: any, name: any) => {
const mentionNode = {
type: 'mention',
value: name,
info: { id },
children: [{ text: '' }]
}
const editor = editorRef.value
if (editor) {
editor.restoreSelection()
editor.deleteBackward('character')
editor.insertNode(mentionNode)
editor.move(1)
}
}
// Dialog 相关
const dialogVisible = ref(false)
const open = async (template: string) => {
dialogVisible.value = true
valueHtml.value = template
}
defineExpose({ open })
const handleConfirm = () => {
emit('confirm', valueHtml.value)
dialogVisible.value = false
}
// Editor 相关
const editorRef = shallowRef<IDomEditor>()
const editorId = ref('wangeEditor-1')
const toolbarConfig = {
excludeKeys: ['group-video'],
insertKeys: {
index: 31,
keys: ['ProcessRecordMenu']
}
}
const editorConfig = {
placeholder: '请输入内容...',
EXTEND_CONF: {
mentionConfig: {
showModal,
hideModal
}
}
}
const valueHtml = ref()
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
}
/** 初始化 */
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) {
return
}
editor.destroy()
})
</script>
<template>
<el-dialog v-model="dialogVisible" title="自定义模板" fullscreen>
<div style="margin: 0 10px">
<el-alert
title="输入 @ 可选择插入流程表单选项和默认选项"
type="info"
show-icon
:closable="false"
/>
</div>
<!-- TODO @unocss 简化 style -->
<div style="border: 1px solid #ccc; margin: 10px">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:editorId="editorId"
:defaultConfig="toolbarConfig"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
:editorId="editorId"
@on-created="handleCreated"
/>
<MentionModal
v-if="isShowModal"
@hide-mention-modal="hideModal"
@insert-mention="insertMention"
/>
</div>
<div style="margin-right: 10px; float: right">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>
</el-dialog>
</template>
<style src="@wangeditor/editor/dist/css/style.css"></style>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
const emit = defineEmits(['hideMentionModal', 'insertMention'])
const inputRef = ref()
const top = ref('')
const left = ref('')
const searchVal = ref('')
const list = ref([
{ id: 'startUser', name: '发起人' },
{ id: 'startUserDept', name: '发起人部门' },
{ id: 'processName', name: '流程名称' },
{ id: 'processNum', name: '流程编号' },
{ id: 'startTime', name: '发起时间' },
{ id: 'endTime', name: '结束时间' },
{ id: 'processStatus', name: '流程状态' },
{ id: 'printUser', name: '打印人' },
{ id: 'printTime', name: '打印时间' }
])
const searchedList = computed(() => {
const searchValStr = searchVal.value.trim().toLowerCase()
return list.value.filter((item) => {
const name = item.name.toLowerCase()
return name.indexOf(searchValStr) >= 0
})
})
const inputKeyupHandler = (event: any) => {
if (event.key === 'Escape') {
emit('hideMentionModal')
}
if (event.key === 'Enter') {
const firstOne = searchedList.value[0]
if (firstOne) {
const { id, name } = firstOne
insertMentionHandler(id, name)
}
}
}
const insertMentionHandler = (id: any, name: any) => {
emit('insertMention', id, name)
emit('hideMentionModal')
}
const formFields = inject<any>('formFieldsObj')
onMounted(() => {
if (formFields.value && formFields.value.length > 0) {
const cloneFormField = formFields.value.map((item) => {
return {
name: '[表单]' + item.title,
id: item.field
}
})
list.value.push(...cloneFormField)
}
const domSelection = document.getSelection()
const domRange = domSelection?.getRangeAt(0)
if (domRange == null) return
const rect = domRange.getBoundingClientRect()
top.value = `${rect.top + 20}px`
left.value = `${rect.left + 5}px`
inputRef.value.focus()
})
</script>
<template>
<div id="mention-modal" :style="{ top: top, left: left }">
<!-- TODO @lesancss 可以用 unocss -->
<input id="mention-input" v-model="searchVal" ref="inputRef" @keyup="inputKeyupHandler" />
<ul id="mention-list">
<li
v-for="item in searchedList"
:key="item.id"
@click="insertMentionHandler(item.id, item.name)"
>
{{ item.name }}
</li>
</ul>
</div>
</template>
<style>
#mention-modal {
position: absolute;
border: 1px solid #ccc;
background-color: #fff;
padding: 5px;
}
#mention-modal input {
width: 100px;
outline: none;
}
#mention-modal ul {
padding: 0;
margin: 0;
}
#mention-modal ul li {
list-style: none;
cursor: pointer;
padding: 3px 0;
text-align: left;
}
#mention-modal ul li:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,9 @@
import { Boot } from '@wangeditor/editor'
import processRecordModule from './module'
import mentionModule from '@wangeditor/plugin-mention'
// 注册:要在创建编辑器之前注册,且只能注册一次,不可重复注册
export const setupWangEditorPlugin = () => {
Boot.registerModule(processRecordModule)
Boot.registerModule(mentionModule)
}

View File

@@ -0,0 +1,12 @@
import { SlateElement } from '@wangeditor/editor'
function processRecordToHtml(_elem: SlateElement, _childrenHtml: string): string {
return `<span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>流程记录</span>`
}
const conf = {
type: 'process-record',
elemToHtml: processRecordToHtml
}
export default conf

View File

@@ -0,0 +1,17 @@
import { IModuleConf } from '@wangeditor/editor'
import withProcessRecord from './plugin'
import renderElemConf from './render-elem'
import elemToHtmlConf from './elem-to-html'
import parseHtmlConf from './parse-elem-html'
import processRecordMenu from './menu/ProcessRecordMenu'
// 可参考 wangEditor 官方文档进行自定义扩展插件https://www.wangeditor.com/v5/development.html#%E5%AE%9A%E4%B9%89%E6%96%B0%E5%85%83%E7%B4%A0
const module: Partial<IModuleConf> = {
editorPlugin: withProcessRecord,
renderElems: [renderElemConf],
elemsToHtml: [elemToHtmlConf],
parseElemsHtml: [parseHtmlConf],
menus: [processRecordMenu]
}
export default module

View File

@@ -0,0 +1,42 @@
import { IButtonMenu, IDomEditor } from '@wangeditor/editor'
class ProcessRecordMenu implements IButtonMenu {
readonly tag: string
readonly title: string
constructor() {
this.title = '流程记录'
this.tag = 'button'
}
getValue(_editor: IDomEditor): string {
return ''
}
isActive(_editor: IDomEditor): boolean {
return false
}
isDisabled(_editor: IDomEditor): boolean {
return false
}
exec(editor: IDomEditor, _value: string) {
if (this.isDisabled(editor)) return
const processRecordElem = {
type: 'process-record',
children: [{ text: '' }]
}
editor.insertNode(processRecordElem)
editor.move(1)
}
}
const ProcessRecordMenuConf = {
key: 'ProcessRecordMenu',
factory() {
return new ProcessRecordMenu()
}
}
export default ProcessRecordMenuConf

View File

@@ -0,0 +1,21 @@
import { DOMElement } from './utils/dom'
import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor/editor'
function parseHtml(
_elem: DOMElement,
_children: SlateDescendant[],
_editor: IDomEditor
): SlateElement {
return {
// TODO @lesan这里有个红色告警可以去掉哇
type: 'process-record',
children: [{ text: '' }]
}
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="process-record"]',
parseElemHtml: parseHtml
}
export default parseHtmlConf

View File

@@ -0,0 +1,28 @@
import { DomEditor, IDomEditor } from '@wangeditor/editor'
function withProcessRecord<T extends IDomEditor>(editor: T) {
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'process-record') {
return true
}
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'process-record') {
return true
}
return isVoid(elem)
}
return newEditor
}
export default withProcessRecord

View File

@@ -0,0 +1,73 @@
import { h, VNode } from 'snabbdom'
import { DomEditor, IDomEditor, SlateElement } from '@wangeditor/editor'
function renderProcessRecord(
elem: SlateElement,
_children: VNode[] | null,
editor: IDomEditor
): VNode {
const selected = DomEditor.isNodeSelected(editor, elem)
return h(
'table',
{
props: {
contentEditable: false
},
style: {
width: '100%',
border: selected ? '2px solid var(--w-e-textarea-selected-border-color)' : ''
}
},
[
h('thead', [h('tr', [h('th', { attrs: { colSpan: 3 } }, '流程记录')])]),
h('tbody', [
h('tr', [
h('td', [
h(
'span',
{
props: {
contentEditable: false
},
style: {
marginLeft: '3px',
marginRight: '3px',
backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
borderRadius: '3px',
padding: '0 3px'
}
},
`节点`
)
]),
h('td', [
h(
'span',
{
props: {
contentEditable: false
},
style: {
marginLeft: '3px',
marginRight: '3px',
backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
borderRadius: '3px',
padding: '0 3px'
}
},
`操作`
)
])
])
])
]
)
}
const conf = {
type: 'process-record',
renderElem: renderProcessRecord
}
export default conf

View File

@@ -0,0 +1,21 @@
import $, { append, on, hide, click } from 'dom7'
if (hide) $.fn.hide = hide
if (append) $.fn.append = append
if (click) $.fn.click = click
if (on) $.fn.on = on
export { Dom7Array } from 'dom7'
export default $
// COMPAT: This is required to prevent TypeScript aliases from doing some very
// weird things for Slate's types with the same name as globals. (2019/11/27)
// https://github.com/microsoft/TypeScript/issues/35002
import DOMNode = globalThis.Node
import DOMComment = globalThis.Comment
import DOMElement = globalThis.Element
import DOMText = globalThis.Text
import DOMRange = globalThis.Range
import DOMSelection = globalThis.Selection
import DOMStaticRange = globalThis.StaticRange
export { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }

View File

@@ -174,7 +174,10 @@ const formData: any = ref({
enable: false,
summary: []
},
allowWithdrawTask: false
allowWithdrawTask: false,
printTemplateSetting: {
enable: false
}
})
// 流程数据

View File

@@ -0,0 +1,230 @@
<script setup lang="ts">
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { useUserStore } from '@/store/modules/user'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { decodeFields } from '@/utils/formCreate'
const userStore = useUserStore()
const visible = ref(false)
const loading = ref(false)
const printData = ref()
const userName = computed(() => userStore.user.nickname ?? '')
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'))
const formFields = ref()
const printDataMap = ref({})
const open = async (id: string) => {
loading.value = true
try {
printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id)
initPrintDataMap()
parseFormFields()
} finally {
loading.value = false
}
visible.value = true
}
defineExpose({ open })
const parseFormFields = () => {
const formFieldsObj = decodeFields(printData.value.processInstance.processDefinition.formFields)
const processVariables = printData.value.processInstance.formVariables
let res: any = []
for (const item of formFieldsObj) {
const id = item['field']
const name = item['title']
const variable = processVariables[item['field']]
let html = variable
switch (item['type']) {
case 'UploadImg': {
let imgEl = document.createElement('img')
imgEl.setAttribute('src', variable)
imgEl.setAttribute('style', 'max-width: 600px;')
html = imgEl.outerHTML
break
}
case 'radio':
case 'checkbox':
case 'select': {
const options = item['options'] || []
const temp: any = []
if (Array.isArray(variable)) {
const labels = options.filter((o) => variable.includes(o.value)).map((o) => o.label)
temp.push(...labels)
} else {
const opt = options.find((o) => o.value === variable)
temp.push(opt.label)
}
html = temp.join(',')
}
// TODO 更多表单打印展示
}
printDataMap.value[item['field']] = html
res.push({ id, name, html })
}
formFields.value = res
}
const initPrintDataMap = () => {
printDataMap.value['startUser'] = printData.value.processInstance.startUser.nickname
printDataMap.value['startUserDept'] = printData.value.processInstance.startUser.deptName
printDataMap.value['processName'] = printData.value.processInstance.name
printDataMap.value['processNum'] = printData.value.processInstance.id
printDataMap.value['startTime'] = formatDate(printData.value.processInstance.startTime)
printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime)
printDataMap.value['processStatus'] = getDictLabel(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
printData.value.processInstance.status
)
printDataMap.value['printUser'] = userName.value
printDataMap.value['printTime'] = printTime.value
}
const getPrintTemplateHTML = () => {
const parser = new DOMParser()
let doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
// table 添加border
let tables = doc.querySelectorAll('table')
tables.forEach((item) => {
item.setAttribute('border', '1')
item.setAttribute('style', (item.getAttribute('style') || '') + 'border-collapse:collapse;')
})
// 替换 mentions
let mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
mentions.forEach((item) => {
const mentionId = JSON.parse(decodeURIComponent(item.getAttribute('data-info') ?? ''))['id']
item.innerHTML = printDataMap.value[mentionId] ?? ''
})
// 替换流程记录
let processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
let processRecordTable: Element = document.createElement('table')
if (processRecords.length > 0) {
// 构建流程记录html
processRecordTable.setAttribute('border', '1')
processRecordTable.setAttribute('style', 'width:100%;border-collapse:collapse;')
const headTr = document.createElement('tr')
const headTd = document.createElement('td')
headTd.setAttribute('colspan', '2')
headTd.setAttribute('width', 'auto')
headTd.setAttribute('style', 'text-align: center;')
headTd.innerHTML = '流程节点'
headTr.appendChild(headTd)
processRecordTable.appendChild(headTr)
printData.value.tasks.forEach((item) => {
const tr = document.createElement('tr')
const td1 = document.createElement('td')
td1.innerHTML = item.name
const td2 = document.createElement('td')
td2.innerHTML = item.description
tr.appendChild(td1)
tr.appendChild(td2)
processRecordTable.appendChild(tr)
})
}
processRecords.forEach((item) => {
item.innerHTML = processRecordTable.outerHTML
})
// 返回 html
return doc.body.innerHTML
}
const printObj = ref({
id: 'printDivTag',
popTitle: '&nbsp',
extraCss: '/print.css',
extraHead: '',
zIndex: 20003
})
</script>
<template>
<el-dialog v-loading="loading" v-model="visible" :show-close="false">
<div id="printDivTag" style="word-break: break-all">
<div v-if="printData.printTemplateEnable" v-html="getPrintTemplateHTML()"></div>
<div v-else>
<h2 class="text-center">{{ printData.processInstance.name }}</h2>
<div class="text-right text-15px">{{ '打印人员: ' + userName }}</div>
<div class="flex justify-between">
<div class="text-15px">{{ '流程编号: ' + printData.processInstance.id }}</div>
<div class="text-15px">{{ '打印时间: ' + printTime }}</div>
</div>
<table class="mt-20px w-100%" border="1" style="border-collapse: collapse">
<tbody>
<tr>
<td class="p-5px w-25%">发起人</td>
<td class="p-5px w-25%">{{ printData.processInstance.startUser.nickname }}</td>
<td class="p-5px w-25%">发起时间</td>
<td class="p-5px w-25%">{{ formatDate(printData.processInstance.startTime) }}</td>
</tr>
<tr>
<td class="p-5px w-25%">所属部门</td>
<td class="p-5px w-25%">{{ printData.processInstance.startUser.deptName }}</td>
<td class="p-5px w-25%">流程状态</td>
<td class="p-5px w-25%">
{{
getDictLabel(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
printData.processInstance.status
)
}}
</td>
</tr>
<tr>
<td class="p-5px w-100% text-center" colspan="4">
<h4>表单内容</h4>
</td>
</tr>
<tr v-for="item in formFields" :key="item.id">
<td class="p-5px w-20%">
{{ item.name }}
</td>
<td class="p-5px w-80%" colspan="3">
<div v-html="item.html"></div>
</td>
</tr>
<tr>
<td class="p-5px w-100% text-center" colspan="4">
<h4>流程节点</h4>
</td>
</tr>
<tr v-for="item in printData.tasks" :key="item.id">
<td class="p-5px w-20%">
{{ item.name }}
</td>
<td class="p-5px w-80%" colspan="3">
{{ item.description }}
<div v-if="item.signPicUrl && item.signPicUrl.length > 0">
<img class="w-90px h-40px" :src="item.signPicUrl" alt="" />
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" v-print="printObj"> </el-button>
</div>
</template>
</el-dialog>
</template>
<style>
/* 修复打印只显示一页 */
@media print {
@page {
size: auto;
}
body,
html,
div {
height: auto !important;
}
}
</style>

View File

@@ -8,7 +8,10 @@
:src="auditIconsMap[processInstance.status]"
alt=""
/>
<div class="text-#878c93 h-15px">编号{{ id }}</div>
<div class="flex">
<div class="text-#878c93 h-15px">编号{{ id }}</div>
<Icon icon="ep:printer" class="ml-15px cursor-pointer" @click="handlePrint" />
</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
@@ -125,6 +128,9 @@
</el-scrollbar>
</div>
</ContentWrap>
<!-- 打印预览弹窗 -->
<PrintDialog ref="printRef" />
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
@@ -146,6 +152,7 @@ import runningSvg from '@/assets/svgs/bpm/running.svg'
import approveSvg from '@/assets/svgs/bpm/approve.svg'
import rejectSvg from '@/assets/svgs/bpm/reject.svg'
import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
import PrintDialog from './PrintDialog.vue'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const props = defineProps<{
@@ -187,6 +194,7 @@ const getDetail = () => {
/** 加载流程实例 */
const BusinessFormComponent = ref<any>(null) // 异步组件
/** 获取审批详情 */
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
const getApprovalDetail = async () => {
processInstanceLoading.value = true
try {
@@ -265,11 +273,7 @@ const getProcessModelView = async () => {
}
}
// 审批节点信息
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
/**
* 设置表单权限
*/
/** 设置表单权限 */
const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) {
//@ts-ignore
@@ -287,15 +291,19 @@ const setFieldPermission = (field: string, permission: string) => {
}
}
/**
* 操作成功后刷新
*/
/** 操作成功后刷新 */
const refresh = () => {
// 重新获取详情
getDetail()
}
/** 当前的Tab */
/** 处理打印 */
const printRef = ref()
const handlePrint = async () => {
printRef.value.open(props.id)
}
/** 当前的 Tab */
const activeTab = ref('form')
/** 初始化 */

View File

@@ -150,7 +150,6 @@ const authDialogVisible = ref(false) // 定义设备认证信息弹框的可见
const authPasswordVisible = ref(false) // 定义密码可见性状态
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) // 定义设备认证信息对象
// TODO @AI注释使用 /** */ 风格,方法注释;
/** 控制地图显示的标志 */
const showMap = computed(() => {
return !!(device.longitude && device.latitude)

View File

@@ -1,15 +1,12 @@
<!-- 值输入组件 -->
<!-- TODO @yunai这个需要在看看 -->
<template>
<div class="w-full min-w-0">
<!-- 布尔值选择 -->
<el-select
v-if="propertyType === 'bool'"
v-if="propertyType === IoTDataSpecsDataTypeEnum.BOOL"
v-model="localValue"
placeholder="请选择布尔值"
@change="handleChange"
class="w-full!"
style="width: 100% !important"
>
<el-option label="真 (true)" value="true" />
<el-option label="假 (false)" value="false" />
@@ -17,12 +14,10 @@
<!-- 枚举值选择 -->
<el-select
v-else-if="propertyType === 'enum' && enumOptions.length > 0"
v-else-if="propertyType === IoTDataSpecsDataTypeEnum.ENUM && enumOptions.length > 0"
v-model="localValue"
placeholder="请选择枚举值"
@change="handleChange"
class="w-full!"
style="width: 100% !important"
>
<el-option
v-for="option in enumOptions"
@@ -34,9 +29,8 @@
<!-- 范围输入 (between 操作符) -->
<div
v-else-if="operator === 'between'"
v-else-if="operator === IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value"
class="w-full! flex items-center gap-8px"
style="width: 100% !important"
>
<el-input
v-model="rangeStart"
@@ -53,19 +47,15 @@
placeholder="最大值"
@input="handleRangeChange"
class="flex-1 min-w-0"
style="width: auto !important"
/>
</div>
<!-- 列表输入 (in 操作符) -->
<div v-else-if="operator === 'in'" class="w-full!" style="width: 100% !important">
<el-input
v-model="localValue"
placeholder="请输入值列表,用逗号分隔"
@input="handleChange"
class="w-full!"
style="width: 100% !important"
>
<div
v-else-if="operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value"
class="w-full!"
>
<el-input v-model="localValue" placeholder="请输入值列表,用逗号分隔" class="w-full!">
<template #suffix>
<el-tooltip content="多个值用逗号分隔1,2,3" placement="top">
<Icon
@@ -85,7 +75,7 @@
<!-- 日期时间输入 -->
<el-date-picker
v-else-if="propertyType === 'date'"
v-else-if="propertyType === IoTDataSpecsDataTypeEnum.DATE"
v-model="dateValue"
type="datetime"
placeholder="请选择日期时间"
@@ -93,7 +83,6 @@
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateChange"
class="w-full!"
style="width: 100% !important"
/>
<!-- 数字输入 -->
@@ -107,7 +96,6 @@
placeholder="请输入数值"
@change="handleNumberChange"
class="w-full!"
style="width: 100% !important"
/>
<!-- 文本输入 -->
@@ -116,9 +104,7 @@
v-model="localValue"
:type="getInputType()"
:placeholder="getPlaceholder()"
@input="handleChange"
class="w-full!"
style="width: 100% !important"
>
<template #suffix>
<el-tooltip
@@ -126,9 +112,9 @@
:content="`单位:${propertyConfig.unit}`"
placement="top"
>
<span class="text-12px text-[var(--el-text-color-secondary)] px-4px">{{
propertyConfig.unit
}}</span>
<span class="text-12px text-[var(--el-text-color-secondary)] px-4px">
{{ propertyConfig.unit }}
</span>
</el-tooltip>
</template>
</el-input>
@@ -137,7 +123,10 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
import {
IoTDataSpecsDataTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum
} from '@/views/iot/utils/constants'
/** 值输入组件 */
defineOptions({ name: 'ValueInput' })
@@ -165,7 +154,7 @@ const rangeEnd = ref('') // 范围结束值
const dateValue = ref('') // 日期值
const numberValue = ref<number>() // 数字值
// 计算属性:枚举选项
/** 计算属性:枚举选项 */
const enumOptions = computed(() => {
if (props.propertyConfig?.enum) {
return props.propertyConfig.enum.map((item: any) => ({
@@ -176,9 +165,12 @@ const enumOptions = computed(() => {
return []
})
// 计算属性:列表预览
/** 计算属性:列表预览 */
const listPreview = computed(() => {
if (props.operator === 'in' && localValue.value) {
if (
props.operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value &&
localValue.value
) {
return localValue.value
.split(',')
.map((item) => item.trim())
@@ -187,10 +179,7 @@ const listPreview = computed(() => {
return []
})
/**
* 判断是否为数字类型
* @returns 是否为数字类型
*/
/** 判断是否为数字类型 */
const isNumericType = () => {
return [
IoTDataSpecsDataTypeEnum.INT,
@@ -199,10 +188,7 @@ const isNumericType = () => {
].includes((props.propertyType || '') as any)
}
/**
* 获取输入框类型
* @returns 输入框类型
*/
/** 获取输入框类型 */
const getInputType = () => {
switch (props.propertyType) {
case IoTDataSpecsDataTypeEnum.INT:
@@ -214,10 +200,7 @@ const getInputType = () => {
}
}
/**
* 获取占位符文本
* @returns 占位符文本
*/
/** 获取占位符文本 */
const getPlaceholder = () => {
const typeMap = {
[IoTDataSpecsDataTypeEnum.TEXT]: '请输入字符串',
@@ -230,48 +213,27 @@ const getPlaceholder = () => {
return typeMap[props.propertyType || ''] || '请输入值'
}
/**
* 获取数字精度
* @returns 数字精度
*/
/** 获取数字精度 */
const getPrecision = () => {
return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 0 : 2
}
/**
* 获取数字步长
* @returns 数字步长
*/
/** 获取数字步长 */
const getStep = () => {
return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 1 : 0.1
}
/**
* 获取最小值
* @returns 最小值
*/
/** 获取最小值 */
const getMin = () => {
return props.propertyConfig?.min || undefined
}
/**
* 获取最大值
* @returns 最大值
*/
/** 获取最大值 */
const getMax = () => {
return props.propertyConfig?.max || undefined
}
/**
* 处理值变化事件
*/
const handleChange = () => {
// 值变化处理
}
/**
* 处理范围变化事件
*/
/** 处理范围变化事件 */
const handleRangeChange = () => {
if (rangeStart.value && rangeEnd.value) {
localValue.value = `${rangeStart.value},${rangeEnd.value}`
@@ -280,23 +242,17 @@ const handleRangeChange = () => {
}
}
/**
* 处理日期变化事件
* @param value 日期值
*/
/** 处理日期变化事件 */
const handleDateChange = (value: string) => {
localValue.value = value || ''
}
/**
* 处理数字变化事件
* @param value 数字值
*/
/** 处理数字变化事件 */
const handleNumberChange = (value: number | undefined) => {
localValue.value = value?.toString() || ''
}
// 监听操作符变化
/** 监听操作符变化 */
watch(
() => props.operator,
() => {

View File

@@ -114,7 +114,7 @@
<el-tag size="small" type="warning">自动执行</el-tag>
</div>
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
当触发条件满足时,系统将自动发送告警通知,无需额外配置
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 -> 告警配置] 管理
</div>
</div>
</div>

View File

@@ -79,9 +79,9 @@
<Icon icon="ep:document" />
</div>
<div>
<div class="text-24px font-600 text-[#303133] leading-none">{{
statistics.total
}}</div>
<div class="text-24px font-600 text-[#303133] leading-none">
{{ statistics.total }}
</div>
<div class="text-14px text-[#909399] mt-4px">总规则数</div>
</div>
</div>
@@ -99,9 +99,9 @@
<Icon icon="ep:check" />
</div>
<div>
<div class="text-24px font-600 text-[#303133] leading-none">{{
statistics.enabled
}}</div>
<div class="text-24px font-600 text-[#303133] leading-none">
{{ statistics.enabled }}
</div>
<div class="text-14px text-[#909399] mt-4px">启用规则</div>
</div>
</div>
@@ -119,9 +119,9 @@
<Icon icon="ep:close" />
</div>
<div>
<div class="text-24px font-600 text-[#303133] leading-none">{{
statistics.disabled
}}</div>
<div class="text-24px font-600 text-[#303133] leading-none">
{{ statistics.disabled }}
</div>
<div class="text-14px text-[#909399] mt-4px">禁用规则</div>
</div>
</div>
@@ -139,9 +139,9 @@
<Icon icon="ep:timer" />
</div>
<div>
<div class="text-24px font-600 text-[#303133] leading-none">{{
statistics.timerRules
}}</div>
<div class="text-24px font-600 text-[#303133] leading-none">
{{ statistics.timerRules }}
</div>
<div class="text-14px text-[#909399] mt-4px">定时规则</div>
</div>
</div>
@@ -214,7 +214,7 @@
</el-table-column>
<el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }">
<div class="flex gap-8px">
<div>
<el-button type="primary" link @click="handleEdit(row)">
<Icon icon="ep:edit" />
编辑
@@ -293,7 +293,6 @@ const statistics = ref({
total: 0,
enabled: 0,
disabled: 0,
triggered: 0, // 已触发的规则数量 (暂时使用启用状态的规则数量)
timerRules: 0 // 定时规则数量
})
@@ -321,9 +320,9 @@ const getRuleSceneSummary = (rule: IotSceneRule) => {
}
// 添加设备信息(如果有)
if (trigger.deviceId) {
description += ` [设备ID: ${trigger.deviceId}]`
description += ` [设备 ID: ${trigger.deviceId}]`
} else if (trigger.productId) {
description += ` [产品ID: ${trigger.productId}]`
description += ` [产品 ID: ${trigger.productId}]`
}
return description
}) || []
@@ -334,13 +333,13 @@ const getRuleSceneSummary = (rule: IotSceneRule) => {
let description = getActionTypeLabel(action.type)
// 添加设备信息(如果有)
if (action.deviceId) {
description += ` [设备ID: ${action.deviceId}]`
description += ` [设备 ID: ${action.deviceId}]`
} else if (action.productId) {
description += ` [产品ID: ${action.productId}]`
description += ` [产品 ID: ${action.productId}]`
}
// 添加告警配置信息(如果有)
if (action.alertConfigId) {
description += ` [告警配置ID: ${action.alertConfigId}]`
description += ` [告警配置 ID: ${action.alertConfigId}]`
}
return description
}) || []
@@ -371,7 +370,6 @@ const updateStatistics = () => {
total: list.value.length,
enabled: list.value.filter((item) => item.status === CommonStatusEnum.ENABLE).length,
disabled: list.value.filter((item) => item.status === CommonStatusEnum.DISABLE).length,
triggered: list.value.filter((item) => item.status === CommonStatusEnum.ENABLE).length,
timerRules: list.value.filter((item) => hasTimerTrigger(item)).length
}
}

View File

@@ -459,6 +459,11 @@ onMounted(() => {
}
getDetail()
})
/** 销毁 */
onBeforeUnmount(() => {
clearQueryInterval()
})
</script>
<style lang="scss" scoped>

View File

@@ -61,7 +61,6 @@
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
style="width: 240px"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始日期"
@@ -122,7 +121,7 @@
label="模板内容"
align="center"
prop="content"
width="200"
min-width="200"
:show-overflow-tooltip="true"
/>
<el-table-column label="邮箱账号" align="center" prop="accountId" width="200">