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

View File

@ -115,7 +115,8 @@ const include = [
'@element-plus/icons-vue', '@element-plus/icons-vue',
'element-plus/es/components/footer/style/css', 'element-plus/es/components/footer/style/css',
'element-plus/es/components/empty/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'] const exclude = ['@iconify/json']

View File

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

View File

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

View File

@ -108,3 +108,8 @@ export const getFormFieldsPermission = async (params: any) => {
export const getProcessInstanceBpmnModelView = async (id: string) => { export const getProcessInstanceBpmnModelView = async (id: string) => {
return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id }) 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 * as FileApi from '@/api/infra/file'
// import CryptoJS from 'crypto-js'
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload' import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import axios from 'axios' import axios from 'axios'
@ -20,7 +19,7 @@ export const useUpload = (directory?: string) => {
// 模式一:前端上传 // 模式一:前端上传
if (isClientUpload) { if (isClientUpload) {
// 1.1 生成文件名称 // 1.1 生成文件名称
const fileName = await generateFileName(options.file) const fileName = options.file.name || options.filename
// 1.2 获取文件预签名地址 // 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory) const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持) // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
@ -32,7 +31,7 @@ export const useUpload = (directory?: string) => {
}) })
.then(() => { .then(() => {
// 1.4. 记录文件信息到后端(异步) // 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, options.file) createFile(presignedInfo, options.file, fileName)
// 通知成功,数据格式保持与后端上传的返回结果一致 // 通知成功,数据格式保持与后端上传的返回结果一致
return { data: presignedInfo.url } return { data: presignedInfo.url }
}) })
@ -64,15 +63,15 @@ export const useUpload = (directory?: string) => {
/** /**
* *
* @param vo * @param vo
* @param name
* @param file * @param file
* @param fileName
*/ */
function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) { function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile, fileName: string) {
const fileVo = { const fileVo = {
configId: vo.configId, configId: vo.configId,
url: vo.url, url: vo.url,
path: vo.path, path: vo.path,
name: file.name, name: fileName,
type: file.type, type: file.type,
size: file.size size: file.size
} }
@ -80,22 +79,6 @@ function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) {
return fileVo 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 的安全隐患 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 setupAll = async () => {
const app = createApp(App) const app = createApp(App)
@ -62,10 +67,16 @@ const setupAll = async () => {
setupAuth(app) setupAuth(app)
setupMountedFocus(app) setupMountedFocus(app)
// wangEditor 插件注册
setupWangEditorPlugin()
await router.isReady() await router.isReady()
app.use(VueDOMPurifyHTML) app.use(VueDOMPurifyHTML)
// 打印
app.use(print)
app.mount('#app') app.mount('#app')
} }

View File

@ -78,6 +78,21 @@ const schema = reactive<FormSchema[]>([
} }
]) ])
const formRef = ref<FormExpose>() // Ref 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 submit = () => {
const elForm = unref(formRef)?.getElFormRef() const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return if (!elForm) return
@ -87,17 +102,19 @@ const submit = () => {
await updateUserProfile(data) await updateUserProfile(data)
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
const profile = await init() const profile = await init()
userStore.setUserNicknameAction(profile.nickname) await userStore.setUserNicknameAction(profile.nickname)
// //
emit('success') emit('success')
} }
}) })
} }
const init = async () => { const init = async () => {
const res = await getUserProfile() const res = await getUserProfile()
unref(formRef)?.setValues(res) unref(formRef)?.setValues(res)
return res return res
} }
onMounted(async () => { onMounted(async () => {
await init() await init()
}) })

View File

