Commit 13706de2 by YunaiV

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

parents d10a3af3 5ef5ee6e
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2.2.0-snapshot", "version": "2.3.0-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 @@ ...@@ -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": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", "build:local": "node ./node_modules/vite/bin/vite.js build",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev", "build:dev": "node ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", "build:test": "node ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", "build:stage": "node ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", "build:prod": "node ./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",
...@@ -26,8 +26,8 @@ ...@@ -26,8 +26,8 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@form-create/designer": "^3.1.3", "@form-create/designer": "^3.2.6",
"@form-create/element-ui": "^3.1.24", "@form-create/element-ui": "^3.2.11",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@videojs-player/vue": "^1.0.0", "@videojs-player/vue": "^1.0.0",
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.8.0", "element-plus": "2.8.4",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
"steady-xml": "^0.1.0", "steady-xml": "^0.1.0",
"url": "^0.11.3", "url": "^0.11.3",
"video.js": "^7.21.5", "video.js": "^7.21.5",
"vue": "3.4.21", "vue": "3.5.12",
"vue-dompurify-html": "^4.1.4", "vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.10.2", "vue-i18n": "9.10.2",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",
...@@ -130,7 +130,7 @@ ...@@ -130,7 +130,7 @@
"vite-plugin-progress": "^0.0.7", "vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.10.0", "vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.1", "vite-plugin-top-level-await": "^1.4.4",
"vue-eslint-parser": "^9.3.2", "vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.27" "vue-tsc": "^1.8.27"
}, },
......
...@@ -30,7 +30,7 @@ export const getModelPage = async (params) => { ...@@ -30,7 +30,7 @@ export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', params }) return await request.get({ url: '/bpm/model/page', params })
} }
export const getModel = async (id: number) => { export const getModel = async (id: string) => {
return await request.get({ url: '/bpm/model/get?id=' + id }) return await request.get({ url: '/bpm/model/get?id=' + id })
} }
...@@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => { ...@@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data }) return await request.put({ url: '/bpm/model/update', data: data })
} }
export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data })
}
// 任务状态修改 // 任务状态修改
export const updateModelState = async (id: number, state: number) => { export const updateModelState = async (id: number, state: number) => {
const data = { const data = {
......
import request from '@/config/axios' import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model' import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = { export type Task = {
id: string id: string
name: string name: string
...@@ -22,6 +22,35 @@ export type ProcessInstanceVO = { ...@@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
processDefinition?: ProcessDefinitionVO processDefinition?: ProcessDefinitionVO
} }
// 用户信息
export type User = {
id: number,
nickname: string,
avatar: string
}
// 审批任务信息
export type ApprovalTaskInfo = {
id: number,
ownerUser: User,
assigneeUser: User,
status: number,
reason: string
}
// 审批节点信息
export type ApprovalNodeInfo = {
id : number
name: string
nodeType: NodeType
status: number
startTime?: Date
endTime?: Date
candidateUserList?: User[]
tasks: ApprovalTaskInfo[]
}
export const getProcessInstanceMyPage = async (params: any) => { export const getProcessInstanceMyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/my-page', params }) return await request.get({ url: '/bpm/process-instance/my-page', params })
} }
...@@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => { ...@@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => { export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params }) return await request.get({ url: '/bpm/process-instance/copy/page', params })
} }
// 获取审批详情
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
}
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
}
import request from '@/config/axios'
export const updateBpmSimpleModel = async (data) => {
return await request.post({
url: '/bpm/model/simple/update',
data: data
})
}
export const getBpmSimpleModel = async (id) => {
return await request.get({
url: '/bpm/model/simple/get?id=' + id
})
}
import request from '@/config/axios' import request from '@/config/axios'
/**
* 任务状态枚举
*/
export enum TaskStatusEnum {
/**
* 未开始
*/
NOT_START = -1,
/**
* 待审批
*/
WAIT = 0,
/**
* 审批中
*/
RUNNING = 1,
/**
* 审批通过
*/
APPROVE = 2,
/**
* 审批不通过
*/
REJECT = 3,
/**
* 已取消
*/
CANCEL = 4,
/**
* 已退回
*/
RETURN = 5,
/**
* 委派中
*/
DELEGATE = 6,
/**
* 审批通过中
*/
APPROVING = 7,
}
export type TaskVO = { export type TaskVO = {
id: number id: number
} }
......
import request from '@/config/axios'
// IoT 设备 VO
export interface DeviceVO {
id: number // 设备 ID,主键,自增
deviceKey: string // 设备唯一标识符
deviceName: string // 设备名称
productId: number // 产品编号
productKey: string // 产品标识
deviceType: number // 设备类型
nickname: string // 设备备注名称
gatewayId: number // 网关设备 ID
status: number // 设备状态
statusLastUpdateTime: Date // 设备状态最后更新时间
lastOnlineTime: Date // 最后上线时间
lastOfflineTime: Date // 最后离线时间
activeTime: Date // 设备激活时间
createTime: Date // 创建时间
ip: string // 设备的 IP 地址
firmwareVersion: string // 设备的固件版本
deviceSecret: string // 设备密钥,用于设备认证,需安全存储
mqttClientId: string // MQTT 客户端 ID
mqttUsername: string // MQTT 用户名
mqttPassword: string // MQTT 密码
authType: string // 认证类型
latitude: number // 设备位置的纬度
longitude: number // 设备位置的经度
areaId: number // 地区编码
address: string // 设备详细地址
serialNumber: string // 设备序列号
}
export interface DeviceUpdateStatusVO {
id: number // 设备 ID,主键,自增
status: number // 设备状态
}
// 设备 API
export const DeviceApi = {
// 查询设备分页
getDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/page`, params })
},
// 查询设备详情
getDevice: async (id: number) => {
return await request.get({ url: `/iot/device/get?id=` + id })
},
// 新增设备
createDevice: async (data: DeviceVO) => {
return await request.post({ url: `/iot/device/create`, data })
},
// 修改设备
updateDevice: async (data: DeviceVO) => {
return await request.put({ url: `/iot/device/update`, data })
},
// 修改设备状态
updateDeviceStatus: async (data: DeviceUpdateStatusVO) => {
return await request.put({ url: `/iot/device/update-status`, data })
},
// 删除设备
deleteDevice: async (id: number) => {
return await request.delete({ url: `/iot/device/delete?id=` + id })
},
// 获取设备数量
getDeviceCount: async (productId: number) => {
return await request.get({ url: `/iot/device/count?productId=` + productId })
}
}
import request from '@/config/axios'
// IoT 产品 VO
export interface ProductVO {
id: number // 产品编号
name: string // 产品名称
productKey: string // 产品标识
protocolId: number // 协议编号
categoryId: number // 产品所属品类标识符
description: string // 产品描述
validateType: number // 数据校验级别
status: number // 产品状态
deviceType: number // 设备类型
netType: number // 联网方式
protocolType: number // 接入网关协议
dataFormat: number // 数据格式
deviceCount: number // 设备数量
createTime: Date // 创建时间
}
// IoT 产品 API
export const ProductApi = {
// 查询产品分页
getProductPage: async (params: any) => {
return await request.get({ url: `/iot/product/page`, params })
},
// 查询产品详情
getProduct: async (id: number) => {
return await request.get({ url: `/iot/product/get?id=` + id })
},
// 新增产品
createProduct: async (data: ProductVO) => {
return await request.post({ url: `/iot/product/create`, data })
},
// 修改产品
updateProduct: async (data: ProductVO) => {
return await request.put({ url: `/iot/product/update`, data })
},
// 删除产品
deleteProduct: async (id: number) => {
return await request.delete({ url: `/iot/product/delete?id=` + id })
},
// 导出产品 Excel
exportProduct: async (params) => {
return await request.download({ url: `/iot/product/export-excel`, params })
},
// 更新产品状态
updateProductStatus: async (id: number, status: number) => {
return await request.put({ url: `/iot/product/update-status?id=` + id + `&status=` + status })
},
// 查询产品(精简)列表
getSimpleProductList() {
return request.get({ url: '/iot/product/list-all-simple' })
}
}
import request from '@/config/axios'
// IoT 产品物模型 VO
export interface ThinkModelFunctionVO {
id: number // 物模型功能编号
identifier: string // 功能标识
name: string // 功能名称
description: string // 功能描述
productId: number // 产品编号
productKey: string // 产品标识
type: number // 功能类型
property: string // 属性
event: string // 事件
service: string // 服务
}
// IoT 产品物模型 API
export const ThinkModelFunctionApi = {
// 查询产品物模型分页
getThinkModelFunctionPage: async (params: any) => {
return await request.get({ url: `/iot/think-model-function/page`, params })
},
// 获得产品物模型
getThinkModelFunctionListByProductId: async (params: any) => {
return await request.get({
url: `/iot/think-model-function/list-by-product-id`,
params
})
},
// 查询产品物模型详情
getThinkModelFunction: async (id: number) => {
return await request.get({ url: `/iot/think-model-function/get?id=` + id })
},
// 新增产品物模型
createThinkModelFunction: async (data: ThinkModelFunctionVO) => {
return await request.post({ url: `/iot/think-model-function/create`, data })
},
// 修改产品物模型
updateThinkModelFunction: async (data: ThinkModelFunctionVO) => {
return await request.put({ url: `/iot/think-model-function/update`, data })
},
// 删除产品物模型
deleteThinkModelFunction: async (id: number) => {
return await request.delete({ url: `/iot/think-model-function/delete?id=` + id })
},
// 导出产品物模型 Excel
exportThinkModelFunction: async (params) => {
return await request.download({ url: `/iot/think-model-function/export-excel`, params })
}
}
...@@ -11,7 +11,7 @@ const prefixCls = getPrefixCls('content-wrap') ...@@ -11,7 +11,7 @@ const prefixCls = getPrefixCls('content-wrap')
defineProps({ defineProps({
title: propTypes.string.def(''), title: propTypes.string.def(''),
message: propTypes.string.def(''), message: propTypes.string.def(''),
bodyStyle: propTypes.object.def({ padding: '20px' }) bodyStyle: propTypes.object.def({ padding: '10px' })
}) })
</script> </script>
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { TabBarProperty, THEME_LIST } from './config' import { TabBarProperty, component, THEME_LIST } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util' import { usePropertyForm } from '@/components/DiyEditor/util'
// 底部导航栏 // 底部导航栏
defineOptions({ name: 'TabBarProperty' }) defineOptions({ name: 'TabBarProperty' })
...@@ -88,6 +88,9 @@ const props = defineProps<{ modelValue: TabBarProperty }>() ...@@ -88,6 +88,9 @@ const props = defineProps<{ modelValue: TabBarProperty }>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit) const { formData } = usePropertyForm(props.modelValue, emit)
// 将数据库的值更新到右侧属性栏
component.property.items = formData.value.items
// 要的主题 // 要的主题
const handleThemeChange = () => { const handleThemeChange = () => {
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme) const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
<div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch"> <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch">
<Icon icon="ep:search" /> <Icon icon="ep:search" />
<el-select <el-select
@click.stop
filterable filterable
:reserve-keyword="false" :reserve-keyword="false"
remote remote
......
/* stylelint-disable order/properties-order */
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" v-model="visible" width="auto">
<div class="add-node-popover-body">
<a class="add-node-popover-item approver" @click="addType(1)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>审批人</p>
</a>
<a class="add-node-popover-item notifier" @click="addType(2)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>抄送人</p>
</a>
<a class="add-node-popover-item condition" @click="addType(4)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>条件分支</p>
</a>
</div>
<template #reference>
<button class="btn" type="button">
<span class="iconfont"></span>
</button>
</template>
</el-popover>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
let props = defineProps({
childNodeP: {
type: Object,
default: () => ({})
}
})
let emits = defineEmits(['update:childNodeP'])
let visible = ref(false)
const addType = (type) => {
visible.value = false
if (type != 4) {
var data
if (type == 1) {
data = {
nodeName: '审核人',
error: true,
type: 1,
settype: 1,
selectMode: 0,
selectRange: 0,
directorLevel: 1,
examineMode: 1,
noHanderAction: 1,
examineEndDirectorLevel: 0,
childNode: props.childNodeP,
nodeUserList: []
}
} else if (type == 2) {
data = {
nodeName: '抄送人',
type: 2,
ccSelfSelectFlag: 1,
childNode: props.childNodeP,
nodeUserList: []
}
}
emits('update:childNodeP', data)
} else {
emits('update:childNodeP', {
nodeName: '路由',
type: 4,
childNode: null,
conditionNodes: [
{
nodeName: '条件1',
error: true,
type: 3,
priorityLevel: 1,
conditionList: [],
nodeUserList: [],
childNode: props.childNodeP
},
{
nodeName: '条件2',
type: 3,
priorityLevel: 2,
conditionList: [],
nodeUserList: [],
childNode: null
}
]
})
}
}
</script>
<style scoped lang="scss">
.add-node-btn-box {
width: 240px;
display: inline-flex;
-ms-flex-negative: 0;
flex-shrink: 0;
-webkit-box-flex: 1;
-ms-flex-positive: 1;
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #cacaca;
}
.add-node-btn {
user-select: none;
width: 240px;
padding: 20px 0 32px;
display: flex;
-webkit-box-pack: center;
justify-content: center;
flex-shrink: 0;
-webkit-box-flex: 1;
flex-grow: 1;
.btn {
outline: none;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
width: 30px;
height: 30px;
background: #3296fa;
border-radius: 50%;
position: relative;
border: none;
line-height: 30px;
-webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
color: #fff;
font-size: 16px;
}
&:hover {
transform: scale(1.3);
box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
}
&:active {
transform: none;
background: #1e83e9;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
}
}
}
.add-node-popover-body {
display: flex;
.add-node-popover-item {
margin-right: 10px;
cursor: pointer;
text-align: center;
flex: 1;
color: #191f25 !important;
.item-wrapper {
user-select: none;
display: inline-block;
width: 80px;
height: 80px;
margin-bottom: 5px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
font-size: 35px;
line-height: 80px;
}
}
&.approver {
.item-wrapper {
color: #ff943e;
}
}
&.notifier {
.item-wrapper {
color: #3296fa;
}
}
&.condition {
.item-wrapper {
color: #15bc83;
}
}
&:hover {
.item-wrapper {
background: #3296fa;
box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
}
.iconfont {
color: #fff;
}
}
&:active {
.item-wrapper {
box-shadow: none;
background: #eaeaea;
}
.iconfont {
color: inherit;
}
}
}
}
</style>
/**
* todo
*/
export const arrToStr = (arr?: [{ name: string }]) => {
if (arr) {
return arr
.map((item) => {
return item.name
})
.toString()
}
}
export const setApproverStr = (nodeConfig: any) => {
if (nodeConfig.settype == 1) {
if (nodeConfig.nodeUserList.length == 1) {
return nodeConfig.nodeUserList[0].name
} else if (nodeConfig.nodeUserList.length > 1) {
if (nodeConfig.examineMode == 1) {
return arrToStr(nodeConfig.nodeUserList)
} else if (nodeConfig.examineMode == 2) {
return nodeConfig.nodeUserList.length + '人会签'
}
}
} else if (nodeConfig.settype == 2) {
const level =
nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
if (nodeConfig.examineMode == 1) {
return level
} else if (nodeConfig.examineMode == 2) {
return level + '会签'
}
} else if (nodeConfig.settype == 4) {
if (nodeConfig.selectRange == 1) {
return '发起人自选'
} else {
if (nodeConfig.nodeUserList.length > 0) {
if (nodeConfig.selectRange == 2) {
return '发起人自选'
} else {
return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
}
} else {
return ''
}
}
} else if (nodeConfig.settype == 5) {
return '发起人自己'
} else if (nodeConfig.settype == 7) {
return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
}
}
export const copyerStr = (nodeConfig: any) => {
if (nodeConfig.nodeUserList.length != 0) {
return arrToStr(nodeConfig.nodeUserList)
} else {
if (nodeConfig.ccSelfSelectFlag == 1) {
return '发起人自选'
}
}
}
export const conditionStr = (nodeConfig, index) => {
const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
if (conditionList.length == 0) {
return index == nodeConfig.conditionNodes.length - 1 &&
nodeConfig.conditionNodes[0].conditionList.length != 0
? '其他条件进入此流程'
: '请设置条件'
} else {
let str = ''
for (let i = 0; i < conditionList.length; i++) {
const {
columnId,
columnType,
showType,
showName,
optType,
zdy1,
opt1,
zdy2,
opt2,
fixedDownBoxValue
} = conditionList[i]
if (columnId == 0) {
if (nodeUserList.length != 0) {
str += '发起人属于:'
str +=
nodeUserList
.map((item) => {
return item.name
})
.join('或') + ' 并且 '
}
}
if (columnType == 'String' && showType == '3') {
if (zdy1) {
str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
}
}
if (columnType == 'Double') {
if (optType != 6 && zdy1) {
const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
str += `${showName} ${optTypeStr} ${zdy1} 并且 `
} else if (optType == 6 && zdy1 && zdy2) {
str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
}
}
}
return str ? str.substring(0, str.length - 4) : '请设置条件'
}
}
export const dealStr = (str: string, obj) => {
const arr = []
const list = str.split(',')
for (const elem in obj) {
list.map((item) => {
if (item == elem) {
arr.push(obj[elem].value)
}
})
}
return arr.join('或')
}
export const removeEle = (arr, elem, key = 'id') => {
let includesIndex
arr.map((item, index) => {
if (item[key] == elem[key]) {
includesIndex = index
}
})
arr.splice(includesIndex, 1)
}
export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
export const placeholderList = ['发起人', '审核人', '抄送人']
export const setTypes = [
{ value: 1, label: '指定成员' },
{ value: 2, label: '主管' },
{ value: 4, label: '发起人自选' },
{ value: 5, label: '发起人自己' },
{ value: 7, label: '连续多级主管' }
]
export const selectModes = [
{ value: 1, label: '选一个人' },
{ value: 2, label: '选多个人' }
]
export const selectRanges = [
{ value: 1, label: '全公司' },
{ value: 2, label: '指定成员' },
{ value: 3, label: '指定角色' }
]
export const optTypes = [
{ value: '1', label: '小于' },
{ value: '2', label: '大于' },
{ value: '3', label: '小于等于' },
{ value: '4', label: '等于' },
{ value: '5', label: '大于等于' },
{ value: '6', label: '介于两个数之间' }
]
<template>
<div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd">
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
<div class="handler-item-text">审批人</div>
</div>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
</div>
<div class="handler-item-text">条件分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-parallel"></span>
</div>
<div class="handler-item-text">并行分支</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
</el-popover>
</div>
</div>
</template>
<script setup lang="ts">
import {
ApproveMethodType,
AssignEmptyHandlerType,
AssignStartUserHandlerType,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
SimpleFlowNode
} from './consts'
import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
})
const popoverShow = ref(false)
const props = defineProps({
childNode: {
type: Object as () => SimpleFlowNode,
default: null
},
showAdd: {
// 是否显示添加节点
type: Boolean,
default: true
}
})
const emits = defineEmits(['update:childNode'])
const addNode = (type: number) => {
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID()
const data: SimpleFlowNode = {
id: id,
name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
showText: '',
type: NodeType.USER_TASK_NODE,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
// 超时处理
rejectHandler: {
type: RejectHandlerType.FINISH_PROCESS
},
timeoutHandler: {
enable: false
},
assignEmptyHandler: {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.COPY_TASK_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
showText: '',
type: NodeType.COPY_TASK_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.CONDITION_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '条件1',
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: 1,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '其它情况进入此流程',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
defaultFlow: true
}
]
}
emits('update:childNode', data)
}
if (type === NodeType.PARALLEL_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '并行分支',
type: NodeType.PARALLEL_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '并行1',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
},
{
id: 'Flow_' + generateUUID(),
name: '并行2',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
}
]
}
emits('update:childNode', data)
}
}
</script>
<style lang="scss" scoped></style>
<template>
<!-- 发起人节点 -->
<StartUserNode
v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
:flow-node="currentNode"
/>
<!-- 审批节点 -->
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 抄送节点 -->
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 并行节点 -->
<ParallelNode
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
v-model:flow-node="currentNode.childNode"
:parent-node="currentNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
<!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
</template>
<script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({
name: 'ProcessNodeTree'
})
const props = defineProps({
parentNode: {
type: Object as () => SimpleFlowNode,
default: () => null
},
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = useWatchNode(props)
// 用于删除节点
const handleModelValueUpdate = (updateValue) => {
emits('update:flowNode', updateValue)
}
const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
findNode: SimpleFlowNode,
nodeType: number
) => {
if (!findNode) {
return
}
if (findNode.type === NodeType.START_USER_NODE) {
nodeList.push(findNode)
return
}
if (findNode.type === nodeType) {
nodeList.push(findNode)
}
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="simple-flow-canvas" v-loading="loading">
<div class="simple-flow-container">
<div class="top-area-container">
<div class="top-actions">
<div class="canvas-control">
<span class="control-scale-group">
<span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
<span class="control-scale-label">{{ scaleValue }}%</span>
<span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
</span>
</div>
<el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
<!-- <el-button type="primary">全局设置</el-button> -->
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
<div
class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
v-for="(item, index) in errorNodes"
:key="index"
>
{{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
</div>
<template #footer>
<el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
import { getForm, FormVO } from '@/api/bpm/form'
import { handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
defineOptions({
name: 'SimpleProcessDesigner'
})
const router = useRouter() // 路由
const props = defineProps({
modelId: {
type: String,
required: true
}
})
const loading = ref(true)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
provide('formFields', formFields)
provide('formType', formType)
provide('roleList', roleOptions)
provide('postList', postOptions)
provide('userList', userOptions)
provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
if (!props.modelId) {
message.error('缺少模型 modelId 编号')
return
}
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
const data = {
id: props.modelId,
simpleModel: processNodeTree.value
}
const result = await updateBpmSimpleModel(data)
if (result) {
message.success('修改成功')
close()
} else {
message.alert('修改失败')
}
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
const { type, showText, conditionNodes } = node
if (type == NodeType.END_EVENT_NODE) {
return
}
if (type == NodeType.START_USER_NODE) {
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.USER_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.COPY_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.CONDITION_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type == NodeType.CONDITION_BRANCH_NODE) {
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
validateNode(node.childNode, errorNodes)
}
}
}
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomOut = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
// 缩小
const zoomIn = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
onMounted(async () => {
try {
loading.value = true
// 获取表单字段
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
}
// 获得角色列表
roleOptions.value = await RoleApi.getSimpleRoleList()
// 获得岗位列表
postOptions.value = await PostApi.getSimplePostList()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
// 获得部门列表
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
} else {
// 初始值
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
}
} finally {
loading.value = false
}
})
</script>
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner }
\ No newline at end of file
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="权限" name="user">
<div> 待实现 </div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
defineOptions({
name: 'StartUserNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限配置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.WRITE
)
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'
currentNode.value.name = nodeName.value!
// TODO 暂时写死。后续可以显示谁有权限可以发起
currentNode.value.showText = '已设置'
// 设置表单权限
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置发起人的按钮权限
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
settingVisible.value = false
return true
}
// 显示发起人节点配置, 由父组件传过来
const showStartUserNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
// 表单字段权限
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
<CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode } from '../node'
import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
defineOptions({
name: 'CopyTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="end-node-wrapper">
<div class="end-node-box">
<span class="node-fixed-name" title="结束">结束</span>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'EndEventNode'
})
</script>
<style lang="scss" scoped></style>
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加条件</div>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"> </div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !item.showText }">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority"> 优先级{{ index + 1 }} </div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
@click="moveNode(index, -1)"
>
<Icon icon="ep:arrow-left" />
</div>
<div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)"
>
<Icon icon="ep:arrow-right" />
</div>
</div>
<NodeHandler v-model:child-node="item.childNode" />
</div>
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ExclusiveNode'
})
const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
// const conditionNodes = computed(() => currentNode.value.conditionNodes);
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
// 失去焦点
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
// 新增条件
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '条件' + len,
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
// 删除条件
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
// 更新此节点为后续孩子节点
emits('update:modelValue', childNode)
}
}
}
// 移动节点
const moveNode = (index: number, to: number) => {
// -1 :向左 1: 向右
if (currentNode.value.conditionNodes) {
currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
index + to,
1,
currentNode.value.conditionNodes[index]
)[0]
}
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加分支</div>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"></div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority">无优先级</div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar">
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<!-- <div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
<Icon icon="ep:arrow-left" />
</div> -->
<!-- <div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)">
<Icon icon="ep:arrow-right" />
</div> -->
</div>
<NodeHandler v-model:child-node="item.childNode" />
</div>
</div>
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { generateUUID } from '@/utils'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ParallelNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
// 失去焦点
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name = conditionNode.name || `并行${index + 1}`
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
// 新增条件
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '并行' + len,
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: []
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
// 删除条件
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
// 更新此节点为后续孩子节点
emits('update:modelValue', childNode)
}
}
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span
></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2 } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
defineOptions({
name: 'StartEventNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
}>()
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
// 把当前节点传递给配置组件
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<UserTaskNodeConfig
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2 } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
defineOptions({
name: 'UserTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>()
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
// 把当前节点传递给配置组件
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
// 查找可以驳回用户节点
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] // 匹配的节点
) => {
// 从父节点查找
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
}
</script>
<style lang="scss" scoped></style>
import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
if (defaultFlow) {
return '其它情况'
}
return '条件' + (index + 1)
}
export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE
}
if (strTimeUnit === 'H') {
return TimeUnitType.HOUR
}
if (strTimeUnit === 'D') {
return TimeUnitType.DAY
}
return TimeUnitType.HOUR
}
export const getApproveTypeText = (approveType: ApproveType): string => {
let approveTypeText = ''
APPROVE_TYPE.forEach((item) => {
if (item.value === approveType) {
approveTypeText = item.label
return
}
})
return approveTypeText
}
...@@ -11,3 +11,14 @@ export const setupAuth = (app: App<Element>) => { ...@@ -11,3 +11,14 @@ export const setupAuth = (app: App<Element>) => {
hasRole(app) hasRole(app)
hasPermi(app) hasPermi(app)
} }
/**
* 导出指令:v-mountedFocus
*/
export const setupMountedFocus = (app: App<Element>) => {
app.directive('mountedFocus', {
mounted(el) {
el.focus()
}
})
}
...@@ -28,8 +28,8 @@ import '@/plugins/animate.css' ...@@ -28,8 +28,8 @@ import '@/plugins/animate.css'
// 路由 // 路由
import router, { setupRouter } from '@/router' import router, { setupRouter } from '@/router'
// 权限 // 指令
import { setupAuth } from '@/directives' import { setupAuth, setupMountedFocus } from '@/directives'
import { createApp } from 'vue' import { createApp } from 'vue'
...@@ -58,7 +58,9 @@ const setupAll = async () => { ...@@ -58,7 +58,9 @@ const setupAll = async () => {
setupRouter(app) setupRouter(app)
// directives 指令
setupAuth(app) setupAuth(app)
setupMountedFocus(app)
await router.isReady() await router.isReady()
......
import type { App } from 'vue' import type { App } from 'vue'
// 👇使用 form-create 需额外全局引入 element plus 组件 // 👇使用 form-create 需额外全局引入 element plus 组件
import { import {
// ElAutocomplete,
// ElButton,
// ElCascader,
// ElCheckbox,
// ElCheckboxButton,
// ElCheckboxGroup,
// ElCol,
// ElColorPicker,
// ElDatePicker,
// ElDialog,
// ElForm,
// ElInput,
// ElInputNumber,
// ElPopover,
// ElRadio,
// ElRadioButton,
// ElRadioGroup,
// ElRate,
// ElRow,
// ElSelect,
// ElSlider,
// ElSwitch,
// ElTimePicker,
// ElTooltip,
// ElTree,
// ElUpload,
// ElIcon,
// ElProgress,
// 以上会由 @form-create/element-ui/auto-import 自动引入
ElAlert, ElAlert,
ElTransfer,
ElAside, ElAside,
ElContainer, ElContainer,
ElDivider, ElDivider,
...@@ -12,7 +42,18 @@ import { ...@@ -12,7 +42,18 @@ import {
ElTableColumn, ElTableColumn,
ElTabPane, ElTabPane,
ElTabs, ElTabs,
ElTransfer ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ElBadge,
ElTag,
ElText,
ElMenu,
ElMenuItem,
ElFooter,
ElMessage
// ElFormItem,
// ElOption
} from 'element-plus' } from 'element-plus'
import FcDesigner from '@form-create/designer' import FcDesigner from '@form-create/designer'
import formCreate from '@form-create/element-ui' import formCreate from '@form-create/element-ui'
...@@ -41,18 +82,30 @@ const ApiSelect = useApiSelect({ ...@@ -41,18 +82,30 @@ const ApiSelect = useApiSelect({
}) })
const components = [ const components = [
ElAlert,
ElTransfer,
ElAside, ElAside,
ElPopconfirm,
ElHeader,
ElMain,
ElContainer, ElContainer,
ElDivider, ElDivider,
ElTransfer, ElHeader,
ElAlert, ElMain,
ElTabs, ElPopconfirm,
ElTable, ElTable,
ElTableColumn, ElTableColumn,
ElTabPane, ElTabPane,
ElTabs,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ElBadge,
ElTag,
ElText,
ElMenu,
ElMenuItem,
ElFooter,
ElMessage,
// ElFormItem,
// ElOption,
UploadImg, UploadImg,
UploadImgs, UploadImgs,
UploadFile, UploadFile,
......
...@@ -292,6 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -292,6 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
}, },
{ {
path: 'process-instance/detail', path: 'process-instance/detail',
// component: () => import('@/views/bpm/processInstance/detail/index_new.vue'), // TODO 芋艿:新审批界面,已适配 simple 模式,未来会适配 bpmn 模式
component: () => import('@/views/bpm/processInstance/detail/index.vue'), component: () => import('@/views/bpm/processInstance/detail/index.vue'),
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
meta: { meta: {
...@@ -300,7 +301,12 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -300,7 +301,12 @@ const remainingRouter: AppRouteRecordRaw[] = [
canTo: true, canTo: true,
title: '流程详情', title: '流程详情',
activeMenu: '/bpm/task/my' activeMenu: '/bpm/task/my'
} },
props: (route) => ({
id: route.query.id,
taskId: route.query.taskId,
activityId: route.query.activityId
})
}, },
{ {
path: 'oa/leave/create', path: 'oa/leave/create',
...@@ -603,6 +609,38 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -603,6 +609,38 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
breadcrumb: false breadcrumb: false
} }
},
{
path: '/iot',
component: Layout,
name: 'IOT',
meta: {
hidden: true
},
children: [
{
path: 'product/detail/:id',
name: 'IoTProductDetail',
meta: {
title: '产品详情',
noCache: true,
hidden: true,
activeMenu: '/iot/product'
},
component: () => import('@/views/iot/product/detail/index.vue')
},
{
path: 'device/detail/:id',
name: 'IoTDeviceDetail',
meta: {
title: '设备详情',
noCache: true,
hidden: true,
activeMenu: '/iot/device'
},
component: () => import('@/views/iot/device/detail/index.vue')
}
]
} }
] ]
......
import { store } from '../index' import { store } from '../../index'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export const useWorkFlowStore = defineStore('simpleWorkflow', { export const useWorkFlowStore = defineStore('simpleWorkflow', {
...@@ -6,15 +6,15 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', { ...@@ -6,15 +6,15 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
tableId: '', tableId: '',
isTried: false, isTried: false,
promoterDrawer: false, promoterDrawer: false,
flowPermission1: {},
approverDrawer: false, approverDrawer: false,
approverConfig1: {}, approverConfig1: {},
copyerDrawer: false, copyerDrawer: false,
copyerConfig1: {}, copyerConfig: {},
conditionDrawer: false, conditionDrawer: false,
conditionsConfig1: { conditionsConfig1: {
conditionNodes: [] conditionNodes: []
} },
userTaskConfig: {}
}), }),
actions: { actions: {
setTableId(payload) { setTableId(payload) {
...@@ -26,26 +26,26 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', { ...@@ -26,26 +26,26 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
setPromoter(payload) { setPromoter(payload) {
this.promoterDrawer = payload this.promoterDrawer = payload
}, },
setFlowPermission(payload) { setApproverDrawer(payload) {
this.flowPermission1 = payload
},
setApprover(payload) {
this.approverDrawer = payload this.approverDrawer = payload
}, },
setApproverConfig(payload) { setApproverConfig(payload) {
this.approverConfig1 = payload this.approverConfig1 = payload
}, },
setCopyer(payload) { setCopyerDrawer(payload) {
this.copyerDrawer = payload this.copyerDrawer = payload
}, },
setCopyerConfig(payload) { setCopyerConfig(payload) {
this.copyerConfig1 = payload this.copyerConfig = payload
}, },
setCondition(payload) { setCondition(payload) {
this.conditionDrawer = payload this.conditionDrawer = payload
}, },
setConditionsConfig(payload) { setConditionsConfig(payload) {
this.conditionsConfig1 = payload this.conditionsConfig1 = payload
},
setUserTaskConfig(payload) {
this.userTaskConfig = payload
} }
} }
}) })
......
...@@ -437,3 +437,15 @@ export const ErpBizType = { ...@@ -437,3 +437,15 @@ export const ErpBizType = {
SALE_OUT: 21, SALE_OUT: 21,
SALE_RETURN: 22 SALE_RETURN: 22
} }
// ========== BPM 模块 ==========
export const BpmModelType = {
BPMN: 10, // BPMN 设计器
SIMPLE: 20 // 简易设计器
}
export const BpmModelFormType = {
NORMAL: 10, // 流程表单
CUSTOM: 20 // 业务表单
}
...@@ -143,6 +143,7 @@ export enum DICT_TYPE { ...@@ -143,6 +143,7 @@ export enum DICT_TYPE {
INFRA_OPERATE_TYPE = 'infra_operate_type', INFRA_OPERATE_TYPE = 'infra_operate_type',
// ========== BPM 模块 ========== // ========== BPM 模块 ==========
BPM_MODEL_TYPE = 'bpm_model_type',
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
...@@ -225,5 +226,18 @@ export enum DICT_TYPE { ...@@ -225,5 +226,18 @@ export enum DICT_TYPE {
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度 AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式 AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气 AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言 AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
// ========== IOT - 物联网模块 ==========
IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
IOT_VALIDATE_TYPE = 'iot_validate_type', // IOT 数据校验级别
IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态
IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
IOT_PRODUCT_FUNCTION_TYPE = 'iot_product_function_type', // IOT 产品功能类型
IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
IOT_RW_TYPE = 'iot_rw_type' // IOT 读写类型
} }
<template> <template>
<ContentWrap> <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
<!-- 表单设计器 --> <!-- 表单设计器 -->
<FcDesigner ref="designer" height="780px"> <div
<template #handle> class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
<el-button round size="small" type="primary" @click="handleSave"> >
<Icon class="mr-5px" icon="ep:plus" /> <fc-designer class="my-designer" ref="designer" :config="designerConfig">
保存 <template #handle>
</el-button> <el-button size="small" type="success" plain @click="handleSave">
</template> <Icon class="mr-5px" icon="ep:plus" />
</FcDesigner> 保存
</el-button>
</template>
</fc-designer>
</div>
</ContentWrap> </ContentWrap>
<!-- 表单保存的弹窗 --> <!-- 表单保存的弹窗 -->
...@@ -55,6 +59,31 @@ const { push, currentRoute } = useRouter() // 路由 ...@@ -55,6 +59,31 @@ const { push, currentRoute } = useRouter() // 路由
const { query } = useRoute() // 路由信息 const { query } = useRoute() // 路由信息
const { delView } = useTagsViewStore() // 视图操作 const { delView } = useTagsViewStore() // 视图操作
// 表单设计器配置
const designerConfig = ref({
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
autoActive: true, // 是否自动选中拖入的组件
useTemplate: false, // 是否生成vue2语法的模板组件
formOptions: {}, // 定义表单配置默认值
fieldReadonly: false, // 配置field是否可以编辑
hiddenDragMenu: false, // 隐藏拖拽操作按钮
hiddenDragBtn: false, // 隐藏拖拽按钮
hiddenMenu: [], // 隐藏部分菜单
hiddenItem: [], // 隐藏部分组件
hiddenItemConfig: {}, // 隐藏组件的部分配置项
disabledItemConfig: {}, // 禁用组件的部分配置项
showSaveBtn: false, // 是否显示保存按钮
showConfig: true, // 是否显示右侧的配置界面
showBaseForm: true, // 是否显示组件的基础配置表单
showControl: true, // 是否显示组件联动
showPropsForm: true, // 是否显示组件的属性配置表单
showEventForm: true, // 是否显示组件的事件配置表单
showValidateForm: true, // 是否显示组件的验证配置表单
showFormConfig: true, // 是否显示表单配置
showInputData: true, // 是否显示录入按钮
showDevice: true, // 是否显示多端适配选项
appendConfigData: [] // 定义渲染规则所需的formData
})
const designer = ref() // 表单设计器 const designer = ref() // 表单设计器
useFormCreateDesigner(designer) // 表单设计器增强 useFormCreateDesigner(designer) // 表单设计器增强
const dialogVisible = ref(false) // 弹窗是否展示 const dialogVisible = ref(false) // 弹窗是否展示
...@@ -119,3 +148,13 @@ onMounted(async () => { ...@@ -119,3 +148,13 @@ onMounted(async () => {
setConfAndFields(designer, data.conf, data.fields) setConfAndFields(designer, data.conf, data.fields)
}) })
</script> </script>
<style>
.my-designer {
._fc-l,
._fc-m,
._fc-r {
border-top: none;
}
}
</style>
<template>
<Dialog v-model="dialogVisible" title="导入流程" width="400">
<div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl"
:auto-upload="false"
:data="formData"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".bpmn, .xml"
drag
name="bpmnFile"
>
<Icon class="el-icon--upload" icon="ep:upload-filled" />
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示:仅允许导入“bpm”或“xml”格式文件!
</div>
<div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
placeholder="请输入流标标识"
style="width: 250px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
</el-form>
</div>
</template>
</el-upload>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'ModelImportForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
key: '',
name: '',
description: ''
})
const formRules = reactive({
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const uploadRef = ref() // 上传 Ref
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref([]) // 文件列表
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
// 提交请求
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = async (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
// 提示成功
message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('导入流程失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
// 重置表单
formData.value = {
key: '',
name: '',
description: ''
}
formRef.value?.resetFields()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>
...@@ -58,17 +58,17 @@ const initModeler = (item) => { ...@@ -58,17 +58,17 @@ const initModeler = (item) => {
} }
/** 添加/修改模型 */ /** 添加/修改模型 */
const save = async (bpmnXml) => { const save = async (bpmnXml: string) => {
const data = { const data = {
...model.value, ...model.value,
bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
} as unknown as ModelApi.ModelVO } as unknown as ModelApi.ModelVO
// 提交 // 提交
if (data.id) { if (data.id) {
await ModelApi.updateModel(data) await ModelApi.updateModelBpmn(data)
message.success('修改成功') message.success('修改成功')
} else { } else {
await ModelApi.createModel(data) await ModelApi.updateModelBpmn(data)
message.success('新增成功') message.success('新增成功')
} }
// 跳转回去 // 跳转回去
......
<template> <template>
<div <div
class="h-50px position-fixed bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container" class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
v-if="runningTask.id"
> >
<el-popover :visible="passVisible" placement="top-end" :width="500" trigger="click"> <!-- 【通过】按钮 -->
<el-popover
:visible="passVisible"
placement="top-end"
:width="500"
trigger="click"
v-if="isShowButton(OperationButtonType.APPROVE)"
>
<template #reference> <template #reference>
<el-button plain type="success" @click="openPopover('1')"> <el-button plain type="success" @click="openPopover('1')">
<Icon icon="ep:select" />&nbsp; 通过 <Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button> </el-button>
</template> </template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form <el-form
label-position="top" label-position="top"
...@@ -50,19 +59,28 @@ ...@@ -50,19 +59,28 @@
<el-form-item> <el-form-item>
<el-button :disabled="formLoading" type="success" @click="handleAudit(true)"> <el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
通过 {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button> </el-button>
<el-button @click="passVisible = false"> 取消 </el-button> <el-button @click="passVisible = false"> 取消 </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</el-popover> </el-popover>
<el-popover :visible="rejectVisible" placement="top-end" :width="500" trigger="click">
<!-- 【拒绝】按钮 -->
<el-popover
:visible="rejectVisible"
placement="top-end"
:width="500"
trigger="click"
v-if="isShowButton(OperationButtonType.REJECT)"
>
<template #reference> <template #reference>
<el-button class="mr-20px" plain type="danger" @click="openPopover('2')"> <el-button class="mr-20px" plain type="danger" @click="openPopover('2')">
<Icon icon="ep:close" />&nbsp; 拒绝 <Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button> </el-button>
</template> </template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form <el-form
label-position="top" label-position="top"
...@@ -105,21 +123,46 @@ ...@@ -105,21 +123,46 @@
<el-form-item> <el-form-item>
<el-button :disabled="formLoading" type="danger" @click="handleAudit(false)"> <el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
拒绝 {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button> </el-button>
<el-button @click="rejectVisible = false"> 取消 </el-button> <el-button @click="rejectVisible = false"> 取消 </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</el-popover> </el-popover>
<!-- 【抄送】按钮 -->
<div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" />&nbsp;抄送 </div> <div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" />&nbsp;抄送 </div>
<div @click="openTaskUpdateAssigneeForm">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;转交 <!-- 【转交】按钮 -->
<div @click="openTaskUpdateAssigneeForm" v-if="isShowButton(OperationButtonType.TRANSFER)">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</div>
<!-- 【委托】按钮 -->
<div @click="handleDelegate" v-if="isShowButton(OperationButtonType.DELEGATE)">
<Icon :size="14" icon="ep:position" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</div>
<!-- 【加签】 -->
<div @click="handleSign" v-if="isShowButton(OperationButtonType.ADD_SIGN)">
<Icon :size="14" icon="ep:plus" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</div>
<!-- TODO @jason:减签 -->
<!-- 【退回】按钮 -->
<div @click="handleBack" v-if="isShowButton(OperationButtonType.RETURN)">
<Icon :size="14" icon="fa:mail-reply" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.RETURN) }}
</div> </div>
<div @click="handleDelegate"> <Icon :size="14" icon="ep:position" />&nbsp;委派 </div>
<div @click="handleSign"> <Icon :size="14" icon="ep:plus" />&nbsp;加签 </div> <!--TODO @jason:撤回 -->
<div @click="handleBack"> <Icon :size="14" icon="fa:mail-reply" />&nbsp;退回 </div> <!--TODO @jason:再次发起 -->
</div> </div>
<!-- 弹窗:转派审批人 --> <!-- 弹窗:转派审批人 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" /> <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
<!-- 弹窗:回退节点 --> <!-- 弹窗:回退节点 -->
...@@ -129,7 +172,6 @@ ...@@ -129,7 +172,6 @@
<!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" /> <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
...@@ -140,7 +182,10 @@ import TaskDelegateForm from './dialog/TaskDelegateForm.vue' ...@@ -140,7 +182,10 @@ import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue' import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue' import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { isEmpty } from '@/utils/is' import { isEmpty } from '@/utils/is'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'ProcessInstanceBtnConatiner' }) defineOptions({ name: 'ProcessInstanceBtnConatiner' })
const userId = useUserStore().getUser.id // 当前登录的编号 const userId = useUserStore().getUser.id // 当前登录的编号
...@@ -175,15 +220,17 @@ watch( ...@@ -175,15 +220,17 @@ watch(
deep: true deep: true
} }
) )
// TODO @jaosn:具体的审批任务,要不改成后端返回。让前端弱化下
/** /**
* 设置 runningTasks 中的任务 * 设置 runningTasks 中的任务
*/ */
const loadRunningTask = (tasks) => { const loadRunningTask = (tasks: any[]) => {
runningTask.value = {} runningTask.value = {}
auditForm.value = {} auditForm.value = {}
approveForm.value = {} approveForm.value = {}
approveFormFApi.value = {} approveFormFApi.value = {}
tasks.forEach((task) => { tasks.forEach((task: any) => {
if (!isEmpty(task.children)) { if (!isEmpty(task.children)) {
loadRunningTask(task.children) loadRunningTask(task.children)
} }
...@@ -214,7 +261,7 @@ const loadRunningTask = (tasks) => { ...@@ -214,7 +261,7 @@ const loadRunningTask = (tasks) => {
} }
/** 处理审批通过和不通过的操作 */ /** 处理审批通过和不通过的操作 */
const handleAudit = async (pass) => { const handleAudit = async (pass: any) => {
formLoading.value = true formLoading.value = true
try { try {
const auditFormRef = proxy.$refs['formRef'] const auditFormRef = proxy.$refs['formRef']
...@@ -254,6 +301,7 @@ const handleAudit = async (pass) => { ...@@ -254,6 +301,7 @@ const handleAudit = async (pass) => {
/* 抄送 TODO */ /* 抄送 TODO */
const handleSend = () => {} const handleSend = () => {}
// TODO 代码优化:这里 flag 改成 approve: boolean 。因为 flag 目前就只有 1 和 2
const openPopover = (flag) => { const openPopover = (flag) => {
passVisible.value = false passVisible.value = false
rejectVisible.value = false rejectVisible.value = false
...@@ -289,6 +337,24 @@ const getDetail = () => { ...@@ -289,6 +337,24 @@ const getDetail = () => {
emit('success') emit('success')
} }
/** 是否显示按钮 */
const isShowButton = (btnType: OperationButtonType): boolean => {
let isShow = true
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
isShow = runningTask.value.buttonsSetting[btnType].enable
}
return isShow
}
/** 获取按钮的显示名称 */
const getButtonDisplayName = (btnType: OperationButtonType) => {
let displayName = OPERATION_BUTTON_NAME.get(btnType)
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
displayName = runningTask.value.buttonsSetting[btnType].displayName
}
return displayName
}
defineExpose({ loadRunningTask }) defineExpose({ loadRunningTask })
</script> </script>
...@@ -299,10 +365,11 @@ defineExpose({ loadRunningTask }) ...@@ -299,10 +365,11 @@ defineExpose({ loadRunningTask })
.btn-container { .btn-container {
> div { > div {
display: flex;
margin: 0 15px; margin: 0 15px;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center; align-items: center;
&:hover { &:hover {
color: #6db5ff; color: #6db5ff;
} }
......
...@@ -56,29 +56,73 @@ ...@@ -56,29 +56,73 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px"> <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
<el-button type="success" @click="handleAudit(item, true)"> <!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
<el-button
type="success"
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
@click="handleAudit(item, true)"
>
<Icon icon="ep:select" /> <Icon icon="ep:select" />
通过 <!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
{{
item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
}}
</el-button> </el-button>
<el-button type="danger" @click="handleAudit(item, false)"> <el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
type="danger"
@click="handleAudit(item, false)"
>
<Icon icon="ep:close" /> <Icon icon="ep:close" />
不通过 {{
item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
}}
</el-button> </el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)"> <el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
type="primary"
@click="openTaskUpdateAssigneeForm(item.id)"
>
<Icon icon="ep:edit" /> <Icon icon="ep:edit" />
转办 {{
item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
}}
</el-button> </el-button>
<el-button type="primary" @click="handleDelegate(item)"> <el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
type="primary"
@click="handleDelegate(item)"
>
<Icon icon="ep:position" /> <Icon icon="ep:position" />
委派 {{
item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
}}
</el-button> </el-button>
<el-button type="primary" @click="handleSign(item)"> <el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
type="primary"
@click="handleSign(item)"
>
<Icon icon="ep:plus" /> <Icon icon="ep:plus" />
加签 {{
item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
}}
</el-button> </el-button>
<el-button type="warning" @click="handleBack(item)"> <el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
type="warning"
@click="handleBack(item)"
>
<Icon icon="ep:back" /> <Icon icon="ep:back" />
回退 {{
item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
}}
</el-button> </el-button>
</div> </div>
</el-col> </el-col>
...@@ -147,6 +191,10 @@ import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue' ...@@ -147,6 +191,10 @@ import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { registerComponent } from '@/utils/routerHelper' import { registerComponent } from '@/utils/routerHelper'
import { isEmpty } from '@/utils/is' import { isEmpty } from '@/utils/is'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'BpmProcessInstanceDetail' }) defineOptions({ name: 'BpmProcessInstanceDetail' })
...@@ -200,8 +248,14 @@ const handleAudit = async (task, pass) => { ...@@ -200,8 +248,14 @@ const handleAudit = async (task, pass) => {
// 1.2 校验表单 // 1.2 校验表单
const elForm = unref(auditFormRef) const elForm = unref(auditFormRef)
if (!elForm) return if (!elForm) return
const valid = await elForm.validate() let valid = await elForm.validate()
if (!valid) return if (!valid) return
// 校验申请表单(可编辑字段)
// TODO @jason:之前这里是 if (!fApi.value) return;针对业务表单的情况下,会导致没办法审核,可能要看下。我这里改了点,看看是不是还有别的地方兼容性
if (fApi.value) {
valid = await fApi.value.validate()
if (!valid) return
}
// 2.1 提交审批 // 2.1 提交审批
const data = { const data = {
...@@ -216,6 +270,11 @@ const handleAudit = async (task, pass) => { ...@@ -216,6 +270,11 @@ const handleAudit = async (task, pass) => {
await formCreateApi.validate() await formCreateApi.validate()
data.variables = approveForms.value[index].value data.variables = approveForms.value[index].value
} }
// 获取表单可编辑字段的值
if (fApi.value) {
data.variables = getWritableValueOfForm(task.fieldsPermission)
}
await TaskApi.approveTask(data) await TaskApi.approveTask(data)
message.success('审批通过成功') message.success('审批通过成功')
} else { } else {
...@@ -251,11 +310,11 @@ const handleSign = async (task: any) => { ...@@ -251,11 +310,11 @@ const handleSign = async (task: any) => {
} }
/** 获得详情 */ /** 获得详情 */
const getDetail = () => { const getDetail = async () => {
// 1. 获得流程实例相关 // 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置
await getTaskList()
// 2. 获得流程实例相关
getProcessInstance() getProcessInstance()
// 2. 获得流程任务列表(审批记录)
getTaskList()
} }
/** 加载流程实例 */ /** 加载流程实例 */
...@@ -273,16 +332,29 @@ const getProcessInstance = async () => { ...@@ -273,16 +332,29 @@ const getProcessInstance = async () => {
// 设置表单信息 // 设置表单信息
const processDefinition = data.processDefinition const processDefinition = data.processDefinition
if (processDefinition.formType === 10) { if (processDefinition.formType === 10) {
setConfAndFields2( if (detailForm.value.rule.length > 0) {
detailForm, detailForm.value.value = data.formVariables
processDefinition.formConf, } else {
processDefinition.formFields, setConfAndFields2(
data.formVariables detailForm,
) processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
}
nextTick().then(() => { nextTick().then(() => {
fApi.value?.btn.show(false) fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false) fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true) fApi.value?.disabled(true)
// 设置表单权限。后续需要改造成。只处理一个运行中的任务
if (runningTasks.value.length > 0) {
const task = runningTasks.value.at(0)
if (task.fieldsPermission) {
Object.keys(task.fieldsPermission).forEach((item) => {
setFieldPermission(item, task.fieldsPermission[item])
})
}
}
}) })
} else { } else {
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
...@@ -353,6 +425,7 @@ const loadRunningTask = (tasks) => { ...@@ -353,6 +425,7 @@ const loadRunningTask = (tasks) => {
if (!task.assigneeUser || task.assigneeUser.id !== userId) { if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return return
} }
// 2.3 添加到处理任务 // 2.3 添加到处理任务
runningTasks.value.push({ ...task }) runningTasks.value.push({ ...task })
auditForms.value.push({ auditForms.value.push({
...@@ -371,6 +444,35 @@ const loadRunningTask = (tasks) => { ...@@ -371,6 +444,35 @@ const loadRunningTask = (tasks) => {
}) })
} }
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => {
if (permission === '1') {
fApi.value?.disabled(true, field)
}
if (permission === '2') {
fApi.value?.disabled(false, field)
}
if (permission === '3') {
fApi.value?.hidden(true, field)
}
}
/**
* 获取可以编辑字段的值
*/
const getWritableValueOfForm = (fieldsPermission: Object) => {
const fieldsValue = {}
if (fieldsPermission && fApi.value) {
Object.keys(fieldsPermission).forEach((item) => {
if (fieldsPermission[item] === '2') {
fieldsValue[item] = fApi.value.getValue(item)
}
})
}
return fieldsValue
}
/** 初始化 */ /** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => { onMounted(async () => {
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="所属流程" prop="processDefinitionId"> <el-form-item label="所属流程" prop="processDefinitionKey">
<el-input <el-input
v-model="queryParams.processDefinitionId" v-model="queryParams.processDefinitionKey"
placeholder="请输入流程定义的编号" placeholder="请输入流程定义的标识"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
...@@ -183,7 +183,7 @@ const queryParams = reactive({ ...@@ -183,7 +183,7 @@ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
name: '', name: '',
processDefinitionId: undefined, processDefinitionKey: undefined,
category: undefined, category: undefined,
status: undefined, status: undefined,
createTime: [] createTime: []
......
...@@ -79,6 +79,10 @@ ...@@ -79,6 +79,10 @@
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
</el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
......
<template> <template>
<div> <SimpleProcessDesigner :model-id="modelId" />
<section class="dingflow-design">
<div class="box-scale">
<nodeWrap v-model:nodeConfig="nodeConfig" />
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
</section>
</div>
</template> </template>
<script lang="ts" setup> <script setup lang="ts">
import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue' import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({ name: 'SimpleWorkflowDesignEditor' })
let nodeConfig = ref({ defineOptions({
nodeName: '发起人', name: 'SimpleWorkflowDesignEditor'
type: 0,
id: 'root',
formPerms: {},
nodeUserList: [],
childNode: {}
}) })
const { query } = useRoute() // 路由的查询
const modelId = query.modelId as string
</script> </script>
<style> <style lang="scss" scoped></style>
@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
</style>
\ No newline at end of file
...@@ -111,11 +111,16 @@ const getList = async () => { ...@@ -111,11 +111,16 @@ const getList = async () => {
/** 处理审批按钮 */ /** 处理审批按钮 */
const handleAudit = (row: any) => { const handleAudit = (row: any) => {
const query = {
id: row.processInstanceId,
activityId: undefined
}
if (row.activityId) {
query.activityId = row.activityId
}
push({ push({
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
query: { query: query
id: row.processInstanceId
}
}) })
} }
......
...@@ -158,7 +158,8 @@ const handleAudit = (row: any) => { ...@@ -158,7 +158,8 @@ const handleAudit = (row: any) => {
push({ push({
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
query: { query: {
id: row.processInstance.id id: row.processInstance.id,
taskId: row.id
} }
}) })
} }
......
...@@ -140,7 +140,8 @@ const handleAudit = (row: any) => { ...@@ -140,7 +140,8 @@ const handleAudit = (row: any) => {
push({ push({
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
query: { query: {
id: row.processInstance.id id: row.processInstance.id,
taskId: row.id
} }
}) })
} }
......
<template> <template>
<ContentWrap> <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
<el-row>
<el-col>
<div class="float-right mb-2">
<el-button size="small" type="primary" @click="showJson">生成 JSON</el-button>
<el-button size="small" type="success" @click="showOption">生成 Options</el-button>
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
</div>
</el-col>
</el-row>
<!-- 表单设计器 --> <!-- 表单设计器 -->
<FcDesigner ref="designer" height="780px" /> <div
class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
>
<fc-designer class="my-designer" ref="designer" :config="designerConfig">
<template #handle>
<el-button size="small" type="primary" plain @click="showJson">生成JSON</el-button>
<el-button size="small" type="success" plain @click="showOption">生成Options</el-button>
<el-button size="small" type="danger" plain @click="showTemplate">生成组件</el-button>
</template>
</fc-designer>
</div>
</ContentWrap> </ContentWrap>
<!-- 弹窗:表单预览 --> <!-- 弹窗:表单预览 -->
...@@ -43,6 +44,31 @@ defineOptions({ name: 'InfraBuild' }) ...@@ -43,6 +44,31 @@ defineOptions({ name: 'InfraBuild' })
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息 const message = useMessage() // 消息
// 表单设计器配置
const designerConfig = ref({
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
autoActive: true, // 是否自动选中拖入的组件
useTemplate: false, // 是否生成vue2语法的模板组件
formOptions: {}, // 定义表单配置默认值
fieldReadonly: false, // 配置field是否可以编辑
hiddenDragMenu: false, // 隐藏拖拽操作按钮
hiddenDragBtn: false, // 隐藏拖拽按钮
hiddenMenu: [], // 隐藏部分菜单
hiddenItem: [], // 隐藏部分组件
hiddenItemConfig: {}, // 隐藏组件的部分配置项
disabledItemConfig: {}, // 禁用组件的部分配置项
showSaveBtn: false, // 是否显示保存按钮
showConfig: true, // 是否显示右侧的配置界面
showBaseForm: true, // 是否显示组件的基础配置表单
showControl: true, // 是否显示组件联动
showPropsForm: true, // 是否显示组件的属性配置表单
showEventForm: true, // 是否显示组件的事件配置表单
showValidateForm: true, // 是否显示组件的验证配置表单
showFormConfig: true, // 是否显示表单配置
showInputData: true, // 是否显示录入按钮
showDevice: true, // 是否显示多端适配选项
appendConfigData: [] // 定义渲染规则所需的formData
})
const designer = ref() // 表单设计器 const designer = ref() // 表单设计器
const dialogVisible = ref(false) // 弹窗的是否展示 const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题 const dialogTitle = ref('') // 弹窗的标题
...@@ -140,3 +166,13 @@ onMounted(async () => { ...@@ -140,3 +166,13 @@ onMounted(async () => {
hljs.registerLanguage('json', json) hljs.registerLanguage('json', json)
}) })
</script> </script>
<style>
.my-designer {
._fc-l,
._fc-m,
._fc-r {
border-top: none;
}
}
</style>
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { useWebSocket } from '@vueuse/core' import { useWebSocket } from '@vueuse/core'
import { getAccessToken } from '@/utils/auth' import { getRefreshToken } from '@/utils/auth'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
defineOptions({ name: 'InfraWebSocket' }) defineOptions({ name: 'InfraWebSocket' })
...@@ -79,7 +79,9 @@ defineOptions({ name: 'InfraWebSocket' }) ...@@ -79,7 +79,9 @@ defineOptions({ name: 'InfraWebSocket' })
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const server = ref( const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken() (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:WebSocket 无法方便的刷新访问令牌
) // WebSocket 服务地址 ) // WebSocket 服务地址
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开 const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色 const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色
......
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="产品" prop="productId">
<el-select
v-model="formData.productId"
placeholder="请选择产品"
:disabled="formType === 'update'"
clearable
>
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="formData.deviceName"
placeholder="请输入 DeviceName"
:disabled="formType === 'update'"
/>
</el-form-item>
<el-form-item label="备注名称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入备注名称" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device'
import { ProductApi } from '@/api/iot/product'
/** IoT 设备 表单 */
defineOptions({ name: 'IoTDeviceForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
productId: undefined,
deviceName: undefined,
nickname: undefined
})
const formRules = reactive({
productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
deviceName: [
{
pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
message:
'支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@,长度限制为 4~32 个字符',
trigger: 'blur'
}
],
nickname: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null) {
callback()
return
}
const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length
if (length < 4 || length > 64) {
callback(new Error('备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符'))
} else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) {
callback(new Error('备注名称只能包含中文、英文字母、日文、数字和下划线(_)'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await DeviceApi.getDevice(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as DeviceVO
if (formType.value === 'create') {
await DeviceApi.createDevice(data)
message.success(t('common.createSuccess'))
} else {
await DeviceApi.updateDevice(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
productId: undefined,
deviceName: undefined,
nickname: undefined
}
formRef.value?.resetFields()
}
/** 查询字典下拉列表 */
const products = ref()
const getProducts = async () => {
products.value = await ProductApi.getSimpleProductList()
}
onMounted(() => {
getProducts()
})
</script>
<template>
<div>
<div class="flex items-start justify-between">
<div>
<el-col>
<el-row>
<span class="text-xl font-bold">{{ device.deviceName }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上:按钮 -->
<el-button
@click="openForm('update', device.id)"
v-hasPermi="['iot:device:update']"
v-if="product.status === 0"
>
编辑
</el-button>
</div>
</div>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="产品">
<el-link @click="goToProductDetail(product.id)">{{ product.name }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<DeviceForm ref="formRef" @success="emit('refresh')" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import DeviceForm from '@/views/iot/device/DeviceForm.vue'
import { ProductVO } from '@/api/iot/product'
import { DeviceVO } from '@/api/iot/device'
import { useRouter } from 'vue-router'
const message = useMessage()
const router = useRouter()
// 操作修改
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
const emit = defineEmits(['refresh'])
/**
* 将文本复制到剪贴板
*
* @param text 需要复制的文本
*/
const copyToClipboard = (text: string) => {
// TODO @haohao:可以考虑用 await 异步转同步哈
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
})
}
/**
* 跳转到产品详情页面
*
* @param productId 产品 ID
*/
const goToProductDetail = (productId: number) => {
router.push({ name: 'IoTProductDetail', params: { id: productId } })
}
</script>
<template>
<ContentWrap>
<el-collapse v-model="activeNames">
<el-descriptions :column="3" title="设备信息">
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
</el-descriptions-item>
<el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item>
<el-descriptions-item label="DeviceName">
{{ device.deviceName }}
<el-button @click="copyToClipboard(device.deviceName)">复制</el-button>
</el-descriptions-item>
<el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(device.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="激活时间">
{{ formatDate(device.activeTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后上线时间">
{{ formatDate(device.lastOnlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="当前状态">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.status" />
</el-descriptions-item>
<el-descriptions-item label="最后离线时间" :span="3">
{{ formatDate(device.lastOfflineTime) }}
</el-descriptions-item>
<el-descriptions-item label="MQTT 连接参数">
<el-button type="primary" @click="openMqttParams">查看</el-button>
</el-descriptions-item>
</el-descriptions>
</el-collapse>
<!-- MQTT 连接参数弹框 -->
<Dialog
title="MQTT 连接参数"
v-model="mqttDialogVisible"
width="50%"
:before-close="handleCloseMqttDialog"
>
<el-form :model="mqttParams" label-width="120px">
<el-form-item label="clientId">
<el-input v-model="mqttParams.mqttClientId" readonly>
<template #append>
<el-button @click="copyToClipboard(mqttParams.mqttClientId)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="username">
<el-input v-model="mqttParams.mqttUsername" readonly>
<template #append>
<el-button @click="copyToClipboard(mqttParams.mqttUsername)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="passwd">
<el-input v-model="mqttParams.mqttPassword" readonly type="password">
<template #append>
<el-button @click="copyToClipboard(mqttParams.mqttPassword)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="mqttDialogVisible = false">关闭</el-button>
</template>
</Dialog>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { ProductVO } from '@/api/iot/product'
import { formatDate } from '@/utils/formatTime'
import { DeviceVO } from '@/api/iot/device'
const message = useMessage() // 消息提示
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
const emit = defineEmits(['refresh']) // 定义 Emits
const activeNames = ref(['basicInfo']) // 展示的折叠面板
const mqttDialogVisible = ref(false) // 定义 MQTT 弹框的可见性
const mqttParams = ref({
mqttClientId: '',
mqttUsername: '',
mqttPassword: ''
}) // 定义 MQTT 参数对象
/** 复制到剪贴板方法 */
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
})
}
/** 打开 MQTT 参数弹框的方法 */
const openMqttParams = () => {
mqttParams.value = {
mqttClientId: device.mqttClientId || 'N/A',
mqttUsername: device.mqttUsername || 'N/A',
mqttPassword: device.mqttPassword || 'N/A'
}
mqttDialogVisible.value = true
}
/** 关闭 MQTT 弹框的方法 */
const handleCloseMqttDialog = () => {
mqttDialogVisible.value = false
}
</script>
<template>
<DeviceDetailsHeader
:loading="loading"
:product="product"
:device="device"
@refresh="getDeviceData(id)"
/>
<el-col>
<el-tabs>
<el-tab-pane label="设备信息">
<DeviceDetailsInfo :product="product" :device="device" />
</el-tab-pane>
<el-tab-pane label="Topic 列表" />
<el-tab-pane label="物模型数据" />
<el-tab-pane label="子设备管理" />
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { DeviceApi, DeviceVO } from '@/api/iot/device'
import { ProductApi, ProductVO } from '@/api/iot/product'
import DeviceDetailsHeader from '@/views/iot/device/detail/DeviceDetailsHeader.vue'
import DeviceDetailsInfo from '@/views/iot/device/detail/DeviceDetailsInfo.vue'
defineOptions({ name: 'IoTDeviceDetail' })
const route = useRoute()
const message = useMessage()
const id = Number(route.params.id) // 编号
const loading = ref(true) // 加载中
const product = ref<ProductVO>({} as ProductVO) // 产品详情
const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
/** 获取设备详情 */
const getDeviceData = async (id: number) => {
loading.value = true
try {
device.value = await DeviceApi.getDevice(id)
console.log(product.value)
await getProductData(device.value.productId)
} finally {
loading.value = false
}
}
/** 获取产品详情 */
const getProductData = async (id: number) => {
product.value = await ProductApi.getProduct(id)
console.log(product.value)
}
/** 获取物模型 */
/** 初始化 */
const { delView } = useTagsViewStore() // 视图操作
const { currentRoute } = useRouter() // 路由
onMounted(async () => {
if (!id) {
message.warning('参数错误,产品不能为空!')
delView(unref(currentRoute))
return
}
await getDeviceData(id)
})
</script>
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
placeholder="请选择产品"
clearable
class="!w-240px"
>
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入 DeviceName"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注名称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入备注名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select
v-model="queryParams.deviceType"
placeholder="请选择设备类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择设备状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['iot:device:create']"
>
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="DeviceName" align="center" prop="deviceName">
<template #default="scope">
<el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
</template>
</el-table-column>
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="设备所属产品" align="center" prop="productId">
<template #default="scope">
{{ productMap[scope.row.productId] }}
</template>
</el-table-column>
<el-table-column label="设备类型" align="center" prop="deviceType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column label="设备状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="最后上线时间"
align="center"
prop="lastOnlineTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openDetail(scope.row.id)"
v-hasPermi="['iot:product:query']"
>
查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['iot:device:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:device:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<DeviceForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { DeviceApi, DeviceVO } from '@/api/iot/device'
import DeviceForm from './DeviceForm.vue'
import { ProductApi } from '@/api/iot/product'
/** IoT 设备 列表 */
defineOptions({ name: 'IoTDevice' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<DeviceVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceName: undefined,
productId: undefined,
deviceType: undefined,
nickname: undefined,
status: undefined
})
const queryFormRef = ref() // 搜索的表单
/** 产品标号和名称的映射 */
const productMap = reactive({})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceApi.getDevicePage(queryParams)
list.value = data.list
total.value = data.total
// 获取产品ID列表
const productIds = [...new Set(data.list.map((device) => device.productId))]
// 获取产品名称
// TODO @haohao:最好后端拼接哈
const products = await Promise.all(productIds.map((id) => ProductApi.getProduct(id)))
products.forEach((product) => {
productMap[product.id] = product.name
})
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 打开详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'IoTDeviceDetail', params: { id } })
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DeviceApi.deleteDevice(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 查询字典下拉列表 */
const products = ref()
const getProducts = async () => {
products.value = await ProductApi.getSimpleProductList()
}
/** 初始化 **/
onMounted(() => {
getList()
getProducts()
})
</script>
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="产品名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select
v-model="formData.deviceType"
placeholder="请选择设备类型"
:disabled="formType === 'update'"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.deviceType === 0 || formData.deviceType === 2"
label="联网方式"
prop="netType"
>
<el-select
v-model="formData.netType"
placeholder="请选择联网方式"
:disabled="formType === 'update'"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_NET_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.deviceType === 1" label="接入网关协议" prop="protocolType">
<el-select
v-model="formData.protocolType"
placeholder="请选择接入网关协议"
:disabled="formType === 'update'"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="数据格式" prop="dataFormat">
<el-select
v-model="formData.dataFormat"
placeholder="请选择接数据格式"
:disabled="formType === 'update'"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_FORMAT)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="数据校验级别" prop="validateType">
<el-radio-group v-model="formData.validateType" :disabled="formType === 'update'">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="产品描述" prop="description">
<el-input type="textarea" v-model="formData.description" placeholder="请输入产品描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ProductApi, ProductVO } from '@/api/iot/product'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
defineOptions({ name: 'IoTProductForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref({
name: undefined,
id: undefined,
productKey: undefined,
protocolId: undefined,
categoryId: undefined,
description: undefined,
validateType: undefined,
status: undefined,
deviceType: undefined,
netType: undefined,
protocolType: undefined,
dataFormat: undefined
})
const formRules = reactive({
name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }],
netType: [
{
// TODO @haohao:0、1、/2 最好前端也枚举下;另外,这里的 required 可以直接设置为 true。然后表单那些 v-if。只要不存在,它自动就不校验了哈
required: formData.deviceType === 0 || formData.deviceType === 2,
message: '联网方式不能为空',
trigger: 'change'
}
],
protocolType: [
{ required: formData.deviceType === 1, message: '接入网关协议不能为空', trigger: 'change' }
],
dataFormat: [{ required: true, message: '数据格式不能为空', trigger: 'change' }],
validateType: [{ required: true, message: '数据校验级别不能为空', trigger: 'change' }]
})
const formRef = ref()
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
formData.value = await ProductApi.getProduct(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open, close: () => (dialogVisible.value = false) })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
try {
const data = formData.value as unknown as ProductVO
if (formType.value === 'create') {
await ProductApi.createProduct(data)
message.success(t('common.createSuccess'))
} else {
await ProductApi.updateProduct(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false // 确保关闭弹框
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
name: undefined,
id: undefined,
productKey: undefined,
protocolId: undefined,
categoryId: undefined,
description: undefined,
validateType: undefined,
status: undefined,
deviceType: undefined,
netType: undefined,
protocolType: undefined,
dataFormat: undefined
}
formRef.value?.resetFields()
}
</script>
<template>
<div>
<div class="flex items-start justify-between">
<div>
<el-col>
<el-row>
<span class="text-xl font-bold">{{ product.name }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上:按钮 -->
<el-button
@click="openForm('update', product.id)"
v-hasPermi="['iot:product:update']"
v-if="product.status === 0"
>
编辑
</el-button>
<el-button
type="primary"
@click="confirmPublish(product.id)"
v-hasPermi="['iot:product:update']"
v-if="product.status === 0"
>
发布
</el-button>
<el-button
type="danger"
@click="confirmUnpublish(product.id)"
v-hasPermi="['iot:product:update']"
v-if="product.status === 1"
>
撤销发布
</el-button>
</div>
</div>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)">复制</el-button>
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="设备数">
{{ product.deviceCount }}
<el-button @click="goToManagement(product.id)">前往管理</el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<ProductForm ref="formRef" @success="emit('refresh')" />
</template>
<script setup lang="ts">
import ProductForm from '@/views/iot/product/ProductForm.vue'
import { ProductApi, ProductVO } from '@/api/iot/product'
const message = useMessage()
const { product } = defineProps<{ product: ProductVO }>() // 定义 Props
/** 处理复制 */
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
})
}
/** 路由跳转到设备管理 */
const { push } = useRouter()
const goToManagement = (productId: string) => {
push({ name: 'IoTDevice', query: { productId } })
}
/** 操作修改 */
const emit = defineEmits(['refresh']) // 定义 Emits
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const confirmPublish = async (id: number) => {
try {
await ProductApi.updateProductStatus(id, 1)
message.success('发布成功')
formRef.value.close() // 关闭弹框
emit('refresh')
} catch (error) {
message.error('发布失败')
}
}
const confirmUnpublish = async (id: number) => {
try {
await ProductApi.updateProductStatus(id, 0)
message.success('撤销发布成功')
formRef.value.close() // 关闭弹框
emit('refresh')
} catch (error) {
message.error('撤销发布失败')
}
}
</script>
<template>
<ContentWrap>
<el-collapse v-model="activeNames">
<el-descriptions :column="3" title="产品信息">
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
<el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(product.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="数据格式">
<dict-tag :type="DICT_TYPE.IOT_DATA_FORMAT" :value="product.dataFormat" />
</el-descriptions-item>
<el-descriptions-item label="数据校验级别">
<dict-tag :type="DICT_TYPE.IOT_VALIDATE_TYPE" :value="product.validateType" />
</el-descriptions-item>
<el-descriptions-item label="产品状态">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
</el-descriptions-item>
<el-descriptions-item
label="联网方式"
v-if="product.deviceType === 0 || product.deviceType === 2"
>
<dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</el-descriptions-item>
<el-descriptions-item label="接入网关协议" v-if="product.deviceType === 1">
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL_TYPE" :value="product.protocolType" />
</el-descriptions-item>
<el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
</el-descriptions>
</el-collapse>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { ProductVO } from '@/api/iot/product'
import { formatDate } from '@/utils/formatTime'
const { product } = defineProps<{ product: ProductVO }>()
// 展示的折叠面板
const activeNames = ref(['basicInfo'])
</script>
<template>
<ContentWrap>
<el-tabs>
<el-tab-pane label="基础通信 Topic">
<Table
:columns="columns1"
:data="data1"
:span-method="createSpanMethod(data1)"
align="left"
headerAlign="left"
border="true"
/>
</el-tab-pane>
<el-tab-pane label="物模型通信 Topic">
<Table
:columns="columns2"
:data="data2"
:span-method="createSpanMethod(data2)"
align="left"
headerAlign="left"
border="true"
/>
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
import { ProductVO } from '@/api/iot/product'
const props = defineProps<{ product: ProductVO }>()
// 定义列
const columns1 = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
const columns2 = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
// TODO @haohao:这个,有没可能写到一个枚举里,方便后续维护? /Users/yunai/Java/yudao-ui-admin-vue3/src/views/ai/utils/constants.ts
const data1 = computed(() => {
if (!props.product || !props.product.productKey) return []
return [
{
function: 'OTA 升级',
topicClass: `/ota/device/inform/${props.product.productKey}/\${deviceName}`,
operationPermission: '发布',
description: '设备上报固件升级信息'
},
{
function: 'OTA 升级',
topicClass: `/ota/device/upgrade/${props.product.productKey}/\${deviceName}`,
operationPermission: '订阅',
description: '固件升级信息下行'
},
{
function: 'OTA 升级',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
operationPermission: '发布',
description: '设备上报固件升级进度'
},
{
function: 'OTA 升级',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/ota/firmware/get`,
operationPermission: '发布',
description: '设备主动拉取固件升级信息'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update`,
operationPermission: '发布',
description: '设备上报标签数据'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/update_reply`,
operationPermission: '订阅',
description: '云端响应标签上报'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete`,
operationPermission: '订阅',
description: '设备删除标签信息'
},
{
function: '设备标签',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/deviceinfo/delete_reply`,
operationPermission: '订阅',
description: '云端响应标签删除'
},
{
function: '时钟同步',
topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/request`,
operationPermission: '发布',
description: 'NTP 时钟同步请求'
},
{
function: '时钟同步',
topicClass: `/ext/ntp/${props.product.productKey}/\${deviceName}/response`,
operationPermission: '订阅',
description: 'NTP 时钟同步响应'
},
{
function: '设备影子',
topicClass: `/shadow/update/${props.product.productKey}/\${deviceName}`,
operationPermission: '发布',
description: '设备影子发布'
},
{
function: '设备影子',
topicClass: `/shadow/get/${props.product.productKey}/\${deviceName}`,
operationPermission: '订阅',
description: '设备接收影子变更'
},
{
function: '配置更新',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/push`,
operationPermission: '订阅',
description: '云端主动下推配置信息'
},
{
function: '配置更新',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get`,
operationPermission: '发布',
description: '设备端查询配置信息'
},
{
function: '配置更新',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/config/get_reply`,
operationPermission: '订阅',
description: '云端响应配置信息'
},
{
function: '广播',
topicClass: `/broadcast/${props.product.productKey}/\${identifier}`,
operationPermission: '订阅',
description: '广播 Topic,identifier 为用户自定义字符串'
}
]
})
const data2 = computed(() => {
if (!props.product || !props.product.productKey) return []
return [
{
function: '属性上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post`,
operationPermission: '发布',
description: '设备属性上报'
},
{
function: '属性上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/property/post_reply`,
operationPermission: '订阅',
description: '云端响应属性上报'
},
{
function: '属性设置',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/property/set`,
operationPermission: '订阅',
description: '设备属性设置'
},
{
function: '事件上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post`,
operationPermission: '发布',
description: '设备事件上报'
},
{
function: '事件上报',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/event/\${tsl.event.identifier}/post_reply`,
operationPermission: '订阅',
description: '云端响应事件上报'
},
{
function: '服务调用',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}`,
operationPermission: '订阅',
description: '设备服务调用'
},
{
function: '服务调用',
topicClass: `/sys/${props.product.productKey}/\${deviceName}/thing/service/\${tsl.service.identifier}_reply`,
operationPermission: '发布',
description: '设备端响应服务调用'
}
]
})
// 通用的单元格合并方法生成器
const createSpanMethod = (data: any[]) => {
// 预处理,计算每个功能的合并行数
const rowspanMap: Record<number, number> = {}
let currentFunction = ''
let startIndex = 0
let count = 0
data.forEach((item, index) => {
if (item.function !== currentFunction) {
if (count > 0) {
rowspanMap[startIndex] = count
}
currentFunction = item.function
startIndex = index
count = 1
} else {
count++
}
})
// 处理最后一组
if (count > 0) {
rowspanMap[startIndex] = count
}
// 返回 span 方法
return ({ row, column, rowIndex, columnIndex }: SpanMethodProps) => {
if (columnIndex === 0) {
// 仅对“功能”列进行合并
const rowspan = rowspanMap[rowIndex] || 0
if (rowspan > 0) {
return {
rowspan,
colspan: 1
}
} else {
return {
rowspan: 0,
colspan: 0
}
}
}
}
}
</script>
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="功能类型" prop="name">
<el-select
v-model="queryParams.type"
placeholder="请选择功能类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['iot:think-model-function:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 添加功能
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-tabs>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="功能类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="功能名称" align="center" prop="name" />
<el-table-column label="标识符" align="center" prop="identifier" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="[`iot:think-model-function:update`]"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:think-model-function:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-tabs>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<ThinkModelFunctionForm ref="formRef" :product="product" @success="getList" />
</template>
<script setup lang="ts">
import { ProductVO } from '@/api/iot/product'
import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ThinkModelFunctionForm from '@/views/iot/product/detail/ThinkModelFunctionForm.vue'
const props = defineProps<{ product: ProductVO }>()
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const list = ref<ThinkModelFunctionVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: undefined,
productId: -1
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
queryParams.productId = props.product.id
const data = await ThinkModelFunctionApi.getThinkModelFunctionPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
queryParams.type = undefined
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ThinkModelFunctionApi.deleteThinkModelFunction(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="功能类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-button value="1"> 属性 </el-radio-button>
<el-radio-button value="2"> 服务 </el-radio-button>
<el-radio-button value="3"> 事件 </el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="功能名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入功能名称" />
</el-form-item>
<el-form-item label="标识符" prop="identifier">
<el-input
v-model="formData.identifier"
placeholder="请输入标识符"
:disabled="formType === 'update'"
/>
</el-form-item>
<el-form-item label="数据类型" prop="type">
<el-select
v-model="formData.property.dataType.type"
placeholder="请选择数据类型"
:disabled="formType === 'update'"
>
<el-option key="int" label="int32 (整数型)" value="int" />
<el-option key="float" label="float (单精度浮点型)" value="float" />
<el-option key="double" label="double (双精度浮点型)" value="double" />
<!-- <el-option key="text" label="text (文本型)" value="text" />-->
<!-- <el-option key="date" label="date (日期型)" value="date" />-->
<!-- <el-option key="bool" label="bool (布尔型)" value="bool" />-->
<!-- <el-option key="enum" label="enum (枚举型)" value="enum" />-->
<!-- <el-option key="struct" label="struct (结构体)" value="struct" />-->
<!-- <el-option key="array" label="array (数组)" value="array" />-->
</el-select>
</el-form-item>
<el-form-item label="取值范围" prop="max">
<el-input v-model="formData.property.dataType.specs.min" placeholder="请输入最小值" />
<span class="mx-2">~</span>
<el-input v-model="formData.property.dataType.specs.max" placeholder="请输入最大值" />
</el-form-item>
<el-form-item label="步长" prop="step">
<el-input v-model="formData.property.dataType.specs.step" placeholder="请输入步长" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="formData.property.dataType.specs.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="读写类型" prop="accessMode">
<el-radio-group v-model="formData.property.accessMode">
<el-radio label="rw">读写</el-radio>
<el-radio label="r">只读</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="属性描述" prop="property.description">
<el-input
type="textarea"
v-model="formData.property.description"
placeholder="请输入属性描述"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ProductVO } from '@/api/iot/product'
import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction'
const props = defineProps<{ product: ProductVO }>()
defineOptions({ name: 'ThinkModelFunctionForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref({
id: undefined,
productId: undefined,
productKey: undefined,
identifier: undefined,
name: undefined,
description: undefined,
type: '1',
property: {
identifier: undefined,
name: undefined,
accessMode: 'rw',
required: true,
dataType: {
type: undefined,
specs: {
min: undefined,
max: undefined,
step: undefined,
unit: undefined
}
},
description: undefined // 添加这一行
}
})
const formRules = reactive({
name: [
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
{
pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
message:
'支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
trigger: 'blur'
}
],
type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
identifier: [
{ required: true, message: '标识符不能为空', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_]{1,50}$/,
message: '支持大小写字母、数字和下划线,不超过 50 个字符',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
if (reservedKeywords.includes(value)) {
callback(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义'
)
)
} else {
callback()
}
},
trigger: 'blur'
}
],
property: {
dataType: {
type: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }]
},
accessMode: [{ required: true, message: '读写类型不能为空', trigger: 'blur' }]
}
})
const formRef = ref()
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
formData.value = await ThinkModelFunctionApi.getThinkModelFunction(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open, close: () => (dialogVisible.value = false) })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
try {
const data = formData.value as unknown as ThinkModelFunctionVO
data.productId = props.product.id
data.productKey = props.product.productKey
if (formType.value === 'create') {
await ThinkModelFunctionApi.createThinkModelFunction(data)
message.success(t('common.createSuccess'))
} else {
await ThinkModelFunctionApi.updateThinkModelFunction(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false // 确保关闭弹框
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
productId: undefined,
productKey: undefined,
identifier: undefined,
name: undefined,
description: undefined,
type: '1', // todo @HAOHAO:看看枚举下
property: {
identifier: undefined,
name: undefined,
accessMode: 'rw',
required: true,
dataType: {
type: undefined,
specs: {
min: undefined,
max: undefined,
step: undefined,
unit: undefined
}
},
description: undefined // 确保重置 description 字段
}
}
formRef.value?.resetFields()
}
</script>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment