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", "name": "yudao-ui-admin-vue3",
"version": "2026.04-snapshot", "version": "2026.03-snapshot",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2026.04-snapshot", "version": "2026.03-snapshot",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "2.3.2", "@element-plus/icons-vue": "2.3.2",

View File

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

View File

@ -258,9 +258,6 @@ importers:
consola: consola:
specifier: ^3.2.3 specifier: ^3.2.3
version: 3.2.3 version: 3.2.3
cross-env:
specifier: ^7.0.3
version: 7.0.3
eslint: eslint:
specifier: ^8.57.0 specifier: ^8.57.0
version: 8.57.1 version: 8.57.1
@ -2743,11 +2740,6 @@ packages:
cropperjs@1.6.2: cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} 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: cross-fetch@3.1.8:
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
@ -8032,10 +8024,6 @@ snapshots:
cropperjs@1.6.2: {} cropperjs@1.6.2: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-fetch@3.1.8: cross-fetch@3.1.8:
dependencies: dependencies:
node-fetch: 2.7.0 node-fetch: 2.7.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -268,6 +268,7 @@ export enum DICT_TYPE {
MES_INDICATOR_TYPE = 'mes_indicator_type', // MES 检测项类型 MES_INDICATOR_TYPE = 'mes_indicator_type', // MES 检测项类型
MES_QC_RESULT_TYPE = 'mes_qc_result_type', // MES 质检结果值类型 MES_QC_RESULT_TYPE = 'mes_qc_result_type', // MES 质检结果值类型
MES_DEFECT_LEVEL = 'mes_defect_level', // 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_STATUS = 'mes_pro_work_order_status', // MES 生产工单状态
MES_PRO_WORK_ORDER_SOURCE_TYPE = 'mes_pro_work_order_source_type', // 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 工单类型 MES_PRO_WORK_ORDER_TYPE = 'mes_pro_work_order_type', // MES 工单类型

View File

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

View File

@ -30,10 +30,6 @@
v-model="formData.config" v-model="formData.config"
/> />
<MqttConfigForm v-if="IotDataSinkTypeEnum.MQTT === formData.type" 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 <RocketMQConfigForm
v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type" v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type"
v-model="formData.config" v-model="formData.config"
@ -73,7 +69,6 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { DataSinkApi, DataSinkVO, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink' import { DataSinkApi, DataSinkVO, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
import { import {
DatabaseConfigForm,
HttpConfigForm, HttpConfigForm,
KafkaMQConfigForm, KafkaMQConfigForm,
MqttConfigForm, MqttConfigForm,
@ -121,9 +116,6 @@ const formRules = reactive({
'config.password': [{ required: true, message: '密码不能为空', trigger: 'blur' }], 'config.password': [{ required: true, message: '密码不能为空', trigger: 'blur' }],
'config.clientId': [{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' }], 'config.clientId': [{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' }],
'config.topic': [{ required: true, message: '主题不能为空', 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 // RocketMQ
'config.nameServer': [{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }], 'config.nameServer': [{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }],
'config.accessKey': [{ required: true, message: 'AccessKey 不能为空', 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 TcpConfigForm from './TcpConfigForm.vue'
import WebSocketConfigForm from './WebSocketConfigForm.vue' import WebSocketConfigForm from './WebSocketConfigForm.vue'
import MqttConfigForm from './MqttConfigForm.vue' import MqttConfigForm from './MqttConfigForm.vue'
import DatabaseConfigForm from './DatabaseConfigForm.vue'
import RocketMQConfigForm from './RocketMQConfigForm.vue' import RocketMQConfigForm from './RocketMQConfigForm.vue'
import KafkaMQConfigForm from './KafkaMQConfigForm.vue' import KafkaMQConfigForm from './KafkaMQConfigForm.vue'
import RabbitMQConfigForm from './RabbitMQConfigForm.vue' import RabbitMQConfigForm from './RabbitMQConfigForm.vue'
@ -13,7 +12,6 @@ export {
TcpConfigForm, TcpConfigForm,
WebSocketConfigForm, WebSocketConfigForm,
MqttConfigForm, MqttConfigForm,
DatabaseConfigForm,
RocketMQConfigForm, RocketMQConfigForm,
KafkaMQConfigForm, KafkaMQConfigForm,
RabbitMQConfigForm, RabbitMQConfigForm,

View File

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

View File

@ -80,20 +80,14 @@
:config="serviceConfig" :config="serviceConfig"
placeholder="请输入 JSON 格式的服务参数" placeholder="请输入 JSON 格式的服务参数"
/> />
<!-- 事件上报比较值标量填裸值结构体数组填 JSON 整体相等留空则事件发生即匹配 --> <!-- 事件上报参数配置 -->
<template v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"> <JsonParamsInput
<el-input v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
:model-value="condition.value" v-model="condition.value"
@update:model-value="(value) => updateConditionField('value', value)" type="event"
placeholder="留空则事件发生即匹配" :config="eventConfig"
/> placeholder="请输入 JSON 格式的事件参数"
<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>
<!-- 普通值输入 --> <!-- 普通值输入 -->
<ValueInput <ValueInput
v-else v-else
@ -282,6 +276,19 @@ const serviceConfig = computed(() => {
return undefined 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 字段名 * @param field 字段名

View File

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

View File

@ -21,7 +21,7 @@
<el-form-item label="检测项类型" prop="type"> <el-form-item label="检测项类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择检测项类型" clearable class="!w-1/1"> <el-select v-model="formData.type" placeholder="请选择检测项类型" clearable class="!w-1/1">
<el-option <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" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"

View File

@ -37,7 +37,7 @@
class="!w-240px" class="!w-240px"
> >
<el-option <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" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :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="name" min-width="200" />
<el-table-column label="检测项类型" align="center" prop="type" width="120"> <el-table-column label="检测项类型" align="center" prop="type" width="120">
<template #default="scope"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="缺陷等级" align="center" prop="level" width="120"> <el-table-column label="缺陷等级" align="center" prop="level" width="120">

View File

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

View File

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

View File

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