@ -49,18 +49,31 @@
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import UserAvatar from './UserAvatar.vue' import UserAvatar from './UserAvatar.vue'
import { useUserStore } from '@/store/modules/user'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile' import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
defineOptions({ name: 'ProfileUser' }) defineOptions({ name: 'ProfileUser' })
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore()
const userInfo = ref({} as ProfileVO) const userInfo = ref({} as ProfileVO)
const getUserInfo = async () => { const getUserInfo = async () => {
const users = await getUserProfile() const users = await getUserProfile()
userInfo.value = users userInfo.value = users
} }
// userStore userInfo
watch(
() => userStore.getUser.avatar,
(newAvatar) => {
if (newAvatar && userInfo.value) {
userInfo.value.avatar = newAvatar
}
}
)
// //
defineExpose({ defineExpose({
refresh: getUserInfo 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> <template>
<div <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" ref="tabsRef"
@scroll="handleTabsScroll" @scroll="handleTabsScroll"
> >

View File

@ -1,14 +1,10 @@
<!-- chat 角色仓库 --> <!-- chat 角色仓库 -->
<template> <template>
<el-container <el-container class="bg-[var(--el-bg-color)] -mt-25px">
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"
>
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" /> <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
<!-- header -->
<RoleHeader title="角色仓库" class="relative" />
<!-- main --> <!-- main -->
<el-main class="flex-1 overflow-hidden m-0 !p-0 relative"> <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 <el-input
:loading="loading" :loading="loading"
@ -30,16 +26,8 @@
</el-button> </el-button>
</div> </div>
<!-- tabs --> <!-- tabs -->
<el-tabs <el-tabs v-model="activeTab" @tab-click="handleTabsClick" class="relative h-full">
v-model="activeTab" <el-tab-pane label="我的角色" name="my-role" class="flex flex-col h-full overflow-y-auto">
@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"
>
<RoleList <RoleList
:loading="loading" :loading="loading"
:role-list="myRoleList" :role-list="myRoleList"
@ -48,12 +36,12 @@
@on-edit="handlerCardEdit" @on-edit="handlerCardEdit"
@on-use="handlerCardUse" @on-use="handlerCardUse"
@on-page="handlerCardPage('my')" @on-page="handlerCardPage('my')"
class="mt-20px" class="mt-3"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="公共角色" name="public-role"> <el-tab-pane label="公共角色" name="public-role" class="!pt-2">
<RoleCategoryList <RoleCategoryList
class="mx-6.75" class="mx-3"
:category-list="categoryList" :category-list="categoryList"
:active="activeCategory" :active="activeCategory"
@on-category-click="handlerCategoryClick" @on-category-click="handlerCategoryClick"
@ -64,7 +52,7 @@
@on-edit="handlerCardEdit" @on-edit="handlerCardEdit"
@on-use="handlerCardUse" @on-use="handlerCardUse"
@on-page="handlerCardPage('public')" @on-page="handlerCardPage('public')"
class="mt-20px" class="mt-3"
loading loading
/> />
</el-tab-pane> </el-tab-pane>
@ -75,7 +63,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import RoleHeader from './RoleHeader.vue'
import RoleList from './RoleList.vue' import RoleList from './RoleList.vue'
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue' import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
import RoleCategoryList from './RoleCategoryList.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 { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import { TabsPaneContext } from 'element-plus' import { TabsPaneContext } from 'element-plus'
import { useTagsViewStore } from '@/store/modules/tagsView'
const router = useRouter() // const router = useRouter() //
const { currentRoute } = useRouter() //
const { delView } = useTagsViewStore() //
// //
const loading = ref<boolean>(false) // const loading = ref<boolean>(false) //
@ -134,7 +124,7 @@ const getPublicRole = async (append?: boolean) => {
name: search.value, name: search.value,
publicStatus: true publicStatus: true
} }
const { total, list } = await ChatRoleApi.getMyPage(params) const { list } = await ChatRoleApi.getMyPage(params)
if (append) { if (append) {
publicRoleList.value.push.apply(publicRoleList.value, list) publicRoleList.value.push.apply(publicRoleList.value, list)
} else { } else {
@ -214,7 +204,8 @@ const handlerCardUse = async (role) => {
const conversationId = await ChatConversationApi.createChatConversationMy(data) const conversationId = await ChatConversationApi.createChatConversationMy(data)
// 2. // 2.
await router.push({ delView(unref(currentRoute))
await router.replace({
name: 'AiChat', name: 'AiChat',
query: { query: {
conversationId: conversationId conversationId: conversationId
@ -233,6 +224,23 @@ onMounted(async () => {
<!-- 覆盖 element plus css --> <!-- 覆盖 element plus css -->
<style lang="scss"> <style lang="scss">
.el-tabs__nav-scroll { .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> </style>

View File

@ -516,7 +516,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
}, },
(error: any) => { (error: any) => {
// //
message.alert(`对话异常! ${error}`) message.alert(`对话异常`)
stopStream() stopStream()
// //
throw error throw error

View File

@ -1,5 +1,5 @@
<template> <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"> <el-form-item class="mb-20px">
<template #label> <template #label>
<el-text size="large" tag="b">提交人权限</el-text> <el-text size="large" tag="b">提交人权限</el-text>
@ -231,7 +231,30 @@
/> />
</div> </div>
</el-form-item> </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> </el-form>
<print-template ref="printTemplateRef" @confirm="confirmPrintTemplate" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -241,6 +264,7 @@ import * as FormApi from '@/api/bpm/form'
import { parseFormFields } from '@/components/FormCreate/src/utils' import { parseFormFields } from '@/components/FormCreate/src/utils'
import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts' import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue' import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
import PrintTemplate from './PrintTemplate/Index.vue'
const modelData = defineModel<any>() const modelData = defineModel<any>()
@ -394,6 +418,7 @@ const formFieldOptions4Summary = computed(() => {
const unParsedFormFields = ref<string[]>([]) const unParsedFormFields = ref<string[]>([])
/** 暴露给子组件 HttpRequestSetting 使用 */ /** 暴露给子组件 HttpRequestSetting 使用 */
provide('formFields', unParsedFormFields) provide('formFields', unParsedFormFields)
provide('formFieldsObj', formFields)
/** 兼容以前未配置更多设置的流程 */ /** 兼容以前未配置更多设置的流程 */
const initData = () => { const initData = () => {
@ -436,6 +461,11 @@ const initData = () => {
if (modelData.value.allowWithdrawTask) { if (modelData.value.allowWithdrawTask) {
modelData.value.allowWithdrawTask = false modelData.value.allowWithdrawTask = false
} }
if (!modelData.value.printTemplateSetting) {
modelData.value.printTemplateSetting = {
enable: false
}
}
} }
defineExpose({ initData }) defineExpose({ initData })
@ -460,4 +490,21 @@ watch(
}, },
{ immediate: true } { 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> </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, enable: false,
summary: [] 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]" :src="auditIconsMap[processInstance.status]"
alt="" 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" /> <el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px"> <div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div> <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
@ -125,6 +128,9 @@
</el-scrollbar> </el-scrollbar>
</div> </div>
</ContentWrap> </ContentWrap>
<!-- 打印预览弹窗 -->
<PrintDialog ref="printRef" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate } from '@/utils/formatTime' 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 approveSvg from '@/assets/svgs/bpm/approve.svg'
import rejectSvg from '@/assets/svgs/bpm/reject.svg' import rejectSvg from '@/assets/svgs/bpm/reject.svg'
import cancelSvg from '@/assets/svgs/bpm/cancel.svg' import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
import PrintDialog from './PrintDialog.vue'
defineOptions({ name: 'BpmProcessInstanceDetail' }) defineOptions({ name: 'BpmProcessInstanceDetail' })
const props = defineProps<{ const props = defineProps<{
@ -187,6 +194,7 @@ const getDetail = () => {
/** 加载流程实例 */ /** 加载流程实例 */
const BusinessFormComponent = ref<any>(null) // const BusinessFormComponent = ref<any>(null) //
/** 获取审批详情 */ /** 获取审批详情 */
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) //
const getApprovalDetail = async () => { const getApprovalDetail = async () => {
processInstanceLoading.value = true processInstanceLoading.value = true
try { try {
@ -265,11 +273,7 @@ const getProcessModelView = async () => {
} }
} }
// /** 设置表单权限 */
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => { const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) { if (permission === FieldPermissionType.READ) {
//@ts-ignore //@ts-ignore
@ -287,15 +291,19 @@ const setFieldPermission = (field: string, permission: string) => {
} }
} }
/** /** 操作成功后刷新 */
* 操作成功后刷新
*/
const refresh = () => { const refresh = () => {
// //
getDetail() getDetail()
} }
/** 当前的Tab */ /** 处理打印 */
const printRef = ref()
const handlePrint = async () => {
printRef.value.open(props.id)
}
/** 当前的 Tab */
const activeTab = ref('form') const activeTab = ref('form')
/** 初始化 */ /** 初始化 */

View File

@ -150,7 +150,6 @@ const authDialogVisible = ref(false) // 定义设备认证信息弹框的可见
const authPasswordVisible = ref(false) // const authPasswordVisible = ref(false) //
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) // const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) //
// TODO @AI使 /** */
/** 控制地图显示的标志 */ /** 控制地图显示的标志 */
const showMap = computed(() => { const showMap = computed(() => {
return !!(device.longitude && device.latitude) return !!(device.longitude && device.latitude)

View File

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

View File

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

View File

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

View File

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

View File

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