Browse Source

Merge remote-tracking branch 'origin/master'

lanceJiang 9 months ago
parent
commit
3aae224d68

+ 1 - 1
.ai-commit.json

@@ -123,6 +123,6 @@
         }
     },
     "prompts": {
-        "conventional": "Here are some best practices for writing commit messages:\n- Write clear, concise, and descriptive messages that explain the changes made in the commit.\n- Use the present tense and active voice in the message, for example, \"Fix bug\" instead of \"Fixed bug.\"\n- Use the imperative mood, which gives the message a sense of command, e.g. \"Add feature\" instead of \"Added feature\"\n- Limit the subject line to 72 characters or less.\n- Capitalize the subject line.\n- Do not end the subject line with a period.\n- Limit the body of the message to 256 characters or less.\n- Use a blank line between the subject and the body of the message.\n- Use the body of the message to provide additional context or explain the reasoning behind the changes.\n- Avoid using general terms like \"update\" or \"change\" in the subject line, be specific about what was updated or changed.\n- Explain, What was done at a glance in the subject line, and provide additional context in the body of the message.\n- Why the change was necessary in the body of the message.\n- The details about what was done in the body of the message.\n- Any useful details concerning the change in the body of the message.\n- Use a hyphen (-) for the bullet points in the body of the message.\n<type-prompt>\nWrite 1 commit messages that accurately summarizes the changes made in the given `git diff` output, following the best practices listed above and the conventional commit format in chinese.\nPlease provide a response in the form of a valid JSON object and do not include \"Output:\", \"Response:\" or anything similar to those two before it, in the following format:\n{\n    \"subject\": \"<type>(<scope>): <subject>\",\n    \"body\": \"<BODY (bullet points)>\"\n}\n\nHere is the output of the `git diff`:\n<diff>"
+        "conventional": "Here are some best practices for writing commit messages:\n- Write clear, concise, and descriptive messages that explain the changes made in the commit.\n- Use the present tense and active voice in the message, for example, \"Fix bug\" instead of \"Fixed bug.\"\n- Use the imperative mood, which gives the message a sense of command, e.g. \"Add feature\" instead of \"Added feature\"\n- Limit the subject line to 72 characters or less.\n- Capitalize the subject line.\n- Do not end the subject line with a period.\n- Limit the body of the message to 256 characters or less.\n- Use a blank line between the subject and the body of the message.\n- Use the body of the message to provide additional context or explain the reasoning behind the changes.\n- Avoid using general terms like \"update\" or \"change\" in the subject line, be specific about what was updated or changed.\n- Explain, What was done at a glance in the subject line, and provide additional context in the body of the message.\n- Why the change was necessary in the body of the message.\n- The details about what was done in the body of the message.\n- Any useful details concerning the change in the body of the message.\n- Use a hyphen (-) for the bullet points in the body of the message.\n<type-prompt>\nWrite 1 commit messages that accurately summarizes the changes made in the given `git diff` output, following the best practices listed above and the conventional commit format in Chinese.\nPlease provide a response in the form of a valid JSON object and do not include \"Output:\", \"Response:\" or anything similar to those two before it, in the following format:\n{\n    \"subject\": \"<type>(<scope>): <subject>\",\n    \"body\": \"<BODY (bullet points)>\"\n}\n\nHere is the output of the `git diff`:\n<diff>"
     }
 }

+ 1 - 0
package.json

@@ -44,6 +44,7 @@
     "dayjs": "^1.11.7",
     "echarts": "^5.4.2",
     "element-plus": "2.3.4",
+    "event-source-polyfill": "^1.0.31",
     "everright-filter": "^1.1.1",
     "html2canvas": "^1.4.1",
     "js-md5": "^0.7.3",

+ 35 - 0
src/.cursorrules

@@ -0,0 +1,35 @@
+You are an expert in TypeScript, Node.js, Vite, Vue.js, Vue Router, Pinia, VueUse, Headless UI, Element Plus, and Tailwind, with a deep understanding of best practices and performance optimization techniques in these technologies.
+
+Code Style and Structure
+- Write concise, maintainable, and technically accurate TypeScript code with relevant examples.
+- Use functional and declarative programming patterns; avoid classes.
+- Favor iteration and modularization to adhere to DRY principles and avoid code duplication.
+- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
+- Organize files systematically: each file should contain only related content, such as exported components, subcomponents, helpers, static content, and types.
+
+Naming Conventions
+- Use lowercase with dashes for directories (e.g., components/auth-wizard).
+- Favor named exports for functions.
+
+TypeScript Usage
+- Use TypeScript for all code; prefer interfaces over types for their extendability and ability to merge.
+- Avoid enums; use maps instead for better type safety and flexibility.
+- Use functional components with TypeScript interfaces.
+
+Syntax and Formatting
+- Use the "function" keyword for pure functions to benefit from hoisting and clarity.
+- Always use the Vue Composition API script setup style.
+
+UI and Styling
+- Use Element UI, Element Plus, and Tailwind for components and styling.
+- Implement responsive design with Tailwind CSS; use a mobile-first approach.
+
+Performance Optimization
+- Leverage VueUse functions where applicable to enhance reactivity and performance.
+- Wrap asynchronous components in Suspense with a fallback UI.
+- Use dynamic loading for non-critical components.
+- Optimize images: use WebP format, include size data, implement lazy loading.
+- Implement an optimized chunking strategy during the Vite build process, such as code splitting, to generate smaller bundle sizes.
+
+Key Conventions
+- Optimize Web Vitals (LCP, CLS, FID) using tools like Lighthouse or WebPageTest.

+ 1 - 1
src/api/flow/processTask.ts

@@ -16,7 +16,7 @@ const api = {
 	transfer: '/v1/process-task/transfer', // 转交任务
 	claimProcess: '/v1/process-task/claim', // 认领任务
 	revokeProcess: '/v1/process-task/revoke', // 撤回任务
-	previousNodeName: '/v1/process-task/previous-node-names', // 以前节点名称列表
+	previousNodeName: '/v1/process-task/previous-nodes', // 以前节点名称列表
 	processTaskJump: '/v1/process-task/jump', // 跳到指定节点任务
 	countTask: '/v1/process-task/count-pending-approval' // 我的待办数量
 }

+ 19 - 1
src/api/system/message.ts

@@ -20,6 +20,14 @@ function getMessageInfo(data: any): AxiosPromise {
 	})
 }
 
+function getMessageMyInfoApi(data: any): AxiosPromise {
+	return request({
+		url: '/sys/message/page-my',
+		method: 'post',
+		data
+	})
+}
+
 function getMessageInfoById(id: any): AxiosPromise {
 	return request({
 		url: '/sys/message/get?id=' + id,
@@ -27,6 +35,14 @@ function getMessageInfoById(id: any): AxiosPromise {
 	})
 }
 
+function readMessageApi(data: any): AxiosPromise {
+	return request({
+		url: '/sys/message/read',
+		method: 'post',
+		data
+	})
+}
+
 /**
  * 扩展配置 - 删除
  */
@@ -41,5 +57,7 @@ export default {
 	getMessage,
 	getMessageInfo,
 	getMessageInfoById,
-	messageDeleteApi
+	messageDeleteApi,
+	getMessageMyInfoApi,
+	readMessageApi
 }

+ 61 - 39
src/components/scWorkflow/nodes/branch.vue

@@ -14,19 +14,20 @@
 								</div>
 								<div class="title">
 									<span class="node-title" :class="[index === nodeConfig.conditionNodes.length - 1 ? 'last-child-title' : '']">{{
-										index === nodeConfig.conditionNodes.length - 1 ? '默认条件' : item.nodeName
-									}}</span>
+											index === nodeConfig.conditionNodes.length - 1 ? '默认条件' : item.nodeName
+										}}</span>
 									<span class="priority-title">优先级{{ item.priorityLevel }}</span>
 									<el-icon v-if="index != nodeConfig.conditionNodes.length - 1" class="close" @click.stop="delTerm(index)">
 										<Close />
 									</el-icon>
 								</div>
-								<div
-									class="content"
-									:class="[index === nodeConfig.conditionNodes.length - 1 ? 'last-child-title' : '']"
-									style="width: 200px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis"
-								>
-									<span v-if="toText(nodeConfig, index)" :title="toText(nodeConfig, index)">{{ toText(nodeConfig, index) }}</span>
+								<div class="content" :class="[index === nodeConfig.conditionNodes.length - 1 ? 'last-child-title' : '']" style="width: 200px">
+									<el-tooltip v-if="toText(nodeConfig, index)" effect="dark" placement="top">
+										<div class="text-overflow_ellipsis_line_2" v-html="toText(nodeConfig, index)" />
+										<template #content>
+											<div v-html="toText(nodeConfig, index)" />
+										</template>
+									</el-tooltip>
 									<span v-else class="placeholder"> 请设置条件 </span>
 								</div>
 								<!-- 最后一个没有,长度大于2倒数第二个没有 -->
@@ -65,7 +66,15 @@
 						<!--							-->
 						<!--						</div>-->
 					</label>
-					<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle" class="w-40"></el-input>
+					<el-input
+						v-if="isEditTitle"
+						ref="nodeTitle"
+						v-model="form.nodeName"
+						clearable
+						class="w-40"
+						@blur="saveTitle"
+						@keyup.enter="saveTitle"
+					></el-input>
 					<el-input v-model="form.nodeKey" clearable class="w-40 pl-1.5" placeholder="请填写nodeKey"></el-input>
 				</div>
 			</template>
@@ -123,12 +132,12 @@
 								</div>
 							</div>
 							<div class="sub-content">
-								<el-button link type="primary" :icon="Plus" @click="addConditionList(conditionGroup, 'custom')"> 添加自定义条件 </el-button>
+								<el-button link type="primary" icon="Plus" @click="addConditionList(conditionGroup, 'custom')"> 添加自定义条件 </el-button>
 								<el-button
 									v-if="expressionFormList.length"
 									link
 									type="primary"
-									:icon="Plus"
+									icon="Plus"
 									style="margin-left: 8px"
 									@click="addConditionList(conditionGroup, 'form')"
 								>
@@ -137,7 +146,7 @@
 							</div>
 						</div>
 					</template>
-					<el-button style="width: 100%" type="info" :icon="Plus" text bg @click="addConditionGroup"> 添加条件组 </el-button>
+					<el-button style="width: 100%" type="info" icon="Plus" text bg @click="addConditionGroup"> 添加条件组 </el-button>
 				</el-main>
 				<el-footer>
 					<el-button type="primary" @click="save"> 保存 </el-button>
@@ -155,7 +164,8 @@ import useFlowStore from '@/store/modules/flow'
 import { Delete, Plus, ArrowLeft, Close, ArrowRight, Edit } from '@element-plus/icons-vue'
 import { mapState } from 'pinia'
 import { getNodeKey } from '@/utils/workflow'
-import {ElMessage} from "element-plus";
+import { ElMessage } from 'element-plus'
+import { $log } from '@/utils'
 
 export default {
 	components: {
@@ -176,18 +186,27 @@ export default {
 			isEditTitle: false,
 			index: 0,
 			form: {},
-			expressionFormList: []
+			expressionFormList: [],
+			local_formStructure: {}
 		}
 	},
 	computed: {
-		Plus() {
-			return Plus
-		},
 		...mapState(useFlowStore, ['processForm']) //映射函数,取出processForm
 	},
 	watch: {
 		modelValue() {
 			this.nodeConfig = this.modelValue
+		},
+		processForm: {
+			handler(processForm) {
+				// console.error(val, 'processForm  change...', typeof val)
+				const { formStructure } = JSON.parse(processForm) // 表单设计字段
+				this.local_formStructure = formStructure
+				// 使用 filter 方法找到 required 属性为 true 的对象
+				this.expressionFormList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
+				$log(this.expressionFormList, '这里打印出符合条件的表单对象')
+			},
+			immediate: true
 		}
 	},
 	mounted() {
@@ -203,11 +222,11 @@ export default {
 			this.index = index
 			this.form = {}
 			this.form = JSON.parse(JSON.stringify(this.nodeConfig.conditionNodes[index]))
-			const { formStructure } = JSON.parse(this.processForm) // 表单设计字段
-			// 使用 filter 方法找到 required 属性为 true 的对象
-			const requiredList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
-			console.log(`%c 这里打印出符合条件的表单对象-=== ${JSON.stringify(requiredList)}`, 'background: orange; color: #fff')
-			this.expressionFormList = requiredList || []
+			// const { formStructure } = JSON.parse(this.processForm) // 表单设计字段
+			// // 使用 filter 方法找到 required 属性为 true 的对象
+			// const requiredList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
+			// console.log(`%c 这里打印出符合条件的表单对象-=== ${JSON.stringify(requiredList)}`, 'background: orange; color: #fff')
+			// this.expressionFormList = requiredList || []
 			this.drawer = true
 		},
 		editTitle() {
@@ -285,27 +304,30 @@ export default {
 		},
 		toText(nodeConfig, index) {
 			var { conditionList } = nodeConfig.conditionNodes[index]
-			if (conditionList && conditionList.length == 1) {
-				const text = conditionList.map(conditionGroup =>
-					conditionGroup
-						.map(item => {
-							const showOperator = this.operatorType.find(i => i.value === item.operator).label
-							if (item.type === 'form') {
-								return `${item.showLabel}${showOperator}${item.value}`
-							} else {
-								return `${item.label}${showOperator}${item.value}`
-							}
-						})
-						.join(' 和 ')
-				)
+			if (conditionList && conditionList.length) {
+				const text = conditionList
+					.map(conditionGroup =>
+						conditionGroup
+							.map(item => {
+								const showOperator = this.operatorType.find(i => i.value === item.operator).label
+								if (item.type === 'form') {
+									let showLabel = item.showLabel
+									if (!showLabel) showLabel = this.expressionFormList.find(i => i.key === item.field)?.label
+									return `${showLabel}${showOperator}${item.value}`
+								} else {
+									return `${item.label}${showOperator}${item.value}`
+								}
+							})
+							.join(' <span style="color: var(--el-color-warning)">且</span> ')
+					)
+					.join('<br/> <span style="color: var(--el-color-success)">或</span> <br/>')
+				// console.error(text, 'text')
 				return text
-			} else if (conditionList && conditionList.length > 1) {
-				return conditionList.length + '个条件,或满足'
 			} else {
-				if (index == nodeConfig.conditionNodes.length - 1) {
+				if (index === nodeConfig.conditionNodes.length - 1) {
 					return '未满足时其他条件时,将进入默认流程'
 				} else {
-					return false
+					return ''
 				}
 			}
 		},

+ 54 - 39
src/components/scWorkflow/nodes/mergeBranch.vue

@@ -16,7 +16,7 @@
 									<span style="display: inline-block; width: 115px"
 										><le-text class="flex-1" tag="b" :value="index === nodeConfig.inclusiveNodes.length - 1 ? '默认条件' : item.nodeName"
 									/></span>
-									<div class="close" v-if="index != nodeConfig.inclusiveNodes.length - 1">
+									<div v-if="index != nodeConfig.inclusiveNodes.length - 1" class="close">
 										<el-icon class="mx-1" @click.stop="copyTerm(index)">
 											<CopyDocument />
 										</el-icon>
@@ -24,14 +24,14 @@
 											<Close />
 										</el-icon>
 									</div>
-
 								</div>
-								<div
-									class="content"
-									:class="[index === nodeConfig.inclusiveNodes.length - 1 ? 'last-child-title' : '']"
-									style="width: 200px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis"
-								>
-									<span v-if="toText(nodeConfig, index)" :title="toText(nodeConfig, index)">{{ toText(nodeConfig, index) }}</span>
+								<div class="content" :class="[index === nodeConfig.inclusiveNodes.length - 1 ? 'last-child-title' : '']" style="width: 200px">
+									<el-tooltip v-if="toText(nodeConfig, index)" effect="dark" placement="top">
+										<div class="text-overflow_ellipsis_line_2" v-html="toText(nodeConfig, index)" />
+										<template #content>
+											<div v-html="toText(nodeConfig, index)" />
+										</template>
+									</el-tooltip>
 									<span v-else class="placeholder"> 请设置条件 </span>
 								</div>
 								<!-- 最后一个没有,长度大于2倒数第二个没有 -->
@@ -73,7 +73,15 @@
 						<!--							-->
 						<!--						</div>-->
 					</label>
-					<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle" class="w-40"></el-input>
+					<el-input
+						v-if="isEditTitle"
+						ref="nodeTitle"
+						v-model="form.nodeName"
+						clearable
+						class="w-40"
+						@blur="saveTitle"
+						@keyup.enter="saveTitle"
+					></el-input>
 					<el-input v-model="form.nodeKey" clearable class="w-40 pl-1.5" placeholder="请填写nodeKey"></el-input>
 				</div>
 			</template>
@@ -131,12 +139,12 @@
 								</div>
 							</div>
 							<div class="sub-content">
-								<el-button link type="primary" :icon="Plus" @click="addConditionList(conditionGroup, 'custom')"> 添加自定义条件 </el-button>
+								<el-button link type="primary" icon="Plus" @click="addConditionList(conditionGroup, 'custom')"> 添加自定义条件 </el-button>
 								<el-button
 									v-if="expressionFormList.length"
 									link
 									type="primary"
-									:icon="Plus"
+									icon="Plus"
 									style="margin-left: 8px"
 									@click="addConditionList(conditionGroup, 'form')"
 								>
@@ -145,7 +153,7 @@
 							</div>
 						</div>
 					</template>
-					<el-button style="width: 100%" type="info" :icon="Plus" text bg @click="addConditionGroup"> 添加条件组 </el-button>
+					<el-button style="width: 100%" type="info" icon="Plus" text bg @click="addConditionGroup"> 添加条件组 </el-button>
 				</el-main>
 				<el-footer>
 					<el-button type="primary" @click="save"> 保存 </el-button>
@@ -163,7 +171,8 @@ import useFlowStore from '@/store/modules/flow'
 import { Delete, Plus, ArrowLeft, Close, ArrowRight, Edit } from '@element-plus/icons-vue'
 import { mapState } from 'pinia'
 import { getNodeKey } from '@/utils/workflow'
-import {ElMessage} from "element-plus";
+import { ElMessage } from 'element-plus'
+import { $log } from '@/utils'
 
 export default {
 	components: {
@@ -184,18 +193,27 @@ export default {
 			isEditTitle: false,
 			index: 0,
 			form: {},
-			expressionFormList: []
+			expressionFormList: [],
+			local_formStructure: {}
 		}
 	},
 	computed: {
-		Plus() {
-			return Plus
-		},
 		...mapState(useFlowStore, ['processForm']) //映射函数,取出processForm
 	},
 	watch: {
 		modelValue() {
 			this.nodeConfig = this.modelValue
+		},
+		processForm: {
+			handler(processForm) {
+				// console.error(val, 'processForm  change...', typeof val)
+				const { formStructure } = JSON.parse(processForm) // 表单设计字段
+				this.local_formStructure = formStructure
+				// 使用 filter 方法找到 required 属性为 true 的对象
+				this.expressionFormList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
+				$log(this.expressionFormList, '这里打印出符合条件的表单对象')
+			},
+			immediate: true
 		}
 	},
 	mounted() {
@@ -211,11 +229,6 @@ export default {
 			this.index = index
 			this.form = {}
 			this.form = JSON.parse(JSON.stringify(this.nodeConfig.inclusiveNodes[index]))
-			const { formStructure } = JSON.parse(this.processForm) // 表单设计字段
-			// 使用 filter 方法找到 required 属性为 true 的对象
-			const requiredList = (formStructure?.fields || []).filter(item => item.options && item.options.required === true)
-			console.log(`%c 这里打印出符合条件的表单对象-=== ${JSON.stringify(requiredList)}`, 'background: orange; color: #fff')
-			this.expressionFormList = requiredList || []
 			this.drawer = true
 		},
 		editTitle() {
@@ -305,27 +318,29 @@ export default {
 		},
 		toText(nodeConfig, index) {
 			var { conditionList } = nodeConfig.inclusiveNodes[index]
-			if (conditionList && conditionList.length == 1) {
-				const text = conditionList.map(conditionGroup =>
-					conditionGroup
-						.map(item => {
-							const showOperator = this.operatorType.find(i => i.value === item.operator).label
-							if (item.type === 'form') {
-								return `${item.showLabel}${showOperator}${item.value}`
-							} else {
-								return `${item.label}${showOperator}${item.value}`
-							}
-						})
-						.join(' 和 ')
-				)
+			if (conditionList && conditionList.length) {
+				const text = conditionList
+					.map(conditionGroup =>
+						conditionGroup
+							.map(item => {
+								const showOperator = this.operatorType.find(i => i.value === item.operator).label
+								if (item.type === 'form') {
+									let showLabel = item.showLabel
+									if (!showLabel) showLabel = this.expressionFormList.find(i => i.key === item.field)?.label
+									return `${showLabel}${showOperator}${item.value}`
+								} else {
+									return `${item.label}${showOperator}${item.value}`
+								}
+							})
+							.join(' <span style="color: var(--el-color-warning)">且</span> ')
+					)
+					.join('<br/> <span style="color: var(--el-color-success)">或</span> <br/>')
 				return text
-			} else if (conditionList && conditionList.length > 1) {
-				return conditionList.length + '个条件,或满足'
 			} else {
-				if (index == nodeConfig.inclusiveNodes.length - 1) {
+				if (index === nodeConfig.inclusiveNodes.length - 1) {
 					return '未满足时其他条件时,将进入默认流程'
 				} else {
-					return false
+					return ''
 				}
 			}
 		},

+ 174 - 42
src/layout/components/Header/components/Message.vue

@@ -4,67 +4,199 @@
 			<div class="menu--message-trigger">
 				<el-tooltip :content="$t('le.message.txt')" effect="dark" placement="bottom">
 					<div class="menu--message menu-item le-hover-effect--bg">
-						<el-badge :value="90" :max="99">
+						<el-badge :value="total" :max="99">
 							<i class="le-iconfont le-notice"></i>
 						</el-badge>
 					</div>
 				</el-tooltip>
 			</div>
 		</template>
-		<ul>
-			<li v-for="(item, idx) in receivedData" :key="idx">{{ item }}</li>
-		</ul>
+		<el-tabs class="message-tabs" v-model="activeTab">
+			<el-tab-pane v-for="v of tabsConfig" :key="v.name" :name="v.name">
+				<template #label> {{ v.label }}({{ v.list.length }}) </template>
+				<template v-if="v.list.length">
+					<div class="message-list">
+						<div v-for="item of v.list" :key="item.id" class="message-item" @click="jumpMessageDetail(item.id)">
+							<!--<img src="" alt="" class="message-icon" />-->
+							<div class="message-content">
+								<div class="message-title">
+									<span class="txt text-overflow_ellipsis" :title="item.title">{{ item.title }}</span>
+									<span class="message-date">{{ item.createTime }}</span>
+								</div>
+								<span class="message-txt">{{ item.content }}</span>
+							</div>
+						</div>
+						<div class="message-fix-item" @click="jumpMessageInfo">查看更多</div>
+					</div>
+				</template>
+				<template v-else>
+					<LeNoData />
+				</template>
+			</el-tab-pane>
+		</el-tabs>
 	</el-popover>
 </template>
-<script setup>
-import { onMounted, ref, onBeforeUnmount } from 'vue'
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import message from '@/api/system/message'
+import router from '@/router'
+import { EventSourcePolyfill } from 'event-source-polyfill'
 import { ls } from '@/utils'
+import { ElNotification } from 'element-plus'
 const token = ls.get('token')
 const { VITE_APP_BASE_API } = import.meta.env
-const serviceUrlApi = ref(`${VITE_APP_BASE_API}/sys/sse/connect`);
-// const serviceUrlApi = ref(`http://localhost:3000/events`)
-let sseService = ref(null)
-let receivedData = ref([])
+const serviceUrlApi = ref(`${VITE_APP_BASE_API}/sys/sse/connect`)
+let eventSource: EventSourcePolyfill | null = null
 
-// 创建一个新的 EventSource,但首先使用 fetch API 发送带有 Authorization 头的请求
-function connectSSEWithHeaders(url, headers = {}) {
-	return fetch(url, {
-		method: 'GET',
-		headers: headers,
-		cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
-		mode: 'cors', // no-cors, cors, *same-origin
-		credentials: 'same-origin', // include, *same-origin, omit
-		redirect: 'follow', // manual, *follow, error
-		referrerPolicy: 'no-referrer' // no-referrer, *client
+// noticeList  通知     messageList   消息     todoList   待办
+const tabsConfig = reactive({
+	noticeList: {
+		name: 'noticeList',
+		label: '通知',
+		list: []
+	},
+	messageList: {
+		name: 'messageList',
+		label: '消息',
+		list: []
+	},
+	todoList: {
+		name: 'todoList',
+		label: '待办',
+		list: []
+	}
+})
+const activeTab = ref('noticeList')
+const total = ref(0)
+message.getMessage().then(res => {
+	let _total = 0
+	Object.keys(tabsConfig).map(key => {
+		if (res[key]) {
+			tabsConfig[key].list = res[key]
+			_total += res[key].length
+		} else {
+			res[key].list = []
+		}
 	})
-		.then(response => {
-			if (!response.ok) {
-				throw new Error('网络响应失败')
-			}
-			return new EventSource(url)
-		})
-		.catch(error => {
-			console.error('There was a problem with your fetch operation:', error)
-		})
+	total.value = _total
+})
+
+const jumpMessageInfo = () => {
+	router.push('/message/list')
+}
+
+const jumpMessageDetail = (id: any) => {
+	router.push('/message/list?id=' + id)
 }
 
 onMounted(() => {
-	connectSSEWithHeaders(serviceUrlApi.value, { accessToken: token }).then(eventSource => {
-		sseService.value = eventSource
-		eventSource.onmessage = function (e) {
-			console.log('后端返回的数据:', e.data)
-			receivedData.value.push(e.data)
-		}
-		eventSource.onerror = function (err) {
-			console.error('EventSource failed:', err)
+	eventSource = new EventSourcePolyfill(serviceUrlApi.value, {
+		headers: { accessToken: token }
+	})
+
+	eventSource.onmessage = (event: MessageEvent) => {
+		// const data = JSON.parse()
+		console.log('Received message-----:', event)
+
+		const eventType = event.type
+		const notificationTitles: { [key: string]: string } = {
+			message: '消息',
+			notice: '通知',
+			remind: '待办'
 		}
+		const notificationTitle = notificationTitles[eventType] || '您有一条新消息'
+		console.log('Received message:', event.data)
+		// 处理接收到的消息
+		ElNotification({
+			title: notificationTitle,
+			message: event.data,
+			type: 'success',
+			duration: 10000
+		})
+	}
+
+	eventSource.addEventListener('remind', (event: MessageEvent) => {
+		const data = JSON.parse(event.data)
+		console.log('Received remind event:', event)
+		// 处理接收到的提醒消息
+		ElNotification({
+			title: data.title,
+			message: data.content,
+			type: 'success',
+			duration: 10000
+		})
 	})
-})
 
-onBeforeUnmount(() => {
-	if (sseService.value) {
-		sseService.value.close()
-		sseService.value = null
+	eventSource.onerror = (error: any) => {
+		console.error('EventSource failed:', error)
 	}
 })
+
+// onBeforeUnmount(() => {
+// 	if (eventSource) {
+// 		eventSource.close()
+// 	}
+// })
 </script>
+
+<style scoped lang="scss">
+.message-list {
+	display: flex;
+	flex-direction: column;
+	max-height: 390px;
+	overflow-y: auto;
+	.message-item {
+		display: flex;
+		align-items: center;
+		padding: 8px 0;
+		border-bottom: 1px solid var(--el-border-color-light);
+		cursor: pointer;
+		&:last-child {
+			border: none;
+		}
+		.message-content {
+			display: flex;
+			flex-direction: column;
+			width: 100%;
+			.message-title {
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+				margin-bottom: 5px;
+				font-weight: 600;
+				color: var(--el-text-color-primary);
+			}
+			.message-date {
+				font-size: 12px;
+				color: var(--el-text-color-secondary);
+				margin-left: 10px;
+				font-weight: 400;
+				flex-shrink: 0;
+			}
+			.message-txt {
+				//margin-bottom: 5px;
+				color: var(--el-text-color-regular);
+			}
+		}
+	}
+	.message-fix-item {
+		// flex 居中
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		height: 40px;
+		cursor: pointer;
+	}
+}
+.menu--message-trigger {
+	height: 100%;
+}
+.menu--message {
+	width: 40px;
+	justify-content: center;
+	//.le-notice {
+	//.le-iconfont {
+	//}
+}
+</style>

+ 26 - 39
src/layout/components/Header/components/Message1.vue

@@ -12,18 +12,18 @@
 			</div>
 		</template>
 		<ul>
-			<li v-for="(item, idx) in receivedData" :key="idx"> {{ item }}</li>
+			<li v-for="(item, idx) in receivedData" :key="idx">{{ item }}</li>
 		</ul>
 	</el-popover>
 </template>
 <script setup>
-import { onMounted, ref, onBeforeUnmount } from 'vue';
-import {ls} from "@/utils";
+import { onMounted, ref, onBeforeUnmount } from 'vue'
+import { ls } from '@/utils'
 const token = ls.get('token')
-const { VITE_APP_BASE_API } = import.meta.env;
+const { VITE_APP_BASE_API } = import.meta.env
 // const serviceUrlApi = ref(`${VITE_APP_BASE_API}/sys/sse/connect`);
-const serviceUrlApi = ref(`http://localhost:3000/events`);
-let sseService = ref(null);
+const serviceUrlApi = ref(`http://localhost:3000/events`)
+let sseService = ref(null)
 let receivedData = ref([])
 
 // 创建一个新的 EventSource,但首先使用 fetch API 发送带有 Authorization 头的请求
@@ -35,49 +35,36 @@ function connectSSEWithHeaders(url, headers = {}) {
 		mode: 'cors', // no-cors, cors, *same-origin
 		credentials: 'same-origin', // include, *same-origin, omit
 		redirect: 'follow', // manual, *follow, error
-		referrerPolicy: 'no-referrer', // no-referrer, *client
+		referrerPolicy: 'no-referrer' // no-referrer, *client
 	})
 		.then(response => {
 			if (!response.ok) {
-				throw new Error('Network response was not ok');
+				throw new Error('Network response was not ok')
 			}
-			return new EventSource(url);
-			// 读取响应体作为文本,然后解析URL来创建 EventSource
-			// const t = response.json()
-			// console.log(t);
-			// return response.text().then(text => {
-			//      console.info('text', text)
-			//       // 这里通常不需要解析text,因为我们只是要确认连接成功。
-			//       // 但如果SSE URL在响应体中,你可能需要处理它。
-			//       // 假设服务器直接支持SSE在原始URL
-			//
-			//   });
+			return new EventSource(url)
 		})
 		.catch(error => {
-			console.error('There was a problem with your fetch operation:', error);
-		});
+			console.error('There was a problem with your fetch operation:', error)
+		})
 }
 
-
 onMounted(() => {
-	connectSSEWithHeaders(serviceUrlApi.value,  { 'accessToken': token })
-		.then(eventSource => {
-			sseService = eventSource
-			eventSource.onmessage = function(e) {
-				console.log(e, '----====----');
-				console.log('后端返回的数据:', e.data);
-				receivedData.value.push(e.data)
-			}
-			eventSource.onerror = function(err) {
-				console.error('EventSource failed:', err);
-			};
-		})
+	connectSSEWithHeaders(serviceUrlApi.value, { accessToken: token }).then(eventSource => {
+		sseService.value = eventSource
+		eventSource.onmessage = function (e) {
+			console.log('后端返回的数据:', e.data)
+			receivedData.value.push(e.data)
+		}
+		eventSource.onerror = function (err) {
+			console.error('EventSource failed:', err)
+		}
+	})
 })
 
 onBeforeUnmount(() => {
-	if (sseService) {
-		sseService.close();
-		sseService = null;
+	if (sseService.value) {
+		sseService.value.close()
+		sseService.value = null
 	}
-});
-</script>
+})
+</script>

+ 145 - 0
src/layout/components/Header/components/Message2.vue

@@ -0,0 +1,145 @@
+<template>
+	<el-popover placement="bottom" :width="310" trigger="click">
+		<template #reference>
+			<div class="menu--message-trigger">
+				<el-tooltip :content="$t('le.message.txt')" effect="dark" placement="bottom">
+					<div class="menu--message menu-item le-hover-effect--bg">
+						<el-badge :value="total" :max="99">
+							<i class="le-iconfont le-notice"></i>
+						</el-badge>
+					</div>
+				</el-tooltip>
+			</div>
+		</template>
+		<el-tabs class="message-tabs" v-model="activeTab">
+			<el-tab-pane v-for="v of tabsConfig" :key="v.name" :name="v.name">
+				<template #label> {{ v.label }}({{ v.list.length }}) </template>
+				<template v-if="v.list.length">
+					<div class="message-list">
+						<div v-for="item of v.list" :key="item.id" class="message-item" @click="jumpMessageDetail(item.id)">
+							<!--<img src="" alt="" class="message-icon" />-->
+							<div class="message-content">
+								<div class="message-title">
+									<span class="txt text-overflow_ellipsis" :title="item.title">{{ item.title }}</span>
+									<span class="message-date">{{ item.createTime }}</span>
+								</div>
+								<span class="message-txt">{{ item.content }}</span>
+							</div>
+						</div>
+						<div class="message-fix-item" @click="jumpMessageInfo">查看更多</div>
+					</div>
+				</template>
+				<template v-else>
+					<LeNoData />
+				</template>
+			</el-tab-pane>
+		</el-tabs>
+	</el-popover>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import message from '@/api/system/message'
+import router from '@/router'
+// noticeList  通知     messageList   消息     todoList   待办
+const tabsConfig = reactive({
+	noticeList: {
+		name: 'noticeList',
+		label: '通知',
+		list: []
+	},
+	messageList: {
+		name: 'messageList',
+		label: '消息',
+		list: []
+	},
+	todoList: {
+		name: 'todoList',
+		label: '待办',
+		list: []
+	}
+})
+const activeTab = ref('noticeList')
+const total = ref(0)
+message.getMessage().then(res => {
+	let _total = 0
+	Object.keys(tabsConfig).map(key => {
+		if (res[key]) {
+			tabsConfig[key].list = res[key]
+			_total += res[key].length
+		} else {
+			res[key].list = []
+		}
+	})
+	total.value = _total
+})
+
+const jumpMessageInfo = () => {
+	router.push('/message/list')
+}
+
+const jumpMessageDetail = (id: any) => {
+	router.push('/message/list?id=' + id)
+}
+</script>
+
+<style scoped lang="scss">
+.message-list {
+	display: flex;
+	flex-direction: column;
+	max-height: 390px;
+	overflow-y: auto;
+	.message-item {
+		display: flex;
+		align-items: center;
+		padding: 8px 0;
+		border-bottom: 1px solid var(--el-border-color-light);
+		cursor: pointer;
+		&:last-child {
+			border: none;
+		}
+		.message-content {
+			display: flex;
+			flex-direction: column;
+			width: 100%;
+			.message-title {
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+				margin-bottom: 5px;
+				font-weight: 600;
+				color: var(--el-text-color-primary);
+			}
+			.message-date {
+				font-size: 12px;
+				color: var(--el-text-color-secondary);
+				margin-left: 10px;
+				font-weight: 400;
+				flex-shrink: 0;
+			}
+			.message-txt {
+				//margin-bottom: 5px;
+				color: var(--el-text-color-regular);
+			}
+		}
+	}
+	.message-fix-item {
+		// flex 居中
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		height: 40px;
+		cursor: pointer;
+	}
+}
+.menu--message-trigger {
+	height: 100%;
+}
+.menu--message {
+	width: 40px;
+	justify-content: center;
+	//.le-notice {
+	//.le-iconfont {
+	//}
+}
+</style>

+ 3 - 1
src/views/approve/components/approvedContent.vue

@@ -485,7 +485,7 @@ const getTaskDetail = () => {
 						}
 						return
 					} else {
-						formContent = `{"formStructure":${data.formTemplate.content}}` || '{}'
+						formContent = data.formContent
 					}
 				} else {
 					formContent = data.formContent
@@ -496,6 +496,7 @@ const getTaskDetail = () => {
 				cur_formStructure = formStructure // 存储一份
 				let newFields = cur_formStructure.fields
 				let newFieldsList = cur_formStructure.list
+
 				// pendingApproval, 除了[待审批]这个状态, 其余只能读取
 				if (props.currentTaskType !== 'pendingApproval') {
 					newFields.map(item1 => (item1.options.disabled = true))
@@ -530,6 +531,7 @@ const getTaskDetail = () => {
 				cur_formData = formData
 				console.log(cur_formStructure, '=/////====newFormStructure');
 				console.log(formData, '=/////====newFormStructure formData');
+
 				EReditorRef.value.setData(cur_formStructure, formData)
 				validateForm.value.rule = newFields
 				/* 这里主要是表单设计的逻辑 end */

+ 4 - 4
src/views/approve/components/rollbackDialog.vue

@@ -2,9 +2,9 @@
 	<!-- 回退审批弹窗 -->
 	<el-dialog v-model="reviewVisibleDialog" class="le-dialog" title="回退审批" width="700" destroy-on-close :close-on-click-modal="false">
 		<el-form ref="formRef" v-loading="uploadLoading" label-position="top" element-loading-text="图片上传中..." :model="form" label-width="80px">
-			<el-form-item label="回退到" prop="nodeName" :rules="[{ required: true, message: '请选择回退节点' }]">
-				<el-select v-model="form.nodeName" placeholder="请选择回退节点">
-					<el-option v-for="item in rollbackOptions" :key="item" :label="item" :value="item" />
+			<el-form-item label="回退到" prop="nodeKey" :rules="[{ required: true, message: '请选择回退节点' }]">
+				<el-select v-model="form.nodeKey" placeholder="请选择回退节点">
+						<el-option v-for="item in rollbackOptions" :key="item.nodeKey" :label="item.nodeName" :value="item.nodeKey" />
 				</el-select>
 			</el-form-item>
 			<el-form-item label="回退原因" prop="content">
@@ -51,7 +51,7 @@ const props = defineProps({
 })
 const btnDisabled = ref(false)
 const form = reactive({
-	nodeName: '',
+	nodeKey: '',
 	content: ''
 	// attachment: []
 })

+ 50 - 9
src/views/message/list/my.vue

@@ -14,8 +14,8 @@
 				:columns="activeColumns"
 			>
 				<template #toolLeft>
-					<el-button plain type="primary">全部标记已读</el-button>
-					<el-button plain :icon="Delete" :disabled="curSelectionRows.length === 0" @click="batch_del" type="danger">删除</el-button>
+					<el-button plain type="primary" @click="readAll">一键已读</el-button>
+					<el-button plain :icon="Delete" :disabled="curSelectionRows.length === 0" type="danger" @click="batch_del">删除</el-button>
 				</template>
 
 				<template #categorySlot="scope">
@@ -24,6 +24,17 @@
 					<el-tag v-if="scope.row.category === 2" size="small" type="warning" effect="plain">待办通知</el-tag>
 				</template>
 
+				<template #viewedSlot="scope">
+					<el-tag
+						v-if="scope.row.viewed !== undefined && scope.row.viewed !== null"
+						:type="getViewedType(scope.row.viewed)"
+						size="small"
+						effect="plain"
+					>
+						{{ getViewedLabel(scope.row.viewed) }}
+					</el-tag>
+				</template>
+
 				<template #actionSlot="{ row }">
 					<div class="flex flex-align-pack-center">
 						<LeIcon class="text-lg text-icon-color cursor-pointer" icon-class="icon-processInfo-icons--view-light" @click="openDetail(row)" />
@@ -34,7 +45,7 @@
 			</LeTable>
 		</div>
 
-		<message-detail v-if="visibleDetail" v-model="visibleDetail" :message-id="currentId" @closed="visibleDetail = false"></message-detail>
+		<message-detail v-if="visibleDetail" v-model="visibleDetail" :message-id="currentId" @closed="queryList"></message-detail>
 	</div>
 </template>
 <script lang="tsx" setup>
@@ -48,15 +59,15 @@ import { ElMessage, ElMessageBox } from 'element-plus'
 
 const route = useRoute()
 const visibleDetail = ref(false) // 权限设置弹窗显示隐藏
-const currentId = ref(null)
+const currentId = ref('')
 
 // 表格搜索条件
 const forms = ref([
 	{
 		prop: 'title',
-		label: '标题:',
+		label: '消息标题:',
 		itemType: 'input',
-		placeholder: '请输入标题'
+		placeholder: '请输入消息标题'
 	},
 	{
 		prop: 'createBy',
@@ -71,7 +82,7 @@ const queryList = async () => {
 	const { options, searchParams } = tableOpts
 	options.loading = true
 	try {
-		const { records: list, total } = await message.getMessageInfo(searchParams)
+		const { records: list, total } = await message.getMessageMyInfoApi(searchParams)
 		tableOpts.total = total
 		tableOpts.list = list
 	} catch {
@@ -88,7 +99,7 @@ const queryList = async () => {
 const columns = [
 	{
 		prop: 'title',
-		label: '标题',
+		label: '消息标题',
 		minWidth: 80
 	},
 	{
@@ -104,6 +115,13 @@ const columns = [
 		label: '发布人',
 		minWidth: 100
 	},
+	{
+		prop: 'viewed',
+		label: '查看状态',
+		slots: {
+			default: 'viewedSlot'
+		}
+	},
 	{
 		prop: 'createTime',
 		label: '发布时间',
@@ -174,12 +192,35 @@ const batch_del = () => {
 		deleteItem(ids)
 	})
 }
+const markAsRead = async (id: string | undefined) => {
+	await message.readMessageApi({ id })
+}
 
-const openDetail = (row: any) => {
+const openDetail = async (row: any) => {
+	if (Number(row.viewed) === 0) {
+		await markAsRead(row.id)
+	}
 	currentId.value = row.id
 	visibleDetail.value = true
 }
 
+const readAll = async () => {
+	try {
+		await markAsRead(undefined)
+		queryList()
+	} catch (error) {
+		console.error('Error marking all messages as read:', error)
+	}
+}
+
+const getViewedType = viewed => {
+	return Number(viewed) === 0 ? 'warning' : 'success'
+}
+
+const getViewedLabel = viewed => {
+	return Number(viewed) === 0 ? '未查看' : '已查看'
+}
+
 nextTick(() => {
 	queryList()
 })