Compare commits

..

No commits in common. "master" and "v2026.03" have entirely different histories.

22 changed files with 132 additions and 622 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "yudao-ui-admin-vue3",
"version": "2026.04-snapshot",
"version": "2026.03-snapshot",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yudao-ui-admin-vue3",
"version": "2026.04-snapshot",
"version": "2026.03-snapshot",
"license": "MIT",
"dependencies": {
"@element-plus/icons-vue": "2.3.2",

View File

@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "2026.04-snapshot",
"version": "2026.03-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@ -9,11 +9,11 @@
"dev": "vite --mode env.local",
"dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit",
"build:local": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode env.local",
"build:dev": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode dev",
"build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode test",
"build:stage": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode stage",
"build:prod": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode prod",
"build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
"build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local && vite preview",
@ -110,7 +110,6 @@
"bpmn-js": "^17.9.2",
"bpmn-js-properties-panel": "5.23.0",
"consola": "^3.2.3",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",

View File

@ -258,9 +258,6 @@ importers:
consola:
specifier: ^3.2.3
version: 3.2.3
cross-env:
specifier: ^7.0.3
version: 7.0.3
eslint:
specifier: ^8.57.0
version: 8.57.1
@ -2743,11 +2740,6 @@ packages:
cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==, tarball: https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-fetch@3.1.8:
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
@ -8032,10 +8024,6 @@ snapshots:
cropperjs@1.6.2: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-fetch@3.1.8:
dependencies:
node-fetch: 2.7.0

View File

@ -13,7 +13,6 @@ export interface DataSinkVO {
| TcpConfig
| WebSocketConfig
| MqttConfig
| DatabaseConfig
| RocketMQConfig
| KafkaMQConfig
| RabbitMQConfig
@ -74,14 +73,6 @@ export interface MqttConfig extends Config {
topic: string
}
/** Database 配置 */
export interface DatabaseConfig extends Config {
jdbcUrl: string
username: string
password: string
tableName: string
}
/** RocketMQ 配置 */
export interface RocketMQConfig extends Config {
nameServer: string

View File

@ -15,7 +15,6 @@ export interface UserVO {
remark: string
loginDate: Date
createTime: Date
disabled?: boolean
}
// 查询用户管理列表
@ -23,11 +22,6 @@ export const getUserPage = (params: PageParam) => {
return request.get({ url: '/system/user/page', params })
}
// 查询用户管理列表
export const getUserList = (ids: number[]) => {
return request.get({ url: '/system/user/list', params: { ids: ids.join(',') } })
}
// 查询用户详情
export const getUser = (id: number) => {
return request.get({ url: '/system/user/get?id=' + id })

View File

@ -74,8 +74,8 @@ export const useUploadImgRule = () => {
{
type: 'switch',
field: 'disabled',
title: '是否禁用',
value: false
title: '是否显示删除按钮',
value: true
},
{
type: 'switch',

View File

@ -629,14 +629,6 @@ export const COMPARISON_OPERATORS: DictDataVO = [
{
value: '<=',
label: '小于等于'
},
{
value: 'contain',
label: '包含'
},
{
value: '!contain',
label: '不包含'
}
]
// 审批操作按钮名称

View File

@ -49,13 +49,15 @@ const service: AxiosInstance = axios.create({
// request拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 是否需要设置 token命中白名单的接口如 /login不带 token
let isToken = (config!.headers || {}).isToken !== false
if (isToken && whiteList.some((v) => config.url?.includes(v))) {
isToken = false
}
if (getAccessToken() && isToken) {
config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义 token
// 是否需要设置 token
let isToken = (config!.headers || {}).isToken === false
whiteList.some((v) => {
if (config.url && config.url.indexOf(v) > -1) {
return (isToken = false)
}
})
if (getAccessToken() && !isToken) {
config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
}
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
@ -143,7 +145,7 @@ service.interceptors.response.use(
}
data = await new Response(response.data).json()
}
const code = data.code ?? result_code
const code = data.code || result_code
// 获取错误信息
const msg = data.msg || errorCode[code] || errorCode['default']
if (ignoreMsgs.indexOf(msg) !== -1) {
@ -209,7 +211,7 @@ service.interceptors.response.use(
'<div>5 分钟搭建本地环境</div>'
})
return Promise.reject(new Error(msg))
} else if (code !== 0 && code !== 200) {
} else if (code !== 200) {
if (msg === '无效的刷新令牌') {
// hard coding忽略这个提示直接登出
console.log(msg)

View File

@ -78,7 +78,7 @@ export const useTagsViewStore = defineStore('tagsView', {
// 删除某个
delView(view: RouteLocationNormalizedLoaded) {
this.delVisitedView(view)
this.addCachedView()
this.delCachedView()
},
// 删除tag
delVisitedView(view: RouteLocationNormalizedLoaded) {
@ -106,7 +106,7 @@ export const useTagsViewStore = defineStore('tagsView', {
// 删除所有缓存和tag
delAllViews() {
this.delAllVisitedViews()
this.addCachedView()
this.delCachedView()
},
// 删除所有tag
delAllVisitedViews() {

View File

@ -268,6 +268,7 @@ export enum DICT_TYPE {
MES_INDICATOR_TYPE = 'mes_indicator_type', // MES 检测项类型
MES_QC_RESULT_TYPE = 'mes_qc_result_type', // MES 质检结果值类型
MES_DEFECT_LEVEL = 'mes_defect_level', // MES 缺陷等级
MES_DEFECT_TYPE = 'mes_defect_type', // MES 缺陷检测项类型
MES_PRO_WORK_ORDER_STATUS = 'mes_pro_work_order_status', // MES 生产工单状态
MES_PRO_WORK_ORDER_SOURCE_TYPE = 'mes_pro_work_order_source_type', // MES 工单来源类型
MES_PRO_WORK_ORDER_TYPE = 'mes_pro_work_order_type', // MES 工单类型

View File

@ -526,7 +526,6 @@ import {
} from '@/components/SimpleProcessDesignerV2/src/consts'
import { BpmModelFormType, BpmProcessInstanceStatus } from '@/utils/constants'
import type { FormInstance, FormRules } from 'element-plus'
import { until, useDebounceFn } from '@vueuse/core'
import SignDialog from './SignDialog.vue'
import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
import { isEmpty } from '@/utils/is'
@ -575,8 +574,6 @@ const signRef = ref()
const approveSignFormRef = ref()
const nextAssigneesActivityNode = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) //
const nextAssigneesTimelineRef = ref() // 线
let nextApprovalRequestId = 0 // onChange
let pendingNextNodesTask: Promise<unknown> | null = null // onChange await
const approveReasonForm = reactive({
reason: '',
signPicUrl: '',
@ -585,11 +582,7 @@ const approveReasonForm = reactive({
const approveReasonRule = computed(() => {
return {
reason: [
{
required: reasonRequire.value,
message: nodeTypeName.value + '意见不能为空',
trigger: 'blur'
}
{ required: reasonRequire.value, message: nodeTypeName.value + '意见不能为空', trigger: 'blur' }
],
signPicUrl: [{ required: true, message: '签名不能为空', trigger: 'change' }],
nextAssignees: [{ required: true, message: '审批人不能为空', trigger: 'blur' }]
@ -703,6 +696,7 @@ const openPopover = async (type: string) => {
message.warning('表单校验不通过,请先完善表单!!')
return
}
initNextAssigneesFormField()
}
if (type === 'return') {
// 退
@ -715,18 +709,6 @@ const openPopover = async (type: string) => {
Object.keys(popOverVisible.value).forEach((item) => {
popOverVisible.value[item] = item === type
})
if (type === 'approve') {
// form-create fApi
// approveFormFApi
if (runningTask.value?.formId > 0) {
// 1s until
await until(() => typeof approveFormFApi.value?.validate === 'function')
.toBeTruthy({ timeout: 1000 })
.catch(() => {})
}
//
await initNextAssigneesFormField()
}
// await nextTick()
// formRef.value.resetFields()
}
@ -746,8 +728,6 @@ const closePopover = (type: string, formRef: FormInstance | undefined) => {
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
const initNextAssigneesFormField = async () => {
//
const requestId = ++nextApprovalRequestId
// ,
const variables = getUpdatedProcessInstanceVariables()
const data = await ProcessInstanceApi.getNextApprovalNodes({
@ -755,12 +735,6 @@ const initNextAssigneesFormField = async () => {
taskId: runningTask.value.id,
processVariablesStr: JSON.stringify(variables)
})
//
if (requestId !== nextApprovalRequestId) {
return
}
//
nextAssigneesActivityNode.value = []
if (data && data.length > 0) {
const customApproveUsersData: Record<string, any[]> = {} // Timeline
data.forEach((node: any) => {
@ -789,9 +763,6 @@ const initNextAssigneesFormField = async () => {
}
}
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
const debouncedInitNextAssigneesFormField = useDebounceFn(initNextAssigneesFormField, 300)
/** 选择下一个节点的审批人 */
const selectNextAssigneesConfirm = (id: string, userList: any[]) => {
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id)
@ -826,10 +797,6 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
}
if (pass) {
// onChange + +
if (pendingNextNodesTask) {
await pendingNextNodesTask
}
const nextAssigneesValid = validateNextAssignees()
if (!nextAssigneesValid) return
const variables = getUpdatedProcessInstanceVariables()
@ -844,10 +811,13 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
if (runningTask.value.signEnable) {
data.signPicUrl = approveReasonForm.signPicUrl
}
// getUpdatedProcessInstanceVariables data.variables
// approveForm + data
// TODO
const formCreateApi = approveFormFApi.value
if (Object.keys(formCreateApi)?.length > 0) {
await formCreateApi.validate()
// @ts-ignore
data.variables = approveForm.value.value
}
await TaskApi.approveTask(data)
popOverVisible.value.approve = false
@ -1105,24 +1075,12 @@ const loadTodoTask = (task: any) => {
approveForm.value = {}
runningTask.value = task
approveFormFApi.value = {}
// pending /Promise
nextApprovalRequestId += 1
pendingNextNodesTask = null
reasonRequire.value = task?.reasonRequire ?? false
nodeTypeName.value = task?.nodeType === NodeType.TRANSACTOR_NODE ? '办理' : '审批'
// approve
// approve .
if (task && task.formId && task.formConf) {
const tempApproveForm: { option?: any; rule?: any; value?: any } = {}
const tempApproveForm = {}
setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables)
// onChange
tempApproveForm.option.onChange = () => {
//
if (!popOverVisible.value.approve) {
return
}
// useDebounceFn Promise reject catch 'cancelled'
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(() => {})
}
approveForm.value = tempApproveForm
} else {
approveForm.value = {} //
@ -1147,17 +1105,9 @@ const validateNormalForm = async () => {
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
const getUpdatedProcessInstanceVariables = () => {
const variables = {}
//
if (props.writableFields?.length && props.normalFormApi) {
props.writableFields.forEach((field) => {
variables[field] = props.normalFormApi.getValue(field)
})
}
// form-create formData()
const nodeFormData = approveFormFApi.value?.formData?.()
if (nodeFormData) {
Object.assign(variables, nodeFormData)
}
props.writableFields.forEach((field) => {
variables[field] = props.normalFormApi.getValue(field)
})
return variables
}

View File

@ -30,10 +30,6 @@
v-model="formData.config"
/>
<MqttConfigForm v-if="IotDataSinkTypeEnum.MQTT === formData.type" v-model="formData.config" />
<DatabaseConfigForm
v-if="IotDataSinkTypeEnum.DATABASE === formData.type"
v-model="formData.config"
/>
<RocketMQConfigForm
v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type"
v-model="formData.config"
@ -73,7 +69,6 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import { DataSinkApi, DataSinkVO, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
import {
DatabaseConfigForm,
HttpConfigForm,
KafkaMQConfigForm,
MqttConfigForm,
@ -121,9 +116,6 @@ const formRules = reactive({
'config.password': [{ required: true, message: '密码不能为空', trigger: 'blur' }],
'config.clientId': [{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' }],
'config.topic': [{ required: true, message: '主题不能为空', trigger: 'blur' }],
// Database
'config.jdbcUrl': [{ required: true, message: 'JDBC 连接地址不能为空', trigger: 'blur' }],
'config.tableName': [{ required: true, message: '目标表名不能为空', trigger: 'blur' }],
// RocketMQ
'config.nameServer': [{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }],
'config.accessKey': [{ required: true, message: 'AccessKey 不能为空', trigger: 'blur' }],

View File

@ -1,280 +0,0 @@
<template>
<el-form-item label="JDBC 地址" prop="config.jdbcUrl">
<el-input
v-model="config.jdbcUrl"
placeholder="请输入JDBC连接地址jdbc:mysql://localhost:3306/iot_data"
/>
</el-form-item>
<el-form-item label="用户名" prop="config.username">
<el-input v-model="config.username" placeholder="请输入数据库用户名" />
</el-form-item>
<el-form-item label="密码" prop="config.password">
<el-input
v-model="config.password"
placeholder="请输入数据库密码"
show-password
type="password"
/>
</el-form-item>
<el-form-item label="目标表名" prop="config.tableName">
<div style="display: flex; align-items: center; gap: 12px; width: 100%">
<el-input v-model="config.tableName" placeholder="目标表名" style="width: 240px" />
<el-button type="primary" link @click="toggleSqlTip">
<el-icon class="mr-1"><component :is="showSqlTip ? 'ArrowUp' : 'Document'" /></el-icon>
{{ showSqlTip ? '收起表结构提示' : '查看表结构提示' }}
</el-button>
</div>
</el-form-item>
<!-- Redesigned Terminal-style SQL snippet -->
<el-collapse-transition>
<div v-show="showSqlTip" class="terminal-card">
<div class="terminal-header">
<div class="terminal-dots">
<div class="dot red"></div>
<div class="dot yellow"></div>
<div class="dot green"></div>
</div>
<div class="terminal-title">Initialization Required</div>
<button class="terminal-copy-btn" type="button" @click="handleCopySQL">
<el-icon class="copy-icon"><Check v-if="isCopied" /><DocumentCopy v-else /></el-icon>
{{ isCopied ? '已复制' : 'Copy SQL' }}
</button>
</div>
<div class="terminal-body">
<div class="terminal-desc">
目标数据库必须包含以下结构的表才能正常接收数据流转的消息
</div>
<div class="terminal-code-wrapper">
<pre
class="terminal-code"
><code><span class="kw">CREATE</span> <span class="kw">TABLE</span> <span class="identifier">iot_device_message_sink</span> (
<span class="identifier">id</span> <span class="type">VARCHAR</span>(64) <span class="kw">NOT NULL COMMENT</span> <span class="string">'消息ID'</span>,
<span class="identifier">device_id</span> <span class="type">BIGINT</span> <span class="kw">NOT NULL COMMENT</span> <span class="string">'设备编号'</span>,
<span class="identifier">tenant_id</span> <span class="type">BIGINT</span> <span class="kw">NOT NULL DEFAULT</span> <span class="num">0</span> <span class="kw">COMMENT</span> <span class="string">'租户编号'</span>,
<span class="identifier">method</span> <span class="type">VARCHAR</span>(128) <span class="kw">COMMENT</span> <span class="string">'请求方法'</span>,
<span class="identifier">report_time</span> <span class="type">DATETIME</span> <span class="kw">COMMENT</span> <span class="string">'上报时间'</span>,
<span class="identifier">data</span> <span class="type">TEXT</span> <span class="kw">COMMENT</span> <span class="string">'完整消息JSON'</span>,
<span class="identifier">create_time</span> <span class="type">DATETIME</span> <span class="kw">NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT</span> <span class="string">'创建时间'</span>,
<span class="kw">PRIMARY KEY</span> (<span class="identifier">id</span>) <span class="kw">USING BTREE</span>,
<span class="kw">INDEX</span> <span class="identifier">idx_create_time</span> (<span class="identifier">create_time</span> <span class="kw">ASC</span>) <span class="kw">USING BTREE</span>
) <span class="kw">ENGINE</span> = <span class="identifier">InnoDB</span> <span class="kw">CHARACTER SET</span> = <span class="identifier">utf8mb4</span> <span class="kw">COLLATE</span> = <span class="identifier">utf8mb4_unicode_ci</span> <span class="kw">COMMENT</span> = <span class="string">'IoT 设备消息流转目标表'</span>;</code></pre>
</div>
</div>
</div>
</el-collapse-transition>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { Check, DocumentCopy } from '@element-plus/icons-vue'
import { DatabaseConfig, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
import { useClipboard, useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { useMessage } from '@/hooks/web/useMessage'
defineOptions({ name: 'DatabaseConfigForm' })
const props = defineProps<{
modelValue: any
}>()
const emit = defineEmits(['update:modelValue'])
const config = useVModel(props, 'modelValue', emit) as Ref<DatabaseConfig>
const message = useMessage()
const rawSQL = `CREATE TABLE iot_device_message_sink (
id VARCHAR(64) NOT NULL COMMENT '消息ID',
device_id BIGINT NOT NULL COMMENT '设备编号',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
method VARCHAR(128) COMMENT '请求方法',
report_time DATETIME COMMENT '上报时间',
data TEXT COMMENT '完整消息JSON',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id) USING BTREE,
INDEX idx_create_time (create_time ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'IoT 设备消息流转目标表';`
const isCopied = ref(false)
const showSqlTip = ref(false)
const { copy } = useClipboard()
const toggleSqlTip = () => {
showSqlTip.value = !showSqlTip.value
}
const handleCopySQL = async () => {
await copy(rawSQL)
isCopied.value = true
message.success('建表 SQL 已复制到剪贴板')
setTimeout(() => {
isCopied.value = false
}, 2000)
}
/** 组件初始化 */
onMounted(() => {
if (!isEmpty(config.value)) {
return
}
config.value = {
type: IotDataSinkTypeEnum.DATABASE + '', // 使
jdbcUrl: '',
username: '',
password: '',
tableName: 'iot_device_message_sink'
}
})
</script>
<style scoped>
/* 终端风卡片设计 (Tokyo Night 极客美学) */
.terminal-card {
margin-top: 32px;
margin-bottom: 8px;
border-radius: 12px;
background-color: #1a1b26;
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.4);
border: 1px solid #24283b;
overflow: hidden;
font-family: 'Fira Code', 'JetBrains Mono', Consolas, Monaco, monospace;
}
.terminal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #24283b;
border-bottom: 1px solid #16161e;
position: relative;
}
.terminal-dots {
display: flex;
gap: 8px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
transition: transform 0.2s ease;
}
.dot:hover {
transform: scale(1.2);
}
.dot.red {
background-color: #f7768e;
box-shadow: 0 0 5px rgba(247, 118, 142, 0.4);
}
.dot.yellow {
background-color: #e0af68;
box-shadow: 0 0 5px rgba(224, 175, 104, 0.4);
}
.dot.green {
background-color: #9ece6a;
box-shadow: 0 0 5px rgba(158, 206, 106, 0.4);
}
.terminal-title {
color: #a9b1d6;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.8px;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.terminal-copy-btn {
background: transparent;
border: 1px solid #414868;
color: #a9b1d6;
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
align-items: center;
gap: 6px;
font-family: inherit;
}
.terminal-copy-btn:hover {
background: #bb9af7;
border-color: #bb9af7;
color: #1a1b26;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(187, 154, 247, 0.3);
}
.terminal-copy-btn:active {
transform: translateY(0);
}
.copy-icon {
font-size: 14px;
}
.terminal-body {
padding: 20px;
color: #c0caf5;
font-size: 13px;
line-height: 1.6;
}
.terminal-desc {
color: #7dcfff;
margin-bottom: 16px;
font-family: var(--el-font-family);
font-size: 13px;
padding-bottom: 16px;
border-bottom: 1px dashed #292e42;
}
.terminal-code-wrapper {
overflow-x: auto;
border-radius: 8px;
}
.terminal-code {
margin: 0;
white-space: pre;
min-width: max-content;
}
.terminal-code code {
font-family: inherit;
}
/* 手工实现的轻量级 SQL 语法高亮 (Tokyo Night Color Palette) */
.kw {
color: #bb9af7;
} /* 紫色 - 关键字 */
.type {
color: #2ac3de;
} /* 青色 - 数据类型 */
.string {
color: #9ece6a;
} /* 绿色 - 字符串/注释 */
.identifier {
color: #c0caf5;
} /* 浅蓝 - 变量名/默认字色 */
.num {
color: #ff9e64;
} /* 橙色 - 数字 */
/* 定制代码块的滚动条 */
.terminal-code-wrapper::-webkit-scrollbar {
height: 8px;
}
.terminal-code-wrapper::-webkit-scrollbar-thumb {
background: #414868;
border-radius: 4px;
}
.terminal-code-wrapper::-webkit-scrollbar-thumb:hover {
background: #565f89;
}
.terminal-code-wrapper::-webkit-scrollbar-track {
background: transparent;
}
</style>

View File

@ -2,7 +2,6 @@ import HttpConfigForm from './HttpConfigForm.vue'
import TcpConfigForm from './TcpConfigForm.vue'
import WebSocketConfigForm from './WebSocketConfigForm.vue'
import MqttConfigForm from './MqttConfigForm.vue'
import DatabaseConfigForm from './DatabaseConfigForm.vue'
import RocketMQConfigForm from './RocketMQConfigForm.vue'
import KafkaMQConfigForm from './KafkaMQConfigForm.vue'
import RabbitMQConfigForm from './RabbitMQConfigForm.vue'
@ -13,7 +12,6 @@ export {
TcpConfigForm,
WebSocketConfigForm,
MqttConfigForm,
DatabaseConfigForm,
RocketMQConfigForm,
KafkaMQConfigForm,
RabbitMQConfigForm,

View File

@ -128,19 +128,13 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
return
}
// / operator '='" / "
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
if (!isEventOrService) {
if (!trigger.operator) {
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
return
}
if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
return
}
if (!trigger.operator) {
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
return
}
if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
return
}
}

View File

@ -80,20 +80,14 @@
:config="serviceConfig"
placeholder="请输入 JSON 格式的服务参数"
/>
<!-- 事件上报比较值标量填裸值结构体数组填 JSON 整体相等留空则事件发生即匹配 -->
<template v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST">
<el-input
:model-value="condition.value"
@update:model-value="(value) => updateConditionField('value', value)"
placeholder="留空则事件发生即匹配"
/>
<div class="text-12px text-[var(--el-text-color-secondary)] mt-4px leading-relaxed">
标量事件值填裸值
<code class="px-2px">normal</code>结构体数组事件值填合法
JSON
<code class="px-2px">{"level":"high"}</code>
</div>
</template>
<!-- 事件上报参数配置 -->
<JsonParamsInput
v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
v-model="condition.value"
type="event"
:config="eventConfig"
placeholder="请输入 JSON 格式的事件参数"
/>
<!-- 普通值输入 -->
<ValueInput
v-else
@ -282,6 +276,19 @@ const serviceConfig = computed(() => {
return undefined
})
// - JsonParamsInput
const eventConfig = computed(() => {
if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
return {
event: {
name: propertyConfig.value.name || '事件',
outputParams: propertyConfig.value.outputParams || []
}
}
}
return undefined
})
/**
* 更新条件字段
* @param field 字段名

View File

@ -114,7 +114,7 @@
<el-form-item label="分销海报图">
<UploadImgs v-model="formData.brokeragePosterUrls" height="125px" width="75px" />
<el-text class="w-full" size="small" type="info">
分销海报图片按上传顺序从左往右依次为<strong>个人分享海报</strong><strong>商品推广海报</strong><strong>拼团推广海报</strong>
个人中心分销海报图片建议尺寸 600x1000
</el-text>
</el-form-item>
<el-form-item label="一级返佣比例" prop="brokerageFirstPercent">

View File

@ -21,7 +21,7 @@
<el-form-item label="检测项类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择检测项类型" clearable class="!w-1/1">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MES_INDICATOR_TYPE)"
v-for="dict in getIntDictOptions(DICT_TYPE.MES_DEFECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"

View File

@ -37,7 +37,7 @@
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MES_INDICATOR_TYPE)"
v-for="dict in getIntDictOptions(DICT_TYPE.MES_DEFECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -90,7 +90,7 @@
<el-table-column label="缺陷描述" align="center" prop="name" min-width="200" />
<el-table-column label="检测项类型" align="center" prop="type" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_INDICATOR_TYPE" :value="scope.row.type" />
<dict-tag :type="DICT_TYPE.MES_DEFECT_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="缺陷等级" align="center" prop="level" width="120">

View File

@ -10,25 +10,28 @@
reset() 清空选中状态供外部重置按钮调用
-->
<template>
<div class="h-full">
<el-input v-model="filterText" class="p-[15px]" clearable :placeholder="filterPlaceholder">
<div>
<el-input
v-model="filterText"
class="mb-15px"
clearable
:placeholder="filterPlaceholder"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<el-scrollbar class="!h-[calc(100%-32px-30px)]">
<el-tree
ref="treeRef"
:data="deptList"
:expand-on-click-node="false"
:filter-node-method="filterNode"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</el-scrollbar>
<el-tree
ref="treeRef"
:data="deptList"
:expand-on-click-node="false"
:filter-node-method="filterNode"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</div>
</template>
@ -95,11 +98,7 @@ const reset = () => {
treeRef.value?.setCurrentKey(undefined)
}
const setCurrent = (deptId: number) => {
treeRef.value?.setCurrentKey(deptId)
}
defineExpose({ reset, setCurrent })
defineExpose({ reset })
/** 初始化 */
onMounted(async () => {

View File

@ -6,41 +6,31 @@
Props:
multiple true 多选checkboxfalse 单选radio默认 true
deptId 部门 ID
Events:
selected(rows: UserVO[]) 确认选择后触发单选时数组长度为 1
Expose:
open(selectedIds?: number[]) 打开弹窗可传入已选 ID 用于预选高亮
-->
<template>
<Dialog :title="title" v-model="dialogVisible" width="80%" align-center append-to-body>
<el-row class="h-[calc(100vh-196px)]" :gutter="15">
<Dialog title="人员选择" v-model="dialogVisible" width="80%">
<el-row :gutter="20">
<!-- 左侧部门树 -->
<el-col class="h-full" :span="5" :xs="24">
<ContentWrap class="h-full" :body-style="{ height: '100%', '--el-card-padding': '0px' }">
<el-col :span="5" :xs="24">
<ContentWrap class="h-full">
<DeptTreeSelect ref="deptTreeRef" @node-click="handleDeptNodeClick" />
</ContentWrap>
</el-col>
<!-- 右侧搜索表单 + 用户表格 -->
<el-col class="h-full overflow-auto" :span="19" :xs="24">
<el-col :span="19" :xs="24">
<ContentWrap>
<el-form class="-mb-[15px]" :inline="true" :model="queryParams" label-width="72px">
<el-form :inline="true" :model="queryParams" label-width="85px">
<el-form-item label="用户名称">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="用户昵称">
<el-input
v-model="queryParams.nickname"
placeholder="请输入用户昵称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="手机号码">
@ -49,7 +39,7 @@
placeholder="请输入手机号码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="状态">
@ -57,7 +47,7 @@
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
class="!w-220px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
@ -78,13 +68,9 @@
</el-form>
</ContentWrap>
<!-- 数据表格单选 radio / 多选 checkbox 统一在一个 table -->
<ContentWrap
class="h-[calc(100%-var(--content-wrap-padding,10px)*2-var(--content-wrap-margin,15px)*2-32px*2-3px*2-2px)] !mb-0"
:body-style="{ height: '100%', padding: 'var(--content-wrap-padding,10px)' }"
>
<ContentWrap>
<el-table
ref="tableRef"
class="!h-[calc(100%-32px-30px+var(--content-wrap-padding,10px))]"
v-loading="loading"
:data="list"
:stripe="true"
@ -99,7 +85,6 @@
<el-table-column
v-if="multiple"
type="selection"
:selectable="selectable"
:reserve-selection="true"
width="50"
align="center"
@ -111,12 +96,10 @@
v-model="selectedRadioId"
:value="row.id"
class="radio-no-label"
:disabled="row.disabled"
@change="handleRadioChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="用户编号" align="center" prop="id" width="150" />
<el-table-column label="用户名称" align="center" prop="username" width="150" />
<el-table-column label="用户昵称" align="left" prop="nickname" min-width="150" />
<el-table-column label="部门" align="center" prop="deptName" width="150" />
@ -126,13 +109,6 @@
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
</el-table>
<Pagination
:total="total"
@ -155,32 +131,27 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import * as UserApi from '@/api/system/user'
import DeptTreeSelect from '@/views/system/dept/components/DeptTreeSelect.vue'
import { dateFormatter } from '@/utils/formatTime'
defineOptions({ name: 'UserSelectDialogV2' })
const props = withDefaults(
defineProps<{
title?: string
multiple?: boolean // true checkboxfalse radio
deptId?: number // ID
}>(),
{
title: '人员选择',
multiple: true
}
)
const message = useMessage()
const emit = defineEmits<{
selected: [rows: UserApi.UserVO[], activityId?: any]
selected: [rows: UserApi.UserVO[]]
}>()
const dialogVisible = ref(false) //
const loading = ref(false) //
const list = ref<UserApi.UserVO[]>([]) //
const total = ref(0) //
const activityId = ref()
// ==================== ====================
const deptTreeRef = ref() // Ref
@ -197,12 +168,6 @@ const selectedRows = ref<UserApi.UserVO[]>([]) // 多选模式:选中行
const selectedRadioId = ref<number>() // ID
const currentRadioRow = ref<UserApi.UserVO>() //
const preSelectedIds = ref<number[]>([]) // ID
const preDisabledIds = ref<number[]>([]) // ID
/** 多选:是否可以选中 */
const selectable = (row: UserApi.UserVO) => {
return !preDisabledIds.value.includes(row.id)
}
/** 多选checkbox 变化 */
const handleSelectionChange = (rows: UserApi.UserVO[]) => {
@ -218,9 +183,6 @@ const handleRadioChange = (row: UserApi.UserVO) => {
/** 单击行:单选模式下点击整行即选中(降低操作成本),多选不处理(避免和 dblclick 冲突) */
const handleRowClick = (row: UserApi.UserVO) => {
if (row.disabled) {
return
}
if (props.multiple) {
return
}
@ -230,9 +192,6 @@ const handleRowClick = (row: UserApi.UserVO) => {
/** 双击行:多选模式切换勾选,单选模式直接确认 */
const handleRowDblClick = (row: UserApi.UserVO) => {
if (row.disabled) {
return
}
if (props.multiple) {
tableRef.value?.toggleRowSelection(row)
return
@ -247,7 +206,6 @@ const queryParams = reactive({
pageNo: 1, //
pageSize: 10, //
username: undefined as string | undefined, //
nickname: undefined as string | undefined, //
mobile: undefined as string | undefined, //
status: CommonStatusEnum.ENABLE as number | undefined, //
deptId: undefined as number | undefined // ID
@ -260,9 +218,6 @@ const getList = async () => {
const data = await UserApi.getUserPage(queryParams)
list.value = data.list
total.value = data.total
list.value.forEach((row) => {
row.disabled = preDisabledIds.value.includes(row.id)
})
await nextTick()
applyPreSelection()
} finally {
@ -318,13 +273,13 @@ const confirmSelect = () => {
message.warning('请至少选择一条数据')
return
}
emit('selected', selectedRows.value, activityId.value)
emit('selected', selectedRows.value)
} else {
if (!currentRadioRow.value) {
message.warning('请选择一条数据')
return
}
emit('selected', [currentRadioRow.value], activityId.value)
emit('selected', [currentRadioRow.value])
}
dialogVisible.value = false
}
@ -332,29 +287,24 @@ const confirmSelect = () => {
// ==================== ====================
/** 打开弹窗,可传入已选 ID 用于预选高亮 */
const open = async (selectedIds?: number[], disabledIds?: number[], _activityId?: any) => {
preDisabledIds.value = disabledIds ?? []
activityId.value = _activityId
const open = async (selectedIds?: number[]) => {
dialogVisible.value = true
// +
queryParams.username = undefined
queryParams.mobile = undefined
queryParams.status = CommonStatusEnum.ENABLE
queryParams.deptId = props.deptId
queryParams.deptId = undefined
queryParams.pageNo = 1
//
selectedRows.value = []
selectedRadioId.value = undefined
currentRadioRow.value = undefined
preSelectedIds.value = (selectedIds ?? []).filter((id) => !preDisabledIds.value.includes(id))
preSelectedIds.value = selectedIds ?? []
// +
await nextTick()
deptTreeRef.value?.reset()
tableRef.value?.clearSelection()
await getList()
if (queryParams.deptId) {
deptTreeRef.value?.setCurrent(queryParams.deptId)
}
}
defineExpose({ open })
</script>

View File

@ -3,19 +3,15 @@
对齐 MdVendorSelect 架构模式
交互显示为只读 el-input点击打开弹窗进行选择
交互显示为只读 el-input点击打开弹窗单选模式进行选择
Props:
modelValue 绑定的用户 IDv-model
defaultCurrentUser 默认选中当前用户
multiple 默认 false
disabled 是否禁用
disabledIds 禁用的用户 ID
clearable 是否允许清空鼠标悬停时显示清除图标
placeholder 占位文字
deptId 部门 ID
Events:
update:modelValue v-model 更新
change(item | items) 选中用户变化时触发传递完整 UserVO清空时为 undefined | []
change(item) 选中用户变化时触发传递完整 UserVO清空时为 undefined
-->
<template>
<div
@ -26,15 +22,13 @@
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<el-tooltip :disabled="selectedItems.length === 0" placement="top" :show-after="500">
<el-tooltip :disabled="!selectedItem" placement="top" :show-after="500">
<template #content>
<div v-if="selectedItems.length > 0" class="flex gap-[10px]">
<div v-for="selectedItem in selectedItems" :key="selectedItem.id" class="leading-6">
<div>用户名称{{ selectedItem.username }}</div>
<div>用户昵称{{ selectedItem.nickname }}</div>
<div>部门{{ (selectedItem as any).deptName || '-' }}</div>
<div>手机号码{{ selectedItem.mobile || '-' }}</div>
</div>
<div v-if="selectedItem" class="leading-6">
<div>用户名称{{ selectedItem.username }}</div>
<div>用户昵称{{ selectedItem.nickname }}</div>
<div>部门{{ (selectedItem as any).deptName || '-' }}</div>
<div>手机号码{{ selectedItem.mobile || '-' }}</div>
</div>
</template>
<el-input
@ -48,20 +42,13 @@
</el-tooltip>
</div>
<!-- 弹窗必须放在 div 外部否则弹窗内的点击事件会冒泡到 div 触发 handleClick -->
<!-- Dialog append-to-body 即可-->
<UserSelectDialogV2
ref="dialogRef"
:multiple="multiple"
:deptId="deptId"
@selected="handleSelected"
/>
<UserSelectDialogV2 ref="dialogRef" :multiple="false" @selected="handleSelected" />
</template>
<script setup lang="ts">
import * as UserApi from '@/api/system/user'
import { Search, CircleClose } from '@element-plus/icons-vue'
import UserSelectDialogV2 from './UserSelectDialogV2.vue'
import { useUserStoreWithOut } from '@/store/modules/user'
// div + DialogVue attrs
// div class / style
@ -71,17 +58,12 @@ defineOptions({ name: 'UserSelectV2', inheritAttrs: false })
const props = withDefaults(
defineProps<{
modelValue?: number | number[] // ID
defaultCurrentUser?: boolean //
multiple?: boolean //
modelValue?: number // ID
disabled?: boolean //
disabledIds?: number[] // ID
clearable?: boolean //
placeholder?: string //
deptId?: number // ID
}>(),
{
defaultCurrentUser: false,
disabled: false,
clearable: true,
placeholder: '请选择用户'
@ -89,19 +71,19 @@ const props = withDefaults(
)
const emit = defineEmits<{
'update:modelValue': [value: number | number[] | undefined]
change: [item: UserApi.UserVO | UserApi.UserVO[] | undefined]
'update:modelValue': [value: number | undefined]
change: [item: UserApi.UserVO | undefined]
}>()
const dialogRef = ref() // Ref
const hovering = ref(false) //
// ==================== ====================
const selectedItems = ref<UserApi.UserVO[]>([]) //
const selectedItem = ref<UserApi.UserVO | undefined>() //
/** 输入框显示文本:展示用户昵称,保持简洁 */
const displayLabel = computed(() => {
return selectedItems.value.map((item) => item.nickname || item.username).join('、')
return selectedItem.value?.nickname ?? ''
})
/** 是否显示清除图标 */
@ -114,21 +96,17 @@ const suffixIcon = computed(() => {
return showClear.value ? CircleClose : Search
})
/** 根据 ID 查询用户信息(用于编辑回显) */
const resolveItemById = async (id: number | number[] | undefined) => {
if (id === null || id === undefined) {
selectedItems.value = []
/** 根据 ID 单条查询用户信息(用于编辑回显) */
const resolveItemById = async (id: number | undefined) => {
if (id == null) {
selectedItem.value = undefined
return
}
const ids: number[] = Array.isArray(id) ? id : [id]
if (
selectedItems.value.length === ids.length &&
selectedItems.value.every((item) => ids.includes(item.id))
) {
if (selectedItem.value?.id === id) {
return
}
try {
selectedItems.value = await UserApi.getUserList(ids)
selectedItem.value = await UserApi.getUser(id)
} catch (e) {
console.error('[UserSelectV2] resolveItemById failed:', e)
}
@ -140,7 +118,7 @@ watch(
(val) => {
resolveItemById(val)
},
{ deep: true, immediate: true }
{ immediate: true }
)
// ==================== ====================
@ -154,23 +132,14 @@ const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (showClear.value && target.closest('.el-input__suffix')) {
e.stopPropagation()
selectedItems.value = []
if (props.multiple) {
emit('update:modelValue', [])
emit('change', [])
} else {
emit('update:modelValue', undefined)
emit('change', undefined)
}
selectedItem.value = undefined
emit('update:modelValue', undefined)
emit('change', undefined)
return
}
// ID
const selectedIds = props.multiple
? props.modelValue || []
: props.modelValue !== null && props.modelValue !== undefined
? [props.modelValue]
: []
dialogRef.value.open(selectedIds, props.disabledIds)
const selectedIds = props.modelValue != null ? [props.modelValue] : []
dialogRef.value.open(selectedIds)
}
/** 弹窗选中回调 */
@ -178,47 +147,11 @@ const handleSelected = (rows: UserApi.UserVO[]) => {
if (!rows || rows.length === 0) {
return
}
selectedItems.value = rows
if (props.multiple) {
emit(
'update:modelValue',
rows.map((item) => item.id)
)
emit('change', rows)
} else {
const item = rows[0]
emit('update:modelValue', item.id)
emit('change', item)
}
const item = rows[0]
selectedItem.value = item
emit('update:modelValue', item.id)
emit('change', item)
}
/** 检查是否有有效的预设值 */
const hasValidPresetValue = (): boolean => {
const value = props.modelValue as any
if (value === undefined || value === null || value === '') {
return false
}
if (Array.isArray(value)) {
return value.length > 0
}
return true
}
onMounted(() => {
/** 默认选中当前用户 */
if (props.defaultCurrentUser && !hasValidPresetValue()) {
const userStore = useUserStoreWithOut()
const user = userStore.getUser
const currentUserId = user?.id
if (currentUserId) {
if (props.multiple) {
emit('update:modelValue', [currentUserId])
} else {
emit('update:modelValue', currentUserId)
}
}
}
})
</script>
<style lang="scss" scoped>