Explorar el Código

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

YunaiV hace 9 meses
padre
commit
eda7d63e4f
Se han modificado 32 ficheros con 884 adiciones y 185 borrados
  1. 2 1
      build/vite/optimize.ts
  2. 3 0
      package.json
  3. 1 1
      src/api/ai/chat/message/index.ts
  4. 5 0
      src/api/bpm/processInstance/index.ts
  5. 5 22
      src/components/UploadFile/src/useUpload.ts
  6. 11 0
      src/main.ts
  7. 18 1
      src/views/Profile/components/BasicInfo.vue
  8. 13 0
      src/views/Profile/components/ProfileUser.vue
  9. 0 21
      src/views/ai/chat/index/components/role/RoleHeader.vue
  10. 1 1
      src/views/ai/chat/index/components/role/RoleList.vue
  11. 32 24
      src/views/ai/chat/index/components/role/RoleRepository.vue
  12. 1 1
      src/views/ai/chat/index/index.vue
  13. 33 1
      src/views/bpm/model/form/ExtraSettings.vue
  14. 116 0
      src/views/bpm/model/form/PrintTemplate/Index.vue
  15. 110 0
      src/views/bpm/model/form/PrintTemplate/MentionModal.vue
  16. 9 0
      src/views/bpm/model/form/PrintTemplate/index.ts
  17. 12 0
      src/views/bpm/model/form/PrintTemplate/module/elem-to-html.ts
  18. 17 0
      src/views/bpm/model/form/PrintTemplate/module/index.ts
  19. 42 0
      src/views/bpm/model/form/PrintTemplate/module/menu/ProcessRecordMenu.ts
  20. 21 0
      src/views/bpm/model/form/PrintTemplate/module/parse-elem-html.ts
  21. 28 0
      src/views/bpm/model/form/PrintTemplate/module/plugin.ts
  22. 73 0
      src/views/bpm/model/form/PrintTemplate/module/render-elem.ts
  23. 21 0
      src/views/bpm/model/form/PrintTemplate/module/utils/dom.ts
  24. 4 1
      src/views/bpm/model/form/index.vue
  25. 230 0
      src/views/bpm/processInstance/detail/PrintDialog.vue
  26. 18 10
      src/views/bpm/processInstance/detail/index.vue
  27. 0 1
      src/views/iot/device/device/detail/DeviceDetailsInfo.vue
  28. 33 77
      src/views/iot/rule/scene/form/inputs/ValueInput.vue
  29. 1 1
      src/views/iot/rule/scene/form/sections/ActionSection.vue
  30. 18 20
      src/views/iot/rule/scene/index.vue
  31. 5 0
      src/views/pay/cashier/index.vue
  32. 1 2
      src/views/system/mail/template/index.vue

+ 2 - 1
build/vite/optimize.ts

@@ -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']

+ 3 - 0
package.json

@@ -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",

+ 1 - 1
src/api/ai/chat/message/index.ts

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

+ 5 - 0
src/api/bpm/processInstance/index.ts

@@ -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 })
+}

+ 5 - 22
src/components/UploadFile/src/useUpload.ts

@@ -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
-}
-
 /**
  * 上传类型
  */

+ 11 - 0
src/main.ts

@@ -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')
 }
 

+ 18 - 1
src/views/Profile/components/BasicInfo.vue

@@ -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()
 })

+ 13 - 0
src/views/Profile/components/ProfileUser.vue

@@ -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

+ 0 - 21
src/views/ai/chat/index/components/role/RoleHeader.vue

@@ -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>

+ 1 - 1
src/views/ai/chat/index/components/role/RoleList.vue

@@ -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"
   >

+ 32 - 24
src/views/ai/chat/index/components/role/RoleRepository.vue

@@ -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>

+ 1 - 1
src/views/ai/chat/index/index.vue

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

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 33 - 1
src/views/bpm/model/form/ExtraSettings.vue


+ 116 - 0
src/views/bpm/model/form/PrintTemplate/Index.vue

@@ -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>

+ 110 - 0
src/views/bpm/model/form/PrintTemplate/MentionModal.vue

@@ -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 @lesan:css 可以用 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>

+ 9 - 0
src/views/bpm/model/form/PrintTemplate/index.ts

@@ -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)
+}

+ 12 - 0
src/views/bpm/model/form/PrintTemplate/module/elem-to-html.ts

@@ -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

+ 17 - 0
src/views/bpm/model/form/PrintTemplate/module/index.ts

@@ -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

+ 42 - 0
src/views/bpm/model/form/PrintTemplate/module/menu/ProcessRecordMenu.ts

@@ -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

+ 21 - 0
src/views/bpm/model/form/PrintTemplate/module/parse-elem-html.ts

@@ -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

+ 28 - 0
src/views/bpm/model/form/PrintTemplate/module/plugin.ts

@@ -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

+ 73 - 0
src/views/bpm/model/form/PrintTemplate/module/render-elem.ts

@@ -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

+ 21 - 0
src/views/bpm/model/form/PrintTemplate/module/utils/dom.ts

@@ -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 }

+ 4 - 1
src/views/bpm/model/form/index.vue

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

+ 230 - 0
src/views/bpm/processInstance/detail/PrintDialog.vue

@@ -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>

+ 18 - 10
src/views/bpm/processInstance/detail/index.vue

@@ -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')
 
 /** 初始化 */

+ 0 - 1
src/views/iot/device/device/detail/DeviceDetailsInfo.vue

@@ -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)

+ 33 - 77
src/views/iot/rule/scene/form/inputs/ValueInput.vue

@@ -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,
   () => {

+ 1 - 1
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -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>

+ 18 - 20
src/views/iot/rule/scene/index.vue

@@ -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
   }
 }

+ 5 - 0
src/views/pay/cashier/index.vue

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

+ 1 - 2
src/views/system/mail/template/index.vue

@@ -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">

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio