Pre Merge pull request !872 from james008/trace-dev
commit
8e7d2e96d1
12
.env
12
.env
|
|
@ -1,5 +1,5 @@
|
||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=芋道管理系统
|
VITE_APP_TITLE=空山管理系统
|
||||||
|
|
||||||
# 项目本地运行端口号
|
# 项目本地运行端口号
|
||||||
VITE_PORT=80
|
VITE_PORT=80
|
||||||
|
|
@ -13,15 +13,17 @@ VITE_APP_TENANT_ENABLE=true
|
||||||
# 验证码的开关
|
# 验证码的开关
|
||||||
VITE_APP_CAPTCHA_ENABLE=true
|
VITE_APP_CAPTCHA_ENABLE=true
|
||||||
|
|
||||||
# 文档地址的开关
|
# 文档地址提示条开关(空山版统一帮助入口)
|
||||||
VITE_APP_DOCALERT_ENABLE=true
|
VITE_APP_DOCALERT_ENABLE=true
|
||||||
|
# 统一帮助入口(帮助中心 / 内部Wiki)
|
||||||
|
VITE_APP_HELP_CENTER_URL=http://m.swelife.cn/#/pages/index
|
||||||
|
|
||||||
# 百度统计
|
# 百度统计
|
||||||
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
|
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
|
||||||
|
|
||||||
# 默认账户密码
|
# 默认账户密码
|
||||||
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
|
VITE_APP_DEFAULT_LOGIN_TENANT = 空山平台
|
||||||
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
||||||
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
||||||
|
|
||||||
# API 加解密
|
# API 加解密
|
||||||
|
|
@ -34,4 +36,4 @@ VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
|
||||||
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
|
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
|
||||||
|
|
||||||
# 百度地图
|
# 百度地图
|
||||||
VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD'
|
VITE_BAIDU_MAP_KEY = 'Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN'
|
||||||
|
|
@ -30,5 +30,10 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000'
|
||||||
# 验证码的开关
|
# 验证码的开关
|
||||||
VITE_APP_CAPTCHA_ENABLE=false
|
VITE_APP_CAPTCHA_ENABLE=false
|
||||||
|
|
||||||
|
# 文档地址提示条开关(本地联调启用统一帮助入口)
|
||||||
|
VITE_APP_DOCALERT_ENABLE=true
|
||||||
|
# 统一帮助入口(帮助中心 / 内部Wiki)
|
||||||
|
VITE_APP_HELP_CENTER_URL=http://m.swelife.cn/#/pages/index
|
||||||
|
|
||||||
# GoView域名
|
# GoView域名
|
||||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||||
|
|
@ -68,8 +68,8 @@ module.exports = defineConfig({
|
||||||
],
|
],
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
'vue/no-v-html': 'off',
|
'vue/no-v-html': 'off',
|
||||||
'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
|
'prettier/prettier': 'off', // 项目组:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
|
||||||
'@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
|
'@unocss/order': 'off', // 项目组:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
|
||||||
'@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
|
'@unocss/order-attributify': 'off' // 项目组:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
49
README.md
49
README.md
|
|
@ -10,15 +10,15 @@
|
||||||
## 🐶 新手必读
|
## 🐶 新手必读
|
||||||
|
|
||||||
* nodejs > 16.18.0 && pnpm > 8.6.0 (强制使用pnpm)
|
* nodejs > 16.18.0 && pnpm > 8.6.0 (强制使用pnpm)
|
||||||
* 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
|
* 演示地址【Vue3 + element-plus】:待补充
|
||||||
* 演示地址【Vue3 + vben5.0(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
|
* 演示地址【Vue3 + vben5.0(ant-design-vue)】:待补充
|
||||||
* 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
|
* 演示地址【Vue2 + element-ui】:待补充
|
||||||
* 启动文档:<https://doc.iocoder.cn/quick-start/>
|
* 启动文档:待补充
|
||||||
* 视频教程:<https://doc.iocoder.cn/video/>
|
* 视频教程:待补充
|
||||||
|
|
||||||
## 🐯 平台简介
|
## 🐯 平台简介
|
||||||
|
|
||||||
**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
|
**空山**,一个 AI + 电商平台项目,聚焦可持续迭代的业务开发与工程落地。
|
||||||
|
|
||||||
* 采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 实现
|
* 采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 实现
|
||||||
* 改换 saas,自动引入等功能
|
* 改换 saas,自动引入等功能
|
||||||
|
|
@ -69,11 +69,11 @@
|
||||||
|
|
||||||
支持 Spring Boot、Spring Cloud 两种架构:
|
支持 Spring Boot、Spring Cloud 两种架构:
|
||||||
|
|
||||||
① Spring Boot 单体架构:<https://doc.iocoder.cn>
|
① Spring Boot 单体架构:待补充
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
|
② Spring Cloud 微服务架构:待补充
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -200,22 +200,23 @@
|
||||||
|
|
||||||
### 微信公众号
|
### 微信公众号
|
||||||
|
|
||||||
| | 功能 | 描述 |
|
| | 功能 | 描述 |
|
||||||
|-----|--------|-------------------------------|
|
|----|--------|-------------------------------|
|
||||||
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
|
||||||
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
|
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
|
||||||
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
|
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
|
||||||
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
|
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
|
||||||
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
|
| 🚀 | 模版消息 | 配置和发送模版消息,用于向粉丝推送通知类消息 |
|
||||||
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
|
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
|
||||||
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
|
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
|
||||||
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
|
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
|
||||||
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
|
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
|
||||||
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
|
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
|
||||||
|
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
|
||||||
|
|
||||||
### 商城系统
|
### 商城系统
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/mall-preview/>
|
演示地址:待补充
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -223,19 +224,19 @@
|
||||||
|
|
||||||
### ERP 系统
|
### ERP 系统
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/erp-preview/>
|
演示地址:待补充
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### CRM 系统
|
### CRM 系统
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/crm-preview/>
|
演示地址:待补充
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### AI 大模型
|
### AI 大模型
|
||||||
|
|
||||||
演示地址:<https://doc.iocoder.cn/ai-preview/>
|
演示地址:待补充
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ const include = [
|
||||||
'echarts/components',
|
'echarts/components',
|
||||||
'echarts/renderers',
|
'echarts/renderers',
|
||||||
'echarts-wordcloud',
|
'echarts-wordcloud',
|
||||||
'@wangeditor/editor',
|
'@wangeditor-next/editor',
|
||||||
'@wangeditor/editor-for-vue',
|
'@wangeditor-next/editor-for-vue',
|
||||||
'@microsoft/fetch-event-source',
|
'@microsoft/fetch-event-source',
|
||||||
'markdown-it',
|
'markdown-it',
|
||||||
'markmap-view',
|
'markmap-view',
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -2,16 +2,16 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/brand-logo.png" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
content="空山管理系统 AI+电商平台 管理后台 vue3 typescript vite element-plus"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
content="空山 AI+电商平台管理后台,基于 Vue3 + TypeScript + Vite + Element Plus。"
|
||||||
/>
|
/>
|
||||||
<title>%VITE_APP_TITLE%</title>
|
<title>%VITE_APP_TITLE%</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 4px solid #2d8cf0;
|
border: 4px solid #1e88e5;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
left: calc(50% - 20px);
|
left: calc(50% - 20px);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 4px solid #87bdff;
|
border: 4px solid #7fbaf2;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
<div class="app-loading">
|
<div class="app-loading">
|
||||||
<div class="app-loading-wrap">
|
<div class="app-loading-wrap">
|
||||||
<div class="app-loading-title">
|
<div class="app-loading-title">
|
||||||
<img src="/logo.gif" class="app-loading-logo" alt="Logo" />
|
<img src="/brand-logo.png" class="app-loading-logo" alt="Logo" />
|
||||||
<div class="app-loading-title">%VITE_APP_TITLE%</div>
|
<div class="app-loading-title">%VITE_APP_TITLE%</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-loading-item">
|
<div class="app-loading-item">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "yudao-ui-admin-vue3",
|
"name": "yudao-ui-admin-vue3",
|
||||||
"version": "2025.09-snapshot",
|
"version": "2026.01-snapshot",
|
||||||
"description": "基于vue3、vite4、element-plus、typesScript",
|
"description": "基于vue3、vite4、element-plus、typesScript",
|
||||||
"author": "xingyu",
|
"author": "xingyu",
|
||||||
"private": false,
|
"private": false,
|
||||||
|
|
@ -32,9 +32,9 @@
|
||||||
"@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",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor-next/editor": "^5.6.46",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
||||||
"@wangeditor/plugin-mention": "^1.0.0",
|
"@wangeditor-next/plugin-mention": "^1.0.16",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "1.9.0",
|
"axios": "1.9.0",
|
||||||
|
|
|
||||||
2534
pnpm-lock.yaml
2534
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -6,6 +6,7 @@ export type ProcessDefinitionVO = {
|
||||||
deploymentTIme: string
|
deploymentTIme: string
|
||||||
suspensionState: number
|
suspensionState: number
|
||||||
formType?: number
|
formType?: number
|
||||||
|
formCustomCreatePath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModelVO = {
|
export type ModelVO = {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,6 @@ export const createFile = (data: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
export const updateFile = (data: any) => {
|
export const updateFile = (data: any, onUploadProgress?: Function) => {
|
||||||
return request.upload({ url: '/infra/file/upload', data })
|
return request.upload({ url: '/infra/file/upload', data, onUploadProgress })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export interface FileClientConfig {
|
||||||
accessSecret?: string
|
accessSecret?: string
|
||||||
enablePathStyleAccess?: boolean
|
enablePathStyleAccess?: boolean
|
||||||
enablePublicAccess?: boolean
|
enablePublicAccess?: boolean
|
||||||
|
region?: string
|
||||||
domain: string
|
domain: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface DeviceVO {
|
||||||
id: number // 设备 ID,主键,自增
|
id: number // 设备 ID,主键,自增
|
||||||
deviceName: string // 设备名称
|
deviceName: string // 设备名称
|
||||||
productId: number // 产品编号
|
productId: number // 产品编号
|
||||||
|
productName?: string // 产品名称(只有部分接口返回,例如 getDeviceLocationList)
|
||||||
productKey: string // 产品标识
|
productKey: string // 产品标识
|
||||||
deviceType: number // 设备类型
|
deviceType: number // 设备类型
|
||||||
nickname: string // 设备备注名称
|
nickname: string // 设备备注名称
|
||||||
|
|
@ -20,8 +21,6 @@ export interface DeviceVO {
|
||||||
mqttClientId: string // MQTT 客户端 ID
|
mqttClientId: string // MQTT 客户端 ID
|
||||||
mqttUsername: string // MQTT 用户名
|
mqttUsername: string // MQTT 用户名
|
||||||
mqttPassword: string // MQTT 密码
|
mqttPassword: string // MQTT 密码
|
||||||
authType: string // 认证类型
|
|
||||||
locationType: number // 定位类型
|
|
||||||
latitude?: number // 设备位置的纬度
|
latitude?: number // 设备位置的纬度
|
||||||
longitude?: number // 设备位置的经度
|
longitude?: number // 设备位置的经度
|
||||||
areaId: number // 地区编码
|
areaId: number // 地区编码
|
||||||
|
|
@ -49,14 +48,6 @@ export interface IotDevicePropertyRespVO {
|
||||||
updateTime: Date // 更新时间
|
updateTime: Date // 更新时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @芋艿:调整到 constants
|
|
||||||
// IoT 设备状态枚举
|
|
||||||
export enum DeviceStateEnum {
|
|
||||||
INACTIVE = 0, // 未激活
|
|
||||||
ONLINE = 1, // 在线
|
|
||||||
OFFLINE = 2 // 离线
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备认证参数 VO
|
// 设备认证参数 VO
|
||||||
export interface IotDeviceAuthInfoVO {
|
export interface IotDeviceAuthInfoVO {
|
||||||
clientId: string // 客户端 ID
|
clientId: string // 客户端 ID
|
||||||
|
|
@ -123,6 +114,11 @@ export const DeviceApi = {
|
||||||
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType, productId } })
|
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType, productId } })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取设备位置列表(用于地图展示)
|
||||||
|
getDeviceLocationList: async () => {
|
||||||
|
return await request.get<DeviceVO[]>({ url: `/iot/device/location-list` })
|
||||||
|
},
|
||||||
|
|
||||||
// 根据产品编号,获取设备的精简信息列表
|
// 根据产品编号,获取设备的精简信息列表
|
||||||
getDeviceListByProductId: async (productId: number) => {
|
getDeviceListByProductId: async (productId: number) => {
|
||||||
return await request.get({ url: `/iot/device/simple-list?`, params: { productId } })
|
return await request.get({ url: `/iot/device/simple-list?`, params: { productId } })
|
||||||
|
|
@ -161,5 +157,28 @@ export const DeviceApi = {
|
||||||
// 发送设备消息
|
// 发送设备消息
|
||||||
sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
|
sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
|
||||||
return await request.post({ url: `/iot/device/message/send`, data: params })
|
return await request.post({ url: `/iot/device/message/send`, data: params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 绑定子设备到网关
|
||||||
|
bindDeviceGateway: async (data: { subIds: number[]; gatewayId: number }) => {
|
||||||
|
return await request.put({ url: `/iot/device/bind-gateway`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 解绑子设备与网关
|
||||||
|
unbindDeviceGateway: async (data: { subIds: number[]; gatewayId: number }) => {
|
||||||
|
return await request.put({ url: `/iot/device/unbind-gateway`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取网关的子设备列表
|
||||||
|
getSubDeviceList: async (gatewayId: number) => {
|
||||||
|
return await request.get<DeviceVO[]>({
|
||||||
|
url: `/iot/device/sub-device-list`,
|
||||||
|
params: { gatewayId }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取未绑定网关的子设备分页
|
||||||
|
getUnboundSubDevicePage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/iot/device/unbound-sub-device-page`, params })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
/** Modbus 连接配置 VO */
|
||||||
|
export interface DeviceModbusConfigVO {
|
||||||
|
id?: number // 主键
|
||||||
|
deviceId: number // 设备编号
|
||||||
|
ip: string // Modbus 服务器 IP 地址
|
||||||
|
port: number // Modbus 服务器端口
|
||||||
|
slaveId: number // 从站地址
|
||||||
|
timeout: number // 连接超时时间,单位:毫秒
|
||||||
|
retryInterval: number // 重试间隔,单位:毫秒
|
||||||
|
mode: number // 模式
|
||||||
|
frameFormat: number // 帧格式
|
||||||
|
status: number // 状态
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modbus 连接配置 API */
|
||||||
|
export const DeviceModbusConfigApi = {
|
||||||
|
/** 获取设备的 Modbus 连接配置 */
|
||||||
|
getModbusConfig: async (deviceId: number) => {
|
||||||
|
return await request.get<DeviceModbusConfigVO>({
|
||||||
|
url: `/iot/device-modbus-config/get`,
|
||||||
|
params: { deviceId }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保存 Modbus 连接配置 */
|
||||||
|
saveModbusConfig: async (data: DeviceModbusConfigVO) => {
|
||||||
|
return await request.post({ url: `/iot/device-modbus-config/save`, data })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
/** Modbus 点位配置 VO */
|
||||||
|
export interface DeviceModbusPointVO {
|
||||||
|
id?: number // 主键
|
||||||
|
deviceId: number // 设备编号
|
||||||
|
thingModelId?: number // 物模型属性编号
|
||||||
|
identifier: string // 属性标识符
|
||||||
|
name: string // 属性名称
|
||||||
|
functionCode?: number // Modbus 功能码
|
||||||
|
registerAddress?: number // 寄存器起始地址
|
||||||
|
registerCount?: number // 寄存器数量
|
||||||
|
byteOrder?: string // 字节序
|
||||||
|
rawDataType?: string // 原始数据类型
|
||||||
|
scale: number // 缩放因子
|
||||||
|
pollInterval: number // 轮询间隔,单位:毫秒
|
||||||
|
status: number // 状态
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modbus 点位配置 API */
|
||||||
|
export const DeviceModbusPointApi = {
|
||||||
|
/** 获取设备的 Modbus 点位分页 */
|
||||||
|
getModbusPointPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/iot/device-modbus-point/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取 Modbus 点位详情 */
|
||||||
|
getModbusPoint: async (id: number) => {
|
||||||
|
return await request.get<DeviceModbusPointVO>({
|
||||||
|
url: `/iot/device-modbus-point/get?id=${id}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 创建 Modbus 点位配置 */
|
||||||
|
createModbusPoint: async (data: DeviceModbusPointVO) => {
|
||||||
|
return await request.post({ url: `/iot/device-modbus-point/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 更新 Modbus 点位配置 */
|
||||||
|
updateModbusPoint: async (data: DeviceModbusPointVO) => {
|
||||||
|
return await request.put({ url: `/iot/device-modbus-point/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 删除 Modbus 点位配置 */
|
||||||
|
deleteModbusPoint: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/iot/device-modbus-point/delete?id=${id}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ export interface ProductVO {
|
||||||
id: number // 产品编号
|
id: number // 产品编号
|
||||||
name: string // 产品名称
|
name: string // 产品名称
|
||||||
productKey: string // 产品标识
|
productKey: string // 产品标识
|
||||||
|
productSecret?: string // 产品密钥
|
||||||
|
registerEnabled?: boolean // 动态注册
|
||||||
protocolId: number // 协议编号
|
protocolId: number // 协议编号
|
||||||
categoryId: number // 产品所属品类标识符
|
categoryId: number // 产品所属品类标识符
|
||||||
categoryName?: string // 产品所属品类名称
|
categoryName?: string // 产品所属品类名称
|
||||||
|
|
@ -13,9 +15,9 @@ export interface ProductVO {
|
||||||
description: string // 产品描述
|
description: string // 产品描述
|
||||||
status: number // 产品状态
|
status: number // 产品状态
|
||||||
deviceType: number // 设备类型
|
deviceType: number // 设备类型
|
||||||
locationType: number // 设备类型
|
|
||||||
netType: number // 联网方式
|
netType: number // 联网方式
|
||||||
codecType: string // 数据格式(编解码器类型)
|
protocolType: string // 协议类型
|
||||||
|
serializeType: string // 序列化类型
|
||||||
deviceCount: number // 设备数量
|
deviceCount: number // 设备数量
|
||||||
createTime: Date // 创建时间
|
createTime: Date // 创建时间
|
||||||
}
|
}
|
||||||
|
|
@ -26,15 +28,23 @@ export enum DeviceTypeEnum {
|
||||||
GATEWAY_SUB = 1, // 网关子设备
|
GATEWAY_SUB = 1, // 网关子设备
|
||||||
GATEWAY = 2 // 网关设备
|
GATEWAY = 2 // 网关设备
|
||||||
}
|
}
|
||||||
// IOT 产品定位类型枚举类 0: 手动定位, 1: IP 定位, 2: 定位模块定位
|
// IoT 协议类型枚举
|
||||||
export enum LocationTypeEnum {
|
export enum ProtocolTypeEnum {
|
||||||
IP = 1, // IP 定位
|
TCP = 'tcp',
|
||||||
MODULE = 2, // 设备定位
|
UDP = 'udp',
|
||||||
MANUAL = 3 // 手动定位
|
WEBSOCKET = 'websocket',
|
||||||
|
HTTP = 'http',
|
||||||
|
MQTT = 'mqtt',
|
||||||
|
EMQX = 'emqx',
|
||||||
|
COAP = 'coap',
|
||||||
|
MODBUS_TCP_CLIENT = 'modbus_tcp_client',
|
||||||
|
MODBUS_TCP_SERVER = 'modbus_tcp_server'
|
||||||
}
|
}
|
||||||
// IOT 数据格式(编解码器类型)枚举类
|
|
||||||
export enum CodecTypeEnum {
|
// IoT 序列化类型枚举
|
||||||
ALINK = 'Alink' // 阿里云 Alink 协议
|
export enum SerializeTypeEnum {
|
||||||
|
JSON = 'json',
|
||||||
|
BINARY = 'binary'
|
||||||
}
|
}
|
||||||
|
|
||||||
// IoT 产品 API
|
// IoT 产品 API
|
||||||
|
|
@ -75,8 +85,8 @@ export const ProductApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// 查询产品(精简)列表
|
// 查询产品(精简)列表
|
||||||
getSimpleProductList() {
|
getSimpleProductList(deviceType?: number) {
|
||||||
return request.get({ url: '/iot/product/simple-list' })
|
return request.get({ url: '/iot/product/simple-list', params: { deviceType } })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 根据 ProductKey 获取产品信息
|
// 根据 ProductKey 获取产品信息
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ export interface DataSinkVO {
|
||||||
type?: number // 桥梁类型
|
type?: number // 桥梁类型
|
||||||
config?:
|
config?:
|
||||||
| HttpConfig
|
| HttpConfig
|
||||||
|
| TcpConfig
|
||||||
|
| WebSocketConfig
|
||||||
| MqttConfig
|
| MqttConfig
|
||||||
| RocketMQConfig
|
| RocketMQConfig
|
||||||
| KafkaMQConfig
|
| KafkaMQConfig
|
||||||
|
|
@ -30,6 +32,38 @@ export interface HttpConfig extends Config {
|
||||||
body: string
|
body: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** TCP 配置 */
|
||||||
|
export interface TcpConfig extends Config {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
connectTimeoutMs: number
|
||||||
|
readTimeoutMs: number
|
||||||
|
ssl: boolean
|
||||||
|
sslCertPath: string
|
||||||
|
dataFormat: string
|
||||||
|
heartbeatIntervalMs: number
|
||||||
|
reconnectIntervalMs: number
|
||||||
|
maxReconnectAttempts: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebSocket 配置 */
|
||||||
|
export interface WebSocketConfig extends Config {
|
||||||
|
serverUrl: string
|
||||||
|
connectTimeoutMs: number
|
||||||
|
sendTimeoutMs: number
|
||||||
|
heartbeatIntervalMs: number
|
||||||
|
heartbeatMessage: string
|
||||||
|
subprotocols: string
|
||||||
|
customHeaders: string
|
||||||
|
verifySslCert: boolean
|
||||||
|
dataFormat: string
|
||||||
|
reconnectIntervalMs: number
|
||||||
|
maxReconnectAttempts: number
|
||||||
|
enableCompression: boolean
|
||||||
|
sendRetryCount: number
|
||||||
|
sendRetryIntervalMs: number
|
||||||
|
}
|
||||||
|
|
||||||
/** MQTT 配置 */
|
/** MQTT 配置 */
|
||||||
export interface MqttConfig extends Config {
|
export interface MqttConfig extends Config {
|
||||||
url: string
|
url: string
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,6 @@ export interface IotStatisticsSummaryRespVO {
|
||||||
productCategoryDeviceCounts: Record<string, number>
|
productCategoryDeviceCounts: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 时间戳-数值的键值对类型 */
|
|
||||||
interface TimeValueItem {
|
|
||||||
[key: string]: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** IoT 消息统计数据类型 */
|
|
||||||
export interface IotStatisticsDeviceMessageSummaryRespVO {
|
|
||||||
statType: number
|
|
||||||
upstreamCounts: TimeValueItem[]
|
|
||||||
downstreamCounts: TimeValueItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 新的消息统计数据项 */
|
/** 新的消息统计数据项 */
|
||||||
export interface IotStatisticsDeviceMessageSummaryByDateRespVO {
|
export interface IotStatisticsDeviceMessageSummaryByDateRespVO {
|
||||||
time: string
|
time: string
|
||||||
|
|
@ -41,6 +29,17 @@ export interface IotStatisticsDeviceMessageReqVO {
|
||||||
times?: string[]
|
times?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 设备位置数据 VO */
|
||||||
|
export interface DeviceLocationRespVO {
|
||||||
|
id: number
|
||||||
|
deviceName: string
|
||||||
|
nickname?: string
|
||||||
|
productName?: string
|
||||||
|
state: number
|
||||||
|
longitude: number
|
||||||
|
latitude: number
|
||||||
|
}
|
||||||
|
|
||||||
// IoT 数据统计 API
|
// IoT 数据统计 API
|
||||||
export const StatisticsApi = {
|
export const StatisticsApi = {
|
||||||
// 查询全局的数据统计
|
// 查询全局的数据统计
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export interface OrderItemRespVO {
|
||||||
orderPartPrice?: number | null //子订单分摊金额(总)
|
orderPartPrice?: number | null //子订单分摊金额(总)
|
||||||
orderDividePrice?: number | null //分摊后子订单实付金额(总)
|
orderDividePrice?: number | null //分摊后子订单实付金额(总)
|
||||||
// ========== 营销基本信息 ==========
|
// ========== 营销基本信息 ==========
|
||||||
// TODO 芋艿:在捉摸一下
|
// TODO 项目组:在捉摸一下
|
||||||
// ========== 售后基本信息 ==========
|
// ========== 售后基本信息 ==========
|
||||||
afterSaleStatus?: number | null // 售后状态
|
afterSaleStatus?: number | null // 售后状态
|
||||||
properties?: ProductPropertiesVO[] //属性数组
|
properties?: ProductPropertiesVO[] //属性数组
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// 消息模板 VO
|
||||||
|
export interface MsgTemplateVO {
|
||||||
|
id: number // 模版主键
|
||||||
|
accountId: number // 公众号账号的编号
|
||||||
|
appId: string // appId
|
||||||
|
templateId: string // 公众号模板 ID
|
||||||
|
title: string // 标题
|
||||||
|
content: string // 模板内容
|
||||||
|
example: string // 模板示例
|
||||||
|
primaryIndustry: string // 模板所属行业的一级行业
|
||||||
|
deputyIndustry: string // 模板所属行业的二级行业
|
||||||
|
createTime: Date // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息模板请求 VO
|
||||||
|
export interface MsgTemplateSendVO {
|
||||||
|
id: number // 模板编号
|
||||||
|
userId: number // 用户编号
|
||||||
|
data?: string // 模板数据(JSON 格式字符串)
|
||||||
|
url?: string // 跳转链接
|
||||||
|
miniProgramAppId?: string // 小程序 appId
|
||||||
|
miniProgramPagePath?: string // 小程序页面路径
|
||||||
|
miniprogram?: string // 小程序信息(JSON 格式字符串)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公众号消息模板 API
|
||||||
|
export const MessageTemplateApi = {
|
||||||
|
// 查询消息模板分页
|
||||||
|
getMessageTemplateList: async (params: any) => {
|
||||||
|
return await request.get({ url: `/mp/message-template/list`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除消息模板
|
||||||
|
deleteMessageTemplate: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/mp/message-template/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 同步公众号模板
|
||||||
|
syncMessageTemplate: async (accountId: number) => {
|
||||||
|
return await request.post({ url: `/mp/message-template/sync?accountId=` + accountId })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送消息模板
|
||||||
|
sendMessageTemplate: async (data: MsgTemplateSendVO) => {
|
||||||
|
return await request.post({ url: `/mp/message-template/send`, data })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
|
|
||||||
export interface MailTemplateVO {
|
export interface MailTemplateVO {
|
||||||
id: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
code: string
|
code: string
|
||||||
accountId: number
|
accountId: number
|
||||||
nickname: string
|
nickname: string
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
params: string
|
|
||||||
status: number
|
status: number
|
||||||
remark: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MailSendReqVO {
|
export interface MailSendReqVO {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface SocialClientVO {
|
||||||
clientId: string
|
clientId: string
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
agentId: string
|
agentId: string
|
||||||
|
publicKey: string
|
||||||
status: number
|
status: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
|
|
@ -0,0 +1,47 @@
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ksAdminBg" x1="14%" y1="10%" x2="88%" y2="92%">
|
||||||
|
<stop offset="0%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="55%" stop-color="#1976d2"/>
|
||||||
|
<stop offset="100%" stop-color="#42a5f5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ksAdminGlow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#b3e5fc" stop-opacity="0.85"/>
|
||||||
|
<stop offset="100%" stop-color="#e1f5fe" stop-opacity="0.2"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<circle cx="64" cy="64" r="60" fill="url(#ksAdminBg)"/>
|
||||||
|
<circle cx="64" cy="64" r="56" fill="none" stroke="url(#ksAdminGlow)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- AI circuit motif -->
|
||||||
|
<g stroke="#b3e5fc" stroke-width="2" stroke-linecap="round" opacity="0.85">
|
||||||
|
<path d="M18 44h12"/>
|
||||||
|
<path d="M98 44h12"/>
|
||||||
|
<path d="M20 84h10"/>
|
||||||
|
<path d="M98 84h10"/>
|
||||||
|
<path d="M31 30l7 7"/>
|
||||||
|
<path d="M90 30l-7 7"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#e1f5fe" opacity="0.95">
|
||||||
|
<circle cx="18" cy="44" r="3"/>
|
||||||
|
<circle cx="110" cy="44" r="3"/>
|
||||||
|
<circle cx="20" cy="84" r="3"/>
|
||||||
|
<circle cx="110" cy="84" r="3"/>
|
||||||
|
<circle cx="31" cy="30" r="2.8"/>
|
||||||
|
<circle cx="90" cy="30" r="2.8"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Admin bust -->
|
||||||
|
<circle cx="64" cy="50" r="18" fill="#ffffff"/>
|
||||||
|
<path d="M32 96c4-14 17-22 32-22s28 8 32 22" fill="#ffffff"/>
|
||||||
|
|
||||||
|
<!-- Face details -->
|
||||||
|
<circle cx="57" cy="50" r="2.5" fill="#1e88e5"/>
|
||||||
|
<circle cx="71" cy="50" r="2.5" fill="#1e88e5"/>
|
||||||
|
<path d="M59 58c3.5 2.5 6.5 2.5 10 0" stroke="#1e88e5" stroke-width="2.6" fill="none" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Crown-like brand accent -->
|
||||||
|
<path d="M45 31l8-6 11 6 11-6 8 6v6H45z" fill="#ffd54f"/>
|
||||||
|
<circle cx="64" cy="31" r="2.8" fill="#ff8f00"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,36 @@
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ksNoticeBg" x1="10%" y1="8%" x2="88%" y2="90%">
|
||||||
|
<stop offset="0%" stop-color="#004d99"/>
|
||||||
|
<stop offset="55%" stop-color="#0288d1"/>
|
||||||
|
<stop offset="100%" stop-color="#29b6f6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<circle cx="64" cy="64" r="60" fill="url(#ksNoticeBg)"/>
|
||||||
|
<circle cx="64" cy="64" r="55" fill="none" stroke="#b3e5fc" stroke-opacity="0.45" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Shopping bag -->
|
||||||
|
<rect x="34" y="40" width="60" height="52" rx="8" fill="#ffffff"/>
|
||||||
|
<path d="M48 46c0-9 7-16 16-16s16 7 16 16" stroke="#1e88e5" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- AI chip symbol on bag -->
|
||||||
|
<rect x="50" y="56" width="28" height="20" rx="4" fill="#e3f2fd" stroke="#0288d1" stroke-width="2"/>
|
||||||
|
<g stroke="#0288d1" stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M64 52v4"/>
|
||||||
|
<path d="M64 76v4"/>
|
||||||
|
<path d="M46 66h4"/>
|
||||||
|
<path d="M78 66h4"/>
|
||||||
|
<path d="M53 59l-3-3"/>
|
||||||
|
<path d="M75 59l3-3"/>
|
||||||
|
<path d="M53 73l-3 3"/>
|
||||||
|
<path d="M75 73l3 3"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="64" cy="66" r="4.2" fill="#29b6f6"/>
|
||||||
|
|
||||||
|
<!-- Notification sparkle -->
|
||||||
|
<g fill="#ffd54f">
|
||||||
|
<path d="M95 28l3 7 7 3-7 3-3 7-3-7-7-3 7-3z"/>
|
||||||
|
<circle cx="28" cy="34" r="3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -5,7 +5,7 @@
|
||||||
<el-avatar :size="60">
|
<el-avatar :size="60">
|
||||||
<Icon icon="ep:avatar" :size="60" />
|
<Icon icon="ep:avatar" :size="60" />
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
<span class="text-18px font-bold">芋道源码</span>
|
<span class="text-18px font-bold">空山平台</span>
|
||||||
</div>
|
</div>
|
||||||
<Icon icon="tdesign:qrcode" :size="20" />
|
<Icon icon="tdesign:qrcode" :size="20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<el-text>手机扫码预览</el-text>
|
<el-text>手机扫码预览</el-text>
|
||||||
<Qrcode :text="previewUrl" logo="/logo.gif" />
|
<Qrcode :text="previewUrl" logo="/brand-logo.png" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<el-alert v-if="getEnable()" type="success" show-icon>
|
<el-alert v-if="getEnable()" type="success" show-icon>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div @click="goToUrl">{{ '【' + title + '】文档地址:' + url }}</div>
|
<div @click="goToUrl">
|
||||||
|
{{ '【' + (title || '模块说明') + '】空山帮助中心 / 内部Wiki:' + helpCenterUrl }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,13 +19,15 @@ const props = defineProps({
|
||||||
|
|
||||||
/** 跳转 URL 链接 */
|
/** 跳转 URL 链接 */
|
||||||
const goToUrl = () => {
|
const goToUrl = () => {
|
||||||
window.open(props.url)
|
window.open(helpCenterUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否开启 */
|
/** 是否开启 */
|
||||||
const getEnable = () => {
|
const getEnable = () => {
|
||||||
return import.meta.env.VITE_APP_DOCALERT_ENABLE !== 'false'
|
return import.meta.env.VITE_APP_DOCALERT_ENABLE !== 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpCenterUrl = import.meta.env.VITE_APP_HELP_CENTER_URL || 'http://m.swelife.cn/#/pages/index'
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-alert--success.is-light {
|
.el-alert--success.is-light {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Editor from './src/Editor.vue'
|
import Editor from './src/Editor.vue'
|
||||||
import { IDomEditor } from '@wangeditor/editor'
|
import { IDomEditor } from '@wangeditor-next/editor'
|
||||||
|
|
||||||
export interface EditorExpose {
|
export interface EditorExpose {
|
||||||
getEditorRef: () => Promise<IDomEditor>
|
getEditorRef: () => Promise<IDomEditor>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor-next/editor-for-vue'
|
||||||
import { i18nChangeLanguage, IDomEditor, IEditorConfig } from '@wangeditor/editor'
|
import { i18nChangeLanguage, IDomEditor, IEditorConfig } from '@wangeditor-next/editor'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { isNumber } from '@/utils/is'
|
import { isNumber } from '@/utils/is'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useLocaleStore } from '@/store/modules/locale'
|
import { useLocaleStore } from '@/store/modules/locale'
|
||||||
import { getRefreshToken, getTenantId } from '@/utils/auth'
|
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||||
import { getUploadUrl } from '@/components/UploadFile/src/useUpload'
|
import merge from 'lodash-es/merge'
|
||||||
|
|
||||||
defineOptions({ name: 'Editor' })
|
defineOptions({ name: 'Editor' })
|
||||||
|
|
||||||
|
|
@ -20,18 +20,23 @@ const currentLocale = computed(() => localeStore.getCurrentLocale)
|
||||||
i18nChangeLanguage(unref(currentLocale).lang)
|
i18nChangeLanguage(unref(currentLocale).lang)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
editorId: propTypes.string.def('wangeEditor-1'),
|
editorId: propTypes.string.def('wangEditor-1'),
|
||||||
height: propTypes.oneOfType([Number, String]).def('500px'),
|
height: propTypes.oneOfType([Number, String]).def('500px'),
|
||||||
editorConfig: {
|
editorConfig: {
|
||||||
type: Object as PropType<Partial<IEditorConfig>>,
|
type: Object as PropType<Partial<IEditorConfig>>,
|
||||||
default: () => undefined
|
default: () => undefined
|
||||||
},
|
},
|
||||||
readonly: propTypes.bool.def(false),
|
readonly: propTypes.bool.def(false),
|
||||||
modelValue: propTypes.string.def('')
|
modelValue: propTypes.string.def(''),
|
||||||
|
directory: propTypes.string.def('editor-default')
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['change', 'update:modelValue'])
|
const emit = defineEmits(['change', 'update:modelValue'])
|
||||||
|
|
||||||
|
// 使用项目的上传方法,实现逐个文件上传
|
||||||
|
const { httpRequest: imageHttpRequest } = useUpload(`${props.directory}-image`)
|
||||||
|
const { httpRequest: videoHttpRequest } = useUpload(`${props.directory}-video`)
|
||||||
|
|
||||||
// 编辑器实例,必须用 shallowRef
|
// 编辑器实例,必须用 shallowRef
|
||||||
const editorRef = shallowRef<IDomEditor>()
|
const editorRef = shallowRef<IDomEditor>()
|
||||||
|
|
||||||
|
|
@ -58,6 +63,20 @@ watch(
|
||||||
emit('update:modelValue', val)
|
emit('update:modelValue', val)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
watch(
|
||||||
|
() => props.readonly,
|
||||||
|
async (val) => {
|
||||||
|
// 特殊:等待 editorRef 渲染完成
|
||||||
|
if (!editorRef.value) {
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
if (val) {
|
||||||
|
editorRef.value?.disable()
|
||||||
|
} else {
|
||||||
|
editorRef.value?.enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const handleCreated = (editor: IDomEditor) => {
|
const handleCreated = (editor: IDomEditor) => {
|
||||||
editorRef.value = editor
|
editorRef.value = editor
|
||||||
|
|
@ -65,7 +84,7 @@ const handleCreated = (editor: IDomEditor) => {
|
||||||
|
|
||||||
// 编辑器配置
|
// 编辑器配置
|
||||||
const editorConfig = computed((): IEditorConfig => {
|
const editorConfig = computed((): IEditorConfig => {
|
||||||
return Object.assign(
|
return merge(
|
||||||
{
|
{
|
||||||
placeholder: '请输入内容...',
|
placeholder: '请输入内容...',
|
||||||
readOnly: props.readonly,
|
readOnly: props.readonly,
|
||||||
|
|
@ -90,101 +109,63 @@ const editorConfig = computed((): IEditorConfig => {
|
||||||
},
|
},
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
scroll: true,
|
scroll: true,
|
||||||
|
EXTEND_CONF: {
|
||||||
|
mentionConfig: {
|
||||||
|
showModal: () => {},
|
||||||
|
hideModal: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
MENU_CONF: {
|
MENU_CONF: {
|
||||||
['uploadImage']: {
|
['uploadImage']: {
|
||||||
server: getUploadUrl(),
|
|
||||||
// 单个文件的最大体积限制,默认为 2M
|
// 单个文件的最大体积限制,默认为 2M
|
||||||
maxFileSize: 5 * 1024 * 1024,
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
// 最多可上传几个文件,默认为 100
|
// 最多可上传几个文件,默认为 100
|
||||||
maxNumberOfFiles: 10,
|
maxNumberOfFiles: 100,
|
||||||
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
|
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
|
||||||
allowedFileTypes: ['image/*'],
|
allowedFileTypes: ['image/*'],
|
||||||
|
|
||||||
// 自定义增加 http header
|
// 使用 customUpload 实现逐个文件上传,复用项目的 httpRequest
|
||||||
headers: {
|
async customUpload(file: File, insertFn: InsertFnType) {
|
||||||
Accept: '*',
|
try {
|
||||||
Authorization: 'Bearer ' + getRefreshToken(), // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:Editor 无法方便的刷新访问令牌
|
const res = await imageHttpRequest({
|
||||||
'tenant-id': getTenantId()
|
file: file as any,
|
||||||
},
|
onProgress: () => {},
|
||||||
|
onSuccess: () => {},
|
||||||
// 超时时间,默认为 10 秒
|
onError: () => {}
|
||||||
timeout: 15 * 1000, // 15 秒
|
} as any)
|
||||||
|
// 兼容前端直连上传和后端上传两种模式的返回格式
|
||||||
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
|
const url = (res as any).data?.data || (res as any).data
|
||||||
fieldName: 'file',
|
insertFn(url, 'image', url)
|
||||||
|
} catch (error: any) {
|
||||||
// 上传之前触发
|
ElMessage.error(error.msg || '图片上传失败')
|
||||||
onBeforeUpload(file: File) {
|
console.error('Upload error:', error)
|
||||||
// console.log(file)
|
}
|
||||||
return file
|
|
||||||
},
|
|
||||||
// 上传进度的回调函数
|
|
||||||
onProgress(progress: number) {
|
|
||||||
// progress 是 0-100 的数字
|
|
||||||
console.log('progress', progress)
|
|
||||||
},
|
|
||||||
onSuccess(file: File, res: any) {
|
|
||||||
console.log('onSuccess', file, res)
|
|
||||||
},
|
|
||||||
onFailed(file: File, res: any) {
|
|
||||||
alert(res.message)
|
|
||||||
console.log('onFailed', file, res)
|
|
||||||
},
|
|
||||||
onError(file: File, err: any, res: any) {
|
|
||||||
alert(err.message)
|
|
||||||
console.error('onError', file, err, res)
|
|
||||||
},
|
|
||||||
// 自定义插入图片
|
|
||||||
customInsert(res: any, insertFn: InsertFnType) {
|
|
||||||
insertFn(res.data, 'image', res.data)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
['uploadVideo']: {
|
['uploadVideo']: {
|
||||||
server: getUploadUrl(),
|
|
||||||
// 单个文件的最大体积限制,默认为 10M
|
// 单个文件的最大体积限制,默认为 10M
|
||||||
maxFileSize: 10 * 1024 * 1024,
|
maxFileSize: 1024 * 1024 * 1024,
|
||||||
// 最多可上传几个文件,默认为 100
|
// 最多可上传几个文件,默认为 100
|
||||||
maxNumberOfFiles: 10,
|
maxNumberOfFiles: 10,
|
||||||
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
|
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
|
||||||
allowedFileTypes: ['video/*'],
|
allowedFileTypes: ['video/*'],
|
||||||
|
|
||||||
// 自定义增加 http header
|
// 使用 customUpload 实现逐个文件上传,复用项目的 httpRequest
|
||||||
headers: {
|
async customUpload(file: File, insertFn: InsertFnType) {
|
||||||
Accept: '*',
|
try {
|
||||||
Authorization: 'Bearer ' + getRefreshToken(), // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:Editor 无法方便的刷新访问令牌
|
const res = await videoHttpRequest({
|
||||||
'tenant-id': getTenantId()
|
file: file as any,
|
||||||
},
|
onProgress: () => {},
|
||||||
|
onSuccess: () => {},
|
||||||
// 超时时间,默认为 30 秒
|
onError: () => {}
|
||||||
timeout: 15 * 1000, // 15 秒
|
} as any)
|
||||||
|
// 兼容前端直连上传和后端上传两种模式的返回格式
|
||||||
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
|
const url = (res as any).data?.data || (res as any).data
|
||||||
fieldName: 'file',
|
insertFn(url, 'mp4', url)
|
||||||
|
} catch (error: any) {
|
||||||
// 上传之前触发
|
ElMessage.error(error.msg || '视频上传失败')
|
||||||
onBeforeUpload(file: File) {
|
console.error('Upload error:', error)
|
||||||
// console.log(file)
|
}
|
||||||
return file
|
|
||||||
},
|
|
||||||
// 上传进度的回调函数
|
|
||||||
onProgress(progress: number) {
|
|
||||||
// progress 是 0-100 的数字
|
|
||||||
console.log('progress', progress)
|
|
||||||
},
|
|
||||||
onSuccess(file: File, res: any) {
|
|
||||||
console.log('onSuccess', file, res)
|
|
||||||
},
|
|
||||||
onFailed(file: File, res: any) {
|
|
||||||
alert(res.message)
|
|
||||||
console.log('onFailed', file, res)
|
|
||||||
},
|
|
||||||
onError(file: File, err: any, res: any) {
|
|
||||||
alert(err.message)
|
|
||||||
console.error('onError', file, err, res)
|
|
||||||
},
|
|
||||||
// 自定义插入图片
|
|
||||||
customInsert(res: any, insertFn: InsertFnType) {
|
|
||||||
insertFn(res.data, 'mp4', res.data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -243,4 +224,4 @@ defineExpose({
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style src="@wangeditor/editor/dist/css/style.css"></style>
|
<style src="@wangeditor-next/editor/dist/css/style.css"></style>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default defineComponent({
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
// 是否需要栅格布局
|
// 是否需要栅格布局
|
||||||
// update by 芋艿:将 true 改成 false,因为项目更常用这种方式
|
// update by 项目组:将 true 改成 false,因为项目更常用这种方式
|
||||||
isCol: propTypes.bool.def(false),
|
isCol: propTypes.bool.def(false),
|
||||||
// 表单数据对象
|
// 表单数据对象
|
||||||
model: {
|
model: {
|
||||||
|
|
@ -49,7 +49,7 @@ export default defineComponent({
|
||||||
isCustom: propTypes.bool.def(false),
|
isCustom: propTypes.bool.def(false),
|
||||||
// 表单label宽度
|
// 表单label宽度
|
||||||
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
|
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
|
||||||
// 是否 loading 数据中 add by 芋艿
|
// 是否 loading 数据中 add by 项目组
|
||||||
vLoading: propTypes.bool.def(false)
|
vLoading: propTypes.bool.def(false)
|
||||||
},
|
},
|
||||||
emits: ['register'],
|
emits: ['register'],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
<!-- 省市区选择器 (Element Plus 版本 - Vue3) -->
|
||||||
|
<template>
|
||||||
|
<el-cascader
|
||||||
|
v-model="selectedValue"
|
||||||
|
class="w-full"
|
||||||
|
:options="areaTree"
|
||||||
|
:props="cascaderProps"
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:clearable="clearable"
|
||||||
|
:show-all-levels="showAllLevels"
|
||||||
|
:separator="separator"
|
||||||
|
:loading="loading"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import { getAreaTree } from '@/api/system/area'
|
||||||
|
import { AreaLevelEnum } from '@/utils/constants'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AreaSelect' })
|
||||||
|
|
||||||
|
interface AreaVO {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
parentId?: number
|
||||||
|
sort?: number
|
||||||
|
status?: number
|
||||||
|
children?: AreaVO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: number[] | string[]
|
||||||
|
level?: typeof AreaLevelEnum[keyof typeof AreaLevelEnum]
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
clearable?: boolean
|
||||||
|
showAllLevels?: boolean
|
||||||
|
separator?: string
|
||||||
|
formCreateInject?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: undefined,
|
||||||
|
level: AreaLevelEnum.DISTRICT,
|
||||||
|
disabled: false,
|
||||||
|
placeholder: '请选择省市区',
|
||||||
|
clearable: true,
|
||||||
|
showAllLevels: true,
|
||||||
|
separator: '/'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: number[] | string[] | undefined): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const cascaderProps = {
|
||||||
|
label: 'name',
|
||||||
|
value: 'id',
|
||||||
|
children: 'children',
|
||||||
|
checkStrictly: true, // 允许选择任意级别
|
||||||
|
emitPath: true // 返回完整路径
|
||||||
|
} // Element Plus Cascader 的 props 配置
|
||||||
|
|
||||||
|
const areaTree = ref<AreaVO[]>([]) // 地区树形数据
|
||||||
|
const selectedValue = ref<number[] | undefined>() // 当前选中值
|
||||||
|
const loading = ref(false) // 加载状态
|
||||||
|
|
||||||
|
/** 加载地区树形数据 */
|
||||||
|
async function loadAreaTree(): Promise<void> {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const data = await getAreaTree()
|
||||||
|
// 根据 level 限制层级
|
||||||
|
areaTree.value = filterTreeByLevel(data || [], props.level)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[AreaSelect] 加载地区数据失败:', error)
|
||||||
|
areaTree.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据层级过滤树形数据 */
|
||||||
|
function filterTreeByLevel(tree: AreaVO[], maxLevel: number): AreaVO[] {
|
||||||
|
if (maxLevel <= 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return tree.map((node) => {
|
||||||
|
const newNode = { ...node }
|
||||||
|
// 如果当前是最后一层,移除 children
|
||||||
|
if (maxLevel === 1) {
|
||||||
|
delete newNode.children
|
||||||
|
} else if (node.children && node.children.length > 0) {
|
||||||
|
// 递归处理子节点
|
||||||
|
newNode.children = filterTreeByLevel(node.children, maxLevel - 1)
|
||||||
|
}
|
||||||
|
return newNode
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理选中值变化 */
|
||||||
|
function handleChange(value: number[] | undefined): void {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
emit('update:modelValue', undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步 modelValue 到内部选中值 */
|
||||||
|
function syncSelectedValue(): void {
|
||||||
|
const newValue = props.modelValue
|
||||||
|
if (newValue === undefined || newValue === null) {
|
||||||
|
selectedValue.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保是数组格式
|
||||||
|
if (Array.isArray(newValue)) {
|
||||||
|
selectedValue.value = newValue as number[]
|
||||||
|
} else {
|
||||||
|
selectedValue.value = [newValue as number]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听 modelValue 变化 */
|
||||||
|
watch(() => props.modelValue, syncSelectedValue, { immediate: true })
|
||||||
|
|
||||||
|
/** 组件挂载时加载数据 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadAreaTree()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
<!-- 部门选择器 - 树形结构显示 -->
|
||||||
|
<template>
|
||||||
|
<el-tree-select
|
||||||
|
v-model="selectedValue"
|
||||||
|
class="w-1/1"
|
||||||
|
:data="deptTree"
|
||||||
|
:props="treeProps"
|
||||||
|
:multiple="multiple"
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder || '请选择部门'"
|
||||||
|
:check-strictly="true"
|
||||||
|
:filterable="true"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
:clearable="true"
|
||||||
|
:render-after-expand="false"
|
||||||
|
node-key="id"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { handleTree } from '@/utils/tree'
|
||||||
|
import { getSimpleDeptList, type DeptVO } from '@/api/system/dept'
|
||||||
|
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'DeptSelect' })
|
||||||
|
|
||||||
|
// 接受父组件参数
|
||||||
|
interface Props {
|
||||||
|
modelValue?: number | string | number[] | string[]
|
||||||
|
multiple?: boolean
|
||||||
|
returnType?: 'id' | 'name'
|
||||||
|
defaultCurrentDept?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
formCreateInject?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
multiple: false,
|
||||||
|
returnType: 'id',
|
||||||
|
defaultCurrentDept: false,
|
||||||
|
disabled: false,
|
||||||
|
placeholder: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: number | string | number[] | string[] | undefined): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 树形选择器配置
|
||||||
|
const treeProps = {
|
||||||
|
label: 'name',
|
||||||
|
value: 'id',
|
||||||
|
children: 'children'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部门树形数据
|
||||||
|
const deptTree = ref<any[]>([])
|
||||||
|
// 原始部门列表(用于 returnType='name' 时查找名称)
|
||||||
|
const deptList = ref<DeptVO[]>([])
|
||||||
|
// 当前选中值
|
||||||
|
const selectedValue = ref<number | string | number[] | string[] | undefined>()
|
||||||
|
|
||||||
|
// 加载部门树形数据
|
||||||
|
const loadDeptTree = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getSimpleDeptList()
|
||||||
|
deptList.value = data
|
||||||
|
deptTree.value = handleTree(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('加载部门数据失败:', error)
|
||||||
|
deptTree.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 ID 获取部门名称
|
||||||
|
const getDeptNameById = (id: number): string | undefined => {
|
||||||
|
const dept = deptList.value.find((item) => item.id === id)
|
||||||
|
return dept?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据名称获取部门 ID
|
||||||
|
const getDeptIdByName = (name: string): number | undefined => {
|
||||||
|
const dept = deptList.value.find((item) => item.name === name)
|
||||||
|
return dept?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选中值变化
|
||||||
|
const handleChange = (value: number | number[] | undefined) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
emit('update:modelValue', props.multiple ? [] : undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 returnType 决定返回值类型
|
||||||
|
if (props.returnType === 'name') {
|
||||||
|
if (props.multiple && Array.isArray(value)) {
|
||||||
|
const names = value.map((id) => getDeptNameById(id)).filter(Boolean) as string[]
|
||||||
|
emit('update:modelValue', names)
|
||||||
|
} else if (!props.multiple && typeof value === 'number') {
|
||||||
|
const name = getDeptNameById(value)
|
||||||
|
emit('update:modelValue', name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树节点过滤方法(支持搜索过滤)
|
||||||
|
const filterNode = (value: string, data: any) => {
|
||||||
|
if (!value) return true
|
||||||
|
return data.name.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 modelValue 变化,同步到内部选中值
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue === undefined || newValue === null) {
|
||||||
|
selectedValue.value = props.multiple ? [] : undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 returnType 是 'name',需要将名称转换为 ID 用于树选择器显示
|
||||||
|
if (props.returnType === 'name') {
|
||||||
|
if (props.multiple && Array.isArray(newValue)) {
|
||||||
|
const ids = (newValue as string[])
|
||||||
|
.map((name) => getDeptIdByName(name))
|
||||||
|
.filter(Boolean) as number[]
|
||||||
|
selectedValue.value = ids
|
||||||
|
} else if (!props.multiple && typeof newValue === 'string') {
|
||||||
|
const id = getDeptIdByName(newValue)
|
||||||
|
selectedValue.value = id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedValue.value = newValue as number | number[]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 检查是否有有效的预设值
|
||||||
|
const hasValidPresetValue = (): boolean => {
|
||||||
|
const value = props.modelValue
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.length > 0
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值(当前用户部门)
|
||||||
|
const setDefaultValue = () => {
|
||||||
|
console.log('[DeptSelect] setDefaultValue called, defaultCurrentDept:', props.defaultCurrentDept)
|
||||||
|
|
||||||
|
// 仅当 defaultCurrentDept 为 true 时处理
|
||||||
|
if (!props.defaultCurrentDept) {
|
||||||
|
console.log('[DeptSelect] defaultCurrentDept is false, skip')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已有预设值(预设值优先级高于默认当前部门)
|
||||||
|
if (hasValidPresetValue()) {
|
||||||
|
console.log('[DeptSelect] has preset value, skip:', props.modelValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户的部门 ID
|
||||||
|
const userStore = useUserStoreWithOut()
|
||||||
|
const user = userStore.getUser
|
||||||
|
const deptId = user?.deptId
|
||||||
|
|
||||||
|
console.log('[DeptSelect] current user:', user, 'deptId:', deptId)
|
||||||
|
|
||||||
|
// 处理 deptId 为空或 0 的边界情况
|
||||||
|
if (!deptId || deptId === 0) {
|
||||||
|
console.log('[DeptSelect] deptId is invalid, skip')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据多选模式决定默认值格式
|
||||||
|
const defaultValue = props.multiple ? [deptId] : deptId
|
||||||
|
console.log('[DeptSelect] setting default value:', defaultValue)
|
||||||
|
emit('update:modelValue', defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据并设置默认值
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadDeptTree()
|
||||||
|
// 数据加载完成后设置默认值
|
||||||
|
setDefaultValue()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!-- 网页 iframe 组件 (Element Plus 版本 - Vue3) -->
|
||||||
|
<template>
|
||||||
|
<div class="iframe-component">
|
||||||
|
<!-- iframe 预览 -->
|
||||||
|
<div v-if="showPreview" class="iframe-preview">
|
||||||
|
<iframe
|
||||||
|
:src="displayUrl"
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
:frameborder="frameborder"
|
||||||
|
:allowfullscreen="allowfullscreen"
|
||||||
|
:loading="loading"
|
||||||
|
:sandbox="sandbox || undefined"
|
||||||
|
class="iframe-content"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无 URL 或无效 URL 提示 -->
|
||||||
|
<div v-else class="iframe-placeholder">
|
||||||
|
<el-empty description="请在右侧属性面板配置 URL 地址" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { isUrl } from '@/utils/is'
|
||||||
|
|
||||||
|
defineOptions({ name: 'IframeComponent' })
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string
|
||||||
|
url?: string
|
||||||
|
height?: string
|
||||||
|
width?: string
|
||||||
|
frameborder?: string
|
||||||
|
allowfullscreen?: boolean
|
||||||
|
loading?: 'eager' | 'lazy'
|
||||||
|
sandbox?: string
|
||||||
|
formCreateInject?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: '',
|
||||||
|
url: '',
|
||||||
|
height: '500px',
|
||||||
|
width: '100%',
|
||||||
|
frameborder: '0',
|
||||||
|
allowfullscreen: true,
|
||||||
|
loading: 'lazy',
|
||||||
|
sandbox: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayUrl = computed(() => props.url || props.modelValue || '') // 显示的 URL(优先使用 url prop,其次使用 modelValue)
|
||||||
|
const showPreview = computed(() => {
|
||||||
|
return displayUrl.value && isUrl(displayUrl.value)
|
||||||
|
}) // 是否显示预览
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.iframe-component {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-preview {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-content {
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 200px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,6 +2,7 @@ import request from '@/config/axios'
|
||||||
import { isEmpty } from '@/utils/is'
|
import { isEmpty } from '@/utils/is'
|
||||||
import { ApiSelectProps } from '@/components/FormCreate/src/type'
|
import { ApiSelectProps } from '@/components/FormCreate/src/type'
|
||||||
import { jsonParse } from '@/utils'
|
import { jsonParse } from '@/utils'
|
||||||
|
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||||
|
|
||||||
export const useApiSelect = (option: ApiSelectProps) => {
|
export const useApiSelect = (option: ApiSelectProps) => {
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
|
|
@ -56,13 +57,58 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||||
remoteField: {
|
remoteField: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'label'
|
default: 'label'
|
||||||
|
},
|
||||||
|
// 返回值类型(用于部门选择器等):id 返回 ID,name 返回名称
|
||||||
|
returnType: {
|
||||||
|
type: String,
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
// 是否默认选中当前用户(仅 UserSelect 使用)
|
||||||
|
defaultCurrentUser: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props, { emit }) {
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
const options = ref<any[]>([]) // 下拉数据
|
const options = ref<any[]>([]) // 下拉数据
|
||||||
const loading = ref(false) // 是否正在从远程获取数据
|
const loading = ref(false) // 是否正在从远程获取数据
|
||||||
const queryParam = ref<any>() // 当前输入的值
|
const queryParam = ref<any>() // 当前输入的值
|
||||||
|
|
||||||
|
// 检查是否有有效的预设值
|
||||||
|
const hasValidPresetValue = (): boolean => {
|
||||||
|
const value = attrs.modelValue
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.length > 0
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认当前用户(仅当 defaultCurrentUser 为 true 且无预设值时)
|
||||||
|
const setDefaultCurrentUser = () => {
|
||||||
|
// 仅当组件名为 UserSelect 且 defaultCurrentUser 为 true 时处理
|
||||||
|
if (option.name !== 'UserSelect' || !props.defaultCurrentUser) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查是否已有预设值(预设值优先级高于默认当前用户)
|
||||||
|
if (hasValidPresetValue()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户 ID
|
||||||
|
const userStore = useUserStoreWithOut()
|
||||||
|
const user = userStore.getUser
|
||||||
|
const currentUserId = user?.id
|
||||||
|
if (currentUserId) {
|
||||||
|
// 根据多选/单选模式设置默认值
|
||||||
|
const defaultValue = props.multiple ? [currentUserId] : currentUserId
|
||||||
|
emit('update:modelValue', defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
options.value = []
|
options.value = []
|
||||||
// 接口选择器
|
// 接口选择器
|
||||||
|
|
@ -119,10 +165,21 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||||
|
|
||||||
function parseOptions0(data: any[]) {
|
function parseOptions0(data: any[]) {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
options.value = data.map((item: any) => ({
|
options.value = data.map((item: any) => {
|
||||||
label: parseExpression(item, props.labelField),
|
const label = parseExpression(item, props.labelField)
|
||||||
value: parseExpression(item, props.valueField)
|
let value = parseExpression(item, props.valueField)
|
||||||
}))
|
|
||||||
|
// 根据 returnType 决定返回值
|
||||||
|
// 如果设置了 returnType 为 'name',则返回 label 作为 value
|
||||||
|
if (props.returnType === 'name') {
|
||||||
|
value = label
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: label,
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.warn(`接口[${props.url}] 返回结果不是一个数组`)
|
console.warn(`接口[${props.url}] 返回结果不是一个数组`)
|
||||||
|
|
@ -172,6 +229,8 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getOptions()
|
await getOptions()
|
||||||
|
// 设置默认当前用户(在数据加载完成后)
|
||||||
|
setDefaultCurrentUser()
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildSelect = () => {
|
const buildSelect = () => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { useUploadImgsRule } from './useUploadImgsRule'
|
||||||
import { useDictSelectRule } from './useDictSelectRule'
|
import { useDictSelectRule } from './useDictSelectRule'
|
||||||
import { useEditorRule } from './useEditorRule'
|
import { useEditorRule } from './useEditorRule'
|
||||||
import { useSelectRule } from './useSelectRule'
|
import { useSelectRule } from './useSelectRule'
|
||||||
|
import { useIframeRule } from './useIframeRule'
|
||||||
|
import { useAreaSelectRule } from './useAreaSelectRule'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useUploadFileRule,
|
useUploadFileRule,
|
||||||
|
|
@ -11,5 +13,7 @@ export {
|
||||||
useUploadImgsRule,
|
useUploadImgsRule,
|
||||||
useDictSelectRule,
|
useDictSelectRule,
|
||||||
useEditorRule,
|
useEditorRule,
|
||||||
useSelectRule
|
useSelectRule,
|
||||||
|
useIframeRule,
|
||||||
|
useAreaSelectRule
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { generateUUID } from '@/utils'
|
||||||
|
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||||
|
import { AreaLevelEnum } from '@/utils/constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 省市区选择器规则
|
||||||
|
*/
|
||||||
|
export const useAreaSelectRule = () => {
|
||||||
|
const label = '省市区选择器'
|
||||||
|
const name = 'AreaSelect'
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: 'icon-location',
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
rule() {
|
||||||
|
return {
|
||||||
|
type: name,
|
||||||
|
field: generateUUID(),
|
||||||
|
title: label,
|
||||||
|
info: '',
|
||||||
|
$required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props(_, { t }) {
|
||||||
|
return localeProps(t, name + '.props', [
|
||||||
|
makeRequiredRule(),
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'level',
|
||||||
|
title: '选择层级',
|
||||||
|
value: AreaLevelEnum.DISTRICT,
|
||||||
|
options: [
|
||||||
|
{ label: '省', value: AreaLevelEnum.PROVINCE },
|
||||||
|
{ label: '省/市', value: AreaLevelEnum.CITY },
|
||||||
|
{ label: '省/市/区', value: AreaLevelEnum.DISTRICT }
|
||||||
|
],
|
||||||
|
info: '限制可选择的地区层级'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'placeholder',
|
||||||
|
title: '占位符',
|
||||||
|
value: '请选择省市区'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'clearable',
|
||||||
|
title: '是否可清空',
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'showAllLevels',
|
||||||
|
title: '显示完整路径',
|
||||||
|
value: true,
|
||||||
|
info: '输入框中是否显示选中值的完整路径'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'separator',
|
||||||
|
title: '分隔符',
|
||||||
|
value: '/',
|
||||||
|
info: '选项分隔符'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'disabled',
|
||||||
|
title: '是否禁用',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { generateUUID } from '@/utils'
|
||||||
|
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iframe 组件规则
|
||||||
|
*/
|
||||||
|
export const useIframeRule = () => {
|
||||||
|
const label = '网页 iframe'
|
||||||
|
const name = 'IframeComponent'
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: 'icon-link',
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
rule() {
|
||||||
|
return {
|
||||||
|
type: name,
|
||||||
|
field: generateUUID(),
|
||||||
|
title: label,
|
||||||
|
info: '',
|
||||||
|
$required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props(_, { t }) {
|
||||||
|
return localeProps(t, name + '.props', [
|
||||||
|
makeRequiredRule(),
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'url',
|
||||||
|
title: 'URL 地址',
|
||||||
|
value: '',
|
||||||
|
info: '请输入完整的 HTTP 或 HTTPS 地址'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'height',
|
||||||
|
title: 'iframe 高度',
|
||||||
|
value: '500px',
|
||||||
|
info: '支持 px、%、vh 等单位'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'width',
|
||||||
|
title: 'iframe 宽度',
|
||||||
|
value: '100%',
|
||||||
|
info: '支持 px、%、vw 等单位'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'loading',
|
||||||
|
title: '加载方式',
|
||||||
|
value: 'lazy',
|
||||||
|
options: [
|
||||||
|
{ label: '懒加载', value: 'lazy' },
|
||||||
|
{ label: '立即加载', value: 'eager' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'allowfullscreen',
|
||||||
|
title: '允许全屏',
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'sandbox',
|
||||||
|
title: 'sandbox 属性',
|
||||||
|
value: '',
|
||||||
|
info: '安全沙箱限制,如:allow-scripts allow-same-origin'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,13 +19,24 @@ export const useSelectRule = (option: SelectRuleOption) => {
|
||||||
name,
|
name,
|
||||||
event: option.event,
|
event: option.event,
|
||||||
rule() {
|
rule() {
|
||||||
return {
|
// 构建基础规则
|
||||||
|
const baseRule: any = {
|
||||||
type: name,
|
type: name,
|
||||||
field: generateUUID(),
|
field: generateUUID(),
|
||||||
title: label,
|
title: label,
|
||||||
info: '',
|
info: '',
|
||||||
$required: false
|
$required: false
|
||||||
}
|
}
|
||||||
|
// 将自定义 props 的默认值添加到 rule 的 props 中
|
||||||
|
if (option.props && option.props.length > 0) {
|
||||||
|
baseRule.props = {}
|
||||||
|
option.props.forEach((prop: any) => {
|
||||||
|
if (prop.field && prop.value !== undefined) {
|
||||||
|
baseRule.props[prop.field] = prop.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return baseRule
|
||||||
},
|
},
|
||||||
props(_, { t }) {
|
props(_, { t }) {
|
||||||
if (!option.props) {
|
if (!option.props) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { Rule } from '@form-create/element-ui' //左侧拖拽按钮
|
|
||||||
|
|
||||||
// 左侧拖拽按钮
|
// 左侧拖拽按钮
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -14,24 +12,6 @@ export interface Menu {
|
||||||
list: MenuItem[]
|
list: MenuItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuList extends Array<Menu> {}
|
|
||||||
|
|
||||||
// 拖拽组件的规则
|
|
||||||
export interface DragRule {
|
|
||||||
icon: string
|
|
||||||
name: string
|
|
||||||
label: string
|
|
||||||
children?: string
|
|
||||||
inside?: true
|
|
||||||
drag?: true | String
|
|
||||||
dragBtn?: false
|
|
||||||
mask?: false
|
|
||||||
|
|
||||||
rule(): Rule
|
|
||||||
|
|
||||||
props(v: any, v1: any): Rule[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用下拉组件 Props 类型
|
// 通用下拉组件 Props 类型
|
||||||
export interface ApiSelectProps {
|
export interface ApiSelectProps {
|
||||||
name: string // 组件名称
|
name: string // 组件名称
|
||||||
|
|
@ -46,6 +26,6 @@ export interface SelectRuleOption {
|
||||||
label: string // label 名称
|
label: string // label 名称
|
||||||
name: string // 组件名称
|
name: string // 组件名称
|
||||||
icon: string // 组件图标
|
icon: string // 组件图标
|
||||||
props?: any[], // 组件规则
|
props?: any[] // 组件规则
|
||||||
event?: any[] // 事件配置
|
event?: any[] // 事件配置
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import {
|
||||||
useSelectRule,
|
useSelectRule,
|
||||||
useUploadFileRule,
|
useUploadFileRule,
|
||||||
useUploadImgRule,
|
useUploadImgRule,
|
||||||
useUploadImgsRule
|
useUploadImgsRule,
|
||||||
|
useIframeRule,
|
||||||
|
useAreaSelectRule
|
||||||
} from './config'
|
} from './config'
|
||||||
import { Ref } from 'vue'
|
import { Ref } from 'vue'
|
||||||
import { Menu } from '@/components/FormCreate/src/type'
|
import { Menu } from '@/components/FormCreate/src/type'
|
||||||
import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
|
import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
|
||||||
|
import { generateUUID } from '@/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单设计器增强 hook
|
* 表单设计器增强 hook
|
||||||
|
|
@ -34,8 +37,10 @@ export const useFormCreateDesigner = async (designer: Ref) => {
|
||||||
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
|
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
|
||||||
designer.value?.removeMenuItem('upload')
|
designer.value?.removeMenuItem('upload')
|
||||||
// 移除自带的富文本组件规则,使用 editorRule 替代
|
// 移除自带的富文本组件规则,使用 editorRule 替代
|
||||||
designer.value?.removeMenuItem('fc-editor')
|
designer.value?.removeMenuItem('fcEditor')
|
||||||
const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule]
|
const iframeRule = useIframeRule()
|
||||||
|
const areaSelectRule = useAreaSelectRule()
|
||||||
|
const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule, iframeRule, areaSelectRule]
|
||||||
components.forEach((component) => {
|
components.forEach((component) => {
|
||||||
// 插入组件规则
|
// 插入组件规则
|
||||||
designer.value?.addComponent(component)
|
designer.value?.addComponent(component)
|
||||||
|
|
@ -51,12 +56,38 @@ export const useFormCreateDesigner = async (designer: Ref) => {
|
||||||
const userSelectRule = useSelectRule({
|
const userSelectRule = useSelectRule({
|
||||||
name: 'UserSelect',
|
name: 'UserSelect',
|
||||||
label: '用户选择器',
|
label: '用户选择器',
|
||||||
icon: 'icon-user-o'
|
icon: 'icon-user-o',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'defaultCurrentUser',
|
||||||
|
title: '默认选中当前用户',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
const deptSelectRule = useSelectRule({
|
const deptSelectRule = useSelectRule({
|
||||||
name: 'DeptSelect',
|
name: 'DeptSelect',
|
||||||
label: '部门选择器',
|
label: '部门选择器',
|
||||||
icon: 'icon-address-card-o'
|
icon: 'icon-address-card-o',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'returnType',
|
||||||
|
title: '返回值类型',
|
||||||
|
value: 'id',
|
||||||
|
options: [
|
||||||
|
{ label: '部门编号', value: 'id' },
|
||||||
|
{ label: '部门名称', value: 'name' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'defaultCurrentDept',
|
||||||
|
title: '默认选中当前部门',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
const dictSelectRule = useDictSelectRule()
|
const dictSelectRule = useDictSelectRule()
|
||||||
const apiSelectRule0 = useSelectRule({
|
const apiSelectRule0 = useSelectRule({
|
||||||
|
|
@ -93,9 +124,60 @@ export const useFormCreateDesigner = async (designer: Ref) => {
|
||||||
designer.value?.addMenu(menu)
|
designer.value?.addMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修复重复的字段 ID 问题
|
||||||
|
* 当复制组件时,自动为新组件生成新的字段 ID
|
||||||
|
*
|
||||||
|
* 对应 issue:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICM22X
|
||||||
|
*/
|
||||||
|
const fixDuplicateFields = () => {
|
||||||
|
// 获取当前所有规则
|
||||||
|
const rules = designer.value?.getRule() || []
|
||||||
|
const fieldIds = new Set<string>()
|
||||||
|
let hasChanges = false
|
||||||
|
|
||||||
|
// 遍历所有规则,检测并修复重复的字段 ID
|
||||||
|
rules.forEach((rule: any) => {
|
||||||
|
if (rule.field) {
|
||||||
|
if (fieldIds.has(rule.field)) {
|
||||||
|
// 发现重复,生成新的ID
|
||||||
|
const oldField = rule.field
|
||||||
|
const newField = generateUUID()
|
||||||
|
console.log(`[FormCreate] 检测到重复字段ID: ${oldField}, 已自动更新为: ${newField}`)
|
||||||
|
rule.field = newField
|
||||||
|
hasChanges = true
|
||||||
|
} else {
|
||||||
|
fieldIds.add(rule.field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果有重复字段被修复,更新设计器
|
||||||
|
if (hasChanges) {
|
||||||
|
designer.value?.setRule(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChanges
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
buildFormComponents()
|
buildFormComponents()
|
||||||
buildSystemMenu()
|
buildSystemMenu()
|
||||||
|
|
||||||
|
// 监听设计器内容变化,自动修复重复字段ID
|
||||||
|
let isFixing = false // 防止无限循环
|
||||||
|
watch(
|
||||||
|
() => designer.value?.getRule(),
|
||||||
|
async () => {
|
||||||
|
if (!isFixing) {
|
||||||
|
isFixing = true
|
||||||
|
await nextTick()
|
||||||
|
fixDuplicateFields()
|
||||||
|
isFixing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import MapDialog from './src/MapDialog.vue'
|
||||||
|
export { loadBaiduMapSdk } from './src/utils'
|
||||||
|
|
||||||
|
export { MapDialog }
|
||||||
|
|
@ -1,268 +0,0 @@
|
||||||
<!-- 地图组件:基于百度地图GL实现 -->
|
|
||||||
<!-- TODO @super:还存在两个没解决的小bug,一个是修改手动定位时一次加载 不知道为何定位点在地图左上角 调了半天没解决 第二个是检索地址确定定位的功能参照百度的文档没也搞好 回头再解决一下 -->
|
|
||||||
<template>
|
|
||||||
<div v-if="props.isWrite">
|
|
||||||
<el-form ref="form" label-width="120px">
|
|
||||||
<el-form-item label="定位位置:">
|
|
||||||
<el-select
|
|
||||||
class="w-full"
|
|
||||||
v-model="state.address"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
reserve-keyword
|
|
||||||
placeholder="可输入地址查询经纬度"
|
|
||||||
:remote-method="autoSearch"
|
|
||||||
@change="handleAddressSelect"
|
|
||||||
:loading="state.loading"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in state.mapAddrOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.name"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="设备地图:">
|
|
||||||
<!-- TODO @super:这里看看 unocss 哈 -->
|
|
||||||
<div id="bdMap" class="mapContainer"></div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<el-descriptions :column="2" border :labelStyle="{ 'font-weight': 'bold' }">
|
|
||||||
<el-descriptions-item label="设备位置:">{{ state.address }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
<div id="bdMap" class="mapContainer"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, onMounted } from 'vue'
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
|
||||||
|
|
||||||
// 扩展 Window 接口以包含百度地图 GL API
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
BMapGL: any
|
|
||||||
initBaiduMap: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emits = defineEmits(['locateChange', 'update:center'])
|
|
||||||
const state = reactive({
|
|
||||||
lonLat: '', // 经度,纬度
|
|
||||||
address: '',
|
|
||||||
loading: false,
|
|
||||||
latitude: '', // 纬度
|
|
||||||
longitude: '', // 经度
|
|
||||||
map: null as any, // 地图对象
|
|
||||||
mapAddrOptions: [] as any[],
|
|
||||||
mapMarker: null as any, // 标记对象
|
|
||||||
geocoder: null as any,
|
|
||||||
autoComplete: null as any,
|
|
||||||
tips: [] // 搜索提示
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
clickMap: propTypes.bool.def(false),
|
|
||||||
isWrite: propTypes.bool.def(false),
|
|
||||||
center: propTypes.string.def('')
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 加载百度地图 */
|
|
||||||
const loadMap = () => {
|
|
||||||
state.address = ''
|
|
||||||
state.latitude = ''
|
|
||||||
state.longitude = ''
|
|
||||||
|
|
||||||
// 创建百度地图 API 脚本,动态加载
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${
|
|
||||||
import.meta.env.VITE_BAIDU_MAP_KEY
|
|
||||||
}&callback=initBaiduMap`
|
|
||||||
document.body.appendChild(script)
|
|
||||||
|
|
||||||
// 定义全局回调函数
|
|
||||||
window.initBaiduMap = () => {
|
|
||||||
initMap()
|
|
||||||
initGeocoder()
|
|
||||||
initAutoComplete()
|
|
||||||
|
|
||||||
// TODO @super:这里加一行注释
|
|
||||||
if (props.clickMap) {
|
|
||||||
state.map.addEventListener('click', (e: any) => {
|
|
||||||
console.log(e)
|
|
||||||
const point = e.latlng
|
|
||||||
console.log(point)
|
|
||||||
state.lonLat = point.lng + ',' + point.lat
|
|
||||||
console.log(state.lonLat)
|
|
||||||
regeoCode(state.lonLat)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @super:这里加一行注释
|
|
||||||
if (props.center) {
|
|
||||||
regeoCode(props.center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化地图 */
|
|
||||||
const initMap = () => {
|
|
||||||
const mapId = 'bdMap'
|
|
||||||
state.map = new window.BMapGL.Map(mapId)
|
|
||||||
// TODO @super:这个是默认的哇?
|
|
||||||
state.map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11)
|
|
||||||
state.map.enableScrollWheelZoom()
|
|
||||||
state.map.disableDoubleClickZoom()
|
|
||||||
|
|
||||||
// 添加地图控件
|
|
||||||
state.map.addControl(new window.BMapGL.NavigationControl())
|
|
||||||
state.map.addControl(new window.BMapGL.ScaleControl())
|
|
||||||
state.map.addControl(new window.BMapGL.ZoomControl())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化地理编码器 */
|
|
||||||
const initGeocoder = () => {
|
|
||||||
state.geocoder = new window.BMapGL.Geocoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化自动完成 */
|
|
||||||
const initAutoComplete = () => {
|
|
||||||
state.autoComplete = new window.BMapGL.Autocomplete({
|
|
||||||
input: 'searchInput',
|
|
||||||
location: state.map
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索地址
|
|
||||||
* @param queryValue 搜索关键词
|
|
||||||
*/
|
|
||||||
const autoSearch = (queryValue: string) => {
|
|
||||||
if (!queryValue) {
|
|
||||||
state.mapAddrOptions = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state.loading = true
|
|
||||||
|
|
||||||
// 使用百度地图地点检索服务
|
|
||||||
const localSearch = new window.BMapGL.LocalSearch(state.map, {
|
|
||||||
onSearchComplete: (results: any) => {
|
|
||||||
state.loading = false
|
|
||||||
const temp: any[] = []
|
|
||||||
|
|
||||||
if (results && results.getPoi) {
|
|
||||||
const pois = results.getPoi()
|
|
||||||
pois.forEach((p: any) => {
|
|
||||||
const point = p.point
|
|
||||||
if (point && point.lng && point.lat) {
|
|
||||||
temp.push({
|
|
||||||
name: p.title,
|
|
||||||
value: point.lng + ',' + point.lat
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
state.mapAddrOptions = temp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
localSearch.search(queryValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理地址选择
|
|
||||||
* @param value 选中的地址值
|
|
||||||
*/
|
|
||||||
const handleAddressSelect = (value: string) => {
|
|
||||||
if (value) {
|
|
||||||
regeoCode(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加标记点
|
|
||||||
* @param lnglat 经纬度数组
|
|
||||||
*/
|
|
||||||
// TODO @super:拼写;尽量不要有 idea 绿色提醒哈
|
|
||||||
const setMarker = (lnglat: any) => {
|
|
||||||
if (!lnglat) return
|
|
||||||
|
|
||||||
// 如果点标记已存在则先移除原点
|
|
||||||
if (state.mapMarker !== null) {
|
|
||||||
state.map.removeOverlay(state.mapMarker)
|
|
||||||
state.lonLat = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的标记点
|
|
||||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
|
|
||||||
state.mapMarker = new window.BMapGL.Marker(point)
|
|
||||||
|
|
||||||
// 添加点标记到地图
|
|
||||||
state.map.addOverlay(state.mapMarker)
|
|
||||||
state.map.centerAndZoom(point, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 经纬度转化为地址、添加标记点
|
|
||||||
* @param lonLat 经度,纬度字符串
|
|
||||||
*/
|
|
||||||
// TODO @super:拼写;尽量不要有 idea 绿色提醒哈
|
|
||||||
const regeoCode = (lonLat: string) => {
|
|
||||||
if (!lonLat) return
|
|
||||||
|
|
||||||
// TODO @super:拼写;尽量不要有 idea 绿色提醒哈
|
|
||||||
const lnglat = lonLat.split(',')
|
|
||||||
if (lnglat.length !== 2) return
|
|
||||||
|
|
||||||
state.longitude = lnglat[0]
|
|
||||||
state.latitude = lnglat[1]
|
|
||||||
|
|
||||||
// 通知父组件位置变更
|
|
||||||
emits('locateChange', lnglat)
|
|
||||||
emits('update:center', lonLat)
|
|
||||||
|
|
||||||
// 先将地图中心点设置到目标位置
|
|
||||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
|
|
||||||
state.map.centerAndZoom(point, 16)
|
|
||||||
|
|
||||||
// 再设置标记并获取地址
|
|
||||||
setMarker(lnglat)
|
|
||||||
getAddress(lnglat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @super:lnglat 拼写
|
|
||||||
/**
|
|
||||||
* 根据经纬度获取地址信息
|
|
||||||
*
|
|
||||||
* @param lnglat 经纬度数组
|
|
||||||
*/
|
|
||||||
const getAddress = (lnglat: any) => {
|
|
||||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
|
|
||||||
|
|
||||||
state.geocoder.getLocation(point, (result: any) => {
|
|
||||||
if (result && result.address) {
|
|
||||||
state.address = result.address
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 显式暴露方法,使其可以被父组件访问 */
|
|
||||||
defineExpose({ regeoCode })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadMap()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.mapContainer {
|
|
||||||
width: 100%;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
<!-- 地图选择弹窗组件:基于百度地图 GL 实现 -->
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
title="百度地图"
|
||||||
|
v-model="dialogVisible"
|
||||||
|
@opened="handleDialogOpened"
|
||||||
|
@closed="handleDialogClosed"
|
||||||
|
>
|
||||||
|
<div class="w-full">
|
||||||
|
<!-- 第一行:位置搜索 -->
|
||||||
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="定位位置">
|
||||||
|
<el-select
|
||||||
|
class="w-full"
|
||||||
|
v-model="state.address"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
placeholder="可输入地址查询经纬度"
|
||||||
|
:remote-method="autoSearch"
|
||||||
|
@change="handleAddressSelect"
|
||||||
|
:loading="state.loading"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in state.mapAddressOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 第二行:坐标显示 -->
|
||||||
|
<el-form-item label="当前坐标">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span>经度: {{ state.longitude || '-' }}</span>
|
||||||
|
<span>纬度: {{ state.latitude || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<!-- 第三行:地图 -->
|
||||||
|
<div
|
||||||
|
v-if="state.mapContainerReady"
|
||||||
|
ref="mapContainerRef"
|
||||||
|
class="w-full h-[400px] mt-[10px]"
|
||||||
|
></div>
|
||||||
|
<div v-else class="w-full h-[400px] mt-[10px] flex items-center justify-center">
|
||||||
|
<span class="text-gray-400">地图加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleConfirm" type="primary">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, nextTick } from 'vue'
|
||||||
|
import { loadBaiduMapSdk } from './utils'
|
||||||
|
|
||||||
|
const emits = defineEmits(['confirm'])
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const mapContainerRef = ref<HTMLElement>()
|
||||||
|
const state = reactive({
|
||||||
|
lonLat: '', // 经纬度字符串,格式为 "经度,纬度"
|
||||||
|
address: '', // 地址信息
|
||||||
|
loading: false, // 地址搜索加载状态
|
||||||
|
latitude: '', // 纬度
|
||||||
|
longitude: '', // 经度
|
||||||
|
map: null as any, // 百度地图实例
|
||||||
|
mapAddressOptions: [] as any[], // 地址搜索选项
|
||||||
|
mapMarker: null as any, // 地图标记点
|
||||||
|
geocoder: null as any, // 地理编码器实例
|
||||||
|
mapContainerReady: false // 地图容器是否准备好
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始经纬度(打开弹窗时传入)
|
||||||
|
const initLongitude = ref<number | undefined>()
|
||||||
|
const initLatitude = ref<number | undefined>()
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = (longitude?: number, latitude?: number) => {
|
||||||
|
initLongitude.value = longitude
|
||||||
|
initLatitude.value = latitude
|
||||||
|
state.longitude = longitude ? String(longitude) : ''
|
||||||
|
state.latitude = latitude ? String(latitude) : ''
|
||||||
|
state.address = ''
|
||||||
|
state.mapAddressOptions = []
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
/** 弹窗打开动画完成后初始化地图 */
|
||||||
|
const handleDialogOpened = async () => {
|
||||||
|
// 先显示地图容器
|
||||||
|
state.mapContainerReady = true
|
||||||
|
|
||||||
|
// 等待下一个 DOM 更新周期,确保地图容器已渲染
|
||||||
|
await nextTick()
|
||||||
|
// 加载百度地图 SDK
|
||||||
|
await loadBaiduMapSdk()
|
||||||
|
initMapInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 弹窗关闭后清理地图 */
|
||||||
|
const handleDialogClosed = () => {
|
||||||
|
// 销毁地图实例
|
||||||
|
if (state.map) {
|
||||||
|
state.map.destroy?.()
|
||||||
|
state.map = null
|
||||||
|
}
|
||||||
|
state.mapMarker = null
|
||||||
|
state.geocoder = null
|
||||||
|
state.mapContainerReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化地图实例 */
|
||||||
|
const initMapInstance = () => {
|
||||||
|
if (!mapContainerRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化地图和地理编码器
|
||||||
|
initMap()
|
||||||
|
initGeocoder()
|
||||||
|
|
||||||
|
// 监听地图点击事件
|
||||||
|
state.map.addEventListener('click', (e: any) => {
|
||||||
|
const point = e.latlng
|
||||||
|
state.lonLat = point.lng + ',' + point.lat
|
||||||
|
regeoCode(state.lonLat)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果有初始经纬度,加载标记点
|
||||||
|
if (initLongitude.value && initLatitude.value) {
|
||||||
|
const lonLat = `${initLongitude.value},${initLatitude.value}`
|
||||||
|
regeoCode(lonLat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化地图 */
|
||||||
|
const initMap = () => {
|
||||||
|
state.map = new window.BMapGL.Map(mapContainerRef.value)
|
||||||
|
state.map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11)
|
||||||
|
state.map.enableScrollWheelZoom()
|
||||||
|
state.map.disableDoubleClickZoom()
|
||||||
|
|
||||||
|
state.map.addControl(new window.BMapGL.NavigationControl())
|
||||||
|
state.map.addControl(new window.BMapGL.ScaleControl())
|
||||||
|
state.map.addControl(new window.BMapGL.ZoomControl())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化地理编码器 */
|
||||||
|
const initGeocoder = () => {
|
||||||
|
state.geocoder = new window.BMapGL.Geocoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索地址 */
|
||||||
|
const autoSearch = (queryValue: string) => {
|
||||||
|
if (!queryValue) {
|
||||||
|
state.mapAddressOptions = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.loading = true
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
const localSearch = new window.BMapGL.LocalSearch(state.map, {
|
||||||
|
onSearchComplete: (results: any) => {
|
||||||
|
state.loading = false
|
||||||
|
const temp: any[] = []
|
||||||
|
|
||||||
|
if (results && results._pois) {
|
||||||
|
results._pois.forEach((p: any) => {
|
||||||
|
const point = p.point
|
||||||
|
if (point && point.lng && point.lat) {
|
||||||
|
temp.push({
|
||||||
|
name: p.title,
|
||||||
|
value: point.lng + ',' + point.lat
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state.mapAddressOptions = temp
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
localSearch.search(queryValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理地址选择 */
|
||||||
|
const handleAddressSelect = (value: string) => {
|
||||||
|
if (value) {
|
||||||
|
regeoCode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加标记点 */
|
||||||
|
const setMarker = (lnglat: string[]) => {
|
||||||
|
if (!lnglat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.mapMarker !== null) {
|
||||||
|
state.map.removeOverlay(state.mapMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
|
||||||
|
state.mapMarker = new window.BMapGL.Marker(point)
|
||||||
|
|
||||||
|
state.map.addOverlay(state.mapMarker)
|
||||||
|
state.map.centerAndZoom(point, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 经纬度转地址、添加标记点 */
|
||||||
|
const regeoCode = (lonLat: string) => {
|
||||||
|
if (!lonLat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lnglat = lonLat.split(',')
|
||||||
|
if (lnglat.length !== 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.longitude = lnglat[0]
|
||||||
|
state.latitude = lnglat[1]
|
||||||
|
const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
|
||||||
|
state.map.centerAndZoom(point, 16)
|
||||||
|
|
||||||
|
setMarker(lnglat)
|
||||||
|
getAddress(lnglat)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据经纬度获取地址信息 */
|
||||||
|
const getAddress = (lnglat: string[]) => {
|
||||||
|
const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
|
||||||
|
state.geocoder.getLocation(point, (result: any) => {
|
||||||
|
if (result && result.address) {
|
||||||
|
state.address = result.address
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认选择 */
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (state.longitude && state.latitude) {
|
||||||
|
emits('confirm', {
|
||||||
|
longitude: state.longitude,
|
||||||
|
latitude: state.latitude,
|
||||||
|
address: state.address
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* 百度地图 SDK 加载工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 扩展 Window 接口以包含百度地图 GL API
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
BMapGL: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局回调名称
|
||||||
|
const CALLBACK_NAME = '__BAIDU_MAP_LOAD_CALLBACK__'
|
||||||
|
|
||||||
|
// SDK 加载状态
|
||||||
|
let loadPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载百度地图 GL SDK
|
||||||
|
* @param timeout 超时时间(毫秒),默认 10000
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export const loadBaiduMapSdk = (timeout = 10000): Promise<void> => {
|
||||||
|
// 已加载完成
|
||||||
|
if (window.BMapGL) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正在加载中,返回同一个 Promise
|
||||||
|
if (loadPromise) {
|
||||||
|
return loadPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPromise = new Promise((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
loadPromise = null
|
||||||
|
reject(new Error('百度地图 SDK 加载超时'))
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
// 全局回调
|
||||||
|
;(window as any)[CALLBACK_NAME] = () => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
delete (window as any)[CALLBACK_NAME]
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 script 标签
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${
|
||||||
|
import.meta.env.VITE_BAIDU_MAP_KEY
|
||||||
|
}&callback=${CALLBACK_NAME}`
|
||||||
|
script.onerror = () => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
loadPromise = null
|
||||||
|
delete (window as any)[CALLBACK_NAME]
|
||||||
|
reject(new Error('百度地图 SDK 加载失败'))
|
||||||
|
}
|
||||||
|
document.body.appendChild(script)
|
||||||
|
})
|
||||||
|
|
||||||
|
return loadPromise
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
const { copy } = useClipboard() // 初始化 copy 到粘贴板
|
const { copy } = useClipboard({ legacy: true }) // 初始化 copy 到粘贴板
|
||||||
const contentRef = ref()
|
const contentRef = ref()
|
||||||
|
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ const setVisible = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- update by 芋艿:class="-mb-15px" 用于降低和 ContentWrap 组件的底部距离,避免空隙过大 -->
|
<!-- update by 项目组:class="-mb-15px" 用于降低和 ContentWrap 组件的底部距离,避免空隙过大 -->
|
||||||
<Form
|
<Form
|
||||||
:inline="inline"
|
:inline="inline"
|
||||||
:is-col="isCol"
|
:is-col="isCol"
|
||||||
|
|
@ -113,12 +113,12 @@ const setVisible = () => {
|
||||||
>
|
>
|
||||||
<template #action>
|
<template #action>
|
||||||
<div v-if="layout === 'inline'">
|
<div v-if="layout === 'inline'">
|
||||||
<!-- update by 芋艿:去除搜索的 type="primary",颜色变淡一点 -->
|
<!-- update by 项目组:去除搜索的 type="primary",颜色变淡一点 -->
|
||||||
<ElButton v-if="showSearch" @click="search">
|
<ElButton v-if="showSearch" @click="search">
|
||||||
<Icon class="mr-5px" icon="ep:search" />
|
<Icon class="mr-5px" icon="ep:search" />
|
||||||
{{ t('common.query') }}
|
{{ t('common.query') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<!-- update by 芋艿:将 icon="ep:refresh-right" 修改成 icon="ep:refresh",和 ruoyi-vue 搜索保持一致 -->
|
<!-- update by 项目组:将 icon="ep:refresh-right" 修改成 icon="ep:refresh",和 ruoyi-vue 搜索保持一致 -->
|
||||||
<ElButton v-if="showReset" @click="reset">
|
<ElButton v-if="showReset" @click="reset">
|
||||||
<Icon class="mr-5px" icon="ep:refresh" />
|
<Icon class="mr-5px" icon="ep:refresh" />
|
||||||
{{ t('common.reset') }}
|
{{ t('common.reset') }}
|
||||||
|
|
@ -127,7 +127,7 @@ const setVisible = () => {
|
||||||
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
||||||
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<!-- add by 芋艿:补充在搜索后的按钮 -->
|
<!-- add by 项目组:补充在搜索后的按钮 -->
|
||||||
<slot name="actionMore"></slot>
|
<slot name="actionMore"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -150,7 +150,7 @@ const setVisible = () => {
|
||||||
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
||||||
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<!-- add by 芋艿:补充在搜索后的按钮 -->
|
<!-- add by 项目组:补充在搜索后的按钮 -->
|
||||||
<slot name="actionMore"></slot>
|
<slot name="actionMore"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ export function useFormFields() {
|
||||||
return parseFormCreateFields(unref(formFields))
|
return parseFormCreateFields(unref(formFields))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
|
// TODO @项目组:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
|
||||||
/**
|
/**
|
||||||
* @description 获取流程表单的字段和发起人字段
|
* @description 获取流程表单的字段和发起人字段
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,6 @@
|
||||||
transform-origin: 50% 0 0;
|
transform-origin: 50% 0 0;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
||||||
background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
|
background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
|
||||||
// 节点容器 定义节点宽度
|
// 节点容器 定义节点宽度
|
||||||
.node-container {
|
.node-container {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = computed(() => {
|
const pagination = computed(() => {
|
||||||
// update by 芋艿:保持和 Pagination 组件的逻辑一致
|
// update by 项目组:保持和 Pagination 组件的逻辑一致
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
{
|
{
|
||||||
small: false,
|
small: false,
|
||||||
|
|
@ -285,7 +285,7 @@ export default defineComponent({
|
||||||
}}
|
}}
|
||||||
</ElTable>
|
</ElTable>
|
||||||
{unref(getProps).pagination ? (
|
{unref(getProps).pagination ? (
|
||||||
// update by 芋艿:保持和 Pagination 组件一致
|
// update by 项目组:保持和 Pagination 组件一致
|
||||||
<ElPagination
|
<ElPagination
|
||||||
v-model:pageSize={pageSizeRef.value}
|
v-model:pageSize={pageSizeRef.value}
|
||||||
v-model:currentPage={currentPageRef.value}
|
v-model:currentPage={currentPageRef.value}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- 列表选择通用组件,参考 ProductList 组件使用 -->
|
<!-- 列表选择通用组件,参考 ProductList 组件使用 -->
|
||||||
<!-- TODO 芋艿:可能会移除 -->
|
<!-- TODO 项目组:可能会移除 -->
|
||||||
<template>
|
<template>
|
||||||
<Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
|
<Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
|
||||||
<el-table
|
<el-table
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import * as FileApi from '@/api/infra/file'
|
import * as FileApi from '@/api/infra/file'
|
||||||
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
|
import {
|
||||||
import axios from 'axios'
|
UploadRawFile,
|
||||||
|
UploadRequestOptions,
|
||||||
|
UploadProgressEvent
|
||||||
|
} from 'element-plus/es/components/upload/src/upload'
|
||||||
|
import axios, { AxiosProgressEvent } from 'axios'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得上传 URL
|
* 获得上传 URL
|
||||||
|
|
@ -16,6 +20,13 @@ export const useUpload = (directory?: string) => {
|
||||||
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
|
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
|
||||||
// 重写ElUpload上传方法
|
// 重写ElUpload上传方法
|
||||||
const httpRequest = async (options: UploadRequestOptions) => {
|
const httpRequest = async (options: UploadRequestOptions) => {
|
||||||
|
// 文件上传进度监听
|
||||||
|
const uploadProgressHandler = (evt: AxiosProgressEvent) => {
|
||||||
|
const upEvt: UploadProgressEvent = Object.assign(evt.event)
|
||||||
|
upEvt.percent = evt.progress ? evt.progress * 100 : 0
|
||||||
|
options.onProgress(upEvt) // 触发 el-upload 的 on-progress
|
||||||
|
}
|
||||||
|
|
||||||
// 模式一:前端上传
|
// 模式一:前端上传
|
||||||
if (isClientUpload) {
|
if (isClientUpload) {
|
||||||
// 1.1 生成文件名称
|
// 1.1 生成文件名称
|
||||||
|
|
@ -26,8 +37,9 @@ export const useUpload = (directory?: string) => {
|
||||||
return axios
|
return axios
|
||||||
.put(presignedInfo.uploadUrl, options.file, {
|
.put(presignedInfo.uploadUrl, options.file, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': options.file.type
|
'Content-Type': options.file.type || 'application/octet-stream'
|
||||||
}
|
},
|
||||||
|
onUploadProgress: uploadProgressHandler
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// 1.4. 记录文件信息到后端(异步)
|
// 1.4. 记录文件信息到后端(异步)
|
||||||
|
|
@ -39,7 +51,7 @@ export const useUpload = (directory?: string) => {
|
||||||
// 模式二:后端上传
|
// 模式二:后端上传
|
||||||
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
FileApi.updateFile({ file: options.file, directory })
|
FileApi.updateFile({ file: options.file, directory }, uploadProgressHandler)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
resolve(res)
|
resolve(res)
|
||||||
|
|
@ -72,7 +84,7 @@ function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile, fil
|
||||||
url: vo.url,
|
url: vo.url,
|
||||||
path: vo.path,
|
path: vo.path,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
type: file.type,
|
type: file.type || 'application/octet-stream',
|
||||||
size: file.size
|
size: file.size
|
||||||
}
|
}
|
||||||
FileApi.createFile(fileVo)
|
FileApi.createFile(fileVo)
|
||||||
|
|
|
||||||
|
|
@ -642,7 +642,7 @@ const previewProcessJson = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
|
/* ------------------------------------------------ 空山团队 methods ------------------------------------------------------ */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initBpmnModeler()
|
initBpmnModeler()
|
||||||
createNewDiagram(props.value)
|
createNewDiagram(props.value)
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ const clearViewer = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 添加自定义箭头 */
|
/** 添加自定义箭头 */
|
||||||
// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
|
// TODO 项目组:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
|
||||||
const addCustomDefs = () => {
|
const addCustomDefs = () => {
|
||||||
if (!bpmnViewer.value) {
|
if (!bpmnViewer.value) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default {
|
||||||
'Append Gateway': '追加网关',
|
'Append Gateway': '追加网关',
|
||||||
'Append Task': '追加任务',
|
'Append Task': '追加任务',
|
||||||
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
|
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
|
||||||
|
TextAnnotation: '文本注释',
|
||||||
'Activate the global connect tool': '激活全局连接工具',
|
'Activate the global connect tool': '激活全局连接工具',
|
||||||
'Append {type}': '添加 {type}',
|
'Append {type}': '添加 {type}',
|
||||||
'Add Lane above': '在上面添加道',
|
'Add Lane above': '在上面添加道',
|
||||||
|
|
@ -29,10 +29,16 @@ export default {
|
||||||
'Create expanded SubProcess': '创建扩展子过程',
|
'Create expanded SubProcess': '创建扩展子过程',
|
||||||
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
|
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
|
||||||
'Create Pool/Participant': '创建池/参与者',
|
'Create Pool/Participant': '创建池/参与者',
|
||||||
'Parallel Multi Instance': '并行多重事件',
|
'Participant Multiplicity': '参与者多重性',
|
||||||
'Sequential Multi Instance': '时序多重事件',
|
'Empty pool/participant (removes content)': '清空池/参与者(移除内容)',
|
||||||
|
'Empty pool/participant': '收缩池/参与者',
|
||||||
|
'Expanded pool/participant': '展开池/参与者',
|
||||||
|
'Parallel Multi-Instance': '并行多重事件',
|
||||||
|
'Sequential Multi-Instance': '时序多重事件',
|
||||||
DataObjectReference: '数据对象参考',
|
DataObjectReference: '数据对象参考',
|
||||||
DataStoreReference: '数据存储参考',
|
DataStoreReference: '数据存储参考',
|
||||||
|
'Data object reference': '数据对象引用 ',
|
||||||
|
'Data store reference': '数据存储引用 ',
|
||||||
Loop: '循环',
|
Loop: '循环',
|
||||||
'Ad-hoc': '即席',
|
'Ad-hoc': '即席',
|
||||||
'Create {type}': '创建 {type}',
|
'Create {type}': '创建 {type}',
|
||||||
|
|
@ -47,6 +53,9 @@ export default {
|
||||||
'Call Activity': '调用活动',
|
'Call Activity': '调用活动',
|
||||||
'Sub-Process (collapsed)': '子流程(折叠的)',
|
'Sub-Process (collapsed)': '子流程(折叠的)',
|
||||||
'Sub-Process (expanded)': '子流程(展开的)',
|
'Sub-Process (expanded)': '子流程(展开的)',
|
||||||
|
'Ad-hoc sub-process': '即席子流程',
|
||||||
|
'Ad-hoc sub-process (collapsed)': '即席子流程(折叠的)',
|
||||||
|
'Ad-hoc sub-process (expanded)': '即席子流程(展开的)',
|
||||||
'Start Event': '开始事件',
|
'Start Event': '开始事件',
|
||||||
StartEvent: '开始事件',
|
StartEvent: '开始事件',
|
||||||
'Intermediate Throw Event': '中间事件',
|
'Intermediate Throw Event': '中间事件',
|
||||||
|
|
@ -109,10 +118,10 @@ export default {
|
||||||
'Parallel Gateway': '并行网关',
|
'Parallel Gateway': '并行网关',
|
||||||
'Inclusive Gateway': '相容网关',
|
'Inclusive Gateway': '相容网关',
|
||||||
'Complex Gateway': '复杂网关',
|
'Complex Gateway': '复杂网关',
|
||||||
'Event based Gateway': '事件网关',
|
'Event-based Gateway': '事件网关',
|
||||||
Transaction: '转运',
|
Transaction: '转运',
|
||||||
'Sub Process': '子流程',
|
'sub-process': '子流程',
|
||||||
'Event Sub Process': '事件子流程',
|
'Event sub-process': '事件子流程',
|
||||||
'Collapsed Pool': '折叠池',
|
'Collapsed Pool': '折叠池',
|
||||||
'Expanded Pool': '展开池',
|
'Expanded Pool': '展开池',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@
|
||||||
<!-- 新增的时间事件配置项 -->
|
<!-- 新增的时间事件配置项 -->
|
||||||
<el-collapse-item v-if="elementType === 'IntermediateCatchEvent'" name="timeEvent">
|
<el-collapse-item v-if="elementType === 'IntermediateCatchEvent'" name="timeEvent">
|
||||||
<template #title><Icon icon="ep:timer" />时间事件</template>
|
<template #title><Icon icon="ep:timer" />时间事件</template>
|
||||||
<TimeEventConfig :businessObject="bpmnElement.value?.businessObject" :key="elementId" />
|
<!-- 相关 issue:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICNRW2 -->
|
||||||
|
<TimeEventConfig :businessObject="elementBusinessObject" :key="elementId" />
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ const handleNameUpdate = (value) => {
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
// const handleDescriptionUpdate=(value)=> {
|
// const handleDescriptionUpdate=(value)=> {
|
||||||
// TODO 芋艿:documentation 暂时无法修改,后续在看看
|
// TODO 项目组:documentation 暂时无法修改,后续在看看
|
||||||
// this.elementBaseInfo['documentation'] = value;
|
// this.elementBaseInfo['documentation'] = value;
|
||||||
// this.updateBaseInfo('documentation');
|
// this.updateBaseInfo('documentation');
|
||||||
// }
|
// }
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
/></el-button>
|
/></el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-setting-item-label">
|
<div class="button-setting-item-label">
|
||||||
<el-switch v-model="item.enable" />
|
<el-switch v-model="item.enable" @change="updateElementExtensions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,7 +241,13 @@ const assignEmptyUserIds = ref()
|
||||||
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
const buttonsSettingEl = ref()
|
const buttonsSettingEl = ref()
|
||||||
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = useButtonsSetting()
|
const { btnDisplayNameEdit, changeBtnDisplayName } = useButtonsSetting()
|
||||||
|
const btnDisplayNameBlurEvent = (index: number) => {
|
||||||
|
btnDisplayNameEdit.value[index] = false
|
||||||
|
const buttonItem = buttonsSettingEl.value[index]
|
||||||
|
buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
|
||||||
|
updateElementExtensions()
|
||||||
|
}
|
||||||
|
|
||||||
// 字段权限
|
// 字段权限
|
||||||
const fieldsPermissionEl = ref([])
|
const fieldsPermissionEl = ref([])
|
||||||
|
|
@ -495,16 +501,10 @@ function useButtonsSetting() {
|
||||||
const changeBtnDisplayName = (index: number) => {
|
const changeBtnDisplayName = (index: number) => {
|
||||||
btnDisplayNameEdit.value[index] = true
|
btnDisplayNameEdit.value[index] = true
|
||||||
}
|
}
|
||||||
const btnDisplayNameBlurEvent = (index: number) => {
|
|
||||||
btnDisplayNameEdit.value[index] = false
|
|
||||||
const buttonItem = buttonsSetting.value![index]
|
|
||||||
buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
buttonsSetting,
|
buttonsSetting,
|
||||||
btnDisplayNameEdit,
|
btnDisplayNameEdit,
|
||||||
changeBtnDisplayName,
|
changeBtnDisplayName
|
||||||
btnDisplayNameBlurEvent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,6 @@ const editingListenerIndex = ref(-1) // 监听器所在下标,-1 为新增
|
||||||
const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增
|
const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增
|
||||||
const listenerTypeObject = ref(listenerType)
|
const listenerTypeObject = ref(listenerType)
|
||||||
const fieldTypeObject = ref(fieldType)
|
const fieldTypeObject = ref(fieldType)
|
||||||
const bpmnElement = ref()
|
|
||||||
const otherExtensionList = ref()
|
const otherExtensionList = ref()
|
||||||
const bpmnElementListeners = ref()
|
const bpmnElementListeners = ref()
|
||||||
const listenerFormRef = ref()
|
const listenerFormRef = ref()
|
||||||
|
|
@ -290,10 +289,19 @@ const listenerFieldFormRef = ref()
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances
|
const bpmnInstances = () => (window as any)?.bpmnInstances
|
||||||
|
|
||||||
const resetListenersList = () => {
|
const resetListenersList = () => {
|
||||||
bpmnElement.value = bpmnInstances().bpmnElement
|
const instances = bpmnInstances()
|
||||||
otherExtensionList.value = []
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
|
const businessObject = bpmnElement.businessObject
|
||||||
|
|
||||||
|
otherExtensionList.value =
|
||||||
|
businessObject?.extensionElements?.values?.filter(
|
||||||
|
(ex) => ex.$type !== `${prefix}:ExecutionListener`
|
||||||
|
) ?? [] // 保留非监听器类型的扩展属性,避免移除监听器时清空其他配置(如审批人等)。相关案例:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
|
||||||
bpmnElementListeners.value =
|
bpmnElementListeners.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
businessObject?.extensionElements?.values?.filter(
|
||||||
(ex) => ex.$type === `${prefix}:ExecutionListener`
|
(ex) => ex.$type === `${prefix}:ExecutionListener`
|
||||||
) ?? []
|
) ?? []
|
||||||
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
||||||
|
|
@ -375,10 +383,13 @@ const removeListener = (index) => {
|
||||||
cancelButtonText: '取 消'
|
cancelButtonText: '取 消'
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
bpmnElementListeners.value.splice(index, 1)
|
bpmnElementListeners.value.splice(index, 1)
|
||||||
elementListenersList.value.splice(index, 1)
|
elementListenersList.value.splice(index, 1)
|
||||||
updateElementExtensions(
|
updateElementExtensions(
|
||||||
bpmnElement.value,
|
instances.bpmnElement,
|
||||||
otherExtensionList.value.concat(bpmnElementListeners.value)
|
otherExtensionList.value.concat(bpmnElementListeners.value)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -389,7 +400,13 @@ const saveListenerConfig = async () => {
|
||||||
// debugger
|
// debugger
|
||||||
let validateStatus = await listenerFormRef.value.validate()
|
let validateStatus = await listenerFormRef.value.validate()
|
||||||
if (!validateStatus) return // 验证不通过直接返回
|
if (!validateStatus) return // 验证不通过直接返回
|
||||||
|
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
const listenerObject = createListenerObject(listenerForm.value, false, prefix)
|
const listenerObject = createListenerObject(listenerForm.value, false, prefix)
|
||||||
|
|
||||||
if (editingListenerIndex.value === -1) {
|
if (editingListenerIndex.value === -1) {
|
||||||
bpmnElementListeners.value.push(listenerObject)
|
bpmnElementListeners.value.push(listenerObject)
|
||||||
elementListenersList.value.push(listenerForm.value)
|
elementListenersList.value.push(listenerForm.value)
|
||||||
|
|
@ -399,11 +416,11 @@ const saveListenerConfig = async () => {
|
||||||
}
|
}
|
||||||
// 保存其他配置
|
// 保存其他配置
|
||||||
otherExtensionList.value =
|
otherExtensionList.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex) => ex.$type !== `${prefix}:ExecutionListener`
|
(ex) => ex.$type !== `${prefix}:ExecutionListener`
|
||||||
) ?? []
|
) ?? []
|
||||||
updateElementExtensions(
|
updateElementExtensions(
|
||||||
bpmnElement.value,
|
bpmnElement,
|
||||||
otherExtensionList.value.concat(bpmnElementListeners.value)
|
otherExtensionList.value.concat(bpmnElementListeners.value)
|
||||||
)
|
)
|
||||||
// 4. 隐藏侧边栏
|
// 4. 隐藏侧边栏
|
||||||
|
|
@ -417,6 +434,10 @@ const openProcessListenerDialog = async () => {
|
||||||
processListenerDialogRef.value.open('execution')
|
processListenerDialogRef.value.open('execution')
|
||||||
}
|
}
|
||||||
const selectProcessListener = (listener) => {
|
const selectProcessListener = (listener) => {
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
const listenerForm = initListenerForm2(listener)
|
const listenerForm = initListenerForm2(listener)
|
||||||
const listenerObject = createListenerObject(listenerForm, false, prefix)
|
const listenerObject = createListenerObject(listenerForm, false, prefix)
|
||||||
bpmnElementListeners.value.push(listenerObject)
|
bpmnElementListeners.value.push(listenerObject)
|
||||||
|
|
@ -424,11 +445,11 @@ const selectProcessListener = (listener) => {
|
||||||
|
|
||||||
// 保存其他配置
|
// 保存其他配置
|
||||||
otherExtensionList.value =
|
otherExtensionList.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex) => ex.$type !== `${prefix}:ExecutionListener`
|
(ex) => ex.$type !== `${prefix}:ExecutionListener`
|
||||||
) ?? []
|
) ?? []
|
||||||
updateElementExtensions(
|
updateElementExtensions(
|
||||||
bpmnElement.value,
|
bpmnElement,
|
||||||
otherExtensionList.value.concat(bpmnElementListeners.value)
|
otherExtensionList.value.concat(bpmnElementListeners.value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -329,7 +329,6 @@ const listenerFieldFormModelVisible = ref(false) // 监听器 注入字段表单
|
||||||
const editingListenerIndex = ref(-1) // 监听器所在下标,-1 为新增
|
const editingListenerIndex = ref(-1) // 监听器所在下标,-1 为新增
|
||||||
const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增
|
const editingListenerFieldIndex = ref(-1) // 字段所在下标,-1 为新增
|
||||||
const listenerFieldForm = ref<any>({}) // 监听器 注入字段 详情表单
|
const listenerFieldForm = ref<any>({}) // 监听器 注入字段 详情表单
|
||||||
const bpmnElement = ref()
|
|
||||||
const bpmnElementListeners = ref()
|
const bpmnElementListeners = ref()
|
||||||
const otherExtensionList = ref()
|
const otherExtensionList = ref()
|
||||||
const listenerFormRef = ref()
|
const listenerFormRef = ref()
|
||||||
|
|
@ -337,14 +336,21 @@ const listenerFieldFormRef = ref()
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances
|
const bpmnInstances = () => (window as any)?.bpmnInstances
|
||||||
|
|
||||||
const resetListenersList = () => {
|
const resetListenersList = () => {
|
||||||
console.log(
|
const instances = bpmnInstances()
|
||||||
bpmnInstances().bpmnElement,
|
if (!instances || !instances.bpmnElement) return
|
||||||
'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement'
|
|
||||||
)
|
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||||
bpmnElement.value = bpmnInstances().bpmnElement
|
const bpmnElement = instances.bpmnElement
|
||||||
otherExtensionList.value = []
|
const businessObject = bpmnElement.businessObject
|
||||||
|
|
||||||
|
console.log(bpmnElement, 'bpmnElement - resetListenersList')
|
||||||
|
|
||||||
|
otherExtensionList.value =
|
||||||
|
businessObject?.extensionElements?.values?.filter(
|
||||||
|
(ex) => ex.$type !== `${prefix}:TaskListener`
|
||||||
|
) ?? [] // 保留非监听器类型的扩展属性,避免移除监听器时清空其他配置(如审批人等)。相关案例:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
|
||||||
bpmnElementListeners.value =
|
bpmnElementListeners.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values.filter(
|
businessObject?.extensionElements?.values?.filter(
|
||||||
(ex) => ex.$type === `${prefix}:TaskListener`
|
(ex) => ex.$type === `${prefix}:TaskListener`
|
||||||
) ?? []
|
) ?? []
|
||||||
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
||||||
|
|
@ -382,10 +388,13 @@ const removeListener = (listener, index?) => {
|
||||||
cancelButtonText: '取 消'
|
cancelButtonText: '取 消'
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
bpmnElementListeners.value.splice(index, 1)
|
bpmnElementListeners.value.splice(index, 1)
|
||||||
elementListenersList.value.splice(index, 1)
|
elementListenersList.value.splice(index, 1)
|
||||||
updateElementExtensions(
|
updateElementExtensions(
|
||||||
bpmnElement.value,
|
instances.bpmnElement,
|
||||||
otherExtensionList.value.concat(bpmnElementListeners.value)
|
otherExtensionList.value.concat(bpmnElementListeners.value)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -395,7 +404,13 @@ const removeListener = (listener, index?) => {
|
||||||
const saveListenerConfig = async () => {
|
const saveListenerConfig = async () => {
|
||||||
let validateStatus = await listenerFormRef.value.validate()
|
let validateStatus = await listenerFormRef.value.validate()
|
||||||
if (!validateStatus) return // 验证不通过直接返回
|
if (!validateStatus) return // 验证不通过直接返回
|
||||||
|
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
const listenerObject = createListenerObject(listenerForm.value, true, prefix)
|
const listenerObject = createListenerObject(listenerForm.value, true, prefix)
|
||||||
|
|
||||||
if (editingListenerIndex.value === -1) {
|
if (editingListenerIndex.value === -1) {
|
||||||
bpmnElementListeners.value.push(listenerObject)
|
bpmnElementListeners.value.push(listenerObject)
|
||||||
elementListenersList.value.push(listenerForm.value)
|
elementListenersList.value.push(listenerForm.value)
|
||||||
|
|
@ -405,11 +420,11 @@ const saveListenerConfig = async () => {
|
||||||
}
|
}
|
||||||
// 保存其他配置
|
// 保存其他配置
|
||||||
otherExtensionList.value =
|
otherExtensionList.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex) => ex.$type !== `${prefix}:TaskListener`
|
(ex) => ex.$type !== `${prefix}:TaskListener`
|
||||||
) ?? []
|
) ?? []
|
||||||
updateElementExtensions(
|
updateElementExtensions(
|
||||||
bpmnElement.value,
|
bpmnElement,
|
||||||
otherExtensionList.value.concat(bpmnElementListeners.value)
|
otherExtensionList.value.concat(bpmnElementListeners.value)
|
||||||
)
|
)
|
||||||
// 4. 隐藏侧边栏
|
// 4. 隐藏侧边栏
|
||||||
|
|
@ -461,6 +476,10 @@ const openProcessListenerDialog = async () => {
|
||||||
processListenerDialogRef.value.open('task')
|
processListenerDialogRef.value.open('task')
|
||||||
}
|
}
|
||||||
const selectProcessListener = (listener) => {
|
const selectProcessListener = (listener) => {
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
const listenerForm = initListenerForm2(listener)
|
const listenerForm = initListenerForm2(listener)
|
||||||
const listenerObject = createListenerObject(listenerForm, true, prefix)
|
const listenerObject = createListenerObject(listenerForm, true, prefix)
|
||||||
bpmnElementListeners.value.push(listenerObject)
|
bpmnElementListeners.value.push(listenerObject)
|
||||||
|
|
@ -468,11 +487,11 @@ const selectProcessListener = (listener) => {
|
||||||
|
|
||||||
// 保存其他配置
|
// 保存其他配置
|
||||||
otherExtensionList.value =
|
otherExtensionList.value =
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||||
(ex) => ex.$type !== `${prefix}:TaskListener`
|
(ex) => ex.$type !== `${prefix}:TaskListener`
|
||||||
) ?? []
|
) ?? []
|
||||||
updateElementExtensions(
|
updateElementExtensions(
|
||||||
bpmnElement.value,
|
bpmnElement,
|
||||||
otherExtensionList.value.concat(bpmnElementListeners.value)
|
otherExtensionList.value.concat(bpmnElementListeners.value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
<el-form-item label="集合" key="collection" v-show="false">
|
<el-form-item label="集合" key="collection" v-show="false">
|
||||||
<el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
|
<el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
|
<!-- add by 项目组:由于「元素变量」暂时用不到,所以这里 display 为 none -->
|
||||||
<el-form-item label="元素变量" key="elementVariable" style="display: none">
|
<el-form-item label="元素变量" key="elementVariable" style="display: none">
|
||||||
<el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
|
<el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
@change="updateLoopCondition"
|
@change="updateLoopCondition"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
|
<!-- add by 项目组:由于「异步状态」暂时用不到,所以这里 display 为 none -->
|
||||||
<el-form-item label="异步状态" key="async" style="display: none">
|
<el-form-item label="异步状态" key="async" style="display: none">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-model="loopInstanceForm.asyncBefore"
|
v-model="loopInstanceForm.asyncBefore"
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ const elementPropertyList = ref<any[]>([])
|
||||||
const propertyForm = ref<any>({})
|
const propertyForm = ref<any>({})
|
||||||
const editingPropertyIndex = ref(-1)
|
const editingPropertyIndex = ref(-1)
|
||||||
const propertyFormModelVisible = ref(false)
|
const propertyFormModelVisible = ref(false)
|
||||||
const bpmnElement = ref()
|
|
||||||
const otherExtensionList = ref()
|
const otherExtensionList = ref()
|
||||||
const bpmnElementProperties = ref()
|
const bpmnElementProperties = ref()
|
||||||
const bpmnElementPropertyList = ref()
|
const bpmnElementPropertyList = ref()
|
||||||
|
|
@ -75,16 +74,21 @@ const attributeFormRef = ref()
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances
|
const bpmnInstances = () => (window as any)?.bpmnInstances
|
||||||
|
|
||||||
const resetAttributesList = () => {
|
const resetAttributesList = () => {
|
||||||
bpmnElement.value = bpmnInstances().bpmnElement
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
|
const businessObject = bpmnElement.businessObject
|
||||||
|
|
||||||
otherExtensionList.value = [] // 其他扩展配置
|
otherExtensionList.value = [] // 其他扩展配置
|
||||||
bpmnElementProperties.value =
|
bpmnElementProperties.value =
|
||||||
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
|
businessObject?.extensionElements?.values?.filter((ex) => {
|
||||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => {
|
|
||||||
if (ex.$type !== `${prefix}:Properties`) {
|
if (ex.$type !== `${prefix}:Properties`) {
|
||||||
otherExtensionList.value.push(ex)
|
otherExtensionList.value.push(ex)
|
||||||
}
|
}
|
||||||
return ex.$type === `${prefix}:Properties`
|
return ex.$type === `${prefix}:Properties`
|
||||||
}) ?? [];
|
}) ?? []
|
||||||
|
|
||||||
// 保存所有的 扩展属性字段
|
// 保存所有的 扩展属性字段
|
||||||
bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(
|
bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(
|
||||||
|
|
@ -123,10 +127,15 @@ const removeAttributes = (attr, index) => {
|
||||||
const saveAttribute = () => {
|
const saveAttribute = () => {
|
||||||
console.log(propertyForm.value, 'propertyForm.value')
|
console.log(propertyForm.value, 'propertyForm.value')
|
||||||
const { name, value } = propertyForm.value
|
const { name, value } = propertyForm.value
|
||||||
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
|
|
||||||
if (editingPropertyIndex.value !== -1) {
|
if (editingPropertyIndex.value !== -1) {
|
||||||
bpmnInstances().modeling.updateModdleProperties(
|
instances.modeling.updateModdleProperties(
|
||||||
toRaw(bpmnElement.value),
|
bpmnElement,
|
||||||
toRaw(bpmnElementPropertyList.value)[toRaw(editingPropertyIndex.value)],
|
bpmnElementPropertyList.value[editingPropertyIndex.value],
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
value
|
value
|
||||||
|
|
@ -134,12 +143,12 @@ const saveAttribute = () => {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 新建属性字段
|
// 新建属性字段
|
||||||
const newPropertyObject = bpmnInstances().moddle.create(`${prefix}:Property`, {
|
const newPropertyObject = instances.moddle.create(`${prefix}:Property`, {
|
||||||
name,
|
name,
|
||||||
value
|
value
|
||||||
})
|
})
|
||||||
// 新建一个属性字段的保存列表
|
// 新建一个属性字段的保存列表
|
||||||
const propertiesObject = bpmnInstances().moddle.create(`${prefix}:Properties`, {
|
const propertiesObject = instances.moddle.create(`${prefix}:Properties`, {
|
||||||
values: bpmnElementPropertyList.value.concat([newPropertyObject])
|
values: bpmnElementPropertyList.value.concat([newPropertyObject])
|
||||||
})
|
})
|
||||||
updateElementExtensions(propertiesObject)
|
updateElementExtensions(propertiesObject)
|
||||||
|
|
@ -148,10 +157,14 @@ const saveAttribute = () => {
|
||||||
resetAttributesList()
|
resetAttributesList()
|
||||||
}
|
}
|
||||||
const updateElementExtensions = (properties) => {
|
const updateElementExtensions = (properties) => {
|
||||||
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
const instances = bpmnInstances()
|
||||||
|
if (!instances || !instances.bpmnElement) return
|
||||||
|
|
||||||
|
const bpmnElement = instances.bpmnElement
|
||||||
|
const extensions = instances.moddle.create('bpmn:ExtensionElements', {
|
||||||
values: otherExtensionList.value.concat([properties])
|
values: otherExtensionList.value.concat([properties])
|
||||||
})
|
})
|
||||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
instances.modeling.updateProperties(bpmnElement, {
|
||||||
extensionElements: extensions
|
extensionElements: extensions
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,25 @@
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="messageList" border>
|
<el-table :data="messageList" border>
|
||||||
<el-table-column type="index" label="序号" width="60px" />
|
<el-table-column type="index" label="序号" width="60px" />
|
||||||
<el-table-column label="消息ID" prop="id" max-width="300px" show-overflow-tooltip />
|
<el-table-column label="消息ID" prop="id" min-width="120px" show-overflow-tooltip />
|
||||||
<el-table-column label="消息名称" prop="name" max-width="300px" show-overflow-tooltip />
|
<el-table-column label="消息名称" prop="name" min-width="120px" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="110px">
|
||||||
|
<!-- 补充“编辑”、“移除”功能。相关 issue:https://github.com/YunaiV/yudao-cloud/issues/270 -->
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link @click="openEditModel('message', scope.row, scope.$index)" size="small">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
style="color: #ff4d4f"
|
||||||
|
@click="removeObject('message', scope.row)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div
|
<div
|
||||||
class="panel-tab__content--title"
|
class="panel-tab__content--title"
|
||||||
|
|
@ -18,8 +35,24 @@
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="signalList" border>
|
<el-table :data="signalList" border>
|
||||||
<el-table-column type="index" label="序号" width="60px" />
|
<el-table-column type="index" label="序号" width="60px" />
|
||||||
<el-table-column label="信号ID" prop="id" max-width="300px" show-overflow-tooltip />
|
<el-table-column label="信号ID" prop="id" min-width="120px" show-overflow-tooltip />
|
||||||
<el-table-column label="信号名称" prop="name" max-width="300px" show-overflow-tooltip />
|
<el-table-column label="信号名称" prop="name" min-width="120px" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="110px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link @click="openEditModel('signal', scope.row, scope.$index)" size="small">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
style="color: #ff4d4f"
|
||||||
|
@click="removeObject('signal', scope.row)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
|
@ -46,6 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
defineOptions({ name: 'SignalAndMassage' })
|
defineOptions({ name: 'SignalAndMassage' })
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
@ -57,15 +91,33 @@ const modelObjectForm = ref<any>({})
|
||||||
const rootElements = ref()
|
const rootElements = ref()
|
||||||
const messageIdMap = ref()
|
const messageIdMap = ref()
|
||||||
const signalIdMap = ref()
|
const signalIdMap = ref()
|
||||||
|
const editingIndex = ref(-1) // 正在编辑的索引,-1 表示新建
|
||||||
const modelConfig = computed(() => {
|
const modelConfig = computed(() => {
|
||||||
|
const isEdit = editingIndex.value !== -1
|
||||||
if (modelType.value === 'message') {
|
if (modelType.value === 'message') {
|
||||||
return { title: '创建消息', idLabel: '消息ID', nameLabel: '消息名称' }
|
return {
|
||||||
|
title: isEdit ? '编辑消息' : '创建消息',
|
||||||
|
idLabel: '消息ID',
|
||||||
|
nameLabel: '消息名称'
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return { title: '创建信号', idLabel: '信号ID', nameLabel: '信号名称' }
|
return {
|
||||||
|
title: isEdit ? '编辑信号' : '创建信号',
|
||||||
|
idLabel: '信号ID',
|
||||||
|
nameLabel: '信号名称'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances
|
const bpmnInstances = () => (window as any)?.bpmnInstances
|
||||||
|
|
||||||
|
// 生成规范化的ID
|
||||||
|
const generateStandardId = (type: string): string => {
|
||||||
|
const prefix = type === 'message' ? 'Message_' : 'Signal_'
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const random = Math.random().toString(36).substring(2, 6).toUpperCase()
|
||||||
|
return `${prefix}${timestamp}_${random}`
|
||||||
|
}
|
||||||
|
|
||||||
const initDataList = () => {
|
const initDataList = () => {
|
||||||
console.log(window, 'window')
|
console.log(window, 'window')
|
||||||
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements
|
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements
|
||||||
|
|
@ -86,27 +138,126 @@ const initDataList = () => {
|
||||||
}
|
}
|
||||||
const openModel = (type) => {
|
const openModel = (type) => {
|
||||||
modelType.value = type
|
modelType.value = type
|
||||||
modelObjectForm.value = {}
|
editingIndex.value = -1
|
||||||
|
modelObjectForm.value = {
|
||||||
|
id: generateStandardId(type),
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditModel = (type, row, index) => {
|
||||||
|
modelType.value = type
|
||||||
|
editingIndex.value = index
|
||||||
|
modelObjectForm.value = { ...row }
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
const addNewObject = () => {
|
const addNewObject = () => {
|
||||||
if (modelType.value === 'message') {
|
if (modelType.value === 'message') {
|
||||||
if (messageIdMap.value[modelObjectForm.value.id]) {
|
// 编辑模式
|
||||||
message.error('该消息已存在,请修改id后重新保存')
|
if (editingIndex.value !== -1) {
|
||||||
|
const targetMessage = messageList.value[editingIndex.value]
|
||||||
|
// 查找 rootElements 中的原始对象
|
||||||
|
const rootMessage = rootElements.value.find(
|
||||||
|
(el) => el.$type === 'bpmn:Message' && el.id === targetMessage.id
|
||||||
|
)
|
||||||
|
if (rootMessage) {
|
||||||
|
rootMessage.id = modelObjectForm.value.id
|
||||||
|
rootMessage.name = modelObjectForm.value.name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新建模式
|
||||||
|
if (messageIdMap.value[modelObjectForm.value.id]) {
|
||||||
|
message.error('该消息已存在,请修改id后重新保存')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const messageRef = bpmnInstances().moddle.create('bpmn:Message', modelObjectForm.value)
|
||||||
|
rootElements.value.push(messageRef)
|
||||||
}
|
}
|
||||||
const messageRef = bpmnInstances().moddle.create('bpmn:Message', modelObjectForm.value)
|
|
||||||
rootElements.value.push(messageRef)
|
|
||||||
} else {
|
} else {
|
||||||
if (signalIdMap.value[modelObjectForm.value.id]) {
|
// 编辑模式
|
||||||
message.error('该信号已存在,请修改id后重新保存')
|
if (editingIndex.value !== -1) {
|
||||||
|
const targetSignal = signalList.value[editingIndex.value]
|
||||||
|
// 查找 rootElements 中的原始对象
|
||||||
|
const rootSignal = rootElements.value.find(
|
||||||
|
(el) => el.$type === 'bpmn:Signal' && el.id === targetSignal.id
|
||||||
|
)
|
||||||
|
if (rootSignal) {
|
||||||
|
rootSignal.id = modelObjectForm.value.id
|
||||||
|
rootSignal.name = modelObjectForm.value.name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新建模式
|
||||||
|
if (signalIdMap.value[modelObjectForm.value.id]) {
|
||||||
|
message.error('该信号已存在,请修改id后重新保存')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const signalRef = bpmnInstances().moddle.create('bpmn:Signal', modelObjectForm.value)
|
||||||
|
rootElements.value.push(signalRef)
|
||||||
}
|
}
|
||||||
const signalRef = bpmnInstances().moddle.create('bpmn:Signal', modelObjectForm.value)
|
|
||||||
rootElements.value.push(signalRef)
|
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
|
// 触发建模器更新以保存更改
|
||||||
|
saveChanges()
|
||||||
initDataList()
|
initDataList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeObject = (type, row) => {
|
||||||
|
ElMessageBox.confirm(`确认移除该${type === 'message' ? '消息' : '信号'}吗?`, '提示', {
|
||||||
|
confirmButtonText: '确 认',
|
||||||
|
cancelButtonText: '取 消'
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// 从 rootElements 中移除
|
||||||
|
const targetType = type === 'message' ? 'bpmn:Message' : 'bpmn:Signal'
|
||||||
|
const elementIndex = rootElements.value.findIndex(
|
||||||
|
(el) => el.$type === targetType && el.id === row.id
|
||||||
|
)
|
||||||
|
if (elementIndex !== -1) {
|
||||||
|
rootElements.value.splice(elementIndex, 1)
|
||||||
|
}
|
||||||
|
// 触发建模器更新以保存更改
|
||||||
|
saveChanges()
|
||||||
|
// 刷新列表
|
||||||
|
initDataList()
|
||||||
|
message.success('移除成功')
|
||||||
|
})
|
||||||
|
.catch(() => console.info('操作取消'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发建模器更新以保存更改
|
||||||
|
const saveChanges = () => {
|
||||||
|
const modeler = bpmnInstances().modeler
|
||||||
|
if (!modeler) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取 canvas,通过它来触发图表的重新渲染
|
||||||
|
const canvas = modeler.get('canvas')
|
||||||
|
|
||||||
|
// 获取根元素(Process)
|
||||||
|
const rootElement = canvas.getRootElement()
|
||||||
|
|
||||||
|
// 触发 changed 事件,通知建模器数据已更改
|
||||||
|
const eventBus = modeler.get('eventBus')
|
||||||
|
if (eventBus) {
|
||||||
|
eventBus.fire('root.added', { element: rootElement })
|
||||||
|
eventBus.fire('elements.changed', { elements: [rootElement] })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记建模器为已修改状态
|
||||||
|
const commandStack = modeler.get('commandStack')
|
||||||
|
if (commandStack && commandStack._stack) {
|
||||||
|
// 添加一个空命令以标记为已修改
|
||||||
|
commandStack.execute('element.updateProperties', {
|
||||||
|
element: rootElement,
|
||||||
|
properties: {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('保存更改时出错:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initDataList()
|
initDataList()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="panel-tab__content">
|
<div class="panel-tab__content">
|
||||||
<el-form size="small" label-width="90px">
|
<el-form size="small" label-width="90px">
|
||||||
<!-- add by 芋艿:由于「异步延续」暂时用不到,所以这里 display 为 none -->
|
<!-- add by 项目组:由于「异步延续」暂时用不到,所以这里 display 为 none -->
|
||||||
<el-form-item label="异步延续" style="display: none">
|
<el-form-item label="异步延续" style="display: none">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-model="taskConfigForm.asyncBefore"
|
v-model="taskConfigForm.asyncBefore"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="编辑请求头"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="header-editor">
|
||||||
|
<div class="header-list">
|
||||||
|
<div v-for="(item, index) in headerList" :key="index" class="header-item">
|
||||||
|
<el-input v-model="item.key" placeholder="请输入参数名" class="header-key" clearable />
|
||||||
|
<span class="separator">:</span>
|
||||||
|
<el-input
|
||||||
|
v-model="item.value"
|
||||||
|
placeholder="请输入参数值 (支持表达式 ${变量名})"
|
||||||
|
class="header-value"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="removeHeader(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" :icon="Plus" class="add-btn" @click="addHeader">
|
||||||
|
添加请求头
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'HttpHeaderEditor' })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'save'])
|
||||||
|
|
||||||
|
interface HeaderItem {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerList = ref<HeaderItem[]>([])
|
||||||
|
|
||||||
|
// 解析请求头字符串为列表
|
||||||
|
const parseHeaders = (headersStr: string): HeaderItem[] => {
|
||||||
|
if (!headersStr || !headersStr.trim()) {
|
||||||
|
return [{ key: '', value: '' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = headersStr.split('\n').filter((line) => line.trim())
|
||||||
|
const parsed = lines.map((line) => {
|
||||||
|
const colonIndex = line.indexOf(':')
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
return {
|
||||||
|
key: line.substring(0, colonIndex).trim(),
|
||||||
|
value: line.substring(colonIndex + 1).trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { key: line.trim(), value: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
return parsed.length > 0 ? parsed : [{ key: '', value: '' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将列表转换为请求头字符串
|
||||||
|
const stringifyHeaders = (headers: HeaderItem[]): string => {
|
||||||
|
return headers
|
||||||
|
.filter((item) => item.key.trim())
|
||||||
|
.map((item) => `${item.key}: ${item.value}`)
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加请求头
|
||||||
|
const addHeader = () => {
|
||||||
|
headerList.value.push({ key: '', value: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除请求头
|
||||||
|
const removeHeader = (index: number) => {
|
||||||
|
if (headerList.value.length === 1) {
|
||||||
|
// 至少保留一行
|
||||||
|
headerList.value = [{ key: '', value: '' }]
|
||||||
|
} else {
|
||||||
|
headerList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = () => {
|
||||||
|
const headersStr = stringifyHeaders(headerList.value)
|
||||||
|
emit('save', headersStr)
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听对话框打开,初始化数据
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
headerList.value = parseHeaders(props.headers)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-editor {
|
||||||
|
.header-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.header-key {
|
||||||
|
flex: 0 0 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-form-item label="执行类型" key="executeType">
|
<el-form-item label="执行类型" key="executeType">
|
||||||
<el-select v-model="serviceTaskForm.executeType">
|
<el-select v-model="serviceTaskForm.executeType" @change="handleExecuteTypeChange">
|
||||||
<el-option label="Java类" value="class" />
|
<el-option label="Java类" value="class" />
|
||||||
<el-option label="表达式" value="expression" />
|
<el-option label="表达式" value="expression" />
|
||||||
<el-option label="代理表达式" value="delegateExpression" />
|
<el-option label="代理表达式" value="delegateExpression" />
|
||||||
|
<el-option label="HTTP 调用" value="http" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
|
|
@ -29,48 +30,345 @@
|
||||||
prop="delegateExpression"
|
prop="delegateExpression"
|
||||||
key="execute-delegate"
|
key="execute-delegate"
|
||||||
>
|
>
|
||||||
<el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" />
|
<el-input
|
||||||
|
v-model="serviceTaskForm.delegateExpression"
|
||||||
|
clearable
|
||||||
|
@change="updateElementTask"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<template v-if="serviceTaskForm.executeType === 'http'">
|
||||||
|
<el-form-item label="请求方法" key="http-method">
|
||||||
|
<el-radio-group v-model="httpTaskForm.requestMethod">
|
||||||
|
<el-radio-button label="GET" value="GET" />
|
||||||
|
<el-radio-button label="POST" value="POST" />
|
||||||
|
<el-radio-button label="PUT" value="PUT" />
|
||||||
|
<el-radio-button label="DELETE" value="DELETE" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求地址" key="http-url" prop="requestUrl">
|
||||||
|
<el-input v-model="httpTaskForm.requestUrl" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求头" key="http-headers">
|
||||||
|
<div style="display: flex; gap: 8px; align-items: flex-start; width: 100%">
|
||||||
|
<el-input
|
||||||
|
v-model="httpTaskForm.requestHeaders"
|
||||||
|
type="textarea"
|
||||||
|
resize="vertical"
|
||||||
|
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||||
|
readonly
|
||||||
|
placeholder="点击右侧编辑按钮添加请求头"
|
||||||
|
style="flex: 1; min-width: 0"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Edit"
|
||||||
|
@click="showHeaderEditor = true"
|
||||||
|
style="flex-shrink: 0"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="禁止重定向" key="http-disallow-redirects">
|
||||||
|
<el-switch v-model="httpTaskForm.disallowRedirects" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="忽略异常" key="http-ignore-exception">
|
||||||
|
<el-switch v-model="httpTaskForm.ignoreException" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="保存返回变量" key="http-save-response">
|
||||||
|
<el-switch v-model="httpTaskForm.saveResponseParameters" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否瞬间变量" key="http-save-transient">
|
||||||
|
<el-switch v-model="httpTaskForm.saveResponseParametersTransient" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="返回变量前缀" key="http-result-variable-prefix">
|
||||||
|
<el-input v-model="httpTaskForm.resultVariablePrefix" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="格式化返回为JSON" key="http-save-json">
|
||||||
|
<el-switch v-model="httpTaskForm.saveResponseVariableAsJson" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 请求头编辑器 -->
|
||||||
|
<HttpHeaderEditor
|
||||||
|
v-model="showHeaderEditor"
|
||||||
|
:headers="httpTaskForm.requestHeaders"
|
||||||
|
@save="handleHeadersSave"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Edit } from '@element-plus/icons-vue'
|
||||||
|
import { updateElementExtensions } from '@/components/bpmnProcessDesigner/package/utils'
|
||||||
|
import HttpHeaderEditor from './HttpHeaderEditor.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ServiceTask' })
|
defineOptions({ name: 'ServiceTask' })
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: String,
|
id: String,
|
||||||
type: String
|
type: String
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultTaskForm = ref({
|
const prefix = (inject('prefix', 'flowable') || 'flowable') as string
|
||||||
|
const flowableTypeKey = `${prefix}:type`
|
||||||
|
const flowableFieldType = `${prefix}:Field`
|
||||||
|
|
||||||
|
const HTTP_FIELD_NAMES = [
|
||||||
|
'requestMethod',
|
||||||
|
'requestUrl',
|
||||||
|
'requestHeaders',
|
||||||
|
'disallowRedirects',
|
||||||
|
'ignoreException',
|
||||||
|
'saveResponseParameters',
|
||||||
|
'resultVariablePrefix',
|
||||||
|
'saveResponseParametersTransient',
|
||||||
|
'saveResponseVariableAsJson'
|
||||||
|
]
|
||||||
|
const HTTP_BOOLEAN_FIELDS = new Set([
|
||||||
|
'disallowRedirects',
|
||||||
|
'ignoreException',
|
||||||
|
'saveResponseParameters',
|
||||||
|
'saveResponseParametersTransient',
|
||||||
|
'saveResponseVariableAsJson'
|
||||||
|
])
|
||||||
|
|
||||||
|
const DEFAULT_TASK_FORM = {
|
||||||
executeType: '',
|
executeType: '',
|
||||||
class: '',
|
class: '',
|
||||||
expression: '',
|
expression: '',
|
||||||
delegateExpression: ''
|
delegateExpression: ''
|
||||||
})
|
}
|
||||||
|
|
||||||
const serviceTaskForm = ref<any>({})
|
const DEFAULT_HTTP_FORM = {
|
||||||
|
requestMethod: 'GET',
|
||||||
|
requestUrl: '',
|
||||||
|
requestHeaders: 'Content-Type: application/json',
|
||||||
|
resultVariablePrefix: '',
|
||||||
|
disallowRedirects: false,
|
||||||
|
ignoreException: false,
|
||||||
|
saveResponseParameters: false,
|
||||||
|
saveResponseParametersTransient: false,
|
||||||
|
saveResponseVariableAsJson: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM })
|
||||||
|
const httpTaskForm = ref({ ...DEFAULT_HTTP_FORM })
|
||||||
const bpmnElement = ref()
|
const bpmnElement = ref()
|
||||||
|
const httpInitializing = ref(false)
|
||||||
|
const showHeaderEditor = ref(false)
|
||||||
|
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances
|
const bpmnInstances = () => (window as any)?.bpmnInstances
|
||||||
|
|
||||||
const resetTaskForm = () => {
|
// 判断字符串是否包含表达式
|
||||||
for (let key in defaultTaskForm.value) {
|
const isExpression = (value: string): boolean => {
|
||||||
let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
|
if (!value) return false
|
||||||
serviceTaskForm.value[key] = value
|
// 检测 ${...} 或 #{...} 格式的表达式
|
||||||
if (value) {
|
return /\${[^}]+}/.test(value) || /#{[^}]+}/.test(value)
|
||||||
serviceTaskForm.value.executeType = key
|
}
|
||||||
|
|
||||||
|
const collectHttpExtensionInfo = () => {
|
||||||
|
const businessObject = bpmnElement.value?.businessObject
|
||||||
|
const extensionElements = businessObject?.extensionElements
|
||||||
|
const httpFields = new Map<string, string>()
|
||||||
|
const httpFieldTypes = new Map<string, 'string' | 'expression'>()
|
||||||
|
const otherExtensions: any[] = []
|
||||||
|
|
||||||
|
extensionElements?.values?.forEach((item: any) => {
|
||||||
|
if (item?.$type === flowableFieldType && HTTP_FIELD_NAMES.includes(item.name)) {
|
||||||
|
const value = item.string ?? item.stringValue ?? item.expression ?? ''
|
||||||
|
const fieldType = item.expression ? 'expression' : 'string'
|
||||||
|
httpFields.set(item.name, value)
|
||||||
|
httpFieldTypes.set(item.name, fieldType)
|
||||||
|
} else {
|
||||||
|
otherExtensions.push(item)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { httpFields, httpFieldTypes, otherExtensions }
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetHttpDefaults = () => {
|
||||||
|
httpInitializing.value = true
|
||||||
|
httpTaskForm.value = { ...DEFAULT_HTTP_FORM }
|
||||||
|
nextTick(() => {
|
||||||
|
httpInitializing.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetHttpForm = () => {
|
||||||
|
httpInitializing.value = true
|
||||||
|
const { httpFields } = collectHttpExtensionInfo()
|
||||||
|
const nextForm = { ...DEFAULT_HTTP_FORM }
|
||||||
|
|
||||||
|
HTTP_FIELD_NAMES.forEach((name) => {
|
||||||
|
const stored = httpFields.get(name)
|
||||||
|
if (stored !== undefined) {
|
||||||
|
nextForm[name] = HTTP_BOOLEAN_FIELDS.has(name) ? stored === 'true' : stored
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
httpTaskForm.value = nextForm
|
||||||
|
nextTick(() => {
|
||||||
|
httpInitializing.value = false
|
||||||
|
updateHttpExtensions(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetServiceTaskForm = () => {
|
||||||
|
const businessObject = bpmnElement.value?.businessObject
|
||||||
|
const nextForm = { ...DEFAULT_TASK_FORM }
|
||||||
|
|
||||||
|
if (businessObject) {
|
||||||
|
if (businessObject.class) {
|
||||||
|
nextForm.class = businessObject.class
|
||||||
|
nextForm.executeType = 'class'
|
||||||
|
}
|
||||||
|
if (businessObject.expression) {
|
||||||
|
nextForm.expression = businessObject.expression
|
||||||
|
nextForm.executeType = 'expression'
|
||||||
|
}
|
||||||
|
if (businessObject.delegateExpression) {
|
||||||
|
nextForm.delegateExpression = businessObject.delegateExpression
|
||||||
|
nextForm.executeType = 'delegateExpression'
|
||||||
|
}
|
||||||
|
if (businessObject.$attrs?.[flowableTypeKey] === 'http') {
|
||||||
|
nextForm.executeType = 'http'
|
||||||
|
} else {
|
||||||
|
// 兜底:如缺少 flowable:type=http,但扩展里已有 HTTP 的字段,也认为是 HTTP
|
||||||
|
const { httpFields } = collectHttpExtensionInfo()
|
||||||
|
if (httpFields.size > 0) {
|
||||||
|
nextForm.executeType = 'http'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceTaskForm.value = nextForm
|
||||||
|
|
||||||
|
if (nextForm.executeType === 'http') {
|
||||||
|
resetHttpForm()
|
||||||
|
} else {
|
||||||
|
resetHttpDefaults()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateElementTask = () => {
|
const shouldPersistField = (name: string, value: any) => {
|
||||||
let taskAttr = Object.create(null);
|
if (HTTP_BOOLEAN_FIELDS.has(name)) return true
|
||||||
const type = serviceTaskForm.value.executeType;
|
if (name === 'requestMethod') return true
|
||||||
for (let key in serviceTaskForm.value) {
|
if (name === 'requestUrl') return !!value
|
||||||
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
|
return value !== undefined && value !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateHttpExtensions = (force = false) => {
|
||||||
|
if (!bpmnElement.value) return
|
||||||
|
if (!force && (httpInitializing.value || serviceTaskForm.value.executeType !== 'http')) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
taskAttr[type] = serviceTaskForm.value[type] || "";
|
|
||||||
|
const {
|
||||||
|
httpFields: existingFields,
|
||||||
|
httpFieldTypes: existingTypes,
|
||||||
|
otherExtensions
|
||||||
|
} = collectHttpExtensionInfo()
|
||||||
|
|
||||||
|
const desiredEntries: [string, string][] = []
|
||||||
|
HTTP_FIELD_NAMES.forEach((name) => {
|
||||||
|
const rawValue = httpTaskForm.value[name]
|
||||||
|
if (!shouldPersistField(name, rawValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = HTTP_BOOLEAN_FIELDS.has(name)
|
||||||
|
? String(!!rawValue)
|
||||||
|
: rawValue === undefined
|
||||||
|
? ''
|
||||||
|
: String(rawValue)
|
||||||
|
|
||||||
|
desiredEntries.push([name, persisted])
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查是否有变化:不仅比较值,还要比较字段类型(string vs expression)
|
||||||
|
if (!force && desiredEntries.length === existingFields.size) {
|
||||||
|
let noChange = true
|
||||||
|
for (const [name, value] of desiredEntries) {
|
||||||
|
const existingValue = existingFields.get(name)
|
||||||
|
const existingType = existingTypes.get(name)
|
||||||
|
const currentType = isExpression(value) ? 'expression' : 'string'
|
||||||
|
if (existingValue !== value || existingType !== currentType) {
|
||||||
|
noChange = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (noChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moddle = bpmnInstances().moddle
|
||||||
|
const httpFieldElements = desiredEntries.map(([name, value]) => {
|
||||||
|
// 根据值是否包含表达式来决定使用 string 还是 expression 属性
|
||||||
|
const isExpr = isExpression(value)
|
||||||
|
return moddle.create(flowableFieldType, {
|
||||||
|
name,
|
||||||
|
...(isExpr ? { expression: value } : { string: value })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
updateElementExtensions(bpmnElement.value, [...otherExtensions, ...httpFieldElements])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeHttpExtensions = () => {
|
||||||
|
if (!bpmnElement.value) return
|
||||||
|
const { httpFields, otherExtensions } = collectHttpExtensionInfo()
|
||||||
|
if (!httpFields.size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otherExtensions.length) {
|
||||||
|
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||||
|
extensionElements: null
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateElementExtensions(bpmnElement.value, otherExtensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateElementTask = () => {
|
||||||
|
if (!bpmnElement.value) return
|
||||||
|
|
||||||
|
const taskAttr: Record<string, any> = {
|
||||||
|
class: null,
|
||||||
|
expression: null,
|
||||||
|
delegateExpression: null,
|
||||||
|
[flowableTypeKey]: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = serviceTaskForm.value.executeType
|
||||||
|
if (type === 'class' || type === 'expression' || type === 'delegateExpression') {
|
||||||
|
taskAttr[type] = serviceTaskForm.value[type] || null
|
||||||
|
} else if (type === 'http') {
|
||||||
|
taskAttr[flowableTypeKey] = 'http'
|
||||||
|
}
|
||||||
|
|
||||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
|
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
|
||||||
|
|
||||||
|
if (type === 'http') {
|
||||||
|
updateHttpExtensions(true)
|
||||||
|
} else {
|
||||||
|
removeHttpExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExecuteTypeChange = (value: string) => {
|
||||||
|
serviceTaskForm.value.executeType = value
|
||||||
|
if (value === 'http') {
|
||||||
|
resetHttpForm()
|
||||||
|
}
|
||||||
|
updateElementTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHeadersSave = (headersStr: string) => {
|
||||||
|
httpTaskForm.value.requestHeaders = headersStr
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
@ -82,10 +380,17 @@ watch(
|
||||||
() => {
|
() => {
|
||||||
bpmnElement.value = bpmnInstances().bpmnElement
|
bpmnElement.value = bpmnInstances().bpmnElement
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
resetTaskForm()
|
resetServiceTaskForm()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => httpTaskForm.value,
|
||||||
|
() => {
|
||||||
|
updateHttpExtensions()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ const resetTaskForm = () => {
|
||||||
const changeCandidateStrategy = () => {
|
const changeCandidateStrategy = () => {
|
||||||
userTaskForm.value.candidateParam = []
|
userTaskForm.value.candidateParam = []
|
||||||
deptLevel.value = 1
|
deptLevel.value = 1
|
||||||
// 注释 by 芋艿:这个交互很多用户反馈费解,https://t.zsxq.com/xNmas 所以暂时屏蔽
|
// 注释 by 项目组:这个交互很多用户反馈费解,https://t.zsxq.com/xNmas 所以暂时屏蔽
|
||||||
// if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
|
// if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
|
||||||
// // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人
|
// // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人
|
||||||
// if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
|
// if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { toRaw } from 'vue'
|
|
||||||
const bpmnInstances = () => (window as any)?.bpmnInstances
|
const bpmnInstances = () => (window as any)?.bpmnInstances
|
||||||
// 创建监听器实例
|
// 创建监听器实例
|
||||||
export function createListenerObject(options, isTask, prefix) {
|
export function createListenerObject(options, isTask, prefix) {
|
||||||
|
|
@ -61,7 +60,8 @@ export function updateElementExtensions(element, extensionList) {
|
||||||
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
||||||
values: extensionList
|
values: extensionList
|
||||||
})
|
})
|
||||||
bpmnInstances().modeling.updateProperties(toRaw(element), {
|
// 直接使用原始元素对象,不需要toRaw包装
|
||||||
|
bpmnInstances().modeling.updateProperties(element, {
|
||||||
extensionElements: extensions
|
extensionElements: extensions
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ service.interceptors.request.use(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 是否 API 加密
|
// 是否 API 加密
|
||||||
if ((config!.headers || {}).isEncrypt) {
|
if ((config!.headers || {}).isEncrypt && !(config!.headers || {}).isEncrypted) {
|
||||||
try {
|
try {
|
||||||
// 加密请求数据
|
// 加密请求数据
|
||||||
if (config.data) {
|
if (config.data) {
|
||||||
|
|
@ -169,6 +169,9 @@ service.interceptors.response.use(
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
requestList = []
|
requestList = []
|
||||||
|
if ((config!.headers || {}).isEncrypt){
|
||||||
|
(config!.headers || {}).isEncrypted = true
|
||||||
|
}
|
||||||
return service(config)
|
return service(config)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
|
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
|
||||||
const tableColumns = treeMap<CrudSchema>(crudSchema, {
|
const tableColumns = treeMap<CrudSchema>(crudSchema, {
|
||||||
conversion: (schema: CrudSchema) => {
|
conversion: (schema: CrudSchema) => {
|
||||||
if (schema?.isTable !== false && schema?.table?.show !== false) {
|
if (schema?.isTable !== false && schema?.table?.show !== false) {
|
||||||
// add by 芋艿:增加对 dict 字典数据的支持
|
// add by 项目组:增加对 dict 字典数据的支持
|
||||||
if (!schema.formatter && schema.dictType) {
|
if (!schema.formatter && schema.dictType) {
|
||||||
schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => {
|
schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => {
|
||||||
return h(DictTag, {
|
return h(DictTag, {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ export const useTable = <T = any>(config?: UseTableConfig<T>) => {
|
||||||
elTableRef,
|
elTableRef,
|
||||||
tableObject,
|
tableObject,
|
||||||
methods,
|
methods,
|
||||||
// add by 芋艿:返回 tableMethods 属性,和 tableObject 更统一
|
// add by 项目组:返回 tableMethods 属性,和 tableObject 更统一
|
||||||
tableMethods: methods
|
tableMethods: methods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ watch(
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
|
class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
|
||||||
src="@/assets/imgs/logo.png"
|
src="/brand-logo.png"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ watch(
|
||||||
// 拷贝
|
// 拷贝
|
||||||
const copyConfig = async () => {
|
const copyConfig = async () => {
|
||||||
const { copy, copied, isSupported } = useClipboard({
|
const { copy, copied, isSupported } = useClipboard({
|
||||||
|
legacy: true,
|
||||||
source: `
|
source: `
|
||||||
// 面包屑
|
// 面包屑
|
||||||
breadcrumb: ${appStore.getBreadcrumb},
|
breadcrumb: ${appStore.getBreadcrumb},
|
||||||
|
|
@ -296,7 +297,7 @@ const clear = () => {
|
||||||
$prefix-cls: #{$namespace}-setting;
|
$prefix-cls: #{$namespace}-setting;
|
||||||
|
|
||||||
.#{$prefix-cls} {
|
.#{$prefix-cls} {
|
||||||
|
z-index: 1200; /* 修正没有z-index会被表格层覆盖,值不要超过4000 */
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 6px 0 0 6px;
|
||||||
z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,15 @@ const canShowIcon = (item: RouteLocationNormalizedLoaded) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeTabOnMouseMidClick = (e: MouseEvent, item) => {
|
||||||
|
// 中键:button === 1
|
||||||
|
if (e.button === 1) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
closeSelectedTag(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
initTags()
|
initTags()
|
||||||
addTags()
|
addTags()
|
||||||
|
|
@ -293,6 +302,7 @@ watch(
|
||||||
v-for="item in visitedViews"
|
v-for="item in visitedViews"
|
||||||
:key="item.fullPath"
|
:key="item.fullPath"
|
||||||
:ref="itemRefs.set"
|
:ref="itemRefs.set"
|
||||||
|
@auxclick="closeTabOnMouseMidClick($event, item)"
|
||||||
:class="[
|
:class="[
|
||||||
`${prefixCls}__item`,
|
`${prefixCls}__item`,
|
||||||
tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
|
tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const toProfile = async () => {
|
||||||
push('/user/profile')
|
push('/user/profile')
|
||||||
}
|
}
|
||||||
const toDocument = () => {
|
const toDocument = () => {
|
||||||
window.open('https://doc.iocoder.cn/')
|
window.open('http://m.swelife.cn/#/pages/index')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
required: 'This is required',
|
required: 'This is required',
|
||||||
loginOut: 'Login out',
|
loginOut: 'Login out',
|
||||||
document: 'Document',
|
document: 'Project Portal',
|
||||||
profile: 'User Center',
|
profile: 'User Center',
|
||||||
reminder: 'Reminder',
|
reminder: 'Reminder',
|
||||||
loginOutMessage: 'Exit the system?',
|
loginOutMessage: 'Exit the system?',
|
||||||
|
|
@ -113,8 +113,8 @@ export default {
|
||||||
small: 'Small'
|
small: 'Small'
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
welcome: 'Welcome to the system',
|
welcome: 'Welcome to Kongshan AI + E-commerce Platform',
|
||||||
message: 'Backstage management system',
|
message: 'An operations console focused on AI-enabled commerce',
|
||||||
tenantname: 'TenantName',
|
tenantname: 'TenantName',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
|
|
@ -163,10 +163,10 @@ export default {
|
||||||
analysis: {
|
analysis: {
|
||||||
newUser: 'New user',
|
newUser: 'New user',
|
||||||
unreadInformation: 'Unread information',
|
unreadInformation: 'Unread information',
|
||||||
transactionAmount: 'Transaction amount',
|
transactionAmount: 'GMV',
|
||||||
totalShopping: 'Total Shopping',
|
totalShopping: 'Paid Orders',
|
||||||
monthlySales: 'Monthly sales',
|
monthlySales: 'Monthly Sales Trend',
|
||||||
userAccessSource: 'User access source',
|
userAccessSource: 'Traffic Sources',
|
||||||
january: 'January',
|
january: 'January',
|
||||||
february: 'February',
|
february: 'February',
|
||||||
march: 'March',
|
march: 'March',
|
||||||
|
|
@ -179,15 +179,15 @@ export default {
|
||||||
october: 'October',
|
october: 'October',
|
||||||
november: 'November',
|
november: 'November',
|
||||||
december: 'December',
|
december: 'December',
|
||||||
estimate: 'Estimate',
|
estimate: 'Target',
|
||||||
actual: 'Actual',
|
actual: 'Actual',
|
||||||
directAccess: 'Airect access',
|
directAccess: 'Organic',
|
||||||
mailMarketing: 'Mail marketing',
|
mailMarketing: 'Private Domain',
|
||||||
allianceAdvertising: 'Alliance advertising',
|
allianceAdvertising: 'Paid Channels',
|
||||||
videoAdvertising: 'Video advertising',
|
videoAdvertising: 'Short Video',
|
||||||
searchEngines: 'Search engines',
|
searchEngines: 'Search',
|
||||||
weeklyUserActivity: 'Weekly user activity',
|
weeklyUserActivity: 'Weekly Ops Activity',
|
||||||
activeQuantity: 'Active quantity',
|
activeQuantity: 'Activity Index',
|
||||||
monday: 'Monday',
|
monday: 'Monday',
|
||||||
tuesday: 'Tuesday',
|
tuesday: 'Tuesday',
|
||||||
wednesday: 'Wednesday',
|
wednesday: 'Wednesday',
|
||||||
|
|
@ -199,13 +199,13 @@ export default {
|
||||||
workplace: {
|
workplace: {
|
||||||
welcome: 'Hello',
|
welcome: 'Hello',
|
||||||
happyDay: 'Wish you happy every day!',
|
happyDay: 'Wish you happy every day!',
|
||||||
toady: `It's sunny today`,
|
toady: 'Today Focus',
|
||||||
notice: 'Announcement',
|
notice: 'Operations Bulletin',
|
||||||
project: 'Project',
|
project: 'Business Modules',
|
||||||
access: 'Project access',
|
access: 'Site Visits',
|
||||||
toDo: 'To do',
|
toDo: 'Pending Tasks',
|
||||||
introduction: 'A serious introduction',
|
introduction: 'A serious introduction',
|
||||||
shortcutOperation: 'Quick entry',
|
shortcutOperation: 'Operations Shortcuts',
|
||||||
operation: 'Operation',
|
operation: 'Operation',
|
||||||
index: 'Index',
|
index: 'Index',
|
||||||
personal: 'Personal',
|
personal: 'Personal',
|
||||||
|
|
@ -376,8 +376,8 @@ export default {
|
||||||
signUpFormTitle: 'Sign up',
|
signUpFormTitle: 'Sign up',
|
||||||
forgetFormTitle: 'Reset password',
|
forgetFormTitle: 'Reset password',
|
||||||
|
|
||||||
signInTitle: 'Backstage management system',
|
signInTitle: 'Kongshan AI + E-commerce Admin Console',
|
||||||
signInDesc: 'Enter your personal details and get started!',
|
signInDesc: 'Sign in with your account to start managing the platform',
|
||||||
policy: 'I agree to the xxx Privacy Policy',
|
policy: 'I agree to the xxx Privacy Policy',
|
||||||
scanSign: `scanning the code to complete the login`,
|
scanSign: `scanning the code to complete the login`,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
required: '该项为必填项',
|
required: '该项为必填项',
|
||||||
loginOut: '退出系统',
|
loginOut: '退出系统',
|
||||||
document: '项目文档',
|
document: '项目入口',
|
||||||
profile: '个人中心',
|
profile: '个人中心',
|
||||||
reminder: '温馨提示',
|
reminder: '温馨提示',
|
||||||
loginOutMessage: '是否退出本系统?',
|
loginOutMessage: '是否退出本系统?',
|
||||||
|
|
@ -114,8 +114,8 @@ export default {
|
||||||
small: '小'
|
small: '小'
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
welcome: '欢迎使用本系统',
|
welcome: '欢迎使用空山 AI+电商平台',
|
||||||
message: '开箱即用的中后台管理系统',
|
message: '聚焦 AI 赋能实业的电商中后台管理系统',
|
||||||
tenantname: '租户名称',
|
tenantname: '租户名称',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
|
|
@ -165,10 +165,10 @@ export default {
|
||||||
analysis: {
|
analysis: {
|
||||||
newUser: '新增用户',
|
newUser: '新增用户',
|
||||||
unreadInformation: '未读消息',
|
unreadInformation: '未读消息',
|
||||||
transactionAmount: '成交金额',
|
transactionAmount: 'GMV 成交额',
|
||||||
totalShopping: '购物总量',
|
totalShopping: '支付订单量',
|
||||||
monthlySales: '每月销售额',
|
monthlySales: '月度销售趋势',
|
||||||
userAccessSource: '用户访问来源',
|
userAccessSource: '流量渠道来源',
|
||||||
january: '一月',
|
january: '一月',
|
||||||
february: '二月',
|
february: '二月',
|
||||||
march: '三月',
|
march: '三月',
|
||||||
|
|
@ -181,15 +181,15 @@ export default {
|
||||||
october: '十月',
|
october: '十月',
|
||||||
november: '十一月',
|
november: '十一月',
|
||||||
december: '十二月',
|
december: '十二月',
|
||||||
estimate: '预计',
|
estimate: '目标',
|
||||||
actual: '实际',
|
actual: '实际',
|
||||||
directAccess: '直接访问',
|
directAccess: '自然访问',
|
||||||
mailMarketing: '邮件营销',
|
mailMarketing: '私域触达',
|
||||||
allianceAdvertising: '联盟广告',
|
allianceAdvertising: '渠道投放',
|
||||||
videoAdvertising: '视频广告',
|
videoAdvertising: '短视频内容',
|
||||||
searchEngines: '搜索引擎',
|
searchEngines: '搜索流量',
|
||||||
weeklyUserActivity: '每周用户活跃量',
|
weeklyUserActivity: '每周经营活跃度',
|
||||||
activeQuantity: '活跃量',
|
activeQuantity: '活跃指标',
|
||||||
monday: '周一',
|
monday: '周一',
|
||||||
tuesday: '周二',
|
tuesday: '周二',
|
||||||
wednesday: '周三',
|
wednesday: '周三',
|
||||||
|
|
@ -201,13 +201,13 @@ export default {
|
||||||
workplace: {
|
workplace: {
|
||||||
welcome: '你好',
|
welcome: '你好',
|
||||||
happyDay: '祝你开心每一天!',
|
happyDay: '祝你开心每一天!',
|
||||||
toady: '今日晴',
|
toady: '今日运营重点',
|
||||||
notice: '通知公告',
|
notice: '运营公告',
|
||||||
project: '项目数',
|
project: '业务模块',
|
||||||
access: '项目访问',
|
access: '全站访问',
|
||||||
toDo: '待办',
|
toDo: '待处理',
|
||||||
introduction: '一个正经的简介',
|
introduction: '一个正经的简介',
|
||||||
shortcutOperation: '快捷入口',
|
shortcutOperation: '运营快捷入口',
|
||||||
operation: '操作',
|
operation: '操作',
|
||||||
index: '指数',
|
index: '指数',
|
||||||
personal: '个人',
|
personal: '个人',
|
||||||
|
|
@ -374,8 +374,8 @@ export default {
|
||||||
qrSignInFormTitle: '二维码登录',
|
qrSignInFormTitle: '二维码登录',
|
||||||
signUpFormTitle: '注册',
|
signUpFormTitle: '注册',
|
||||||
forgetFormTitle: '重置密码',
|
forgetFormTitle: '重置密码',
|
||||||
signInTitle: '开箱即用的中后台管理系统',
|
signInTitle: '空山 AI+电商平台管理端',
|
||||||
signInDesc: '输入您的个人详细信息开始使用!',
|
signInDesc: '输入账号信息后开始使用平台能力',
|
||||||
policy: '我同意xxx隐私政策',
|
policy: '我同意xxx隐私政策',
|
||||||
scanSign: `扫码后点击"确认",即可完成登录`,
|
scanSign: `扫码后点击"确认",即可完成登录`,
|
||||||
loginButton: '登录',
|
loginButton: '登录',
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,13 @@ router.beforeEach(async (to, from, next) => {
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
next({ path: '/' })
|
next({ path: '/' })
|
||||||
} else {
|
} else {
|
||||||
// 获取所有字典
|
|
||||||
const dictStore = useDictStoreWithOut()
|
const dictStore = useDictStoreWithOut()
|
||||||
const userStore = useUserStoreWithOut()
|
const userStore = useUserStoreWithOut()
|
||||||
const permissionStore = usePermissionStoreWithOut()
|
const permissionStore = usePermissionStoreWithOut()
|
||||||
|
// 异步加载字典
|
||||||
|
// 另外,间接 issue:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ID9FLI
|
||||||
if (!dictStore.getIsSetDict) {
|
if (!dictStore.getIsSetDict) {
|
||||||
await dictStore.setDictMap()
|
dictStore.setDictMap().then()
|
||||||
}
|
}
|
||||||
if (!userStore.getIsSetUser) {
|
if (!userStore.getIsSetUser) {
|
||||||
isRelogin.show = true
|
isRelogin.show = true
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'
|
||||||
import { useApiSelect } from '@/components/FormCreate'
|
import { useApiSelect } from '@/components/FormCreate'
|
||||||
import { Editor } from '@/components/Editor'
|
import { Editor } from '@/components/Editor'
|
||||||
import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue'
|
import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue'
|
||||||
|
import DeptSelect from '@/components/FormCreate/src/components/DeptSelect.vue'
|
||||||
|
import IframeComponent from '@/components/FormCreate/src/components/IframeComponent.vue'
|
||||||
|
import AreaSelect from '@/components/FormCreate/src/components/AreaSelect.vue'
|
||||||
|
|
||||||
const UserSelect = useApiSelect({
|
const UserSelect = useApiSelect({
|
||||||
name: 'UserSelect',
|
name: 'UserSelect',
|
||||||
|
|
@ -75,12 +78,6 @@ const UserSelect = useApiSelect({
|
||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
url: '/system/user/simple-list'
|
url: '/system/user/simple-list'
|
||||||
})
|
})
|
||||||
const DeptSelect = useApiSelect({
|
|
||||||
name: 'DeptSelect',
|
|
||||||
labelField: 'name',
|
|
||||||
valueField: 'id',
|
|
||||||
url: '/system/dept/simple-list'
|
|
||||||
})
|
|
||||||
const ApiSelect = useApiSelect({
|
const ApiSelect = useApiSelect({
|
||||||
name: 'ApiSelect'
|
name: 'ApiSelect'
|
||||||
})
|
})
|
||||||
|
|
@ -119,6 +116,8 @@ const components = [
|
||||||
DeptSelect,
|
DeptSelect,
|
||||||
ApiSelect,
|
ApiSelect,
|
||||||
Editor,
|
Editor,
|
||||||
|
IframeComponent,
|
||||||
|
AreaSelect,
|
||||||
ElCollapse,
|
ElCollapse,
|
||||||
ElCollapseItem,
|
ElCollapseItem,
|
||||||
ElCard
|
ElCard
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@ const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||||
strict: true,
|
strict: true,
|
||||||
routes: remainingRouter as RouteRecordRaw[],
|
routes: remainingRouter as RouteRecordRaw[],
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
scrollBehavior: () => {
|
||||||
|
// 新开标签时、返回标签时,滚动条回到顶部,否则会保留上次标签的滚动位置。
|
||||||
|
const scrollbarWrap = document.querySelector('.v-layout-content-scrollbar .el-scrollbar__wrap')
|
||||||
|
if (scrollbarWrap) {
|
||||||
|
// scrollbarWrap.scrollTo({ left: 0, top: 0, behavior: 'auto' })
|
||||||
|
scrollbarWrap.scrollTop = 0
|
||||||
|
}
|
||||||
|
return { left: 0, top: 0 }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const resetRouter = (): void => {
|
export const resetRouter = (): void => {
|
||||||
|
|
|
||||||
|
|
@ -73,13 +73,13 @@ export const useAppStore = defineStore('app', {
|
||||||
currentSize: wsCache.get('default') || 'default', // 组件尺寸
|
currentSize: wsCache.get('default') || 'default', // 组件尺寸
|
||||||
theme: wsCache.get(CACHE_KEY.THEME) || {
|
theme: wsCache.get(CACHE_KEY.THEME) || {
|
||||||
// 主题色
|
// 主题色
|
||||||
elColorPrimary: '#409eff',
|
elColorPrimary: '#1e88e5',
|
||||||
// 左侧菜单边框颜色
|
// 左侧菜单边框颜色
|
||||||
leftMenuBorderColor: 'inherit',
|
leftMenuBorderColor: 'inherit',
|
||||||
// 左侧菜单背景颜色
|
// 左侧菜单背景颜色
|
||||||
leftMenuBgColor: '#001529',
|
leftMenuBgColor: '#0f4c9a',
|
||||||
// 左侧菜单浅色背景颜色
|
// 左侧菜单浅色背景颜色
|
||||||
leftMenuBgLightColor: '#0f2438',
|
leftMenuBgLightColor: '#1763c3',
|
||||||
// 左侧菜单选中背景颜色
|
// 左侧菜单选中背景颜色
|
||||||
leftMenuBgActiveColor: 'var(--el-color-primary)',
|
leftMenuBgActiveColor: 'var(--el-color-primary)',
|
||||||
// 左侧菜单收起选中背景颜色
|
// 左侧菜单收起选中背景颜色
|
||||||
|
|
@ -93,11 +93,11 @@ export const useAppStore = defineStore('app', {
|
||||||
// logo边框颜色
|
// logo边框颜色
|
||||||
logoBorderColor: 'inherit',
|
logoBorderColor: 'inherit',
|
||||||
// 头部背景颜色
|
// 头部背景颜色
|
||||||
topHeaderBgColor: '#fff',
|
topHeaderBgColor: '#f5f9ff',
|
||||||
// 头部字体颜色
|
// 头部字体颜色
|
||||||
topHeaderTextColor: 'inherit',
|
topHeaderTextColor: 'inherit',
|
||||||
// 头部悬停颜色
|
// 头部悬停颜色
|
||||||
topHeaderHoverColor: '#f6f6f6',
|
topHeaderHoverColor: '#eaf2ff',
|
||||||
// 头部边框颜色
|
// 头部边框颜色
|
||||||
topToolBorderColor: '#eee'
|
topToolBorderColor: '#eee'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ export const useDictStore = defineStore('dict', {
|
||||||
this.isSetDict = true
|
this.isSetDict = true
|
||||||
} else {
|
} else {
|
||||||
const res = await getSimpleDictDataList()
|
const res = await getSimpleDictDataList()
|
||||||
|
if (!res || res.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// 设置数据
|
// 设置数据
|
||||||
const dictDataMap = new Map<string, any>()
|
const dictDataMap = new Map<string, any>()
|
||||||
res.forEach((dictData: DictDataVO) => {
|
res.forEach((dictData: DictDataVO) => {
|
||||||
|
|
@ -76,6 +79,9 @@ export const useDictStore = defineStore('dict', {
|
||||||
async resetDict() {
|
async resetDict() {
|
||||||
wsCache.delete(CACHE_KEY.DICT_CACHE)
|
wsCache.delete(CACHE_KEY.DICT_CACHE)
|
||||||
const res = await getSimpleDictDataList()
|
const res = await getSimpleDictDataList()
|
||||||
|
if (!res || res.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// 设置数据
|
// 设置数据
|
||||||
const dictDataMap = new Map<string, any>()
|
const dictDataMap = new Map<string, any>()
|
||||||
res.forEach((dictData: DictDataVO) => {
|
res.forEach((dictData: DictDataVO) => {
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,12 @@ export const useTagsViewStore = defineStore('tagsView', {
|
||||||
delCachedView() {
|
delCachedView() {
|
||||||
const route = router.currentRoute.value
|
const route = router.currentRoute.value
|
||||||
const index = findIndex<string>(this.getCachedViews, (v) => v === route.name)
|
const index = findIndex<string>(this.getCachedViews, (v) => v === route.name)
|
||||||
|
// 需要注释,解决“标签页刷新无效”。相关案例:https://github.com/yudaocode/yudao-ui-admin-vue3/issues/180
|
||||||
|
// for (const v of this.visitedViews) {
|
||||||
|
// if (v.name === route.name) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.cachedViews.delete(this.getCachedViews[index])
|
this.cachedViews.delete(this.getCachedViews[index])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Created by 芋道源码
|
* Created by 空山团队
|
||||||
*
|
*
|
||||||
* 枚举类
|
* 枚举类
|
||||||
*/
|
*/
|
||||||
|
|
@ -463,3 +463,13 @@ export const BpmAutoApproveType = {
|
||||||
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
|
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
|
||||||
APPROVE_SEQUENT: 2 // 仅针对连续审批的节点自动通过
|
APPROVE_SEQUENT: 2 // 仅针对连续审批的节点自动通过
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== SYSTEM - 地区模块 ==========
|
||||||
|
/**
|
||||||
|
* 地区选择器层级枚举
|
||||||
|
*/
|
||||||
|
export const AreaLevelEnum = {
|
||||||
|
PROVINCE: 1, // 省
|
||||||
|
CITY: 2, // 市
|
||||||
|
DISTRICT: 3 // 区
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Independent time operation tool to facilitate subsequent switch to dayjs
|
* Independent time operation tool to facilitate subsequent switch to dayjs
|
||||||
*/
|
*/
|
||||||
// TODO 芋艿:【锁屏】可能后面删除掉
|
// TODO 项目组:【锁屏】可能后面删除掉
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
|
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
|
|
||||||
|
|
@ -233,13 +233,14 @@ export enum DICT_TYPE {
|
||||||
IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
|
IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
|
||||||
IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态
|
IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态
|
||||||
IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
|
IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
|
||||||
IOT_CODEC_TYPE = 'iot_codec_type', // IOT 数据格式(编解码器类型)
|
IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 协议类型
|
||||||
|
IOT_SERIALIZE_TYPE = 'iot_serialize_type', // IOT 序列化类型
|
||||||
IOT_LOCATION_TYPE = 'iot_location_type', // IOT 定位类型
|
IOT_LOCATION_TYPE = 'iot_location_type', // IOT 定位类型
|
||||||
IOT_DEVICE_STATE = 'iot_device_state', // IOT 设备状态
|
IOT_DEVICE_STATE = 'iot_device_state', // IOT 设备状态
|
||||||
IOT_THING_MODEL_TYPE = 'iot_thing_model_type', // IOT 产品功能类型
|
IOT_THING_MODEL_TYPE = 'iot_thing_model_type', // IOT 产品功能类型
|
||||||
IOT_THING_MODEL_UNIT = 'iot_thing_model_unit', // IOT 物模型单位
|
IOT_THING_MODEL_UNIT = 'iot_thing_model_unit', // IOT 物模型单位
|
||||||
IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型
|
IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型
|
||||||
// TODO @芋艿:貌似这几个多了 _enum 后缀
|
// TODO @项目组:貌似这几个多了 _enum 后缀
|
||||||
IOT_DATA_SINK_TYPE_ENUM = 'iot_data_sink_type_enum', // IoT 数据流转目的类型
|
IOT_DATA_SINK_TYPE_ENUM = 'iot_data_sink_type_enum', // IoT 数据流转目的类型
|
||||||
IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举
|
IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举
|
||||||
IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum', // IoT 规则场景的触发类型枚举
|
IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum', // IoT 规则场景的触发类型枚举
|
||||||
|
|
@ -247,5 +248,7 @@ export enum DICT_TYPE {
|
||||||
IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type', // IoT 告警接收类型
|
IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type', // IoT 告警接收类型
|
||||||
IOT_OTA_TASK_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA任务设备范围
|
IOT_OTA_TASK_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA任务设备范围
|
||||||
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
|
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
|
||||||
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status' // IoT OTA 记录状态
|
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status', // IoT OTA 记录状态
|
||||||
|
IOT_MODBUS_MODE = 'iot_modbus_mode', // IoT Modbus 工作模式
|
||||||
|
IOT_MODBUS_FRAME_FORMAT = 'iot_modbus_frame_format' // IoT Modbus 帧格式
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,30 @@
|
||||||
import { isRef } from 'vue'
|
import { isRef } from 'vue'
|
||||||
import formCreate from '@form-create/element-ui'
|
import formCreate from '@form-create/element-ui'
|
||||||
|
|
||||||
// 编码表单 Conf
|
/** 编码表单 Conf */
|
||||||
export const encodeConf = (designerRef: object) => {
|
export const encodeConf = (designerRef: object) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return JSON.stringify(designerRef.value.getOption())
|
// 关联案例:https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/834/
|
||||||
|
return formCreate.toJson(designerRef.value.getOption())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编码表单 Fields
|
/** 解码表单 Conf */
|
||||||
|
export const decodeConf = (conf: string) => {
|
||||||
|
return formCreate.parseJson(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编码表单 Fields */
|
||||||
export const encodeFields = (designerRef: object) => {
|
export const encodeFields = (designerRef: object) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const rule = JSON.parse(designerRef.value.getJson())
|
const rule = designerRef.value.getRule()
|
||||||
const fields: string[] = []
|
const fields: string[] = []
|
||||||
rule.forEach((item) => {
|
rule.forEach((item: any) => {
|
||||||
fields.push(JSON.stringify(item))
|
fields.push(formCreate.toJson(item))
|
||||||
})
|
})
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解码表单 Fields
|
/** 解码表单 Fields */
|
||||||
export const decodeFields = (fields: string[]) => {
|
export const decodeFields = (fields: string[]) => {
|
||||||
const rule: object[] = []
|
const rule: object[] = []
|
||||||
fields.forEach((item) => {
|
fields.forEach((item) => {
|
||||||
|
|
@ -30,15 +36,15 @@ export const decodeFields = (fields: string[]) => {
|
||||||
return rule
|
return rule
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置表单的 Conf 和 Fields,适用 FcDesigner 场景
|
/** 设置表单的 Conf 和 Fields,适用 FcDesigner 场景 */
|
||||||
export const setConfAndFields = (designerRef: object, conf: string, fields: string) => {
|
export const setConfAndFields = (designerRef: object, conf: string, fields: string[]) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
designerRef.value.setOption(formCreate.parseJson(conf))
|
designerRef.value.setOption(decodeConf(conf))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
designerRef.value.setRule(decodeFields(fields))
|
designerRef.value.setRule(decodeFields(fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置表单的 Conf 和 Fields,适用 form-create 场景
|
/** 设置表单的 Conf 和 Fields,适用 form-create 场景 */
|
||||||
export const setConfAndFields2 = (
|
export const setConfAndFields2 = (
|
||||||
detailPreview: object,
|
detailPreview: object,
|
||||||
conf: string,
|
conf: string,
|
||||||
|
|
@ -51,7 +57,7 @@ export const setConfAndFields2 = (
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
detailPreview.option = formCreate.parseJson(conf)
|
detailPreview.option = decodeConf(conf)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
detailPreview.rule = decodeFields(fields)
|
detailPreview.rule = decodeFields(fields)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@ export const formatToFraction = (num: number | string | undefined): string => {
|
||||||
*
|
*
|
||||||
* @param num 整数
|
* @param num 整数
|
||||||
*/
|
*/
|
||||||
// TODO @芋艿:看看怎么融合掉
|
// TODO @项目组:看看怎么融合掉
|
||||||
export const floatToFixed2 = (num: number | string | undefined): string => {
|
export const floatToFixed2 = (num: number | string | undefined): string => {
|
||||||
let str = '0.00'
|
let str = '0.00'
|
||||||
if (typeof num === 'undefined') {
|
if (typeof num === 'undefined') {
|
||||||
|
|
@ -355,7 +355,7 @@ export const floatToFixed2 = (num: number | string | undefined): string => {
|
||||||
* 将一个分数转换为整数
|
* 将一个分数转换为整数
|
||||||
* @param num
|
* @param num
|
||||||
*/
|
*/
|
||||||
// TODO @芋艿:看看怎么融合掉
|
// TODO @项目组:看看怎么融合掉
|
||||||
export const convertToInteger = (num: number | string | undefined): number => {
|
export const convertToInteger = (num: number | string | undefined): number => {
|
||||||
if (typeof num === 'undefined') return 0
|
if (typeof num === 'undefined') return 0
|
||||||
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
|
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@
|
||||||
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
|
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<el-avatar :src="avatar" :size="70" class="mr-16px">
|
<el-avatar :src="avatar" :size="70" class="mr-16px">
|
||||||
<img src="@/assets/imgs/avatar.gif" alt="" />
|
<img :src="avatar" alt="" />
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-20px">
|
<div class="text-20px">
|
||||||
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
|
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10px text-14px text-gray-500">
|
<div class="mt-10px text-14px text-gray-500">
|
||||||
{{ t('workplace.toady') }},20℃ - 32℃!
|
{{ t('workplace.toady') }}:转化率、客单价、复购率
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
:underline="false"
|
:underline="false"
|
||||||
href="https://github.com/yudaocode"
|
href="https://github.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{{ t('action.more') }}
|
{{ t('action.more') }}
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
<el-card
|
<el-card
|
||||||
shadow="hover"
|
shadow="hover"
|
||||||
class="mr-5px mt-5px cursor-pointer"
|
class="mr-5px mt-5px cursor-pointer"
|
||||||
@click="handleProjectClick(item.message)"
|
@click="handleProjectClick(item.url)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Icon
|
<Icon
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
/>
|
/>
|
||||||
<span class="text-16px">{{ item.name }}</span>
|
<span class="text-16px">{{ item.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
|
<div class="mt-12px text-12px text-gray-400">{{ item.message }}</div>
|
||||||
<div class="mt-12px flex justify-between text-12px text-gray-400">
|
<div class="mt-12px flex justify-between text-12px text-gray-400">
|
||||||
<span>{{ item.personal }}</span>
|
<span>{{ item.personal }}</span>
|
||||||
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
|
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
|
||||||
|
|
@ -159,8 +159,8 @@
|
||||||
<el-skeleton :loading="loading" animated>
|
<el-skeleton :loading="loading" animated>
|
||||||
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
|
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<el-avatar :src="avatar" :size="35" class="mr-16px">
|
<el-avatar :src="noticeAvatar" :size="35" class="mr-16px">
|
||||||
<img src="@/assets/imgs/avatar.gif" alt="" />
|
<img :src="noticeAvatar" alt="" />
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-14px">
|
<div class="text-14px">
|
||||||
|
|
@ -184,6 +184,8 @@
|
||||||
import { set } from 'lodash-es'
|
import { set } from 'lodash-es'
|
||||||
import { EChartsOption } from 'echarts'
|
import { EChartsOption } from 'echarts'
|
||||||
import { formatTime } from '@/utils'
|
import { formatTime } from '@/utils'
|
||||||
|
import adminAvatar from '@/assets/imgs/kongshan-admin-avatar.svg'
|
||||||
|
import noticeAvatar from '@/assets/imgs/kongshan-notice-avatar.svg'
|
||||||
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
// import { useWatermark } from '@/hooks/web/useWatermark'
|
// import { useWatermark } from '@/hooks/web/useWatermark'
|
||||||
|
|
@ -198,7 +200,7 @@ const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
// const { setWatermark } = useWatermark()
|
// const { setWatermark } = useWatermark()
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const avatar = userStore.getUser.avatar
|
const avatar = adminAvatar
|
||||||
const username = userStore.getUser.nickname
|
const username = userStore.getUser.nickname
|
||||||
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
|
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
|
||||||
// 获取统计数
|
// 获取统计数
|
||||||
|
|
@ -210,9 +212,9 @@ let totalSate = reactive<WorkplaceTotal>({
|
||||||
|
|
||||||
const getCount = async () => {
|
const getCount = async () => {
|
||||||
const data = {
|
const data = {
|
||||||
project: 40,
|
project: 28,
|
||||||
access: 2340,
|
access: 18640,
|
||||||
todo: 10
|
todo: 7
|
||||||
}
|
}
|
||||||
totalSate = Object.assign(totalSate, data)
|
totalSate = Object.assign(totalSate, data)
|
||||||
}
|
}
|
||||||
|
|
@ -222,52 +224,58 @@ let projects = reactive<Project[]>([])
|
||||||
const getProject = async () => {
|
const getProject = async () => {
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
name: 'ruoyi-vue-pro',
|
name: '空山管理后台',
|
||||||
icon: 'simple-icons:springboot',
|
icon: 'ep:monitor',
|
||||||
message: 'github.com/YunaiV/ruoyi-vue-pro',
|
message: 'kshan.yootron.net',
|
||||||
personal: 'Spring Boot 单体架构',
|
url: 'https://kshan.yootron.net/',
|
||||||
time: new Date('2025-01-02'),
|
personal: 'Vue3 + Element Plus 管理端',
|
||||||
color: '#6DB33F'
|
time: new Date('2026-04-10'),
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'yudao-ui-admin-vue3',
|
|
||||||
icon: 'ep:element-plus',
|
|
||||||
message: 'github.com/yudaocode/yudao-ui-admin-vue3',
|
|
||||||
personal: 'Vue3 + element-plus 管理后台',
|
|
||||||
time: new Date('2025-02-03'),
|
|
||||||
color: '#409EFF'
|
color: '#409EFF'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yudao-ui-mall-uniapp',
|
name: '空山商城 H5',
|
||||||
icon: 'icon-park-outline:mall-bag',
|
icon: 'icon-park-outline:mall-bag',
|
||||||
message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
|
message: 'm.swelife.cn/#/pages/index',
|
||||||
personal: 'Vue3 + uniapp 商城手机端',
|
url: 'http://m.swelife.cn/#/pages/index',
|
||||||
time: new Date('2025-03-04'),
|
personal: '用户端商城与活动运营',
|
||||||
color: '#ff4d4f'
|
time: new Date('2026-04-11'),
|
||||||
|
color: '#ff6b6b'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yudao-cloud',
|
name: '空山 AI 助手',
|
||||||
icon: 'material-symbols:cloud-outline',
|
icon: 'tabler:ai',
|
||||||
message: 'github.com/YunaiV/yudao-cloud',
|
message: 'kshan.yootron.net',
|
||||||
personal: 'Spring Cloud 微服务架构',
|
url: '/ai/chat',
|
||||||
time: new Date('2025-04-05'),
|
personal: '客服/内容/运营 AI 工作台',
|
||||||
color: '#1890ff'
|
time: new Date('2026-04-12'),
|
||||||
|
color: '#7c3aed'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yudao-ui-admin-vben',
|
name: '订单履约中心',
|
||||||
icon: 'devicon:antdesign',
|
icon: 'solar:box-outline',
|
||||||
message: 'github.com/yudaocode/yudao-ui-admin-vben',
|
message: 'kshan.yootron.net',
|
||||||
personal: 'Vue3 + vben5(antd) 管理后台',
|
url: '/mall/trade/order',
|
||||||
time: new Date('2025-05-06'),
|
personal: '订单、发货、售后与履约监控',
|
||||||
color: '#e18525'
|
time: new Date('2026-04-13'),
|
||||||
|
color: '#14b8a6'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yudao-ui-admin-uniapp',
|
name: '会员增长中心',
|
||||||
icon: 'ant-design:mobile',
|
icon: 'mdi:account-group-outline',
|
||||||
message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
|
message: 'kshan.yootron.net',
|
||||||
personal: 'Vue3 + uniapp 管理手机端',
|
url: '/member/user',
|
||||||
time: new Date('2025-06-01'),
|
personal: '会员分层、积分、营销活动',
|
||||||
color: '#2979ff'
|
time: new Date('2026-04-14'),
|
||||||
|
color: '#f59e0b'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '数据与报表中心',
|
||||||
|
icon: 'carbon:chart-line-data',
|
||||||
|
message: 'kshan.yootron.net',
|
||||||
|
url: '/report/jmreport',
|
||||||
|
personal: '经营数据、渠道分析、复盘报表',
|
||||||
|
time: new Date('2026-04-15'),
|
||||||
|
color: '#0ea5e9'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
projects = Object.assign(projects, data)
|
projects = Object.assign(projects, data)
|
||||||
|
|
@ -278,27 +286,27 @@ let notice = reactive<Notice[]>([])
|
||||||
const getNotice = async () => {
|
const getNotice = async () => {
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
title: '系统支持 JDK 8/17/21,Vue 2/3',
|
title: '空山 V1 联调已打通,管理端与后端可稳定登录',
|
||||||
type: '技术兼容性',
|
type: '项目进展',
|
||||||
keys: ['JDK', 'Vue'],
|
keys: ['联调', '登录'],
|
||||||
date: new Date()
|
date: new Date()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
|
title: 'AI+电商看板上线,支持客服与运营场景快速入口',
|
||||||
type: '架构灵活性',
|
type: '功能更新',
|
||||||
keys: ['Boot', 'Cloud'],
|
keys: ['AI', '运营'],
|
||||||
date: new Date()
|
date: new Date()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
|
title: '本周重点:商品中心、订单履约、会员体系数据校验',
|
||||||
type: '开源免授权',
|
type: '本周任务',
|
||||||
keys: ['无需授权'],
|
keys: ['商品', '订单'],
|
||||||
date: new Date()
|
date: new Date()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
|
title: '研发协同规范:需求、问题、上线节奏统一在任务平台跟踪',
|
||||||
type: '广泛企业认可',
|
type: '协作规范',
|
||||||
keys: ['最广泛', '10w+'],
|
keys: ['需求', '问题反馈'],
|
||||||
date: new Date()
|
date: new Date()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -317,7 +325,7 @@ const getShortcut = async () => {
|
||||||
color: '#1fdaca'
|
color: '#1fdaca'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '商城中心',
|
name: '商品中心',
|
||||||
icon: 'ep:shop',
|
icon: 'ep:shop',
|
||||||
url: '/mall/home',
|
url: '/mall/home',
|
||||||
color: '#ff6b6b'
|
color: '#ff6b6b'
|
||||||
|
|
@ -329,21 +337,21 @@ const getShortcut = async () => {
|
||||||
color: '#7c3aed'
|
color: '#7c3aed'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ERP 系统',
|
name: '订单履约',
|
||||||
icon: 'simple-icons:erpnext',
|
icon: 'simple-icons:erpnext',
|
||||||
url: '/erp/home',
|
url: '/erp/home',
|
||||||
color: '#3fb27f'
|
color: '#3fb27f'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'CRM 系统',
|
name: '会员运营',
|
||||||
icon: 'simple-icons:civicrm',
|
icon: 'simple-icons:civicrm',
|
||||||
url: '/crm/backlog',
|
url: '/crm/backlog',
|
||||||
color: '#4daf1bc9'
|
color: '#4daf1bc9'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'IoT 物联网',
|
name: '数据看板',
|
||||||
icon: 'fa-solid:hdd',
|
icon: 'mdi:chart-bar',
|
||||||
url: '/iot/home',
|
url: '/report/jmreport',
|
||||||
color: '#1a73e8'
|
color: '#1a73e8'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -410,8 +418,12 @@ const getAllApi = async () => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProjectClick = (message: string) => {
|
const handleProjectClick = (url: string) => {
|
||||||
window.open(`https://${message}`, '_blank')
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShortcutClick = (url: string) => {
|
const handleShortcutClick = (url: string) => {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export type Project = {
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
message: string
|
message: string
|
||||||
|
url: string
|
||||||
personal: string
|
personal: string
|
||||||
time: Date | number | string
|
time: Date | number | string
|
||||||
color: string
|
color: string
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
>
|
>
|
||||||
<div class="relative mx-auto h-full flex">
|
<div class="relative mx-auto h-full flex">
|
||||||
<div
|
<div
|
||||||
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
|
:class="`${prefixCls}__left flex-1 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
|
||||||
>
|
>
|
||||||
<!-- 左上角的 logo + 系统标题 -->
|
<!-- 左上角的 logo + 系统标题 -->
|
||||||
<div class="relative flex items-center text-white">
|
<div class="relative flex items-center text-white">
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
<img alt="" class="mr-10px h-48px w-48px" src="/brand-logo.png" />
|
||||||
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 左边的背景图 + 欢迎语 -->
|
<!-- 左边的背景图 + 欢迎语 -->
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
enter-active-class="animate__animated animate__bounceInLeft"
|
enter-active-class="animate__animated animate__bounceInLeft"
|
||||||
tag="div"
|
tag="div"
|
||||||
>
|
>
|
||||||
<img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
|
<img key="1" alt="" class="w-350px rounded-16px shadow-lg" src="@/assets/svgs/login-box-bg.svg" />
|
||||||
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
|
<div key="2" class="text-3xl text-white font-700">{{ t('login.welcome') }}</div>
|
||||||
<div key="3" class="mt-5 text-14px font-normal text-white">
|
<div key="3" class="mt-5 text-14px font-normal text-white/90">
|
||||||
{{ t('login.message') }}
|
{{ t('login.message') }}
|
||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
style="color: var(--el-text-color-primary);"
|
style="color: var(--el-text-color-primary);"
|
||||||
>
|
>
|
||||||
<div class="flex items-center at-2xl:hidden at-xl:hidden">
|
<div class="flex items-center at-2xl:hidden at-xl:hidden">
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
<img alt="" class="mr-10px h-48px w-48px" src="/brand-logo.png" />
|
||||||
<span class="text-20px font-bold" >{{ underlineToHump(appStore.getTitle) }}</span>
|
<span class="text-20px font-bold" >{{ underlineToHump(appStore.getTitle) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end space-x-10px h-48px">
|
<div class="flex items-center justify-end space-x-10px h-48px">
|
||||||
|
|
@ -92,6 +92,8 @@ $prefix-cls: #{$namespace}-login;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
&__left {
|
&__left {
|
||||||
|
background: linear-gradient(135deg, #0f4c9a 0%, #1e88e5 55%, #46b3ff 100%);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -102,6 +104,7 @@ $prefix-cls: #{$namespace}-login;
|
||||||
background-image: url('@/assets/svgs/login-bg.svg');
|
background-image: url('@/assets/svgs/login-bg.svg');
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
opacity: 0.18;
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
>
|
>
|
||||||
<!-- 左上角的 logo + 系统标题 -->
|
<!-- 左上角的 logo + 系统标题 -->
|
||||||
<div class="relative flex items-center text-white">
|
<div class="relative flex items-center text-white">
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
<img alt="" class="mr-10px h-48px w-48px" src="/brand-logo.png" />
|
||||||
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 左边的背景图 + 欢迎语 -->
|
<!-- 左边的背景图 + 欢迎语 -->
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
|
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
|
||||||
>
|
>
|
||||||
<div class="flex items-center at-2xl:hidden at-xl:hidden">
|
<div class="flex items-center at-2xl:hidden at-xl:hidden">
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
<img alt="" class="mr-10px h-48px w-48px" src="/brand-logo.png" />
|
||||||
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end space-x-10px h-48px">
|
<div class="flex items-center justify-end space-x-10px h-48px">
|
||||||
|
|
@ -199,7 +199,7 @@ const loginData = reactive({
|
||||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
|
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
|
||||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
|
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
|
||||||
loginForm: {
|
loginForm: {
|
||||||
tenantName: '芋道源码',
|
tenantName: '空山平台',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: 'admin123',
|
password: 'admin123',
|
||||||
captchaVerification: '',
|
captchaVerification: '',
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
<Verify
|
<Verify
|
||||||
ref="verify"
|
ref="verify"
|
||||||
|
v-if="resetPasswordData.captchaEnable === 'true'"
|
||||||
:captchaType="captchaType"
|
:captchaType="captchaType"
|
||||||
:imgSize="{ width: '400px', height: '200px' }"
|
:imgSize="{ width: '400px', height: '200px' }"
|
||||||
mode="pop"
|
mode="pop"
|
||||||
|
|
|
||||||
|
|
@ -129,17 +129,17 @@
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-divider content-position="center">萌新必读</el-divider>
|
<el-divider content-position="center">空山项目入口</el-divider>
|
||||||
<el-col :span="24" class="px-10px">
|
<el-col :span="24" class="px-10px">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
|
<el-link href="http://m.swelife.cn/#/pages/index" target="_blank">🏠项目官网</el-link>
|
||||||
<el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
|
<el-link href="http://127.0.0.1:5173/" target="_blank">🧭管理端入口</el-link>
|
||||||
<el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
|
<el-link href="https://github.com/" target="_blank">
|
||||||
⚡面试手册
|
📋需求与任务
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
|
<el-link href="https://github.com/" target="_blank">
|
||||||
🤝外包咨询
|
🛠问题反馈
|
||||||
</el-link>
|
</el-link>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ const loginData = reactive({
|
||||||
},
|
},
|
||||||
loginForm: {
|
loginForm: {
|
||||||
uuid: '',
|
uuid: '',
|
||||||
tenantName: '芋道源码',
|
tenantName: '空山平台',
|
||||||
mobileNumber: '',
|
mobileNumber: '',
|
||||||
code: ''
|
code: ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import logoImg from '@/assets/imgs/logo.png'
|
const logoImg = '/brand-logo.png'
|
||||||
|
|
||||||
import LoginFormTitle from './LoginFormTitle.vue'
|
import LoginFormTitle from './LoginFormTitle.vue'
|
||||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- TODO @芋艿:可优化,对标 vben 版本 -->
|
<!-- TODO @项目组:可优化,对标 vben 版本 -->
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<el-card class="user w-1/3" shadow="hover">
|
<el-card class="user w-1/3" shadow="hover">
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const rules = reactive<FormRules>({
|
||||||
mobile: [
|
mobile: [
|
||||||
{ required: true, message: t('profile.rules.phone'), trigger: 'blur' },
|
{ required: true, message: t('profile.rules.phone'), trigger: 'blur' },
|
||||||
{
|
{
|
||||||
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
message: t('profile.rules.truephone'),
|
message: t('profile.rules.truephone'),
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ const equalToPassword = (_rule, value, callback) => {
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
oldPassword: [
|
oldPassword: [
|
||||||
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
|
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
|
||||||
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
{ min: 4, max: 16, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
newPassword: [
|
newPassword: [
|
||||||
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
|
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
|
||||||
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
{ min: 4, max: 16, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
confirmPassword: [
|
confirmPassword: [
|
||||||
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
|
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue