diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0278f8370..663c2b5b5 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -2,6 +2,15 @@
"$schema": "https://json.schemastore.org/launchsettings.json",
"version": "0.2.0",
"configurations": [
+ {
+ "type": "chrome",
+ "name": "vben admin antd dev",
+ "request": "launch",
+ "url": "http://localhost:5999",
+ "env": { "NODE_ENV": "development" },
+ "sourceMaps": true,
+ "webRoot": "${workspaceFolder}/apps/web-antdv-next"
+ },
{
"type": "chrome",
"name": "vben admin antd dev",
diff --git a/README.md b/README.md
index 3e8f10586..da1d8d551 100644
--- a/README.md
+++ b/README.md
@@ -82,9 +82,9 @@

-* 通用模块(必选):系统功能、基础设施
-* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
-* 业务系统(按需):ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
+- 通用模块(必选):系统功能、基础设施
+- 通用模块(可选):工作流程、支付系统、数据报表、会员中心
+- 业务系统(按需):ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
### 系统功能
@@ -221,13 +221,13 @@
### 会员中心
-| | 功能 | 描述 |
-|-----|------|----------------------------------|
-| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 |
-| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 |
-| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 |
-| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 |
-| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
+| | 功能 | 描述 |
+| --- | --- | --- |
+| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 |
+| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 |
+| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 |
+| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 |
+| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
### ERP 系统
diff --git a/apps/web-antd/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js b/apps/web-antd/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
index 15429d952..f343a787d 100644
--- a/apps/web-antd/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
+++ b/apps/web-antd/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
@@ -1,5 +1,4 @@
import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
-// eslint-disable-next-line n/no-extraneous-import
import inherits from 'inherits';
function CustomRules(eventBus) {
diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue
index f7e155338..ed0d81325 100644
--- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue
+++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue
@@ -276,9 +276,7 @@ async function openPopover(type: string) {
// 没有节点表单时,approveFormFApi 永远不会被赋值,跳过等待
if (runningTask.value?.formId > 0) {
// 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行
- await until(
- () => typeof approveFormFApi.value?.validate === 'function',
- )
+ await until(() => typeof approveFormFApi.value?.validate === 'function')
.toBeTruthy({ timeout: 1000 })
.catch(() => {});
}
diff --git a/apps/web-antdv-next/.env b/apps/web-antdv-next/.env
index 40b046d24..a0bfde11e 100644
--- a/apps/web-antdv-next/.env
+++ b/apps/web-antdv-next/.env
@@ -1,8 +1,38 @@
# 应用标题
-VITE_APP_TITLE=Vben Admin Antdv Next
+VITE_APP_TITLE=芋道管理系统
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
-VITE_APP_NAMESPACE=vben-web-antdv-next
+VITE_APP_NAMESPACE=yudao-vben-antd
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
+
+# 是否开启模拟数据
+VITE_NITRO_MOCK=false
+
+# 租户开关
+VITE_APP_TENANT_ENABLE=true
+
+# 验证码的开关
+VITE_APP_CAPTCHA_ENABLE=false
+
+# 文档地址的开关
+VITE_APP_DOCALERT_ENABLE=true
+
+# 百度统计
+VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093
+
+# GoView域名
+VITE_GOVIEW_URL='http://127.0.0.1:3000'
+
+# API 加解密
+VITE_APP_API_ENCRYPT_ENABLE = true
+VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
+VITE_APP_API_ENCRYPT_ALGORITHM = AES
+VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
+VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
+# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
+# 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=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN
\ No newline at end of file
diff --git a/apps/web-antdv-next/.env.development b/apps/web-antdv-next/.env.development
index f2b444287..5c71e36f2 100644
--- a/apps/web-antdv-next/.env.development
+++ b/apps/web-antdv-next/.env.development
@@ -3,14 +3,19 @@ VITE_PORT=5999
VITE_BASE=/
+# 请求路径
+VITE_BASE_URL=http://127.0.0.1:48080
# 接口地址
-VITE_GLOB_API_URL=/api
-
-# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
-VITE_NITRO_MOCK=true
-
+VITE_GLOB_API_URL=/admin-api
+# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
+VITE_UPLOAD_TYPE=server
# 是否打开 devtools,true 为打开,false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
+
+# 默认登录用户名
+VITE_APP_DEFAULT_USERNAME=admin
+# 默认登录密码
+VITE_APP_DEFAULT_PASSWORD=admin123
diff --git a/apps/web-antdv-next/.env.production b/apps/web-antdv-next/.env.production
index 5375847a6..a8f3d29a9 100644
--- a/apps/web-antdv-next/.env.production
+++ b/apps/web-antdv-next/.env.production
@@ -1,7 +1,11 @@
VITE_BASE=/
+# 请求路径
+VITE_BASE_URL=http://127.0.0.1:48080
# 接口地址
-VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
+VITE_GLOB_API_URL=http://127.0.0.1:48080/admin-api
+# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
+VITE_UPLOAD_TYPE=server
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=none
@@ -17,3 +21,6 @@ VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
+
+# 验证码的开关
+VITE_APP_CAPTCHA_ENABLE=true
\ No newline at end of file
diff --git a/apps/web-antdv-next/index.html b/apps/web-antdv-next/index.html
index e3b9ae5c5..f390d1d76 100644
--- a/apps/web-antdv-next/index.html
+++ b/apps/web-antdv-next/index.html
@@ -15,13 +15,12 @@
%VITE_APP_TITLE%
diff --git a/apps/web-antdv-next/src/assets/imgs/diy/app-nav-bar-mp.png b/apps/web-antdv-next/src/assets/imgs/diy/app-nav-bar-mp.png
new file mode 100644
index 000000000..c982804c7
Binary files /dev/null and b/apps/web-antdv-next/src/assets/imgs/diy/app-nav-bar-mp.png differ
diff --git a/apps/web-antdv-next/src/assets/imgs/diy/statusBar.png b/apps/web-antdv-next/src/assets/imgs/diy/statusBar.png
new file mode 100644
index 000000000..b85562e42
Binary files /dev/null and b/apps/web-antdv-next/src/assets/imgs/diy/statusBar.png differ
diff --git a/apps/web-antdv-next/src/assets/imgs/wechat.png b/apps/web-antdv-next/src/assets/imgs/wechat.png
new file mode 100644
index 000000000..6afc5e41c
Binary files /dev/null and b/apps/web-antdv-next/src/assets/imgs/wechat.png differ
diff --git a/apps/web-antdv-next/src/bootstrap.ts b/apps/web-antdv-next/src/bootstrap.ts
index 8c1617030..be5a5eda7 100644
--- a/apps/web-antdv-next/src/bootstrap.ts
+++ b/apps/web-antdv-next/src/bootstrap.ts
@@ -1,15 +1,17 @@
import { createApp, watchEffect } from 'vue';
+import VueDOMPurifyHTML from 'vue-dompurify-html';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
-import '@vben/styles/antdv-next';
+import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
+import { setupFormCreate } from '#/plugins/form-create';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
@@ -33,6 +35,7 @@ async function bootstrap(namespace: string) {
// });
const app = createApp(App);
+ app.use(VueDOMPurifyHTML);
// 注册v-loading指令
registerLoadingDirective(app, {
@@ -43,7 +46,7 @@ async function bootstrap(namespace: string) {
// 国际化 i18n 配置
await setupI18n(app);
- // 配置 pinia-tore
+ // 配置 pinia-store
await initStores(app, { namespace });
// 安装权限指令
@@ -56,6 +59,9 @@ async function bootstrap(namespace: string) {
// 配置路由及路由守卫
app.use(router);
+ // formCreate
+ setupFormCreate(app);
+
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
diff --git a/apps/web-antdv-next/src/components/cron-tab/cron-tab.vue b/apps/web-antdv-next/src/components/cron-tab/cron-tab.vue
new file mode 100644
index 000000000..242ecb554
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cron-tab/cron-tab.vue
@@ -0,0 +1,961 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
秒
+ {{ value_second }}
+
+
+
+
+ 任意值
+ 范围
+ 间隔
+ 指定
+
+
+
+
+ -
+
+
+
+
+ 秒开始,每
+
+ 秒执行一次
+
+
+
+
+
+
+
+
+
+
+
分钟
+ {{ value_minute }}
+
+
+
+
+ 任意值
+ 范围
+ 间隔
+ 指定
+
+
+
+
+ -
+
+
+
+
+ 分钟开始,每
+
+ 分钟执行一次
+
+
+
+
+
+
+
+
+
+
+
小时
+ {{ value_hour }}
+
+
+
+
+ 任意值
+ 范围
+ 间隔
+ 指定
+
+
+
+
+ -
+
+
+
+
+ 小时开始,每
+
+ 小时执行一次
+
+
+
+
+
+
+
+
+
+
+
日
+ {{ value_day }}
+
+
+
+
+ 任意值
+ 范围
+ 间隔
+ 指定
+ 本月最后一天
+ 不指定
+
+
+
+
+ -
+
+
+
+
+ 号开始,每
+
+ 天执行一次
+
+
+
+
+
+
+
+
+
+
+
月
+ {{ value_month }}
+
+
+
+
+ 任意值
+ 范围
+ 间隔
+ 指定
+
+
+
+
+ -
+
+
+
+
+ 月开始,每
+
+ 月执行一次
+
+
+
+
+
+
+
+
+
+
+
周
+ {{ value_week }}
+
+
+
+
+ 任意值
+ 范围
+ 间隔
+ 指定
+ 本月最后一周
+ 不指定
+
+
+
+
+ -
+
+
+
+ 第
+
+ 周的星期
+
+ 执行一次
+
+
+
+
+
+
+
+
+
+
+
+
+
+
年
+ {{ value_year }}
+
+
+
+
+ 忽略
+ 任意值
+ 范围
+ 间隔
+ 指定
+
+
+
+
+ -
+
+
+
+
+ 年开始,每
+
+ 年执行一次
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/cron-tab/index.ts b/apps/web-antdv-next/src/components/cron-tab/index.ts
new file mode 100644
index 000000000..8f4baae59
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cron-tab/index.ts
@@ -0,0 +1 @@
+export { default as CronTab } from './cron-tab.vue';
diff --git a/apps/web-antdv-next/src/components/cron-tab/types.ts b/apps/web-antdv-next/src/components/cron-tab/types.ts
new file mode 100644
index 000000000..2adf942b7
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cron-tab/types.ts
@@ -0,0 +1,266 @@
+export interface ShortcutsType {
+ text: string;
+ value: string;
+}
+
+export interface CronRange {
+ start: number | string | undefined;
+ end: number | string | undefined;
+}
+
+export interface CronLoop {
+ start: number | string | undefined;
+ end: number | string | undefined;
+}
+
+export interface CronItem {
+ type: string;
+ range: CronRange;
+ loop: CronLoop;
+ appoint: string[];
+ last?: string;
+}
+
+export interface CronValue {
+ second: CronItem;
+ minute: CronItem;
+ hour: CronItem;
+ day: CronItem;
+ month: CronItem;
+ week: CronItem & { last: string };
+ year: CronItem;
+}
+
+export interface WeekOption {
+ value: string;
+ label: string;
+}
+
+export interface CronData {
+ second: string[];
+ minute: string[];
+ hour: string[];
+ day: string[];
+ month: string[];
+ week: WeekOption[];
+ year: number[];
+}
+
+const getYear = (): number[] => {
+ const v: number[] = [];
+ const y = new Date().getFullYear();
+ for (let i = 0; i < 11; i++) {
+ v.push(y + i);
+ }
+ return v;
+};
+
+export const CronValueDefault: CronValue = {
+ second: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2,
+ },
+ loop: {
+ start: 0,
+ end: 1,
+ },
+ appoint: [],
+ },
+ minute: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2,
+ },
+ loop: {
+ start: 0,
+ end: 1,
+ },
+ appoint: [],
+ },
+ hour: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2,
+ },
+ loop: {
+ start: 0,
+ end: 1,
+ },
+ appoint: [],
+ },
+ day: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2,
+ },
+ loop: {
+ start: 1,
+ end: 1,
+ },
+ appoint: [],
+ },
+ month: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2,
+ },
+ loop: {
+ start: 1,
+ end: 1,
+ },
+ appoint: [],
+ },
+ week: {
+ type: '5',
+ range: {
+ start: '2',
+ end: '3',
+ },
+ loop: {
+ start: 0,
+ end: '2',
+ },
+ last: '2',
+ appoint: [],
+ },
+ year: {
+ type: '-1',
+ range: {
+ start: getYear()[0],
+ end: getYear()[1],
+ },
+ loop: {
+ start: getYear()[0],
+ end: 1,
+ },
+ appoint: [],
+ },
+};
+
+export const CronDataDefault: CronData = {
+ second: [
+ '0',
+ '5',
+ '15',
+ '20',
+ '25',
+ '30',
+ '35',
+ '40',
+ '45',
+ '50',
+ '55',
+ '59',
+ ],
+ minute: [
+ '0',
+ '5',
+ '15',
+ '20',
+ '25',
+ '30',
+ '35',
+ '40',
+ '45',
+ '50',
+ '55',
+ '59',
+ ],
+ hour: [
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ '12',
+ '13',
+ '14',
+ '15',
+ '16',
+ '17',
+ '18',
+ '19',
+ '20',
+ '21',
+ '22',
+ '23',
+ ],
+ day: [
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ '12',
+ '13',
+ '14',
+ '15',
+ '16',
+ '17',
+ '18',
+ '19',
+ '20',
+ '21',
+ '22',
+ '23',
+ '24',
+ '25',
+ '26',
+ '27',
+ '28',
+ '29',
+ '30',
+ '31',
+ ],
+ month: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+ week: [
+ {
+ value: '1',
+ label: '周日',
+ },
+ {
+ value: '2',
+ label: '周一',
+ },
+ {
+ value: '3',
+ label: '周二',
+ },
+ {
+ value: '4',
+ label: '周三',
+ },
+ {
+ value: '5',
+ label: '周四',
+ },
+ {
+ value: '6',
+ label: '周五',
+ },
+ {
+ value: '7',
+ label: '周六',
+ },
+ ],
+ year: getYear(),
+};
diff --git a/apps/web-antdv-next/src/components/cropper/cropper-avatar.vue b/apps/web-antdv-next/src/components/cropper/cropper-avatar.vue
new file mode 100644
index 000000000..72c71d031
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cropper/cropper-avatar.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
![avatar]()
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/cropper/cropper-modal.vue b/apps/web-antdv-next/src/components/cropper/cropper-modal.vue
new file mode 100644
index 000000000..01effd727
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cropper/cropper-modal.vue
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/cropper/cropper.vue b/apps/web-antdv-next/src/components/cropper/cropper.vue
new file mode 100644
index 000000000..54b2629b6
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cropper/cropper.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
![]()
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/cropper/index.ts b/apps/web-antdv-next/src/components/cropper/index.ts
new file mode 100644
index 000000000..43fd89ff3
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cropper/index.ts
@@ -0,0 +1,3 @@
+export { default as CropperAvatar } from './cropper-avatar.vue';
+export { default as CropperImage } from './cropper.vue';
+export type { CropperType } from './typing';
diff --git a/apps/web-antdv-next/src/components/cropper/typing.ts b/apps/web-antdv-next/src/components/cropper/typing.ts
new file mode 100644
index 000000000..f471274b4
--- /dev/null
+++ b/apps/web-antdv-next/src/components/cropper/typing.ts
@@ -0,0 +1,68 @@
+import type { ButtonProps } from 'ant-design-vue';
+import type Cropper from 'cropperjs';
+
+import type { CSSProperties } from 'vue';
+
+export interface apiFunParams {
+ file: Blob;
+ filename: string;
+ name: string;
+}
+
+export interface CropendResult {
+ imgBase64: string;
+ imgInfo: Cropper.Data;
+}
+
+export interface CropperProps {
+ src?: string;
+ alt?: string;
+ circled?: boolean;
+ realTimePreview?: boolean;
+ height?: number | string;
+ crossorigin?: '' | 'anonymous' | 'use-credentials' | undefined;
+ imageStyle?: CSSProperties;
+ options?: Cropper.Options;
+}
+
+export interface CropperAvatarProps {
+ width?: number | string;
+ value?: string;
+ showBtn?: boolean;
+ btnProps?: ButtonProps;
+ btnText?: string;
+ uploadApi?: (params: apiFunParams) => Promise;
+ size?: number;
+}
+
+export interface CropperModalProps {
+ circled?: boolean;
+ uploadApi?: (params: apiFunParams) => Promise;
+ src?: string;
+ size?: number;
+}
+
+export const defaultOptions: Cropper.Options = {
+ aspectRatio: 1,
+ zoomable: true,
+ zoomOnTouch: true,
+ zoomOnWheel: true,
+ cropBoxMovable: true,
+ cropBoxResizable: true,
+ toggleDragModeOnDblclick: true,
+ autoCrop: true,
+ background: true,
+ highlight: true,
+ center: true,
+ responsive: true,
+ restore: true,
+ checkCrossOrigin: true,
+ checkOrientation: true,
+ scalable: true,
+ modal: true,
+ guides: true,
+ movable: true,
+ rotatable: true,
+};
+
+export type { Cropper as CropperType };
diff --git a/apps/web-antdv-next/src/components/description/description.vue b/apps/web-antdv-next/src/components/description/description.vue
new file mode 100644
index 000000000..d591e5722
--- /dev/null
+++ b/apps/web-antdv-next/src/components/description/description.vue
@@ -0,0 +1,195 @@
+
diff --git a/apps/web-antdv-next/src/components/description/index.ts b/apps/web-antdv-next/src/components/description/index.ts
new file mode 100644
index 000000000..a707c4865
--- /dev/null
+++ b/apps/web-antdv-next/src/components/description/index.ts
@@ -0,0 +1,3 @@
+export { default as Description } from './description.vue';
+export * from './typing';
+export { useDescription } from './use-description';
diff --git a/apps/web-antdv-next/src/components/description/typing.ts b/apps/web-antdv-next/src/components/description/typing.ts
new file mode 100644
index 000000000..97cd8cab9
--- /dev/null
+++ b/apps/web-antdv-next/src/components/description/typing.ts
@@ -0,0 +1,43 @@
+import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
+import type { JSX } from 'vue/jsx-runtime';
+
+import type { CSSProperties, VNode } from 'vue';
+
+import type { Recordable } from '@vben/types';
+
+export interface DescriptionItemSchema {
+ labelMinWidth?: number;
+ contentMinWidth?: number;
+ labelStyle?: CSSProperties; // 自定义标签样式
+ field: string; // 对应 data 中的字段名
+ label: JSX.Element | string | VNode; // 内容的描述
+ span?: number; // 包含列的数量
+ show?: (...arg: any) => boolean; // 是否显示
+ slot?: string; // 插槽名称
+ render?: (
+ val: any,
+ data?: Recordable,
+ ) => Element | JSX.Element | number | string | undefined | VNode; // 自定义需要展示的内容
+}
+
+export interface DescriptionProps extends DescriptionsProps {
+ useCard?: boolean; // 是否包含卡片组件
+ schema: DescriptionItemSchema[]; // 描述项配置
+ data: Recordable; // 数据
+ title?: string; // 标题
+ bordered?: boolean; // 是否包含边框
+ column?:
+ | number
+ | {
+ lg: number;
+ md: number;
+ sm: number;
+ xl: number;
+ xs: number;
+ xxl: number;
+ }; // 列数
+}
+
+export interface DescInstance {
+ setDescProps(descProps: Partial): void;
+}
diff --git a/apps/web-antdv-next/src/components/description/use-description.ts b/apps/web-antdv-next/src/components/description/use-description.ts
new file mode 100644
index 000000000..e3b676af2
--- /dev/null
+++ b/apps/web-antdv-next/src/components/description/use-description.ts
@@ -0,0 +1,31 @@
+import type { Component } from 'vue';
+
+import type { DescInstance, DescriptionProps } from './typing';
+
+import { h, reactive } from 'vue';
+
+import Description from './description.vue';
+
+export function useDescription(options?: Partial) {
+ const propsState = reactive>(options || {});
+
+ const api: DescInstance = {
+ setDescProps: (descProps: Partial): void => {
+ Object.assign(propsState, descProps);
+ },
+ };
+
+ // 创建一个包装组件,将 propsState 合并到 props 中
+ const DescriptionWrapper: Component = {
+ name: 'UseDescription',
+ inheritAttrs: false,
+ setup(_props, { attrs, slots }) {
+ return () => {
+ // @ts-expect-error - 避免类型实例化过深
+ return h(Description, { ...propsState, ...attrs }, slots);
+ };
+ },
+ };
+
+ return [DescriptionWrapper, api] as const;
+}
diff --git a/apps/web-antdv-next/src/components/dict-tag/dict-tag.vue b/apps/web-antdv-next/src/components/dict-tag/dict-tag.vue
new file mode 100644
index 000000000..14cc34d86
--- /dev/null
+++ b/apps/web-antdv-next/src/components/dict-tag/dict-tag.vue
@@ -0,0 +1,71 @@
+
+
+
+
+ {{ dictTag.label }}
+
+
diff --git a/apps/web-antdv-next/src/components/dict-tag/index.ts b/apps/web-antdv-next/src/components/dict-tag/index.ts
new file mode 100644
index 000000000..881265a39
--- /dev/null
+++ b/apps/web-antdv-next/src/components/dict-tag/index.ts
@@ -0,0 +1 @@
+export { default as DictTag } from './dict-tag.vue';
diff --git a/apps/web-antdv-next/src/components/form-create/components/area-select.vue b/apps/web-antdv-next/src/components/form-create/components/area-select.vue
new file mode 100644
index 000000000..9fa8d5b05
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/components/area-select.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/form-create/components/dept-select.vue b/apps/web-antdv-next/src/components/form-create/components/dept-select.vue
new file mode 100644
index 000000000..45c153f85
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/components/dept-select.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/form-create/components/dict-select.vue b/apps/web-antdv-next/src/components/form-create/components/dict-select.vue
new file mode 100644
index 000000000..03a527458
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/components/dict-select.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+ {{ dict.label }}
+
+
+
diff --git a/apps/web-antdv-next/src/components/form-create/components/iframe.vue b/apps/web-antdv-next/src/components/form-create/components/iframe.vue
new file mode 100644
index 000000000..04c1acc20
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/components/iframe.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/form-create/components/use-api-select.tsx b/apps/web-antdv-next/src/components/form-create/components/use-api-select.tsx
new file mode 100644
index 000000000..023869350
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/components/use-api-select.tsx
@@ -0,0 +1,374 @@
+import type { ApiSelectProps } from '#/components/form-create/typing';
+
+import { defineComponent, onMounted, ref, useAttrs } from 'vue';
+
+import { useUserStore } from '@vben/stores';
+import { isEmpty } from '@vben/utils';
+
+import {
+ Checkbox,
+ CheckboxGroup,
+ Radio,
+ RadioGroup,
+ Select,
+ SelectOption,
+} from 'ant-design-vue';
+
+import { requestClient } from '#/api/request';
+
+export function useApiSelect(option: ApiSelectProps) {
+ return defineComponent({
+ name: option.name,
+ props: {
+ // 选项标签
+ labelField: {
+ type: String,
+ default: () => option.labelField ?? 'label',
+ },
+ // 选项的值
+ valueField: {
+ type: String,
+ default: () => option.valueField ?? 'value',
+ },
+ // api 接口
+ url: {
+ type: String,
+ default: () => option.url ?? '',
+ },
+ // 请求类型
+ method: {
+ type: String,
+ default: 'GET',
+ },
+ // 选项解析函数
+ parseFunc: {
+ type: String,
+ default: '',
+ },
+ // 请求参数
+ data: {
+ type: String,
+ default: '',
+ },
+ // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
+ selectType: {
+ type: String,
+ default: 'select',
+ },
+ // 是否多选
+ multiple: {
+ type: Boolean,
+ default: false,
+ },
+ // 是否远程搜索
+ remote: {
+ type: Boolean,
+ default: false,
+ },
+ // 远程搜索时携带的参数
+ remoteField: {
+ type: String,
+ default: 'label',
+ },
+ // 返回值类型(用于部门选择器等):id 返回 ID,name 返回名称
+ returnType: {
+ type: String,
+ default: 'id',
+ },
+ // 是否默认选中当前用户(仅用于 UserSelect)
+ defaultCurrentUser: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ setup(props, { emit }) {
+ const attrs = useAttrs();
+ const options = ref([]); // 下拉数据
+ const loading = ref(false); // 是否正在从远程获取数据
+ const queryParam = ref(); // 当前输入的值
+
+ // 检查是否有有效的预设值
+ function hasValidPresetValue(): boolean {
+ const value = attrs.modelValue;
+ if (value === undefined || value === null || value === '') {
+ return false;
+ }
+ if (Array.isArray(value)) {
+ return value.length > 0;
+ }
+ return true;
+ }
+
+ // 设置默认当前用户
+ function setDefaultCurrentUser(): void {
+ if (option.name !== 'UserSelect' || !props.defaultCurrentUser) {
+ return;
+ }
+ if (hasValidPresetValue()) {
+ return;
+ }
+ const userStore = useUserStore();
+ const currentUserId = userStore.userInfo?.id;
+ if (currentUserId) {
+ const defaultValue = props.multiple ? [currentUserId] : currentUserId;
+ emit('update:modelValue', defaultValue);
+ }
+ }
+
+ const getOptions = async () => {
+ options.value = [];
+ // 接口选择器
+ if (isEmpty(props.url)) {
+ return;
+ }
+
+ switch (props.method) {
+ case 'GET': {
+ let url: string = props.url;
+ if (props.remote && queryParam.value !== undefined) {
+ url = url.includes('?')
+ ? `${url}&${props.remoteField}=${queryParam.value}`
+ : `${url}?${props.remoteField}=${queryParam.value}`;
+ }
+ parseOptions(await requestClient.get(url));
+ break;
+ }
+ case 'POST': {
+ const data: any = JSON.parse(props.data);
+ if (props.remote) {
+ data[props.remoteField] = queryParam.value;
+ }
+ parseOptions(await requestClient.post(props.url, data));
+ break;
+ }
+ }
+ };
+
+ function parseOptions(data: any) {
+ // 情况一:如果有自定义解析函数优先使用自定义解析
+ if (!isEmpty(props.parseFunc)) {
+ options.value = parseFunc()?.(data);
+ return;
+ }
+ // 情况二:返回的直接是一个列表
+ if (Array.isArray(data)) {
+ parseOptions0(data);
+ return;
+ }
+ // 情况二:返回的是分页数据,尝试读取 list
+ data = data.list;
+ if (!!data && Array.isArray(data)) {
+ parseOptions0(data);
+ return;
+ }
+ // 情况三:不是 yudao-vue-pro 标准返回
+ console.warn(
+ `接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理`,
+ );
+ }
+
+ function parseOptions0(data: any[]) {
+ if (Array.isArray(data)) {
+ options.value = data.map((item: any) => {
+ const label = parseExpression(item, props.labelField);
+ let value = parseExpression(item, props.valueField);
+
+ // 根据 returnType 决定返回值
+ // 如果设置了 returnType 为 'name',则返回 label 作为 value
+ if (props.returnType === 'name') {
+ value = label;
+ }
+
+ return {
+ label,
+ value,
+ };
+ });
+ return;
+ }
+ console.warn(`接口[${props.url}] 返回结果不是一个数组`);
+ }
+
+ function parseFunc() {
+ let parse: any = null;
+ if (props.parseFunc) {
+ // 解析字符串函数
+ // oxlint-disable-next-line typescript/no-implied-eval
+ // oxlint-disable-next-line no-new-func, typescript/no-implied-eval
+ parse = new Function(`return ${props.parseFunc}`)();
+ }
+ return parse;
+ }
+
+ function parseExpression(data: any, template: string) {
+ // 检测是否使用了表达式
+ if (!template.includes('${')) {
+ return data[template];
+ }
+ // 正则表达式匹配模板字符串中的 ${...}
+ const pattern = /\$\{([^}]*)\}/g;
+ // 使用replace函数配合正则表达式和回调函数来进行替换
+ return template.replaceAll(pattern, (_, expr) => {
+ // expr 是匹配到的 ${} 内的表达式(这里是属性名),从 data 中获取对应的值
+ const result = data[expr.trim()]; // 去除前后空白,以防用户输入带空格的属性名
+ if (!result) {
+ console.warn(
+ `接口选择器选项模版[${template}][${expr.trim()}] 解析值失败结果为[${result}], 请检查属性名称是否存在于接口返回值中,存在则忽略此条!!!`,
+ );
+ }
+ return result;
+ });
+ }
+
+ const remoteMethod = async (query: any) => {
+ if (!query) {
+ return;
+ }
+ loading.value = true;
+ try {
+ queryParam.value = query;
+ await getOptions();
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onMounted(async () => {
+ await getOptions();
+ // 设置默认当前用户(仅用于 UserSelect)
+ setDefaultCurrentUser();
+ });
+
+ const buildSelect = () => {
+ const {
+ modelValue,
+ 'onUpdate:modelValue': onUpdateModelValue,
+ ...restAttrs
+ } = attrs;
+
+ if (props.multiple) {
+ // fix:多写此步是为了解决 multiple 属性问题
+ return (
+
+ );
+ }
+ return (
+
+ );
+ };
+ const buildCheckbox = () => {
+ const {
+ modelValue,
+ 'onUpdate:modelValue': onUpdateModelValue,
+ ...restAttrs
+ } = attrs;
+ if (isEmpty(options.value)) {
+ options.value = [
+ { label: '选项1', value: '选项1' },
+ { label: '选项2', value: '选项2' },
+ ];
+ }
+ return (
+
+ {options.value.map(
+ (item: { label: any; value: any }, index: any) => (
+
+ {item.label}
+
+ ),
+ )}
+
+ );
+ };
+ const buildRadio = () => {
+ const {
+ modelValue,
+ 'onUpdate:modelValue': onUpdateModelValue,
+ ...restAttrs
+ } = attrs;
+ if (isEmpty(options.value)) {
+ options.value = [
+ { label: '选项1', value: '选项1' },
+ { label: '选项2', value: '选项2' },
+ ];
+ }
+ return (
+
+ {options.value.map(
+ (item: { label: any; value: any }, index: any) => (
+
+ {item.label}
+
+ ),
+ )}
+
+ );
+ };
+ return () => (
+ <>
+ {(() => {
+ switch (props.selectType) {
+ case 'checkbox': {
+ return buildCheckbox();
+ }
+ case 'radio': {
+ return buildRadio();
+ }
+ case 'select': {
+ return buildSelect();
+ }
+ default: {
+ return buildSelect();
+ }
+ }
+ })()}
+ >
+ );
+ },
+ });
+}
diff --git a/apps/web-antdv-next/src/components/form-create/components/use-images-upload.tsx b/apps/web-antdv-next/src/components/form-create/components/use-images-upload.tsx
new file mode 100644
index 000000000..28a23a016
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/components/use-images-upload.tsx
@@ -0,0 +1,42 @@
+import { defineComponent } from 'vue';
+
+import ImageUpload from '#/components/upload/image-upload.vue';
+
+export function useImagesUpload() {
+ return defineComponent({
+ name: 'ImagesUpload',
+ props: {
+ accept: {
+ type: Array,
+ default: () => ['image/jpeg', 'image/png', 'image/gif'],
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ maxNumber: {
+ type: Number,
+ default: 5,
+ },
+ maxSize: {
+ type: Number,
+ default: 5,
+ },
+ multiple: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ setup(props) {
+ return () => (
+
+ );
+ },
+ });
+}
diff --git a/apps/web-antdv-next/src/components/form-create/helpers.ts b/apps/web-antdv-next/src/components/form-create/helpers.ts
new file mode 100644
index 000000000..12d07a68a
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/helpers.ts
@@ -0,0 +1,273 @@
+import type { Rule } from '@form-create/ant-design-vue';
+
+import type { Ref } from 'vue';
+
+import type { Menu } from '#/components/form-create/typing';
+
+import { isRef, nextTick, onMounted } from 'vue';
+
+import formCreate from '@form-create/ant-design-vue';
+
+import { apiSelectRule } from '#/components/form-create/rules/data';
+
+import {
+ useAreaSelectRule,
+ useDictSelectRule,
+ useEditorRule,
+ useIframeRule,
+ useSelectRule,
+ useUploadFileRule,
+ useUploadImageRule,
+ useUploadImagesRule,
+} from './rules';
+
+/** 编码表单 Conf */
+export function encodeConf(designerRef: any) {
+ // 关联案例:https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/834/
+ return formCreate.toJson(designerRef.value.getOption());
+}
+
+/** 解码表单 Conf */
+export function decodeConf(conf: string) {
+ return formCreate.parseJson(conf);
+}
+
+/** 编码表单 Fields */
+export function encodeFields(designerRef: any) {
+ const rule = designerRef.value.getRule();
+ const fields: string[] = [];
+ rule.forEach((item: any) => {
+ fields.push(formCreate.toJson(item));
+ });
+ return fields;
+}
+
+/** 解码表单 Fields */
+export function decodeFields(fields: string[]) {
+ const rule: Rule[] = [];
+ fields.forEach((item) => {
+ rule.push(formCreate.parseJson(item));
+ });
+ return rule;
+}
+
+/** 设置表单的 Conf 和 Fields,适用 FcDesigner 场景 */
+export function setConfAndFields(
+ designerRef: any,
+ conf: string,
+ fields: string | string[],
+) {
+ designerRef.value.setOption(decodeConf(conf));
+ // 处理 fields 参数类型,确保传入 decodeFields 的是 string[] 类型
+ const fieldsArray = Array.isArray(fields) ? fields : [fields];
+ designerRef.value.setRule(decodeFields(fieldsArray));
+}
+
+/** 设置表单的 Conf 和 Fields,适用 form-create 场景 */
+export function setConfAndFields2(
+ detailPreview: any,
+ conf: string,
+ fields: string[],
+ value?: any,
+) {
+ if (isRef(detailPreview)) {
+ detailPreview = detailPreview.value;
+ }
+ detailPreview.option = decodeConf(conf);
+ detailPreview.rule = decodeFields(fields);
+ if (value) {
+ detailPreview.value = value;
+ }
+}
+
+export function makeRequiredRule() {
+ return {
+ type: 'Required',
+ field: 'formCreate$required',
+ title: '是否必填',
+ };
+}
+
+export function localeProps(
+ t: (msg: string) => any,
+ prefix: string,
+ rules: any[],
+) {
+ return rules.map((rule: { field: string; title: any }) => {
+ if (rule.field === 'formCreate$required') {
+ rule.title = t('props.required') || rule.title;
+ } else if (rule.field && rule.field !== '_optionType') {
+ rule.title = t(`components.${prefix}.${rule.field}`) || rule.title;
+ }
+ return rule;
+ });
+}
+
+/**
+ * 解析表单组件的 field, title 等字段(递归,如果组件包含子组件)
+ *
+ * @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule
+ * @param fields 解析后表单组件字段
+ * @param parentTitle 如果是子表单,子表单的标题,默认为空
+ */
+export function parseFormFields(
+ rule: Record,
+ fields: Array> = [],
+ parentTitle: string = '',
+) {
+ const { type, field, $required, title: tempTitle, children } = rule;
+ if (field && tempTitle) {
+ let title = tempTitle;
+ if (parentTitle) {
+ title = `${parentTitle}.${tempTitle}`;
+ }
+ let required = false;
+ if ($required) {
+ required = true;
+ }
+ fields.push({
+ field,
+ title,
+ type,
+ required,
+ });
+ // TODO 子表单 需要处理子表单字段
+ // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+ // // 解析子表单的字段
+ // rule.props.rule.forEach((item) => {
+ // parseFields(item, fieldsPermission, title)
+ // })
+ // }
+ }
+ if (children && Array.isArray(children)) {
+ children.forEach((rule) => {
+ parseFormFields(rule, fields);
+ });
+ }
+}
+
+/**
+ * 表单设计器增强 hook
+ * 新增
+ * - 文件上传
+ * - 单图上传
+ * - 多图上传
+ * - 字典选择器
+ * - 用户选择器
+ * - 部门选择器
+ * - 富文本
+ */
+export async function useFormCreateDesigner(designer: Ref) {
+ const editorRule = useEditorRule();
+ const uploadFileRule = useUploadFileRule();
+ const uploadImageRule = useUploadImageRule();
+ const uploadImagesRule = useUploadImagesRule();
+ const iframeRule = useIframeRule();
+ const areaSelectRule = useAreaSelectRule();
+
+ /** 构建表单组件 */
+ function buildFormComponents() {
+ // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
+ designer.value?.removeMenuItem('upload');
+ // 移除自带的富文本组件规则,使用 editorRule 替代
+ designer.value?.removeMenuItem('fc-editor');
+ const components = [
+ editorRule,
+ uploadFileRule,
+ uploadImageRule,
+ uploadImagesRule,
+ iframeRule,
+ areaSelectRule,
+ ];
+ components.forEach((component) => {
+ // 插入组件规则
+ designer.value?.addComponent(component);
+ // 插入拖拽按钮到 `main` 分类下
+ designer.value?.appendMenuItem('main', {
+ icon: component.icon,
+ name: component.name,
+ label: component.label,
+ });
+ });
+ }
+
+ const userSelectRule = useSelectRule({
+ name: 'UserSelect',
+ label: '用户选择器',
+ icon: 'icon-eye',
+ props: [
+ {
+ type: 'switch',
+ field: 'defaultCurrentUser',
+ title: '默认选中当前用户',
+ value: false,
+ },
+ ],
+ });
+ const deptSelectRule = useSelectRule({
+ name: 'DeptSelect',
+ label: '部门选择器',
+ icon: 'icon-tree',
+ 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 apiSelectRule0 = useSelectRule({
+ name: 'ApiSelect',
+ label: '接口选择器',
+ icon: 'icon-json',
+ props: [...apiSelectRule],
+ event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus'],
+ });
+
+ /** 构建系统字段菜单 */
+ function buildSystemMenu() {
+ // 移除自带的下拉选择器组件,使用 currencySelectRule 替代
+ // designer.value?.removeMenuItem('select')
+ // designer.value?.removeMenuItem('radio')
+ // designer.value?.removeMenuItem('checkbox')
+ const components = [
+ userSelectRule,
+ deptSelectRule,
+ dictSelectRule,
+ apiSelectRule0,
+ ];
+ const menu: Menu = {
+ name: 'system',
+ title: '系统字段',
+ list: components.map((component) => {
+ // 插入组件规则
+ designer.value?.addComponent(component);
+ // 插入拖拽按钮到 `system` 分类下
+ return {
+ icon: component.icon,
+ name: component.name,
+ label: component.label,
+ };
+ }),
+ };
+ designer.value?.addMenu(menu);
+ }
+
+ onMounted(async () => {
+ await nextTick();
+ buildFormComponents();
+ buildSystemMenu();
+ });
+}
diff --git a/apps/web-antdv-next/src/components/form-create/index.ts b/apps/web-antdv-next/src/components/form-create/index.ts
new file mode 100644
index 000000000..2c512158d
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/index.ts
@@ -0,0 +1,3 @@
+export { useApiSelect } from './components/use-api-select';
+
+export * from './helpers';
diff --git a/apps/web-antdv-next/src/components/form-create/rules/data.ts b/apps/web-antdv-next/src/components/form-create/rules/data.ts
new file mode 100644
index 000000000..e6f659c7d
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/data.ts
@@ -0,0 +1,181 @@
+const selectRule = [
+ {
+ type: 'select',
+ field: 'selectType',
+ title: '选择器类型',
+ value: 'select',
+ options: [
+ { label: '下拉框', value: 'select' },
+ { label: '单选框', value: 'radio' },
+ { label: '多选框', value: 'checkbox' },
+ ],
+ // 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
+ control: [
+ {
+ value: 'select',
+ condition: '==',
+ method: 'hidden',
+ rule: [
+ 'multiple',
+ 'clearable',
+ 'collapseTags',
+ 'multipleLimit',
+ 'allowCreate',
+ 'filterable',
+ 'noMatchText',
+ 'remote',
+ 'remoteMethod',
+ 'reserveKeyword',
+ 'defaultFirstOption',
+ 'automaticDropdown',
+ ],
+ },
+ ],
+ },
+ {
+ type: 'switch',
+ field: 'filterable',
+ title: '是否可搜索',
+ },
+ { type: 'switch', field: 'multiple', title: '是否多选' },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '是否禁用',
+ },
+ { type: 'switch', field: 'clearable', title: '是否可以清空选项' },
+ {
+ type: 'switch',
+ field: 'collapseTags',
+ title: '多选时是否将选中值按文字的形式展示',
+ },
+ {
+ type: 'inputNumber',
+ field: 'multipleLimit',
+ title: '多选时用户最多可以选择的项目数,为 0 则不限制',
+ props: { min: 0 },
+ },
+ {
+ type: 'input',
+ field: 'autocomplete',
+ title: 'autocomplete 属性',
+ },
+ { type: 'input', field: 'placeholder', title: '占位符' },
+ { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
+ {
+ type: 'input',
+ field: 'noMatchText',
+ title: '搜索条件无匹配时显示的文字',
+ },
+ { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
+ {
+ type: 'switch',
+ field: 'reserveKeyword',
+ title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词',
+ },
+ {
+ type: 'switch',
+ field: 'defaultFirstOption',
+ title: '在输入框按下回车,选择第一个匹配项',
+ },
+ {
+ type: 'switch',
+ field: 'popperAppendToBody',
+ title: '是否将弹出框插入至 body 元素',
+ value: true,
+ },
+ {
+ type: 'switch',
+ field: 'automaticDropdown',
+ title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单',
+ },
+];
+
+const apiSelectRule = [
+ {
+ type: 'input',
+ field: 'url',
+ title: 'url 地址',
+ props: {
+ placeholder: '/system/user/simple-list',
+ },
+ },
+ {
+ type: 'select',
+ field: 'method',
+ title: '请求类型',
+ value: 'GET',
+ options: [
+ { label: 'GET', value: 'GET' },
+ { label: 'POST', value: 'POST' },
+ ],
+ control: [
+ {
+ value: 'GET',
+ condition: '!=',
+ method: 'hidden',
+ rule: [
+ {
+ type: 'input',
+ field: 'data',
+ title: '请求参数 JSON 格式',
+ props: {
+ autoSize: true, // 特殊:ele 里是 autosize,antd 里是 autoSize
+ type: 'textarea',
+ placeholder: '{"type": 1}',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'input',
+ field: 'labelField',
+ title: 'label 属性',
+ info: `可以使用 el 表达式:\${属性},来实现复杂数据组合。如:\${nickname}-\${id}`,
+ props: {
+ placeholder: 'nickname',
+ },
+ },
+ {
+ type: 'input',
+ field: 'valueField',
+ title: 'value 属性',
+ info: `可以使用 el 表达式:\${属性},来实现复杂数据组合。如:\${nickname}-\${id}`,
+ props: {
+ placeholder: 'id',
+ },
+ },
+ {
+ type: 'input',
+ field: 'parseFunc',
+ title: '选项解析函数',
+ info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
+ (data: any)=>{ label: string; value: any }[]`,
+ props: {
+ autoSize: true, // 特殊:ele 里是 autosize,antd 里是 autoSize
+ rows: { minRows: 2, maxRows: 6 },
+ type: 'textarea',
+ placeholder: `
+ function (data) {
+ console.log(data)
+ return data.list.map(item=> ({label: item.nickname,value: item.id}))
+ }`,
+ },
+ },
+ {
+ type: 'switch',
+ field: 'remote',
+ info: '是否可搜索',
+ title: '其中的选项是否从服务器远程加载',
+ },
+ {
+ type: 'input',
+ field: 'remoteField',
+ title: '请求参数',
+ info: '远程请求时请求携带的参数名称,如:name',
+ },
+];
+
+export { apiSelectRule, selectRule };
diff --git a/apps/web-antdv-next/src/components/form-create/rules/index.ts b/apps/web-antdv-next/src/components/form-create/rules/index.ts
new file mode 100644
index 000000000..dc3391f8d
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/index.ts
@@ -0,0 +1,8 @@
+export { useAreaSelectRule } from './use-area-select-rule';
+export { useDictSelectRule } from './use-dict-select';
+export { useEditorRule } from './use-editor-rule';
+export { useIframeRule } from './use-iframe-rule';
+export { useSelectRule } from './use-select-rule';
+export { useUploadFileRule } from './use-upload-file-rule';
+export { useUploadImageRule } from './use-upload-image-rule';
+export { useUploadImagesRule } from './use-upload-images-rule';
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-area-select-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-area-select-rule.ts
new file mode 100644
index 000000000..b118a1799
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-area-select-rule.ts
@@ -0,0 +1,77 @@
+import { AreaLevelEnum } from '@vben/constants';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+
+/** 省市区选择器规则 */
+export function useAreaSelectRule() {
+ const label = '省市区选择器';
+ const name = 'AreaSelect';
+
+ return {
+ icon: 'icon-location',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: `area_${Date.now()}`,
+ title: label,
+ info: '',
+ $required: false,
+ modelField: 'value', // 特殊:ele 里是 model-value,antd 里是 value
+ };
+ },
+ props(_: any, { t }: any) {
+ 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,
+ },
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-dict-select.ts b/apps/web-antdv-next/src/components/form-create/rules/use-dict-select.ts
new file mode 100644
index 000000000..08171a24b
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-dict-select.ts
@@ -0,0 +1,70 @@
+import type { SystemDictTypeApi } from '#/api/system/dict/type';
+
+import { onMounted, ref } from 'vue';
+
+import { buildUUID, cloneDeep } from '@vben/utils';
+
+import { getSimpleDictTypeList } from '#/api/system/dict/type';
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+import { selectRule } from '#/components/form-create/rules/data';
+
+/** 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule */
+export function useDictSelectRule() {
+ const label = '字典选择器';
+ const name = 'DictSelect';
+ const rules = cloneDeep(selectRule);
+ const dictOptions = ref<{ label: string; value: string }[]>([]); // 字典类型下拉数据
+ onMounted(async () => {
+ const data = await getSimpleDictTypeList();
+ if (!data || data.length === 0) {
+ return;
+ }
+ dictOptions.value =
+ data?.map((item: SystemDictTypeApi.DictType) => ({
+ label: item.name,
+ value: item.type,
+ })) ?? [];
+ });
+ return {
+ icon: 'icon-descriptions',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $required: false,
+ modelField: 'value', // 特殊:ele 里是 model-value,antd 里是 value
+ };
+ },
+ props(_: any, { t }: any) {
+ return localeProps(t, `${name}.props`, [
+ makeRequiredRule(),
+ {
+ type: 'select',
+ field: 'dictType',
+ title: '字典类型',
+ value: '',
+ options: dictOptions.value,
+ },
+ {
+ type: 'select',
+ field: 'valueType',
+ title: '字典值类型',
+ value: 'str',
+ options: [
+ { label: '数字', value: 'int' },
+ { label: '字符串', value: 'str' },
+ { label: '布尔值', value: 'bool' },
+ ],
+ },
+ ...rules,
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-editor-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-editor-rule.ts
new file mode 100644
index 000000000..dcde682cb
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-editor-rule.ts
@@ -0,0 +1,36 @@
+import { buildUUID } from '@vben/utils';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+
+export function useEditorRule() {
+ const label = '富文本';
+ const name = 'Tinymce';
+ return {
+ icon: 'icon-editor',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $required: false,
+ };
+ },
+ props(_: any, { t }: any) {
+ return localeProps(t, `${name}.props`, [
+ makeRequiredRule(),
+ {
+ type: 'input',
+ field: 'height',
+ title: '高度',
+ },
+ { type: 'switch', field: 'readonly', title: '是否只读' },
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-iframe-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-iframe-rule.ts
new file mode 100644
index 000000000..39d26d766
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-iframe-rule.ts
@@ -0,0 +1,77 @@
+import { buildUUID } from '@vben/utils';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+
+/** iframe 组件规则 */
+export function useIframeRule() {
+ const label = '网页 iframe';
+ const name = 'IframeComponent';
+
+ return {
+ icon: 'icon-link',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $required: false,
+ modelField: 'value', // 特殊:ele 里是 model-value,antd 里是 value
+ };
+ },
+ props(_: any, { t }: any) {
+ 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',
+ },
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-select-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-select-rule.ts
new file mode 100644
index 000000000..fc89b9a24
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-select-rule.ts
@@ -0,0 +1,56 @@
+import type { SelectRuleOption } from '#/components/form-create/typing';
+
+import { buildUUID, cloneDeep } from '@vben/utils';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+import { selectRule } from '#/components/form-create/rules/data';
+
+/**
+ * 通用选择器规则 hook
+ *
+ * @param option 规则配置
+ */
+export function useSelectRule(option: SelectRuleOption) {
+ const label = option.label;
+ const name = option.name;
+ const rules = cloneDeep(selectRule);
+ return {
+ icon: option.icon,
+ label,
+ name,
+ event: option.event,
+ rule() {
+ // 构建基础规则
+ const baseRule: any = {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $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(_: any, { t }: any) {
+ if (!option.props) {
+ option.props = [];
+ }
+ return localeProps(t, `${name}.props`, [
+ makeRequiredRule(),
+ ...option.props,
+ ...rules,
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-upload-file-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-upload-file-rule.ts
new file mode 100644
index 000000000..770aca6f5
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-upload-file-rule.ts
@@ -0,0 +1,78 @@
+import { buildUUID } from '@vben/utils';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+
+export function useUploadFileRule() {
+ const label = '文件上传';
+ const name = 'FileUpload';
+ return {
+ icon: 'icon-upload',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $required: false,
+ };
+ },
+ props(_: any, { t }: any) {
+ return localeProps(t, `${name}.props`, [
+ makeRequiredRule(),
+ {
+ type: 'select',
+ field: 'accept',
+ title: '文件类型',
+ value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
+ options: [
+ { label: 'doc', value: 'doc' },
+ { label: 'xls', value: 'xls' },
+ { label: 'ppt', value: 'ppt' },
+ { label: 'txt', value: 'txt' },
+ { label: 'pdf', value: 'pdf' },
+ ],
+ props: {
+ mode: 'multiple',
+ },
+ },
+ {
+ type: 'switch',
+ field: 'drag',
+ title: '拖拽上传',
+ value: false,
+ },
+ {
+ type: 'switch',
+ field: 'showDescription',
+ title: '是否显示提示',
+ value: true,
+ },
+ {
+ type: 'inputNumber',
+ field: 'maxSize',
+ title: '大小限制(MB)',
+ value: 5,
+ props: { min: 0 },
+ },
+ {
+ type: 'inputNumber',
+ field: 'maxNumber',
+ title: '数量限制',
+ value: 5,
+ props: { min: 1 },
+ },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '是否禁用',
+ value: false,
+ },
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-upload-image-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-upload-image-rule.ts
new file mode 100644
index 000000000..dcba9b8bb
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-upload-image-rule.ts
@@ -0,0 +1,63 @@
+import { buildUUID } from '@vben/utils';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+
+export function useUploadImageRule() {
+ const label = '单图上传';
+ const name = 'ImageUpload';
+ return {
+ icon: 'icon-image',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $required: false,
+ };
+ },
+ props(_: any, { t }: any) {
+ return localeProps(t, `${name}.props`, [
+ makeRequiredRule(),
+ {
+ type: 'select',
+ field: 'accept',
+ title: '图片类型限制',
+ value: ['image/jpeg', 'image/png', 'image/gif'],
+ options: [
+ { label: 'image/apng', value: 'image/apng' },
+ { label: 'image/bmp', value: 'image/bmp' },
+ { label: 'image/gif', value: 'image/gif' },
+ { label: 'image/jpeg', value: 'image/jpeg' },
+ { label: 'image/pjpeg', value: 'image/pjpeg' },
+ { label: 'image/svg+xml', value: 'image/svg+xml' },
+ { label: 'image/tiff', value: 'image/tiff' },
+ { label: 'image/webp', value: 'image/webp' },
+ { label: 'image/x-icon', value: 'image/x-icon' },
+ ],
+ props: {
+ mode: 'multiple',
+ },
+ },
+ {
+ type: 'inputNumber',
+ field: 'maxSize',
+ title: '大小限制(MB)',
+ value: 5,
+ props: { min: 0 },
+ },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '是否禁用',
+ value: false,
+ },
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/rules/use-upload-images-rule.ts b/apps/web-antdv-next/src/components/form-create/rules/use-upload-images-rule.ts
new file mode 100644
index 000000000..b5a5cea98
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/rules/use-upload-images-rule.ts
@@ -0,0 +1,70 @@
+import { buildUUID } from '@vben/utils';
+
+import {
+ localeProps,
+ makeRequiredRule,
+} from '#/components/form-create/helpers';
+
+export function useUploadImagesRule() {
+ const label = '多图上传';
+ const name = 'ImagesUpload';
+ return {
+ icon: 'icon-image',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: buildUUID(),
+ title: label,
+ info: '',
+ $required: false,
+ };
+ },
+ props(_: any, { t }: any) {
+ return localeProps(t, `${name}.props`, [
+ makeRequiredRule(),
+ {
+ type: 'select',
+ field: 'accept',
+ title: '图片类型限制',
+ value: ['image/jpeg', 'image/png', 'image/gif'],
+ options: [
+ { label: 'image/apng', value: 'image/apng' },
+ { label: 'image/bmp', value: 'image/bmp' },
+ { label: 'image/gif', value: 'image/gif' },
+ { label: 'image/jpeg', value: 'image/jpeg' },
+ { label: 'image/pjpeg', value: 'image/pjpeg' },
+ { label: 'image/svg+xml', value: 'image/svg+xml' },
+ { label: 'image/tiff', value: 'image/tiff' },
+ { label: 'image/webp', value: 'image/webp' },
+ { label: 'image/x-icon', value: 'image/x-icon' },
+ ],
+ props: {
+ mode: 'multiple',
+ },
+ },
+ {
+ type: 'inputNumber',
+ field: 'maxSize',
+ title: '大小限制(MB)',
+ value: 5,
+ props: { min: 0 },
+ },
+ {
+ type: 'inputNumber',
+ field: 'maxNumber',
+ title: '数量限制',
+ value: 5,
+ props: { min: 1 },
+ },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '是否禁用',
+ value: false,
+ },
+ ]);
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/components/form-create/typing.ts b/apps/web-antdv-next/src/components/form-create/typing.ts
new file mode 100644
index 000000000..35c1a39fc
--- /dev/null
+++ b/apps/web-antdv-next/src/components/form-create/typing.ts
@@ -0,0 +1,39 @@
+/** 数据字典 Select 选择器组件 Props 类型 */
+export interface DictSelectProps {
+ dictType: string; // 字典类型
+ valueType?: 'bool' | 'int' | 'str'; // 字典值类型
+ selectType?: 'checkbox' | 'radio' | 'select'; // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
+ formCreateInject?: any;
+}
+
+/** 左侧拖拽按钮 */
+export interface MenuItem {
+ label: string;
+ name: string;
+ icon: string;
+}
+
+/** 左侧拖拽按钮分类 */
+export interface Menu {
+ title: string;
+ name: string;
+ list: MenuItem[];
+}
+
+/** 通用 API 下拉组件 Props 类型 */
+export interface ApiSelectProps {
+ name: string; // 组件名称
+ labelField?: string; // 选项标签
+ valueField?: string; // 选项的值
+ url?: string; // url 接口
+ isDict?: boolean; // 是否字典选择器
+}
+
+/** 选择组件规则配置类型 */
+export interface SelectRuleOption {
+ label: string; // label 名称
+ name: string; // 组件名称
+ icon: string; // 组件图标
+ props?: any[]; // 组件规则
+ event?: any[]; // 事件配置
+}
diff --git a/apps/web-antdv-next/src/components/map/index.ts b/apps/web-antdv-next/src/components/map/index.ts
new file mode 100644
index 000000000..c8b84da5a
--- /dev/null
+++ b/apps/web-antdv-next/src/components/map/index.ts
@@ -0,0 +1,3 @@
+export { default as MapDialog } from './src/map-dialog.vue';
+
+export { loadBaiduMapSdk } from './src/utils';
diff --git a/apps/web-antdv-next/src/components/map/src/map-dialog.vue b/apps/web-antdv-next/src/components/map/src/map-dialog.vue
new file mode 100644
index 000000000..7b8c0bd87
--- /dev/null
+++ b/apps/web-antdv-next/src/components/map/src/map-dialog.vue
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 地图加载中...
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/map/src/utils.ts b/apps/web-antdv-next/src/components/map/src/utils.ts
new file mode 100644
index 000000000..c5ff378e1
--- /dev/null
+++ b/apps/web-antdv-next/src/components/map/src/utils.ts
@@ -0,0 +1,63 @@
+/* eslint-disable @typescript-eslint/no-dynamic-delete */
+/**
+ * 百度地图 SDK 加载工具
+ */
+
+// 扩展 Window 接口以包含百度地图 GL API
+declare global {
+ interface Window {
+ BMapGL: any;
+ }
+}
+
+// 全局回调名称
+const CALLBACK_NAME = '__BAIDU_MAP_LOAD_CALLBACK__';
+
+// SDK 加载状态
+let loadPromise: null | Promise = null;
+
+/**
+ * 加载百度地图 GL SDK
+ * @param timeout 超时时间(毫秒),默认 10000
+ * @returns Promise
+ */
+export const loadBaiduMapSdk = (timeout = 10_000): Promise => {
+ // 已加载完成
+ 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.addEventListener('onerror', () => {
+ clearTimeout(timeoutId);
+ loadPromise = null;
+ delete (window as any)[CALLBACK_NAME];
+ reject(new Error('百度地图 SDK 加载失败'));
+ });
+ document.body.append(script);
+ });
+
+ return loadPromise;
+};
diff --git a/apps/web-antdv-next/src/components/markdown-view/index.ts b/apps/web-antdv-next/src/components/markdown-view/index.ts
new file mode 100644
index 000000000..742bce8f5
--- /dev/null
+++ b/apps/web-antdv-next/src/components/markdown-view/index.ts
@@ -0,0 +1,3 @@
+export { default as MarkdownView } from './markdown-view.vue';
+
+export * from './typing';
diff --git a/apps/web-antdv-next/src/components/markdown-view/markdown-view.vue b/apps/web-antdv-next/src/components/markdown-view/markdown-view.vue
new file mode 100644
index 000000000..a0e527f1b
--- /dev/null
+++ b/apps/web-antdv-next/src/components/markdown-view/markdown-view.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/markdown-view/typing.ts b/apps/web-antdv-next/src/components/markdown-view/typing.ts
new file mode 100644
index 000000000..350bbcf3a
--- /dev/null
+++ b/apps/web-antdv-next/src/components/markdown-view/typing.ts
@@ -0,0 +1,3 @@
+export type MarkdownViewProps = {
+ content: string;
+};
diff --git a/apps/web-antdv-next/src/components/operate-log/index.ts b/apps/web-antdv-next/src/components/operate-log/index.ts
new file mode 100644
index 000000000..cf38b5e70
--- /dev/null
+++ b/apps/web-antdv-next/src/components/operate-log/index.ts
@@ -0,0 +1,3 @@
+export { default as OperateLog } from './operate-log.vue';
+
+export type { OperateLogProps } from './typing';
diff --git a/apps/web-antdv-next/src/components/operate-log/operate-log.vue b/apps/web-antdv-next/src/components/operate-log/operate-log.vue
new file mode 100644
index 000000000..8b49466ce
--- /dev/null
+++ b/apps/web-antdv-next/src/components/operate-log/operate-log.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+ {{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
+
+
+
+
+ {{ formatDateTime(log.createTime) }}
+
+ {{ log.userName }}
+ {{ log.action }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/operate-log/typing.ts b/apps/web-antdv-next/src/components/operate-log/typing.ts
new file mode 100644
index 000000000..773237b96
--- /dev/null
+++ b/apps/web-antdv-next/src/components/operate-log/typing.ts
@@ -0,0 +1,5 @@
+import type { SystemOperateLogApi } from '#/api/system/operate-log';
+
+export interface OperateLogProps {
+ logList: SystemOperateLogApi.OperateLog[]; // 操作日志列表
+}
diff --git a/apps/web-antdv-next/src/components/shortcut-date-range-picker/index.ts b/apps/web-antdv-next/src/components/shortcut-date-range-picker/index.ts
new file mode 100644
index 000000000..a3ee51539
--- /dev/null
+++ b/apps/web-antdv-next/src/components/shortcut-date-range-picker/index.ts
@@ -0,0 +1 @@
+export { default as ShortcutDateRangePicker } from './shortcut-date-range-picker.vue';
diff --git a/apps/web-antdv-next/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue b/apps/web-antdv-next/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue
new file mode 100644
index 000000000..5a3ce0526
--- /dev/null
+++ b/apps/web-antdv-next/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/table-action/icons.ts b/apps/web-antdv-next/src/components/table-action/icons.ts
new file mode 100644
index 000000000..578265f4c
--- /dev/null
+++ b/apps/web-antdv-next/src/components/table-action/icons.ts
@@ -0,0 +1,16 @@
+export const ACTION_ICON = {
+ DOWNLOAD: 'lucide:download',
+ UPLOAD: 'lucide:upload',
+ ADD: 'lucide:plus',
+ EDIT: 'lucide:edit',
+ DELETE: 'lucide:trash-2',
+ REFRESH: 'lucide:refresh-cw',
+ SEARCH: 'lucide:search',
+ FILTER: 'lucide:filter',
+ MORE: 'lucide:ellipsis-vertical',
+ VIEW: 'lucide:eye',
+ COPY: 'lucide:copy',
+ CLOSE: 'lucide:x',
+ BOOK: 'lucide:book',
+ AUDIT: 'lucide:file-check',
+};
diff --git a/apps/web-antdv-next/src/components/table-action/index.ts b/apps/web-antdv-next/src/components/table-action/index.ts
new file mode 100644
index 000000000..672c0a533
--- /dev/null
+++ b/apps/web-antdv-next/src/components/table-action/index.ts
@@ -0,0 +1,4 @@
+export * from './icons';
+
+export { default as TableAction } from './table-action.vue';
+export * from './typing';
diff --git a/apps/web-antdv-next/src/components/table-action/table-action.vue b/apps/web-antdv-next/src/components/table-action/table-action.vue
new file mode 100644
index 000000000..679f6fcaa
--- /dev/null
+++ b/apps/web-antdv-next/src/components/table-action/table-action.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/table-action/typing.ts b/apps/web-antdv-next/src/components/table-action/typing.ts
new file mode 100644
index 000000000..1e44fb293
--- /dev/null
+++ b/apps/web-antdv-next/src/components/table-action/typing.ts
@@ -0,0 +1,31 @@
+import type {
+ ButtonProps,
+ ButtonType,
+} from 'ant-design-vue/es/button/buttonTypes';
+import type { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
+
+export interface PopConfirm {
+ title: string;
+ okText?: string;
+ cancelText?: string;
+ confirm: () => void;
+ cancel?: () => void;
+ icon?: string;
+ disabled?: boolean;
+}
+
+export interface ActionItem extends ButtonProps {
+ onClick?: () => void;
+ type?: ButtonType;
+ label?: string;
+ color?: 'error' | 'success' | 'warning';
+ icon?: string;
+ popConfirm?: PopConfirm;
+ disabled?: boolean;
+ divider?: boolean;
+ // 权限编码控制是否显示
+ auth?: string[];
+ // 业务控制是否显示
+ ifShow?: ((action: ActionItem) => boolean) | boolean;
+ tooltip?: string | TooltipProps;
+}
diff --git a/apps/web-antdv-next/src/components/tinymce/editor.vue b/apps/web-antdv-next/src/components/tinymce/editor.vue
new file mode 100644
index 000000000..722073cf7
--- /dev/null
+++ b/apps/web-antdv-next/src/components/tinymce/editor.vue
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/tinymce/helper.ts b/apps/web-antdv-next/src/components/tinymce/helper.ts
new file mode 100644
index 000000000..1f98dda46
--- /dev/null
+++ b/apps/web-antdv-next/src/components/tinymce/helper.ts
@@ -0,0 +1,85 @@
+const validEvents = new Set([
+ 'onActivate',
+ 'onAddUndo',
+ 'onBeforeAddUndo',
+ 'onBeforeExecCommand',
+ 'onBeforeGetContent',
+ 'onBeforePaste',
+ 'onBeforeRenderUI',
+ 'onBeforeSetContent',
+ 'onBlur',
+ 'onChange',
+ 'onClearUndos',
+ 'onClick',
+ 'onContextMenu',
+ 'onCopy',
+ 'onCut',
+ 'onDblclick',
+ 'onDeactivate',
+ 'onDirty',
+ 'onDrag',
+ 'onDragDrop',
+ 'onDragEnd',
+ 'onDragGesture',
+ 'onDragOver',
+ 'onDrop',
+ 'onExecCommand',
+ 'onFocus',
+ 'onFocusIn',
+ 'onFocusOut',
+ 'onGetContent',
+ 'onHide',
+ 'onInit',
+ 'onKeyDown',
+ 'onKeyPress',
+ 'onKeyUp',
+ 'onLoadContent',
+ 'onMouseDown',
+ 'onMouseEnter',
+ 'onMouseLeave',
+ 'onMouseMove',
+ 'onMouseOut',
+ 'onMouseOver',
+ 'onMouseUp',
+ 'onNodeChange',
+ 'onObjectResized',
+ 'onObjectResizeStart',
+ 'onObjectSelected',
+ 'onPaste',
+ 'onPostProcess',
+ 'onPostRender',
+ 'onPreProcess',
+ 'onProgressState',
+ 'onRedo',
+ 'onRemove',
+ 'onReset',
+ 'onSaveContent',
+ 'onSelectionChange',
+ 'onSetAttrib',
+ 'onSetContent',
+ 'onShow',
+ 'onSubmit',
+ 'onUndo',
+ 'onVisualAid',
+]);
+
+const isValidKey = (key: string) => validEvents.has(key);
+
+export const bindHandlers = (
+ initEvent: Event,
+ listeners: any,
+ editor: any,
+): void => {
+ Object.keys(listeners)
+ .filter((element) => isValidKey(element))
+ .forEach((key: string) => {
+ const handler = listeners[key];
+ if (typeof handler === 'function') {
+ if (key === 'onInit') {
+ handler(initEvent, editor);
+ } else {
+ editor.on(key.slice(2), (e: any) => handler(e, editor));
+ }
+ }
+ });
+};
diff --git a/apps/web-antdv-next/src/components/tinymce/img-upload.vue b/apps/web-antdv-next/src/components/tinymce/img-upload.vue
new file mode 100644
index 000000000..e5d47bbc1
--- /dev/null
+++ b/apps/web-antdv-next/src/components/tinymce/img-upload.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/tinymce/index.ts b/apps/web-antdv-next/src/components/tinymce/index.ts
new file mode 100644
index 000000000..c277d781d
--- /dev/null
+++ b/apps/web-antdv-next/src/components/tinymce/index.ts
@@ -0,0 +1 @@
+export { default as Tinymce } from './editor.vue';
diff --git a/apps/web-antdv-next/src/components/tinymce/tinymce.ts b/apps/web-antdv-next/src/components/tinymce/tinymce.ts
new file mode 100644
index 000000000..45a867b61
--- /dev/null
+++ b/apps/web-antdv-next/src/components/tinymce/tinymce.ts
@@ -0,0 +1,17 @@
+// Any plugins you want to setting has to be imported
+// Detail plugins list see https://www.tiny.cloud/docs/plugins/
+// Custom builds see https://www.tiny.cloud/download/custom-builds/
+// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
+
+export const plugins =
+ 'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help emoticons accordion';
+
+// 和 vben2.0 不同,从 https://www.tiny.cloud/ 拷贝 Vue 部分,然后去掉 importword exportword exportpdf | math 部分,并额外增加最后一行(来自 vben2.0 差异的部分)
+export const toolbar =
+ 'undo redo | accordion accordionremove | \\\n' +
+ ' blocks fontfamily fontsize | bold italic underline strikethrough | \\\n' +
+ ' align numlist bullist | link image | table media | \\\n' +
+ ' lineheight outdent indent | forecolor backcolor removeformat | \\\n' +
+ ' charmap emoticons | code fullscreen preview | save print | \\\n' +
+ ' pagebreak anchor codesample | ltr rtl | \\\n' +
+ ' hr searchreplace alignleft aligncenter alignright blockquote subscript superscript';
diff --git a/apps/web-antdv-next/src/components/upload/file-upload.vue b/apps/web-antdv-next/src/components/upload/file-upload.vue
new file mode 100644
index 000000000..3838642ea
--- /dev/null
+++ b/apps/web-antdv-next/src/components/upload/file-upload.vue
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
点击或拖拽文件到此区域上传
+
+ 支持{{ accept.join('/') }}格式文件,不超过{{ maxSize }}MB
+
+
+
+
+
+
+ 请上传不超过
+
{{ maxSize }}MB
+ 的
+
{{ accept.join('/') }}
+ 格式文件
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/upload/image-upload.vue b/apps/web-antdv-next/src/components/upload/image-upload.vue
new file mode 100644
index 000000000..7f0a62c1e
--- /dev/null
+++ b/apps/web-antdv-next/src/components/upload/image-upload.vue
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
{{ $t('ui.upload.imgUpload') }}
+
+
+
+ 请上传不超过
+
{{ maxSize }}MB
+ 的
+
{{ accept.join('/') }}
+ 格式文件
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/upload/index.ts b/apps/web-antdv-next/src/components/upload/index.ts
new file mode 100644
index 000000000..14e57fede
--- /dev/null
+++ b/apps/web-antdv-next/src/components/upload/index.ts
@@ -0,0 +1,3 @@
+export { default as FileUpload } from './file-upload.vue';
+export { default as ImageUpload } from './image-upload.vue';
+export { default as InputUpload } from './input-upload.vue';
diff --git a/apps/web-antdv-next/src/components/upload/input-upload.vue b/apps/web-antdv-next/src/components/upload/input-upload.vue
new file mode 100644
index 000000000..90b2f4f7c
--- /dev/null
+++ b/apps/web-antdv-next/src/components/upload/input-upload.vue
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/components/upload/typing.ts b/apps/web-antdv-next/src/components/upload/typing.ts
new file mode 100644
index 000000000..f3c16bc4d
--- /dev/null
+++ b/apps/web-antdv-next/src/components/upload/typing.ts
@@ -0,0 +1,32 @@
+import type { AxiosResponse } from '@vben/request';
+
+import type { AxiosProgressEvent } from '#/api/infra/file';
+
+export enum UploadResultStatus {
+ DONE = 'done',
+ ERROR = 'error',
+ SUCCESS = 'success',
+ UPLOADING = 'uploading',
+}
+
+export type UploadListType = 'picture' | 'picture-card' | 'text';
+
+export interface FileUploadProps {
+ accept?: string[]; // 根据后缀,或者其他
+ api?: (
+ file: File,
+ onUploadProgress?: AxiosProgressEvent,
+ ) => Promise;
+ directory?: string; // 上传的目录
+ disabled?: boolean;
+ drag?: boolean; // 是否支持拖拽上传
+ helpText?: string;
+ listType?: UploadListType;
+ maxNumber?: number; // 最大数量的文件,Infinity不限制
+ modelValue?: string | string[]; // v-model 支持
+ maxSize?: number; // 文件最大多少MB
+ multiple?: boolean; // 是否支持多选
+ resultField?: string; // support xxx.xxx.xx
+ showDescription?: boolean; // 是否显示下面的描述
+ value?: string | string[];
+}
diff --git a/apps/web-antdv-next/src/components/upload/use-upload.ts b/apps/web-antdv-next/src/components/upload/use-upload.ts
new file mode 100644
index 000000000..99b79c0f2
--- /dev/null
+++ b/apps/web-antdv-next/src/components/upload/use-upload.ts
@@ -0,0 +1,180 @@
+import type { Ref } from 'vue';
+
+import type { AxiosProgressEvent, InfraFileApi } from '#/api/infra/file';
+
+import { computed, unref } from 'vue';
+
+import { useAppConfig } from '@vben/hooks';
+import { $t } from '@vben/locales';
+
+import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file';
+import { baseRequestClient } from '#/api/request';
+
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+
+/**
+ * 上传类型
+ */
+enum UPLOAD_TYPE {
+ // 客户端直接上传(只支持S3服务)
+ CLIENT = 'client',
+ // 客户端发送到后端上传
+ SERVER = 'server',
+}
+
+/**
+ * 上传类型钩子函数
+ * @param acceptRef 接受的文件类型
+ * @param helpTextRef 帮助文本
+ * @param maxNumberRef 最大文件数量
+ * @param maxSizeRef 最大文件大小
+ * @returns 文件类型限制和帮助文本的计算属性
+ */
+export function useUploadType({
+ acceptRef,
+ helpTextRef,
+ maxNumberRef,
+ maxSizeRef,
+}: {
+ acceptRef: Ref;
+ helpTextRef: Ref;
+ maxNumberRef: Ref;
+ maxSizeRef: Ref;
+}) {
+ // 文件类型限制
+ const getAccept = computed(() => {
+ const accept = unref(acceptRef);
+ if (accept && accept.length > 0) {
+ return accept;
+ }
+ return [];
+ });
+ const getStringAccept = computed(() => {
+ return unref(getAccept)
+ .map((item) => {
+ return item.indexOf('/') > 0 || item.startsWith('.')
+ ? item
+ : `.${item}`;
+ })
+ .join(',');
+ });
+
+ // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。
+ const getHelpText = computed(() => {
+ const helpText = unref(helpTextRef);
+ if (helpText) {
+ return helpText;
+ }
+ const helpTexts: string[] = [];
+
+ const accept = unref(acceptRef);
+ if (accept.length > 0) {
+ helpTexts.push($t('ui.upload.accept', [accept.join(',')]));
+ }
+
+ const maxSize = unref(maxSizeRef);
+ if (maxSize) {
+ helpTexts.push($t('ui.upload.maxSize', [maxSize]));
+ }
+
+ const maxNumber = unref(maxNumberRef);
+ if (maxNumber && maxNumber !== Infinity) {
+ helpTexts.push($t('ui.upload.maxNumber', [maxNumber]));
+ }
+ return helpTexts.join(',');
+ });
+ return { getAccept, getStringAccept, getHelpText };
+}
+
+/**
+ * 上传钩子函数
+ * @param directory 上传目录
+ * @returns 上传 URL 和自定义上传方法
+ */
+export function useUpload(directory?: string) {
+ // 后端上传地址
+ const uploadUrl = getUploadUrl();
+ // 是否使用前端直连上传
+ const isClientUpload =
+ UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE;
+ // 重写ElUpload上传方法
+ async function httpRequest(
+ file: File,
+ onUploadProgress?: AxiosProgressEvent,
+ ) {
+ // 模式一:前端上传
+ if (isClientUpload) {
+ // 1.1 生成文件名称
+ const fileName = await generateFileName(file);
+ // 1.2 获取文件预签名地址
+ const presignedInfo = await getFilePresignedUrl(fileName, directory);
+ // 1.3 上传文件
+ return baseRequestClient
+ .put(presignedInfo.uploadUrl, file, {
+ headers: {
+ 'Content-Type': file.type,
+ },
+ })
+ .then(() => {
+ // 1.4. 记录文件信息到后端(异步)
+ createFile0(presignedInfo, file);
+ // 通知成功,数据格式保持与后端上传的返回结果一致
+ return { url: presignedInfo.url };
+ });
+ } else {
+ // 模式二:后端上传
+ return uploadFile({ file, directory }, onUploadProgress);
+ }
+ }
+
+ return {
+ uploadUrl,
+ httpRequest,
+ };
+}
+
+/**
+ * 获得上传 URL
+ */
+export function getUploadUrl(): string {
+ return `${apiURL}/infra/file/upload`;
+}
+
+/**
+ * 创建文件信息
+ *
+ * @param vo 文件预签名信息
+ * @param file 文件
+ */
+function createFile0(
+ vo: InfraFileApi.FilePresignedUrlRespVO,
+ file: File,
+): InfraFileApi.File {
+ const fileVO = {
+ configId: vo.configId,
+ url: vo.url,
+ path: vo.path,
+ name: file.name,
+ type: file.type,
+ size: file.size,
+ };
+ createFile(fileVO);
+ return fileVO;
+}
+
+/**
+ * 生成文件名称(使用算法SHA256)
+ *
+ * @param file 要上传的文件
+ */
+async function generateFileName(file: File) {
+ // // 读取文件内容
+ // const data = await file.arrayBuffer();
+ // const wordArray = CryptoJS.lib.WordArray.create(data);
+ // // 计算SHA256
+ // const sha256 = CryptoJS.SHA256(wordArray).toString();
+ // // 拼接后缀
+ // const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
+ // return `${sha256}${ext}`;
+ return file.name;
+}
diff --git a/apps/web-antdv-next/src/layouts/basic.vue b/apps/web-antdv-next/src/layouts/basic.vue
index 2226c68a2..e8715a895 100644
--- a/apps/web-antdv-next/src/layouts/basic.vue
+++ b/apps/web-antdv-next/src/layouts/basic.vue
@@ -1,96 +1,68 @@
([]); // 租户列表
+async function fetchTenantList() {
+ if (!tenantEnable) {
+ return;
+ }
+ try {
+ // 获取租户列表、域名对应租户
+ const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
+ tenantList.value = await getTenantSimpleList();
+
+ // 选中租户:域名 > store 中的租户 > 首个租户
+ let tenantId: null | number = null;
+ const websiteTenant = await websiteTenantPromise;
+ if (websiteTenant?.id) {
+ tenantId = websiteTenant.id;
+ }
+ // 如果没有从域名获取到租户,尝试从 store 中获取
+ if (!tenantId && accessStore.tenantId) {
+ tenantId = accessStore.tenantId;
+ }
+ // 如果还是没有租户,使用列表中的第一个
+ if (!tenantId && tenantList.value?.[0]?.id) {
+ tenantId = tenantList.value[0].id;
+ }
+
+ // 设置选中的租户编号
+ accessStore.setTenantId(tenantId);
+ forgetPasswordRef.value
+ .getFormApi()
+ .setFieldValue('tenantId', tenantId?.toString());
+ } catch (error) {
+ console.error('获取租户列表失败:', error);
+ }
+}
+
+/** 组件挂载时获取租户信息 */
+onMounted(() => {
+ fetchTenantList();
+});
const formSchema = computed((): VbenFormSchema[] => {
return [
+ {
+ component: 'VbenSelect',
+ componentProps: {
+ options: tenantList.value.map((item) => ({
+ label: item.name,
+ value: item.id.toString(),
+ })),
+ placeholder: $t('authentication.tenantTip'),
+ },
+ fieldName: 'tenantId',
+ label: $t('authentication.tenant'),
+ rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
+ dependencies: {
+ triggerFields: ['tenantId'],
+ if: tenantEnable,
+ trigger(values) {
+ if (values.tenantId) {
+ accessStore.setTenantId(Number(values.tenantId));
+ }
+ },
+ },
+ },
{
component: 'VbenInput',
componentProps: {
- placeholder: 'example@example.com',
+ placeholder: $t('authentication.mobile'),
},
- fieldName: 'email',
- label: $t('authentication.email'),
+ fieldName: 'mobile',
+ label: $t('authentication.mobile'),
rules: z
.string()
- .min(1, { message: $t('authentication.emailTip') })
- .email($t('authentication.emailValidErrorTip')),
+ .min(1, { message: $t('authentication.mobileTip') })
+ .refine((v) => /^\d{11}$/.test(v), {
+ message: $t('authentication.mobileErrortip'),
+ }),
+ },
+ {
+ component: 'VbenPinInput',
+ componentProps: {
+ codeLength: CODE_LENGTH,
+ createText: (countdown: number) => {
+ const text =
+ countdown > 0
+ ? $t('authentication.sendText', [countdown])
+ : $t('authentication.sendCode');
+ return text;
+ },
+ placeholder: $t('authentication.code'),
+ handleSendCode: async () => {
+ loading.value = true;
+ try {
+ const formApi = forgetPasswordRef.value?.getFormApi();
+ if (!formApi) {
+ throw new Error('表单未准备好');
+ }
+ // 验证手机号
+ await formApi.validateField('mobile');
+ const isMobileValid = await formApi.isFieldValid('mobile');
+ if (!isMobileValid) {
+ throw new Error('请输入有效的手机号码');
+ }
+
+ // 发送验证码
+ const { mobile } = await formApi.getValues();
+ const scene = 23; // 场景:重置密码
+ await sendSmsCode({ mobile, scene });
+ message.success('验证码发送成功');
+ } finally {
+ loading.value = false;
+ }
+ },
+ },
+ fieldName: 'code',
+ label: $t('authentication.code'),
+ rules: z.string().length(CODE_LENGTH, {
+ message: $t('authentication.codeTip', [CODE_LENGTH]),
+ }),
+ },
+ {
+ component: 'VbenInputPassword',
+ componentProps: {
+ passwordStrength: true,
+ placeholder: $t('authentication.password'),
+ },
+ fieldName: 'password',
+ label: $t('authentication.password'),
+ renderComponentContent() {
+ return {
+ strengthText: () => $t('authentication.passwordStrength'),
+ };
+ },
+ rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
+ },
+ {
+ component: 'VbenInputPassword',
+ componentProps: {
+ placeholder: $t('authentication.confirmPassword'),
+ },
+ dependencies: {
+ rules(values) {
+ const { password } = values;
+ return z
+ .string({ required_error: $t('authentication.passwordTip') })
+ .min(1, { message: $t('authentication.passwordTip') })
+ .refine((value) => value === password, {
+ message: $t('authentication.confirmPasswordTip'),
+ });
+ },
+ triggerFields: ['password'],
+ },
+ fieldName: 'confirmPassword',
+ label: $t('authentication.confirmPassword'),
},
];
});
-function handleSubmit(value: Recordable) {
- void value;
+/**
+ * 处理重置密码操作
+ * @param values 表单数据
+ */
+async function handleSubmit(values: Recordable) {
+ loading.value = true;
+ try {
+ const { mobile, code, password } = values;
+ await smsResetPassword({ mobile, code, password });
+ message.success($t('authentication.resetPasswordSuccess'));
+ // 重置成功后跳转到首页
+ await router.push('/');
+ } catch (error) {
+ console.error('重置密码失败:', error);
+ } finally {
+ loading.value = false;
+ }
}
import type { VbenFormSchema } from '@vben/common-ui';
-import type { BasicOption } from '@vben/types';
-import { computed, markRaw } from 'vue';
+import type { AuthApi } from '#/api/core/auth';
-import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
+import { computed, onMounted, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
+import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
import { $t } from '@vben/locales';
+import { useAccessStore } from '@vben/stores';
+import {
+ checkCaptcha,
+ getCaptcha,
+ getTenantByWebsite,
+ getTenantSimpleList,
+ socialAuthRedirect,
+} from '#/api/core/auth';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
+const { query } = useRoute();
const authStore = useAuthStore();
+const accessStore = useAccessStore();
+const tenantEnable = isTenantEnable();
+const captchaEnable = isCaptchaEnable();
-const MOCK_USER_OPTIONS: BasicOption[] = [
- {
- label: 'Super',
- value: 'vben',
- },
- {
- label: 'Admin',
- value: 'admin',
- },
- {
- label: 'User',
- value: 'jack',
- },
-];
+const loginRef = ref();
+const verifyRef = ref();
+
+const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
+
+/** 获取租户列表,并默认选中 */
+const tenantList = ref([]); // 租户列表
+async function fetchTenantList() {
+ if (!tenantEnable) {
+ return;
+ }
+ try {
+ // 获取租户列表、域名对应租户
+ const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
+ tenantList.value = await getTenantSimpleList();
+
+ // 选中租户:域名 > store 中的租户 > 首个租户
+ let tenantId: null | number = null;
+ const websiteTenant = await websiteTenantPromise;
+ if (websiteTenant?.id) {
+ tenantId = websiteTenant.id;
+ }
+ // 如果没有从域名获取到租户,尝试从 store 中获取
+ if (!tenantId && accessStore.tenantId) {
+ tenantId = accessStore.tenantId;
+ }
+ // 如果还是没有租户,使用列表中的第一个
+ if (!tenantId && tenantList.value?.[0]?.id) {
+ tenantId = tenantList.value[0].id;
+ }
+
+ // 设置选中的租户编号
+ accessStore.setTenantId(tenantId);
+ loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
+ } catch (error) {
+ console.error('获取租户列表失败:', error);
+ }
+}
+
+/** 处理登录 */
+async function handleLogin(values: any) {
+ // 如果开启验证码,则先验证验证码
+ if (captchaEnable) {
+ verifyRef.value.show();
+ return;
+ }
+ // 无验证码,直接登录
+ await authStore.authLogin('username', values);
+}
+
+/** 验证码通过,执行登录 */
+async function handleVerifySuccess({ captchaVerification }: any) {
+ try {
+ await authStore.authLogin('username', {
+ ...(await loginRef.value.getFormApi().getValues()),
+ captchaVerification,
+ });
+ } catch (error) {
+ console.error('Error in handleLogin:', error);
+ }
+}
+
+/** 处理第三方登录 */
+const redirect = query?.redirect;
+async function handleThirdLogin(type: number) {
+ if (type <= 0) {
+ return;
+ }
+ try {
+ // 计算 redirectUri
+ // tricky: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。配合 social-login.vue#getUrlValue() 使用
+ const redirectUri = `${
+ location.origin
+ }/auth/social-login?${encodeURIComponent(
+ `type=${type}&redirect=${redirect || '/'}`,
+ )}`;
+
+ // 进行跳转
+ window.location.href = await socialAuthRedirect(type, redirectUri);
+ } catch (error) {
+ console.error('第三方登录处理失败:', error);
+ }
+}
+
+/** 组件挂载时获取租户信息 */
+onMounted(() => {
+ fetchTenantList();
+});
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
componentProps: {
- options: MOCK_USER_OPTIONS,
- placeholder: $t('authentication.selectAccount'),
+ options: tenantList.value.map((item) => ({
+ label: item.name,
+ value: item.id.toString(),
+ })),
+ placeholder: $t('authentication.tenantTip'),
+ },
+ fieldName: 'tenantId',
+ label: $t('authentication.tenant'),
+ rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
+ dependencies: {
+ triggerFields: ['tenantId'],
+ if: tenantEnable,
+ trigger(values) {
+ if (values.tenantId) {
+ accessStore.setTenantId(Number(values.tenantId));
+ }
+ },
},
- fieldName: 'selectAccount',
- label: $t('authentication.selectAccount'),
- rules: z
- .string()
- .min(1, { message: $t('authentication.selectAccount') })
- .optional()
- .default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
- dependencies: {
- trigger(values, form) {
- if (values.selectAccount) {
- const findUser = MOCK_USER_OPTIONS.find(
- (item) => item.value === values.selectAccount,
- );
- if (findUser) {
- form.setValues({
- password: '123456',
- username: findUser.value,
- });
- }
- }
- },
- triggerFields: ['selectAccount'],
- },
fieldName: 'username',
label: $t('authentication.username'),
- rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
+ rules: z
+ .string()
+ .min(1, { message: $t('authentication.usernameTip') })
+ .default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
},
{
component: 'VbenInputPassword',
componentProps: {
- placeholder: $t('authentication.password'),
+ placeholder: $t('authentication.passwordTip'),
},
fieldName: 'password',
label: $t('authentication.password'),
- rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
- },
- {
- component: markRaw(SliderCaptcha),
- fieldName: 'captcha',
- rules: z.boolean().refine((value) => value, {
- message: $t('authentication.verifyRequiredTip'),
- }),
+ rules: z
+ .string()
+ .min(1, { message: $t('authentication.passwordTip') })
+ .default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
},
];
});
-
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/register.vue b/apps/web-antdv-next/src/views/_core/authentication/register.vue
index 8c4295310..734f4c27d 100644
--- a/apps/web-antdv-next/src/views/_core/authentication/register.vue
+++ b/apps/web-antdv-next/src/views/_core/authentication/register.vue
@@ -1,18 +1,126 @@
-
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/social-login.vue b/apps/web-antdv-next/src/views/_core/authentication/social-login.vue
new file mode 100644
index 000000000..1052a9a18
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/social-login.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/authentication/sso-login.vue b/apps/web-antdv-next/src/views/_core/authentication/sso-login.vue
new file mode 100644
index 000000000..aba72ac83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/authentication/sso-login.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+ {{ `${client.name} 👋🏻` }}
+
+
+
+ 此第三方应用请求获得以下权限:
+
+
+
+
+
+
+
+
+ 同意授权
+
+
+ 拒绝
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/base-setting.vue b/apps/web-antdv-next/src/views/_core/profile/base-setting.vue
index aa8a4c260..0a10f1304 100644
--- a/apps/web-antdv-next/src/views/_core/profile/base-setting.vue
+++ b/apps/web-antdv-next/src/views/_core/profile/base-setting.vue
@@ -1,65 +1,102 @@
-
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/index.vue b/apps/web-antdv-next/src/views/_core/profile/index.vue
index 8740894e3..712044425 100644
--- a/apps/web-antdv-next/src/views/_core/profile/index.vue
+++ b/apps/web-antdv-next/src/views/_core/profile/index.vue
@@ -1,49 +1,67 @@
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/modules/base-info.vue b/apps/web-antdv-next/src/views/_core/profile/modules/base-info.vue
new file mode 100644
index 000000000..d2d759033
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/modules/base-info.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/modules/profile-user.vue b/apps/web-antdv-next/src/views/_core/profile/modules/profile-user.vue
new file mode 100644
index 000000000..57384e3ee
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/modules/profile-user.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 用户账号
+
+
+ {{ profile.username }}
+
+
+
+
+
+ 所属角色
+
+
+ {{ profile.roles.map((role) => role.name).join(',') }}
+
+
+
+
+
+ 手机号码
+
+
+ {{ profile.mobile }}
+
+
+
+
+
+ 用户邮箱
+
+
+ {{ profile.email }}
+
+
+
+
+
+ 所属部门
+
+
+ {{ profile.dept?.name }}
+
+
+
+
+
+ 所属岗位
+
+
+ {{
+ profile.posts && profile.posts.length > 0
+ ? profile.posts.map((post) => post.name).join(',')
+ : '-'
+ }}
+
+
+
+
+
+ 创建时间
+
+
+ {{ formatDateTime(profile.createTime) }}
+
+
+
+
+
+ 登录时间
+
+
+ {{ formatDateTime(profile.loginDate) }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/modules/reset-pwd.vue b/apps/web-antdv-next/src/views/_core/profile/modules/reset-pwd.vue
new file mode 100644
index 000000000..e7a0103fb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/modules/reset-pwd.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/modules/user-social.vue b/apps/web-antdv-next/src/views/_core/profile/modules/user-social.vue
new file mode 100644
index 000000000..ddb806567
--- /dev/null
+++ b/apps/web-antdv-next/src/views/_core/profile/modules/user-social.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
+
+
+
+ {{ item.socialUser?.nickname || item.socialUser?.openid }}
+
+
+ 绑定
+ {{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
+ 账号
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/_core/profile/password-setting.vue b/apps/web-antdv-next/src/views/_core/profile/password-setting.vue
index adb065a23..e5609c0b6 100644
--- a/apps/web-antdv-next/src/views/_core/profile/password-setting.vue
+++ b/apps/web-antdv-next/src/views/_core/profile/password-setting.vue
@@ -5,7 +5,7 @@ import { computed } from 'vue';
import { ProfilePasswordSetting, z } from '@vben/common-ui';
-import { message } from 'antdv-next';
+import { message } from 'ant-design-vue';
const formSchema = computed((): VbenFormSchema[] => {
return [
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/data.ts b/apps/web-antdv-next/src/views/ai/chat/index/data.ts
new file mode 100644
index 000000000..3fa7928a6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/data.ts
@@ -0,0 +1,74 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { AiModelTypeEnum } from '@vben/constants';
+
+import { getModelSimpleList } from '#/api/ai/model/model';
+
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'systemMessage',
+ label: '角色设定',
+ component: 'Textarea',
+ componentProps: {
+ rows: 4,
+ placeholder: '请输入角色设定',
+ },
+ },
+ {
+ component: 'ApiSelect',
+ fieldName: 'modelId',
+ label: '模型',
+ componentProps: {
+ api: () => getModelSimpleList(AiModelTypeEnum.CHAT),
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择模型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'temperature',
+ label: '温度参数',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入温度参数',
+ precision: 2,
+ min: 0,
+ max: 2,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'maxTokens',
+ label: '回复数 Token 数',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入回复数 Token 数',
+ min: 0,
+ max: 8192,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'maxContexts',
+ label: '上下文数量',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入上下文数量',
+ min: 0,
+ max: 20,
+ },
+ rules: 'required',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/index.vue b/apps/web-antdv-next/src/views/ai/chat/index/index.vue
new file mode 100644
index 000000000..4a42447c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/index.vue
@@ -0,0 +1,676 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activeConversation?.title ? activeConversation?.title : '对话' }}
+
+ ({{ activeMessageList.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/conversation/list.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/conversation/list.vue
new file mode 100644
index 000000000..f150e62f5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/conversation/list.vue
@@ -0,0 +1,435 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ conversationKey }}
+
+
+
+
+
+
+
+
+
+ {{ conversation.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 角色仓库
+
+
+
+ 清空未置顶对话
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/conversation/update-form.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/conversation/update-form.vue
new file mode 100644
index 000000000..fda49d734
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/conversation/update-form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/file-upload.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/file-upload.vue
new file mode 100644
index 000000000..3d5e27fb0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/file-upload.vue
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/files.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/files.vue
new file mode 100644
index 000000000..ed163a026
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/files.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+ {{ getFileNameFromUrl(url) }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/knowledge.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/knowledge.vue
new file mode 100644
index 000000000..960188c04
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/knowledge.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+ 知识引用
+
+
+
+
+ {{ doc.title }}
+
+ ({{ doc.segments.length }} 条)
+
+
+
+
+
+
+
+
+ {{ document?.title }}
+
+
+
+ 分段 {{ segment.id }}
+
+
+ {{ segment.content }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/list-empty.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/list-empty.vue
new file mode 100644
index 000000000..f97a054b6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/list-empty.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
芋道 AI
+
+
+
+ {{ prompt.prompt }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/list.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/list.vue
new file mode 100644
index 000000000..a841ee29b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/list.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(item.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(item.createTime) }}
+
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/loading.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/loading.vue
new file mode 100644
index 000000000..5928a19a1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/loading.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/new-conversation.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/new-conversation.vue
new file mode 100644
index 000000000..41c08ce64
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/new-conversation.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ 点击下方按钮,开始你的对话吧
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/reasoning.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/reasoning.vue
new file mode 100644
index 000000000..de3175daa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/reasoning.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+ {{ titleText }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/message/web-search.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/web-search.vue
new file mode 100644
index 000000000..d48a94c24
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/message/web-search.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+ 联网搜索结果 ({{ webSearchPages.length }} 条)
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+ {{ page.name }}
+
+
+
+ {{ page.title }}
+
+
+
+ {{ page.snippet }}
+
+
+
+ {{ page.url }}
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+ {{ selectedResult.title }}
+
+
+ {{ selectedResult.name }}
+
+
+ {{ selectedResult.url }}
+
+
+
+
+
+
+
+
简短描述
+
+ {{ selectedResult.snippet }}
+
+
+
+
+
内容摘要
+
+ {{ selectedResult.summary }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/role/category-list.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/role/category-list.vue
new file mode 100644
index 000000000..91f6d6409
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/role/category-list.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/role/list.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/role/list.vue
new file mode 100644
index 000000000..544dfb082
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/role/list.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ role.name }}
+
+
+
+
+
+ {{ role.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/index/modules/role/repository.vue b/apps/web-antdv-next/src/views/ai/chat/index/modules/role/repository.vue
new file mode 100644
index 000000000..e9236a6a5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/index/modules/role/repository.vue
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/manager/data.ts b/apps/web-antdv-next/src/views/ai/chat/manager/data.ts
new file mode 100644
index 000000000..f8dcb1fa7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/manager/data.ts
@@ -0,0 +1,223 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let userList: SystemUserApi.User[] = [];
+getSimpleUserList().then((data) => (userList = data));
+
+/** 列表的搜索表单 */
+export function useGridFormSchemaConversation(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '聊天标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入聊天标题',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumnsConversation(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '对话编号',
+ fixed: 'left',
+ minWidth: 180,
+ },
+ {
+ field: 'title',
+ title: '对话标题',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ title: '用户',
+ minWidth: 180,
+ field: 'userId',
+ formatter: ({ cellValue }) => {
+ if (cellValue === 0) {
+ return '系统';
+ }
+ return userList.find((user) => user.id === cellValue)?.nickname || '-';
+ },
+ },
+ {
+ field: 'roleName',
+ title: '角色',
+ minWidth: 180,
+ },
+ {
+ field: 'model',
+ title: '模型标识',
+ minWidth: 180,
+ },
+ {
+ field: 'messageCount',
+ title: '消息数',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'temperature',
+ title: '温度参数',
+ minWidth: 80,
+ },
+ {
+ title: '回复数 Token 数',
+ field: 'maxTokens',
+ minWidth: 120,
+ },
+ {
+ title: '上下文数量',
+ field: 'maxContexts',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchemaMessage(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'conversationId',
+ label: '对话编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入对话编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumnsMessage(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '消息编号',
+ fixed: 'left',
+ minWidth: 180,
+ },
+ {
+ field: 'conversationId',
+ title: '对话编号',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ title: '用户',
+ minWidth: 180,
+ field: 'userId',
+ formatter: ({ cellValue }) =>
+ userList.find((user) => user.id === cellValue)?.nickname || '-',
+ },
+ {
+ field: 'roleName',
+ title: '角色',
+ minWidth: 180,
+ },
+ {
+ field: 'type',
+ title: '消息类型',
+ minWidth: 100,
+ },
+ {
+ field: 'model',
+ title: '模型标识',
+ minWidth: 180,
+ },
+ {
+ field: 'content',
+ title: '消息内容',
+ minWidth: 300,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'replyId',
+ title: '回复消息编号',
+ minWidth: 180,
+ },
+ {
+ title: '携带上下文',
+ field: 'useContext',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ minWidth: 100,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/chat/manager/index.vue b/apps/web-antdv-next/src/views/ai/chat/manager/index.vue
new file mode 100644
index 000000000..69c4e844b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/manager/index.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/manager/modules/conversation-list.vue b/apps/web-antdv-next/src/views/ai/chat/manager/modules/conversation-list.vue
new file mode 100644
index 000000000..6bfe70216
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/manager/modules/conversation-list.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/chat/manager/modules/message-list.vue b/apps/web-antdv-next/src/views/ai/chat/manager/modules/message-list.vue
new file mode 100644
index 000000000..bf6bd5747
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/chat/manager/modules/message-list.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/index.vue b/apps/web-antdv-next/src/views/ai/image/index/index.vue
new file mode 100644
index 000000000..5ddc5c8bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/index.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/card.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/card.vue
new file mode 100644
index 000000000..57b12e655
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/card.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ detail?.errorMessage }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/common/index.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/common/index.vue
new file mode 100644
index 000000000..bd1098775
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/common/index.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
画面描述
+
建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开
+
+
+
+
+
+ 随机热词
+
+
+
+
+
+
+
+
+ 平台
+
+
+
+
+
+
+
+
+ 模型
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/dall3/index.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/dall3/index.vue
new file mode 100644
index 000000000..7da66fd72
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/dall3/index.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
画面描述
+
建议使用"形容词 + 动词 + 风格"的格式,使用","隔开
+
+
+
+
+
随机热词
+
+
+
+
+
+
+
模型选择
+
+
+
+
+ {{ model.name }}
+
+
+
+
+
+
+
风格选择
+
+
+
+
+ {{ imageStyle.name }}
+
+
+
+
+
+
+
画面比例
+
+
+
+
+ {{ imageSize.name }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/detail.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/detail.vue
new file mode 100644
index 000000000..30a8c4f7d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/detail.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
时间
+
+
提交时间:{{ formatDateTime(detail.createTime) }}
+
生成时间:{{ formatDateTime(detail.finishTime) }}
+
+
+
+
+
+
模型
+
+ {{ detail.model }}({{ detail.height }}x{{ detail.width }})
+
+
+
+
+
+
提示词
+
+ {{ detail.prompt }}
+
+
+
+
+
+
图片地址
+
+ {{ detail.picUrl }}
+
+
+
+
+
+
采样方法
+
+ {{
+ StableDiffusionSamplers.find(
+ (item) => item.key === detail?.options?.sampler,
+ )?.name
+ }}
+
+
+
+
+
CLIP
+
+ {{
+ StableDiffusionClipGuidancePresets.find(
+ (item) => item.key === detail?.options?.clipGuidancePreset,
+ )?.name
+ }}
+
+
+
+
+
风格
+
+ {{
+ StableDiffusionStylePresets.find(
+ (item) => item.key === detail?.options?.stylePreset,
+ )?.name
+ }}
+
+
+
+
+
迭代步数
+
{{ detail?.options?.steps }}
+
+
+
+
引导系数
+
{{ detail?.options?.scale }}
+
+
+
+
随机因子
+
{{ detail?.options?.seed }}
+
+
+
+
+
风格选择
+
+ {{
+ Dall3StyleList.find((item) => item.key === detail?.options?.style)?.name
+ }}
+
+
+
+
+
+
模型版本
+
{{ detail?.options?.version }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/list.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/list.vue
new file mode 100644
index 000000000..339ddf8f4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/list.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+ 绘画任务
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/midjourney/index.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/midjourney/index.vue
new file mode 100644
index 000000000..3078f295e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/midjourney/index.vue
@@ -0,0 +1,254 @@
+
+
+
+
+
画面描述
+
建议使用“形容词+动词+风格”的格式,使用“,”隔开.
+
+
+
+
+
随机热词
+
+
+
+
+
+
+
尺寸
+
+
+
+
{{ imageSize.key }}
+
+
+
+
+
+
+
+
版本
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/index/modules/stable-diffusion/index.vue b/apps/web-antdv-next/src/views/ai/image/index/modules/stable-diffusion/index.vue
new file mode 100644
index 000000000..1cfe6f288
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/index/modules/stable-diffusion/index.vue
@@ -0,0 +1,301 @@
+
+
+
+
+
画面描述
+
建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开
+
+
+
+
+
+
随机热词
+
+
+
+
+
+
+
+
采样方法
+
+
+
+
+
+
+
+
CLIP
+
+
+
+
+
+
+
+
风格
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/manager/data.ts b/apps/web-antdv-next/src/views/ai/image/manager/data.ts
new file mode 100644
index 000000000..7bd63bb8a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/manager/data.ts
@@ -0,0 +1,180 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+let userList: SystemUserApi.User[] = [];
+async function getUserData() {
+ userList = await getSimpleUserList();
+}
+
+getUserData();
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'platform',
+ label: '平台',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择平台',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '绘画状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择绘画状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.AI_IMAGE_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'publicStatus',
+ label: '是否发布',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择是否发布',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onPublicStatusChange?: (
+ newStatus: boolean,
+ row: any,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ field: 'picUrl',
+ title: '图片',
+ minWidth: 110,
+ fixed: 'left',
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'userId',
+ title: '用户',
+ minWidth: 180,
+ formatter: ({ cellValue }) =>
+ userList.find((user) => user.id === cellValue)?.nickname || '-',
+ },
+ {
+ field: 'platform',
+ title: '平台',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_PLATFORM },
+ },
+ },
+ {
+ field: 'model',
+ title: '模型',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '绘画状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_IMAGE_STATUS },
+ },
+ },
+ {
+ minWidth: 100,
+ title: '是否发布',
+ field: 'publicStatus',
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onPublicStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: true,
+ unCheckedValue: false,
+ },
+ },
+ },
+ {
+ field: 'prompt',
+ title: '提示词',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'width',
+ title: '宽度',
+ minWidth: 180,
+ },
+ {
+ field: 'height',
+ title: '高度',
+ minWidth: 180,
+ },
+ {
+ field: 'errorMessage',
+ title: '错误信息',
+ minWidth: 180,
+ },
+ {
+ field: 'taskId',
+ title: '任务编号',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/image/manager/index.vue b/apps/web-antdv-next/src/views/ai/image/manager/index.vue
new file mode 100644
index 000000000..62087c743
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/manager/index.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/image/square/index.vue b/apps/web-antdv-next/src/views/ai/image/square/index.vue
new file mode 100644
index 000000000..6bddebe1c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/image/square/index.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/document/data.ts b/apps/web-antdv-next/src/views/ai/knowledge/document/data.ts
new file mode 100644
index 000000000..46f251a18
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/document/data.ts
@@ -0,0 +1,178 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { AiKnowledgeDocumentApi } from '#/api/ai/knowledge/document';
+
+import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getModelSimpleList } from '#/api/ai/model/model';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '知识库名称',
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '知识库描述',
+ component: 'Textarea',
+ componentProps: {
+ rows: 3,
+ placeholder: '请输入知识库描述',
+ },
+ },
+ {
+ component: 'ApiSelect',
+ fieldName: 'embeddingModelId',
+ label: '向量模型',
+ componentProps: {
+ api: () => getModelSimpleList(AiModelTypeEnum.EMBEDDING),
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择向量模型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'topK',
+ label: '检索 topK',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入检索 topK',
+ min: 0,
+ max: 10,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'similarityThreshold',
+ label: '检索相似度阈值',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入检索相似度阈值',
+ min: 0,
+ max: 1,
+ step: 0.01,
+ precision: 2,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '是否启用',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '文件名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文件名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '是否启用',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择是否启用',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: AiKnowledgeDocumentApi.KnowledgeDocument,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '文档编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '文件名称',
+ minWidth: 200,
+ },
+ {
+ field: 'contentLength',
+ title: '字符数',
+ minWidth: 100,
+ },
+ {
+ field: 'tokens',
+ title: 'Token 数',
+ minWidth: 100,
+ },
+ {
+ field: 'segmentMaxTokens',
+ title: '分片最大 Token 数',
+ minWidth: 150,
+ },
+ {
+ field: 'retrievalCount',
+ title: '召回次数',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '是否启用',
+ minWidth: 100,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: CommonStatusEnum.ENABLE,
+ unCheckedValue: CommonStatusEnum.DISABLE,
+ },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '上传时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ minWidth: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/document/form/index.vue b/apps/web-antdv-next/src/views/ai/knowledge/document/form/index.vue
new file mode 100644
index 000000000..297c4643a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/document/form/index.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formData.id ? '编辑知识库文档' : '创建知识库文档' }}
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+ {{ step.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/process-step.vue b/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/process-step.vue
new file mode 100644
index 000000000..7f8579ebf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/process-step.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+
+
+
+
+
+ 分段数量:{{ file.count ? file.count : '-' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/split-step.vue b/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/split-step.vue
new file mode 100644
index 000000000..b0c32d2d7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/split-step.vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+
+ 分段设置
+
+
+ 系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
分段预览
+
+
+
+
+
+ {{ currentFile?.name || '请选择文件' }}
+
+ ({{ currentFile.segments.length }}个分片)
+
+
+
+
+
+
+
+
暂无上传文件
+
+
+
+
+
+ 正在加载分段内容...
+
+
+
+
+ 分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
+ {{ segment.tokens || 0 }} Token
+
+
+ {{ segment.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/upload-step.vue b/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/upload-step.vue
new file mode 100644
index 000000000..8ee05655c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/document/form/modules/upload-step.vue
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+ 拖拽文件至此,或者
+
+ 选择文件
+
+
+
+ 已支持 {{ supportedFileTypes.join('、') }},每个文件不超过
+ {{ maxFileSize }} MB。
+
+
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/document/index.vue b/apps/web-antdv-next/src/views/ai/knowledge/document/index.vue
new file mode 100644
index 000000000..e1dd551fa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/document/index.vue
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/knowledge/data.ts b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/data.ts
new file mode 100644
index 000000000..fcfa96da2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/data.ts
@@ -0,0 +1,170 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getModelSimpleList } from '#/api/ai/model/model';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '知识库名称',
+ componentProps: {
+ placeholder: '请输入知识库名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '知识库描述',
+ component: 'Textarea',
+ componentProps: {
+ rows: 3,
+ placeholder: '请输入知识库描述',
+ },
+ },
+ {
+ component: 'ApiSelect',
+ fieldName: 'embeddingModelId',
+ label: '向量模型',
+ componentProps: {
+ api: () => getModelSimpleList(AiModelTypeEnum.EMBEDDING),
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择向量模型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'topK',
+ label: '检索 topK',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入检索 topK',
+ min: 0,
+ max: 10,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'similarityThreshold',
+ label: '检索相似度阈值',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入检索相似度阈值',
+ min: 0,
+ max: 1,
+ step: 0.01,
+ precision: 2,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '是否启用',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '知识库名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入知识库名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '是否启用',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择是否启用',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '知识库名称',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '知识库描述',
+ minWidth: 200,
+ },
+ {
+ field: 'embeddingModel',
+ title: '向量化模型',
+ minWidth: 150,
+ },
+ {
+ field: 'status',
+ title: '是否启用',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/knowledge/index.vue b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/index.vue
new file mode 100644
index 000000000..6f9d1fdf9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/index.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/knowledge/modules/form.vue b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/modules/form.vue
new file mode 100644
index 000000000..73211ebd3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/knowledge/retrieval/index.vue b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/retrieval/index.vue
new file mode 100644
index 000000000..f7b4e4fab
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/knowledge/retrieval/index.vue
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+ 召回测试
+
+
+ 根据给定的查询文本测试召回效果。
+
+
+
+
+
+
+ {{ queryParams.content?.length }} / 200
+
+
+
+ topK:
+
+
+
+ 相似度:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ segments.length }} 个召回段落
+
+
+
+
+ 分段({{ segment.id }}) · {{ segment.contentLength }} 字符数 ·
+ {{ segment.tokens }} Token
+
+
+ score: {{ segment.score }}
+
+
+
+ {{ segment.content }}
+
+
+
+
+ {{ segment.documentName || '未知文档' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/segment/data.ts b/apps/web-antdv-next/src/views/ai/knowledge/segment/data.ts
new file mode 100644
index 000000000..109adda2c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/segment/data.ts
@@ -0,0 +1,130 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { AiKnowledgeSegmentApi } from '#/api/ai/knowledge/segment';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'documentId',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'content',
+ label: '切片内容',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入切片内容',
+ rows: 6,
+ showCount: true,
+ },
+ rules: 'required',
+ },
+ ];
+}
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'documentId',
+ label: '文档编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文档编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '是否启用',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择是否启用',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: AiKnowledgeSegmentApi.KnowledgeSegment,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '分段编号',
+ minWidth: 100,
+ },
+ {
+ type: 'expand',
+ width: 40,
+ slots: { content: 'expand_content' },
+ },
+ {
+ field: 'content',
+ title: '切片内容',
+ minWidth: 250,
+ },
+ {
+ field: 'contentLength',
+ title: '字符数',
+ minWidth: 100,
+ },
+ {
+ field: 'tokens',
+ title: 'token 数量',
+ minWidth: 120,
+ },
+ {
+ field: 'retrievalCount',
+ title: '召回次数',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: CommonStatusEnum.ENABLE,
+ unCheckedValue: CommonStatusEnum.DISABLE,
+ },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/segment/index.vue b/apps/web-antdv-next/src/views/ai/knowledge/segment/index.vue
new file mode 100644
index 000000000..363e6b1ad
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/segment/index.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
完整内容:
+ {{ row.content }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/knowledge/segment/modules/form.vue b/apps/web-antdv-next/src/views/ai/knowledge/segment/modules/form.vue
new file mode 100644
index 000000000..9b656fa0c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/knowledge/segment/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/mindmap/index/index.vue b/apps/web-antdv-next/src/views/ai/mindmap/index/index.vue
new file mode 100644
index 000000000..325268661
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/mindmap/index/index.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/mindmap/index/modules/left.vue b/apps/web-antdv-next/src/views/ai/mindmap/index/modules/left.vue
new file mode 100644
index 000000000..fb8504d67
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/mindmap/index/modules/left.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ 思维导图创作中心
+
+
+
+ 您的需求?
+
+
+
+
+ 使用已有内容生成?
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/mindmap/index/modules/right.vue b/apps/web-antdv-next/src/views/ai/mindmap/index/modules/right.vue
new file mode 100644
index 000000000..3e03e3735
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/mindmap/index/modules/right.vue
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
思维导图预览
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/mindmap/manager/data.ts b/apps/web-antdv-next/src/views/ai/mindmap/manager/data.ts
new file mode 100644
index 000000000..967981d40
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/mindmap/manager/data.ts
@@ -0,0 +1,97 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let userList: SystemUserApi.User[] = [];
+getSimpleUserList().then((data) => (userList = data));
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择用户',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'prompt',
+ label: '提示词',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入提示词',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ field: 'userId',
+ title: '用户',
+ minWidth: 180,
+ formatter: ({ cellValue }) =>
+ userList.find((user) => user.id === cellValue)?.nickname || '-',
+ },
+ {
+ field: 'prompt',
+ title: '提示词',
+ minWidth: 180,
+ },
+ {
+ field: 'generatedContent',
+ title: '思维导图',
+ minWidth: 300,
+ },
+ {
+ field: 'model',
+ title: '模型',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'errorMessage',
+ title: '错误信息',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/mindmap/manager/index.vue b/apps/web-antdv-next/src/views/ai/mindmap/manager/index.vue
new file mode 100644
index 000000000..5cfc0d1c4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/mindmap/manager/index.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/apiKey/data.ts b/apps/web-antdv-next/src/views/ai/model/apiKey/data.ts
new file mode 100644
index 000000000..47b36d59f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/apiKey/data.ts
@@ -0,0 +1,149 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'platform',
+ label: '所属平台',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择所属平台',
+ options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入名称',
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'apiKey',
+ label: '密钥',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入密钥',
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'url',
+ label: '自定义 API URL',
+ componentProps: {
+ placeholder: '请输入自定义 API URL',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'platform',
+ label: '平台',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择平台',
+ options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择状态',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'platform',
+ title: '所属平台',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_PLATFORM },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '名称',
+ minWidth: 120,
+ },
+ {
+ field: 'apiKey',
+ title: '密钥',
+ minWidth: 140,
+ },
+ {
+ field: 'url',
+ title: '自定义 API URL',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ minWidth: 80,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/model/apiKey/index.vue b/apps/web-antdv-next/src/views/ai/model/apiKey/index.vue
new file mode 100644
index 000000000..1746ba49e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/apiKey/index.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/apiKey/modules/form.vue b/apps/web-antdv-next/src/views/ai/model/apiKey/modules/form.vue
new file mode 100644
index 000000000..21f21f305
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/apiKey/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/chatRole/data.ts b/apps/web-antdv-next/src/views/ai/model/chatRole/data.ts
new file mode 100644
index 000000000..81ea7e7c1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/chatRole/data.ts
@@ -0,0 +1,320 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleKnowledgeList } from '#/api/ai/knowledge/knowledge';
+import { getModelSimpleList } from '#/api/ai/model/model';
+import { getToolSimpleList } from '#/api/ai/model/tool';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'formType',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '角色名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入角色名称',
+ },
+ },
+ {
+ component: 'ImageUpload',
+ fieldName: 'avatar',
+ label: '角色头像',
+ rules: 'required',
+ },
+ {
+ fieldName: 'modelId',
+ label: '绑定模型',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择绑定模型',
+ api: () => getModelSimpleList(AiModelTypeEnum.CHAT),
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['formType'],
+ show: (values) => {
+ return values.formType === 'create' || values.formType === 'update';
+ },
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'category',
+ label: '角色类别',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入角色类别',
+ },
+ dependencies: {
+ triggerFields: ['formType'],
+ show: (values) => {
+ return values.formType === 'create' || values.formType === 'update';
+ },
+ },
+ },
+ {
+ component: 'Textarea',
+ fieldName: 'description',
+ label: '角色描述',
+ componentProps: {
+ placeholder: '请输入角色描述',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'systemMessage',
+ label: '角色设定',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入角色设定',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'knowledgeIds',
+ label: '引用知识库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择引用知识库',
+ api: getSimpleKnowledgeList,
+ labelField: 'name',
+ mode: 'multiple',
+ valueField: 'id',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'toolIds',
+ label: '引用工具',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择引用工具',
+ api: getToolSimpleList,
+ mode: 'multiple',
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mcpClientNames',
+ label: '引用 MCP',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择 MCP',
+ options: getDictOptions(DICT_TYPE.AI_MCP_CLIENT_NAME, 'string'),
+ mode: 'multiple',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'publicStatus',
+ label: '是否公开',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: true,
+ dependencies: {
+ triggerFields: ['formType'],
+ show: (values) => {
+ return values.formType === 'create' || values.formType === 'update';
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '角色排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入角色排序',
+ },
+ dependencies: {
+ triggerFields: ['formType'],
+ show: (values) => {
+ return values.formType === 'create' || values.formType === 'update';
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ dependencies: {
+ triggerFields: ['formType'],
+ show: (values) => {
+ return values.formType === 'create' || values.formType === 'update';
+ },
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '角色名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入角色名称',
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '角色类别',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入角色类别',
+ },
+ },
+ {
+ fieldName: 'publicStatus',
+ label: '是否公开',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择是否公开',
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ allowClear: true,
+ },
+ defaultValue: true,
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '角色名称',
+ minWidth: 100,
+ },
+ {
+ title: '绑定模型',
+ field: 'modelName',
+ minWidth: 100,
+ },
+ {
+ title: '角色头像',
+ field: 'avatar',
+ minWidth: 140,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ width: 40,
+ height: 40,
+ },
+ },
+ },
+ {
+ title: '角色类别',
+ field: 'category',
+ minWidth: 100,
+ },
+ {
+ title: '角色描述',
+ field: 'description',
+ minWidth: 100,
+ },
+ {
+ title: '角色设定',
+ field: 'systemMessage',
+ minWidth: 100,
+ },
+ {
+ title: '知识库',
+ field: 'knowledgeIds',
+ minWidth: 100,
+ formatter: ({ cellValue }) => {
+ return !cellValue || cellValue.length === 0
+ ? '-'
+ : `引用${cellValue.length}个`;
+ },
+ },
+ {
+ title: '工具',
+ field: 'toolIds',
+ minWidth: 100,
+ formatter: ({ cellValue }) => {
+ return !cellValue || cellValue.length === 0
+ ? '-'
+ : `引用${cellValue.length}个`;
+ },
+ },
+ {
+ title: 'MCP',
+ field: 'mcpClientNames',
+ minWidth: 100,
+ formatter: ({ cellValue }) => {
+ return !cellValue || cellValue.length === 0
+ ? '-'
+ : `引用${cellValue.length}个`;
+ },
+ },
+ {
+ field: 'publicStatus',
+ title: '是否公开',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ minWidth: 80,
+ },
+ {
+ title: '角色排序',
+ field: 'sort',
+ minWidth: 80,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/model/chatRole/index.vue b/apps/web-antdv-next/src/views/ai/model/chatRole/index.vue
new file mode 100644
index 000000000..bbb940f70
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/chatRole/index.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/chatRole/modules/form.vue b/apps/web-antdv-next/src/views/ai/model/chatRole/modules/form.vue
new file mode 100644
index 000000000..e5abac2b7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/chatRole/modules/form.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/model/data.ts b/apps/web-antdv-next/src/views/ai/model/model/data.ts
new file mode 100644
index 000000000..1ef554c59
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/model/data.ts
@@ -0,0 +1,266 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
+
+import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getApiKeySimpleList } from '#/api/ai/model/apiKey';
+
+/** 关联数据 */
+let apiKeyList: AiModelApiKeyApi.ApiKey[] = [];
+getApiKeySimpleList().then((data) => (apiKeyList = data));
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'platform',
+ label: '所属平台',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择所属平台',
+ options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '模型类型',
+ component: 'Select',
+ componentProps: (values) => {
+ return {
+ placeholder: '请输入模型类型',
+ disabled: !!values.id,
+ options: getDictOptions(DICT_TYPE.AI_MODEL_TYPE, 'number'),
+ allowClear: true,
+ };
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'keyId',
+ label: 'API 秘钥',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择 API 秘钥',
+ api: getApiKeySimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '模型名字',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入模型名字',
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'model',
+ label: '模型标识',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入模型标识',
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '模型排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入模型排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'temperature',
+ label: '温度参数',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入温度参数',
+ min: 0,
+ max: 2,
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [AiModelTypeEnum.CHAT].includes(values.type);
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'maxTokens',
+ label: '回复数 Token 数',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ max: 8192,
+ placeholder: '请输入回复数 Token 数',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [AiModelTypeEnum.CHAT].includes(values.type);
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'maxContexts',
+ label: '上下文数量',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ max: 20,
+ placeholder: '请输入上下文数量',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [AiModelTypeEnum.CHAT].includes(values.type);
+ },
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '模型名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模型名字',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'model',
+ label: '模型标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模型标识',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'platform',
+ label: '模型平台',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模型平台',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'platform',
+ title: '所属平台',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_PLATFORM },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'type',
+ title: '模型类型',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_MODEL_TYPE },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '模型名字',
+ minWidth: 180,
+ },
+ {
+ title: '模型标识',
+ field: 'model',
+ minWidth: 180,
+ },
+ {
+ title: 'API 秘钥',
+ field: 'keyId',
+ formatter: ({ cellValue }) => {
+ return (
+ apiKeyList.find((apiKey) => apiKey.id === cellValue)?.name || '-'
+ );
+ },
+ minWidth: 140,
+ },
+ {
+ title: '排序',
+ field: 'sort',
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'temperature',
+ title: '温度参数',
+ minWidth: 100,
+ },
+ {
+ title: '回复数 Token 数',
+ field: 'maxTokens',
+ minWidth: 140,
+ },
+ {
+ title: '上下文数量',
+ field: 'maxContexts',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/model/model/index.vue b/apps/web-antdv-next/src/views/ai/model/model/index.vue
new file mode 100644
index 000000000..158d7b43a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/model/index.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/model/modules/form.vue b/apps/web-antdv-next/src/views/ai/model/model/modules/form.vue
new file mode 100644
index 000000000..3a47c5910
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/model/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/tool/data.ts b/apps/web-antdv-next/src/views/ai/model/tool/data.ts
new file mode 100644
index 000000000..0c9f020f5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/tool/data.ts
@@ -0,0 +1,125 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '工具名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入工具名称',
+ },
+ },
+ {
+ component: 'Textarea',
+ fieldName: 'description',
+ label: '工具描述',
+ componentProps: {
+ placeholder: '请输入工具描述',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: CommonStatusEnum.ENABLE,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '工具名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入工具名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '工具编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '工具名称',
+ minWidth: 120,
+ },
+ {
+ field: 'description',
+ title: '工具描述',
+ minWidth: 140,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/model/tool/index.vue b/apps/web-antdv-next/src/views/ai/model/tool/index.vue
new file mode 100644
index 000000000..a6c27cd85
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/tool/index.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/model/tool/modules/form.vue b/apps/web-antdv-next/src/views/ai/model/tool/modules/form.vue
new file mode 100644
index 000000000..3e3c90e78
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/model/tool/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/index.vue b/apps/web-antdv-next/src/views/ai/music/index/index.vue
new file mode 100644
index 000000000..af00e1819
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/index.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/list/audioBar/index.vue b/apps/web-antdv-next/src/views/ai/music/index/list/audioBar/index.vue
new file mode 100644
index 000000000..351dcac3e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/list/audioBar/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
{{ currentSong.name }}
+
{{ currentSong.singer }}
+
+
+
+
+
+
+
+
+ {{ audioProps.currentTime }}
+
+ {{ audioProps.duration }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/list/index.vue b/apps/web-antdv-next/src/views/ai/music/index/list/index.vue
new file mode 100644
index 000000000..3076d35b3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/list/index.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/list/songCard/index.vue b/apps/web-antdv-next/src/views/ai/music/index/list/songCard/index.vue
new file mode 100644
index 000000000..723b33793
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/list/songCard/index.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
{{ songInfo.title }}
+
+ {{ songInfo.desc }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/list/songInfo/index.vue b/apps/web-antdv-next/src/views/ai/music/index/list/songInfo/index.vue
new file mode 100644
index 000000000..b041feaa0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/list/songInfo/index.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ {{ currentSong.title }}
+
+ {{ currentSong.desc }}
+
+
+ {{ currentSong.date }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/mode/desc.vue b/apps/web-antdv-next/src/views/ai/music/index/mode/desc.vue
new file mode 100644
index 000000000..c57beb407
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/mode/desc.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/mode/index.vue b/apps/web-antdv-next/src/views/ai/music/index/mode/index.vue
new file mode 100644
index 000000000..35df306da
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/mode/index.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+ 描述模式
+ 歌词模式
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/mode/lyric.vue b/apps/web-antdv-next/src/views/ai/music/index/mode/lyric.vue
new file mode 100644
index 000000000..e8d5e38ad
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/mode/lyric.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ tag }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/index/title/index.vue b/apps/web-antdv-next/src/views/ai/music/index/title/index.vue
new file mode 100644
index 000000000..04dc58bcc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/index/title/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+ {{ desc }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/music/manager/data.ts b/apps/web-antdv-next/src/views/ai/music/manager/data.ts
new file mode 100644
index 000000000..68a96227e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/manager/data.ts
@@ -0,0 +1,202 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let userList: SystemUserApi.User[] = [];
+getSimpleUserList().then((data) => (userList = data));
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '音乐名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入音乐名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '绘画状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择绘画状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.AI_MUSIC_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'generateMode',
+ label: '生成模式',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择生成模式',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.AI_GENERATE_MODE, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'publicStatus',
+ label: '是否发布',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择是否发布',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onPublicStatusChange?: (
+ newStatus: boolean,
+ row: any,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ title: '音乐名称',
+ minWidth: 180,
+ fixed: 'left',
+ field: 'title',
+ },
+ {
+ minWidth: 180,
+ title: '用户',
+ field: 'userId',
+ formatter: ({ cellValue }) => {
+ return userList.find((user) => user.id === cellValue)?.nickname || '-';
+ },
+ },
+ {
+ field: 'status',
+ title: '音乐状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_MUSIC_STATUS },
+ },
+ },
+ {
+ field: 'model',
+ title: '模型',
+ minWidth: 180,
+ },
+ {
+ title: '内容',
+ minWidth: 180,
+ slots: { default: 'content' },
+ },
+ {
+ field: 'duration',
+ title: '时长(秒)',
+ minWidth: 100,
+ },
+ {
+ field: 'prompt',
+ title: '提示词',
+ minWidth: 180,
+ },
+ {
+ field: 'lyric',
+ title: '歌词',
+ minWidth: 180,
+ },
+ {
+ field: 'gptDescriptionPrompt',
+ title: '描述',
+ minWidth: 180,
+ },
+ {
+ field: 'generateMode',
+ title: '生成模式',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_GENERATE_MODE },
+ },
+ },
+ {
+ field: 'tags',
+ title: '风格标签',
+ minWidth: 180,
+ cellRender: {
+ name: 'CellTags',
+ },
+ },
+ {
+ minWidth: 100,
+ title: '是否发布',
+ field: 'publicStatus',
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onPublicStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: true,
+ unCheckedValue: false,
+ },
+ },
+ },
+ {
+ field: 'taskId',
+ title: '任务编号',
+ minWidth: 180,
+ },
+ {
+ field: 'errorMessage',
+ title: '错误信息',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/music/manager/index.vue b/apps/web-antdv-next/src/views/ai/music/manager/index.vue
new file mode 100644
index 000000000..bfd4d4502
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/music/manager/index.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/workflow/data.ts b/apps/web-antdv-next/src/views/ai/workflow/data.ts
new file mode 100644
index 000000000..5decf9f3f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/workflow/data.ts
@@ -0,0 +1,97 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '流程标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入流程标识',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '流程名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入流程名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'code',
+ title: '流程标识',
+ minWidth: 150,
+ },
+ {
+ field: 'name',
+ title: '流程名称',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/workflow/form/index.vue b/apps/web-antdv-next/src/views/ai/workflow/form/index.vue
new file mode 100644
index 000000000..f815483c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/workflow/form/index.vue
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formData.name || '创建AI 工作流' }}
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+ {{ step.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/workflow/form/modules/basic-info.vue b/apps/web-antdv-next/src/views/ai/workflow/form/modules/basic-info.vue
new file mode 100644
index 000000000..a691defaa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/workflow/form/modules/basic-info.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/workflow/form/modules/workflow-design.vue b/apps/web-antdv-next/src/views/ai/workflow/form/modules/workflow-design.vue
new file mode 100644
index 000000000..176b81555
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/workflow/form/modules/workflow-design.vue
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/workflow/index.vue b/apps/web-antdv-next/src/views/ai/workflow/index.vue
new file mode 100644
index 000000000..60e1281bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/workflow/index.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/write/index/index.vue b/apps/web-antdv-next/src/views/ai/write/index/index.vue
new file mode 100644
index 000000000..a907d0dfa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/write/index/index.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/write/index/modules/left.vue b/apps/web-antdv-next/src/views/ai/write/index/modules/left.vue
new file mode 100644
index 000000000..b3eaf66ee
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/write/index/modules/left.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+ {{ text }}
+
+
+
+
+
+ {{ label }}
+
+
+ {{ hint }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/write/index/modules/right.vue b/apps/web-antdv-next/src/views/ai/write/index/modules/right.vue
new file mode 100644
index 000000000..5e0d8e67a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/write/index/modules/right.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+ 预览
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/write/index/modules/tag.vue b/apps/web-antdv-next/src/views/ai/write/index/modules/tag.vue
new file mode 100644
index 000000000..ed758fc0a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/write/index/modules/tag.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ {{ tag.label }}
+
+
+
diff --git a/apps/web-antdv-next/src/views/ai/write/manager/data.ts b/apps/web-antdv-next/src/views/ai/write/manager/data.ts
new file mode 100644
index 000000000..02f39e8e5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/write/manager/data.ts
@@ -0,0 +1,171 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let userList: SystemUserApi.User[] = [];
+getSimpleUserList().then((data) => (userList = data));
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择用户',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '写作类型',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择写作类型',
+ options: getDictOptions(DICT_TYPE.AI_WRITE_TYPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'platform',
+ label: '平台',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择平台',
+ options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ minWidth: 180,
+ title: '用户',
+ field: 'userId',
+ formatter: ({ cellValue }) => {
+ return userList.find((user) => user.id === cellValue)?.nickname || '-';
+ },
+ },
+ {
+ field: 'type',
+ title: '写作类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_WRITE_TYPE },
+ },
+ },
+ {
+ field: 'platform',
+ title: '平台',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_WRITE_TYPE },
+ },
+ },
+ {
+ field: 'model',
+ title: '模型',
+ minWidth: 180,
+ },
+ {
+ field: 'prompt',
+ title: '生成内容提示',
+ minWidth: 180,
+ },
+ {
+ field: 'generatedContent',
+ title: '生成的内容',
+ minWidth: 180,
+ },
+ {
+ field: 'originalContent',
+ title: '原文',
+ minWidth: 180,
+ },
+ {
+ field: 'length',
+ title: '长度',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_WRITE_LENGTH },
+ },
+ },
+ {
+ field: 'format',
+ title: '格式',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_WRITE_FORMAT },
+ },
+ },
+ {
+ field: 'tone',
+ title: '语气',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_WRITE_TONE },
+ },
+ },
+ {
+ field: 'language',
+ title: '语言',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AI_WRITE_LANGUAGE },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'errorMessage',
+ title: '错误信息',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/ai/write/manager/index.vue b/apps/web-antdv-next/src/views/ai/write/manager/index.vue
new file mode 100644
index 000000000..6921f23bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/ai/write/manager/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/category/data.ts b/apps/web-antdv-next/src/views/bpm/category/data.ts
new file mode 100644
index 000000000..14b2b98ed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/category/data.ts
@@ -0,0 +1,178 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '分类名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名',
+ },
+ rules: 'required',
+ },
+ {
+ label: '分类标志',
+ fieldName: 'code',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类标志',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '分类描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入分类描述',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '分类状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'sort',
+ label: '分类排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入分类排序',
+ },
+ },
+ ];
+}
+
+/** 重命名的表单 */
+export function useRenameFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分类名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分类名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '分类标志',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类标志',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '分类状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择分类状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '分类编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '分类名',
+ minWidth: 200,
+ },
+ {
+ field: 'code',
+ title: '分类标志',
+ minWidth: 200,
+ },
+ {
+ field: 'description',
+ title: '分类描述',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '分类状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'sort',
+ title: '分类排序',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/category/index.vue b/apps/web-antdv-next/src/views/bpm/category/index.vue
new file mode 100644
index 000000000..46172f4ce
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/category/index.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/category/modules/form.vue b/apps/web-antdv-next/src/views/bpm/category/modules/form.vue
new file mode 100644
index 000000000..7402f6a6e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/category/modules/form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/category/modules/rename-form.vue b/apps/web-antdv-next/src/views/bpm/category/modules/rename-form.vue
new file mode 100644
index 000000000..e9bc301a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/category/modules/rename-form.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/index.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/index.ts
new file mode 100644
index 000000000..411e2ceba
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/index.ts
@@ -0,0 +1,2 @@
+// 导出 BPMN 流程设计器相关组件
+export * from './package';
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/ProcessDesigner.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/ProcessDesigner.vue
new file mode 100644
index 000000000..add772d32
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/ProcessDesigner.vue
@@ -0,0 +1,677 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/ProcessViewer.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/ProcessViewer.vue
new file mode 100644
index 000000000..eb0dd63cf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/ProcessViewer.vue
@@ -0,0 +1,417 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+
+
+ {{ record.assigneeUser?.nickname || record.ownerUser?.nickname }}
+
+
+
+
+
+ {{ record.assigneeUser?.deptName || record.ownerUser?.deptName }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatPast2(record.durationInMillis) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/index.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/index.ts
new file mode 100644
index 000000000..cc2dc24fc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/index.ts
@@ -0,0 +1,8 @@
+import MyProcessDesigner from './ProcessDesigner.vue';
+
+MyProcessDesigner.install = function (Vue: any) {
+ Vue.component(MyProcessDesigner.name, MyProcessDesigner);
+};
+
+// 流程图的设计器,可编辑
+export default MyProcessDesigner;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/index2.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/index2.ts
new file mode 100644
index 000000000..9f085a72e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/index2.ts
@@ -0,0 +1,8 @@
+import MyProcessViewer from './ProcessViewer.vue';
+
+MyProcessViewer.install = function (Vue: any) {
+ Vue.component(MyProcessViewer.name, MyProcessViewer);
+};
+
+// 流程图的查看器,不可编辑
+export default MyProcessViewer;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/content-pad/contentPadProvider.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/content-pad/contentPadProvider.js
new file mode 100644
index 000000000..b8f1dfef7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/content-pad/contentPadProvider.js
@@ -0,0 +1,445 @@
+import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil';
+import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
+import { isEventSubProcess, isExpanded } from 'bpmn-js/lib/util/DiUtil';
+import { is } from 'bpmn-js/lib/util/ModelUtil';
+import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
+
+/**
+ * A provider for BPMN 2.0 elements context pad
+ */
+function ContextPadProvider(
+ config,
+ injector,
+ eventBus,
+ contextPad,
+ modeling,
+ elementFactory,
+ connect,
+ create,
+ popupMenu,
+ canvas,
+ rules,
+ translate,
+) {
+ config = config || {};
+
+ contextPad.registerProvider(this);
+
+ this._contextPad = contextPad;
+
+ this._modeling = modeling;
+
+ this._elementFactory = elementFactory;
+ this._connect = connect;
+ this._create = create;
+ this._popupMenu = popupMenu;
+ this._canvas = canvas;
+ this._rules = rules;
+ this._translate = translate;
+
+ if (config.autoPlace !== false) {
+ this._autoPlace = injector.get('autoPlace', false);
+ }
+
+ eventBus.on('create.end', 250, (event) => {
+ const context = event.context;
+ const shape = context.shape;
+
+ if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
+ return;
+ }
+
+ const entries = contextPad.getEntries(shape);
+
+ if (entries.replace) {
+ entries.replace.action.click(event, shape);
+ }
+ });
+}
+
+export default ContextPadProvider;
+
+ContextPadProvider.$inject = [
+ 'config.contextPad',
+ 'injector',
+ 'eventBus',
+ 'contextPad',
+ 'modeling',
+ 'elementFactory',
+ 'connect',
+ 'create',
+ 'popupMenu',
+ 'canvas',
+ 'rules',
+ 'translate',
+ 'elementRegistry',
+];
+
+ContextPadProvider.prototype.getContextPadEntries = function (element) {
+ const autoPlace = this._autoPlace;
+ const canvas = this._canvas;
+ const connect = this._connect;
+ const contextPad = this._contextPad;
+ const create = this._create;
+ const elementFactory = this._elementFactory;
+ const modeling = this._modeling;
+ const popupMenu = this._popupMenu;
+ const rules = this._rules;
+ const translate = this._translate;
+
+ const actions = {};
+
+ if (element.type === 'label') {
+ return actions;
+ }
+
+ const businessObject = element.businessObject;
+
+ function startConnect(event, element) {
+ connect.start(event, element);
+ }
+
+ function removeElement() {
+ modeling.removeElements([element]);
+ }
+
+ function getReplaceMenuPosition(element) {
+ const Y_OFFSET = 5;
+
+ const diagramContainer = canvas.getContainer();
+ const pad = contextPad.getPad(element).html;
+
+ const diagramRect = diagramContainer.getBoundingClientRect();
+ const padRect = pad.getBoundingClientRect();
+
+ const top = padRect.top - diagramRect.top;
+ const left = padRect.left - diagramRect.left;
+
+ const pos = {
+ x: left,
+ y: top + padRect.height + Y_OFFSET,
+ };
+
+ return pos;
+ }
+
+ /**
+ * Create an append action
+ *
+ * @param {string} type
+ * @param {string} className
+ * @param {string} [title]
+ * @param {object} [options]
+ *
+ * @return {object} descriptor
+ */
+ function appendAction(type, className, title, options) {
+ if (typeof title !== 'string') {
+ options = title;
+ title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') });
+ }
+
+ function appendStart(event, element) {
+ const shape = elementFactory.createShape(
+ Object.assign({ type }, options),
+ );
+ create.start(event, shape, {
+ source: element,
+ });
+ }
+
+ const append = autoPlace
+ ? function (event, element) {
+ const shape = elementFactory.createShape(
+ Object.assign({ type }, options),
+ );
+
+ autoPlace.append(element, shape);
+ }
+ : appendStart;
+
+ return {
+ group: 'model',
+ className,
+ title,
+ action: {
+ dragstart: appendStart,
+ click: append,
+ },
+ };
+ }
+
+ function splitLaneHandler(count) {
+ return function (event, element) {
+ // actual split
+ modeling.splitLane(element, count);
+
+ // refresh context pad after split to
+ // get rid of split icons
+ contextPad.open(element, true);
+ };
+ }
+
+ if (
+ isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) &&
+ isExpanded(businessObject)
+ ) {
+ const childLanes = getChildLanes(element);
+
+ Object.assign(actions, {
+ 'lane-insert-above': {
+ group: 'lane-insert-above',
+ className: 'bpmn-icon-lane-insert-above',
+ title: translate('Add Lane above'),
+ action: {
+ click(event, element) {
+ modeling.addLane(element, 'top');
+ },
+ },
+ },
+ });
+
+ if (childLanes.length < 2) {
+ if (element.height >= 120) {
+ Object.assign(actions, {
+ 'lane-divide-two': {
+ group: 'lane-divide',
+ className: 'bpmn-icon-lane-divide-two',
+ title: translate('Divide into two Lanes'),
+ action: {
+ click: splitLaneHandler(2),
+ },
+ },
+ });
+ }
+
+ if (element.height >= 180) {
+ Object.assign(actions, {
+ 'lane-divide-three': {
+ group: 'lane-divide',
+ className: 'bpmn-icon-lane-divide-three',
+ title: translate('Divide into three Lanes'),
+ action: {
+ click: splitLaneHandler(3),
+ },
+ },
+ });
+ }
+ }
+
+ Object.assign(actions, {
+ 'lane-insert-below': {
+ group: 'lane-insert-below',
+ className: 'bpmn-icon-lane-insert-below',
+ title: translate('Add Lane below'),
+ action: {
+ click(event, element) {
+ modeling.addLane(element, 'bottom');
+ },
+ },
+ },
+ });
+ }
+
+ if (is(businessObject, 'bpmn:FlowNode')) {
+ if (is(businessObject, 'bpmn:EventBasedGateway')) {
+ Object.assign(actions, {
+ 'append.receive-task': appendAction(
+ 'bpmn:ReceiveTask',
+ 'bpmn-icon-receive-task',
+ translate('Append ReceiveTask'),
+ ),
+ 'append.message-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-message',
+ translate('Append MessageIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:MessageEventDefinition' },
+ ),
+ 'append.timer-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-timer',
+ translate('Append TimerIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:TimerEventDefinition' },
+ ),
+ 'append.condition-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-condition',
+ translate('Append ConditionIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:ConditionalEventDefinition' },
+ ),
+ 'append.signal-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-signal',
+ translate('Append SignalIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:SignalEventDefinition' },
+ ),
+ });
+ } else if (
+ isEventType(
+ businessObject,
+ 'bpmn:BoundaryEvent',
+ 'bpmn:CompensateEventDefinition',
+ )
+ ) {
+ Object.assign(actions, {
+ 'append.compensation-activity': appendAction(
+ 'bpmn:Task',
+ 'bpmn-icon-task',
+ translate('Append compensation activity'),
+ {
+ isForCompensation: true,
+ },
+ ),
+ });
+ } else if (
+ !is(businessObject, 'bpmn:EndEvent') &&
+ !businessObject.isForCompensation &&
+ !isEventType(
+ businessObject,
+ 'bpmn:IntermediateThrowEvent',
+ 'bpmn:LinkEventDefinition',
+ ) &&
+ !isEventSubProcess(businessObject)
+ ) {
+ Object.assign(actions, {
+ 'append.end-event': appendAction(
+ 'bpmn:EndEvent',
+ 'bpmn-icon-end-event-none',
+ translate('Append EndEvent'),
+ ),
+ 'append.gateway': appendAction(
+ 'bpmn:ExclusiveGateway',
+ 'bpmn-icon-gateway-none',
+ translate('Append Gateway'),
+ ),
+ 'append.append-task': appendAction(
+ 'bpmn:UserTask',
+ 'bpmn-icon-user-task',
+ translate('Append Task'),
+ ),
+ 'append.intermediate-event': appendAction(
+ 'bpmn:IntermediateThrowEvent',
+ 'bpmn-icon-intermediate-event-none',
+ translate('Append Intermediate/Boundary Event'),
+ ),
+ });
+ }
+ }
+
+ if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
+ // Replace menu entry
+ Object.assign(actions, {
+ replace: {
+ group: 'edit',
+ className: 'bpmn-icon-screw-wrench',
+ title: '修改类型',
+ action: {
+ click(event, element) {
+ const position = Object.assign(getReplaceMenuPosition(element), {
+ cursor: { x: event.x, y: event.y },
+ });
+
+ popupMenu.open(element, 'bpmn-replace', position);
+ },
+ },
+ },
+ });
+ }
+
+ if (
+ isAny(businessObject, [
+ 'bpmn:FlowNode',
+ 'bpmn:InteractionNode',
+ 'bpmn:DataObjectReference',
+ 'bpmn:DataStoreReference',
+ ])
+ ) {
+ Object.assign(actions, {
+ 'append.text-annotation': appendAction(
+ 'bpmn:TextAnnotation',
+ 'bpmn-icon-text-annotation',
+ ),
+
+ connect: {
+ group: 'connect',
+ className: 'bpmn-icon-connection-multi',
+ title: translate(
+ `Connect using ${
+ businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or '
+ }Association`,
+ ),
+ action: {
+ click: startConnect,
+ dragstart: startConnect,
+ },
+ },
+ });
+ }
+
+ if (
+ isAny(businessObject, [
+ 'bpmn:DataObjectReference',
+ 'bpmn:DataStoreReference',
+ ])
+ ) {
+ Object.assign(actions, {
+ connect: {
+ group: 'connect',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Connect using DataInputAssociation'),
+ action: {
+ click: startConnect,
+ dragstart: startConnect,
+ },
+ },
+ });
+ }
+
+ if (is(businessObject, 'bpmn:Group')) {
+ Object.assign(actions, {
+ 'append.text-annotation': appendAction(
+ 'bpmn:TextAnnotation',
+ 'bpmn-icon-text-annotation',
+ ),
+ });
+ }
+
+ // delete element entry, only show if allowed by rules
+ let deleteAllowed = rules.allowed('elements.delete', { elements: [element] });
+
+ if (Array.isArray(deleteAllowed)) {
+ // was the element returned as a deletion candidate?
+ deleteAllowed = deleteAllowed[0] === element;
+ }
+
+ if (deleteAllowed) {
+ Object.assign(actions, {
+ delete: {
+ group: 'edit',
+ className: 'bpmn-icon-trash',
+ title: translate('Remove'),
+ action: {
+ click: removeElement,
+ },
+ },
+ });
+ }
+
+ return actions;
+};
+
+// helpers /////////
+
+function isEventType(eventBo, type, definition) {
+ const isType = eventBo.$instanceOf(type);
+ let isDefinition = false;
+
+ const definitions = eventBo.eventDefinitions || [];
+ definitions.forEach((def) => {
+ if (def.$type === definition) {
+ isDefinition = true;
+ }
+ });
+
+ return isType && isDefinition;
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/content-pad/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/content-pad/index.js
new file mode 100644
index 000000000..d5325aedb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/content-pad/index.js
@@ -0,0 +1,6 @@
+import CustomContextPadProvider from './contentPadProvider';
+
+export default {
+ __init__: ['contextPadProvider'],
+ contextPadProvider: ['type', CustomContextPadProvider],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/defaultEmpty.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/defaultEmpty.js
new file mode 100644
index 000000000..5c25d8e8d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/defaultEmpty.js
@@ -0,0 +1,26 @@
+function defaultEmpty(key, name, type) {
+ if (!type) type = 'camunda';
+ const TYPE_TARGET = {
+ activiti: 'http://activiti.org/bpmn',
+ camunda: 'http://bpmn.io/schema/bpmn',
+ flowable: 'http://flowable.org/bpmn',
+ };
+ return `
+
+
+
+
+
+
+
+`;
+}
+
+export default defaultEmpty;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/activitiDescriptor.json b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/activitiDescriptor.json
new file mode 100644
index 000000000..879785225
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/activitiDescriptor.json
@@ -0,0 +1,1007 @@
+{
+ "name": "Activiti",
+ "uri": "http://activiti.org/bpmn",
+ "prefix": "activiti",
+ "xml": {
+ "tagAlias": "lowerCase"
+ },
+ "associations": [],
+ "types": [
+ {
+ "name": "Definitions",
+ "isAbstract": true,
+ "extends": ["bpmn:Definitions"],
+ "properties": [
+ {
+ "name": "diagramRelationId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InOutBinding",
+ "superClass": ["Element"],
+ "isAbstract": true,
+ "properties": [
+ {
+ "name": "source",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "sourceExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "target",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "local",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "variables",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "In",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "Out",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "AsyncCapable",
+ "isAbstract": true,
+ "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncBefore",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncAfter",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "exclusive",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "JobPriorized",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "activiti:AsyncCapable"],
+ "properties": [
+ {
+ "name": "jobPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "SignalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:SignalEventDefinition"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ }
+ ]
+ },
+ {
+ "name": "ErrorEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ErrorEventDefinition"],
+ "properties": [
+ {
+ "name": "errorCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "errorMessageVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Error",
+ "isAbstract": true,
+ "extends": ["bpmn:Error"],
+ "properties": [
+ {
+ "name": "activiti:errorMessage",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "PotentialStarter",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "resourceAssignmentExpression",
+ "type": "bpmn:ResourceAssignmentExpression"
+ }
+ ]
+ },
+ {
+ "name": "FormSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "formHandlerClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formKey",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TemplateSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "bpmn:FlowElement"],
+ "properties": [
+ {
+ "name": "modelerTemplate",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Initiator",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent"],
+ "properties": [
+ {
+ "name": "initiator",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ScriptTask",
+ "isAbstract": true,
+ "extends": ["bpmn:ScriptTask"],
+ "properties": [
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Process",
+ "isAbstract": true,
+ "extends": ["bpmn:Process"],
+ "properties": [
+ {
+ "name": "candidateStarterGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStarterUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "versionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "historyTimeToLive",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "isStartableInTasklist",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ },
+ {
+ "name": "executionListener",
+ "isAbstract": true,
+ "type": "Expression"
+ }
+ ]
+ },
+ {
+ "name": "EscalationEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:EscalationEventDefinition"],
+ "properties": [
+ {
+ "name": "escalationCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FormalExpression",
+ "isAbstract": true,
+ "extends": ["bpmn:FormalExpression"],
+ "properties": [
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "multiinstance_type",
+ "superClass": ["Element"]
+ },
+ {
+ "name": "multiinstance_condition",
+ "superClass": ["Element"]
+ },
+ {
+ "name": "Assignable",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "assignee",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "dueDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "followUpDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "priority",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "multiinstance_condition",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStrategy",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateParam",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "CallActivity",
+ "extends": ["bpmn:CallActivity"],
+ "properties": [
+ {
+ "name": "calledElementBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "calledElementVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementVersionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "caseVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingDelegateExpression",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ServiceTaskLike",
+ "extends": [
+ "bpmn:ServiceTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:SendTask",
+ "bpmn:MessageEventDefinition"
+ ],
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "DmnCapable",
+ "extends": ["bpmn:BusinessRuleTask"],
+ "properties": [
+ {
+ "name": "decisionRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "decisionRefBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "decisionRefVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "mapDecisionResult",
+ "isAttr": true,
+ "type": "String",
+ "default": "resultList"
+ },
+ {
+ "name": "decisionRefTenantId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExternalCapable",
+ "extends": ["activiti:ServiceTaskLike"],
+ "properties": [
+ {
+ "name": "type",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "topic",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TaskPriorized",
+ "extends": ["bpmn:Process", "activiti:ExternalCapable"],
+ "properties": [
+ {
+ "name": "taskPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Properties",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "values",
+ "type": "Property",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Property",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Connector",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["activiti:ServiceTaskLike"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputOutput",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:FlowNode", "activiti:Connector"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ },
+ {
+ "name": "inputParameters",
+ "isMany": true,
+ "type": "InputParameter"
+ },
+ {
+ "name": "outputParameters",
+ "isMany": true,
+ "type": "OutputParameter"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameter",
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameterDefinition",
+ "isAbstract": true
+ },
+ {
+ "name": "List",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "items",
+ "isMany": true,
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Map",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "entries",
+ "isMany": true,
+ "type": "Entry"
+ }
+ ]
+ },
+ {
+ "name": "Entry",
+ "properties": [
+ {
+ "name": "key",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Value",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "id",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Script",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "scriptFormat",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Field",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "activiti:ServiceTaskLike",
+ "activiti:ExecutionListener",
+ "activiti:TaskListener"
+ ]
+ },
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "expression",
+ "type": "String"
+ },
+ {
+ "name": "stringValue",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "string",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "OutputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "Collectable",
+ "isAbstract": true,
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+ "superClass": ["activiti:AsyncCapable"],
+ "properties": [
+ {
+ "name": "collection",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "elementVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FailedJobRetryTimeCycle",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "activiti:AsyncCapable",
+ "bpmn:MultiInstanceLoopCharacteristics"
+ ]
+ },
+ "properties": [
+ {
+ "name": "body",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExecutionListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "bpmn:Task",
+ "bpmn:ServiceTask",
+ "bpmn:UserTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:ScriptTask",
+ "bpmn:ReceiveTask",
+ "bpmn:ManualTask",
+ "bpmn:ExclusiveGateway",
+ "bpmn:SequenceFlow",
+ "bpmn:ParallelGateway",
+ "bpmn:InclusiveGateway",
+ "bpmn:EventBasedGateway",
+ "bpmn:StartEvent",
+ "bpmn:IntermediateCatchEvent",
+ "bpmn:IntermediateThrowEvent",
+ "bpmn:EndEvent",
+ "bpmn:BoundaryEvent",
+ "bpmn:CallActivity",
+ "bpmn:SubProcess",
+ "bpmn:Process"
+ ]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "TaskListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "defaultValue",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "properties",
+ "type": "Properties"
+ },
+ {
+ "name": "validation",
+ "type": "Validation"
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Validation",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "constraints",
+ "type": "Constraint",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Constraint",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "config",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "ConditionalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ConditionalEventDefinition"],
+ "properties": [
+ {
+ "name": "variableName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableEvent",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ }
+ ],
+ "emumerations": []
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/camundaDescriptor.json b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/camundaDescriptor.json
new file mode 100644
index 000000000..18fe80288
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/camundaDescriptor.json
@@ -0,0 +1,1023 @@
+{
+ "name": "Camunda",
+ "uri": "http://camunda.org/schema/1.0/bpmn",
+ "prefix": "camunda",
+ "xml": {
+ "tagAlias": "lowerCase"
+ },
+ "associations": [],
+ "types": [
+ {
+ "name": "Definitions",
+ "isAbstract": true,
+ "extends": ["bpmn:Definitions"],
+ "properties": [
+ {
+ "name": "diagramRelationId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InOutBinding",
+ "superClass": ["Element"],
+ "isAbstract": true,
+ "properties": [
+ {
+ "name": "source",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "sourceExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "target",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "local",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "variables",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "In",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity", "bpmn:SignalEventDefinition"]
+ }
+ },
+ {
+ "name": "Out",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "AsyncCapable",
+ "isAbstract": true,
+ "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncBefore",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncAfter",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "exclusive",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "JobPriorized",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "camunda:AsyncCapable"],
+ "properties": [
+ {
+ "name": "jobPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "SignalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:SignalEventDefinition"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ }
+ ]
+ },
+ {
+ "name": "ErrorEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ErrorEventDefinition"],
+ "properties": [
+ {
+ "name": "errorCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "errorMessageVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Error",
+ "isAbstract": true,
+ "extends": ["bpmn:Error"],
+ "properties": [
+ {
+ "name": "camunda:errorMessage",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "PotentialStarter",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "resourceAssignmentExpression",
+ "type": "bpmn:ResourceAssignmentExpression"
+ }
+ ]
+ },
+ {
+ "name": "FormSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "formHandlerClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formKey",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TemplateSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "bpmn:FlowElement"],
+ "properties": [
+ {
+ "name": "modelerTemplate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "modelerTemplateVersion",
+ "isAttr": true,
+ "type": "Integer"
+ }
+ ]
+ },
+ {
+ "name": "Initiator",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent"],
+ "properties": [
+ {
+ "name": "initiator",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ScriptTask",
+ "isAbstract": true,
+ "extends": ["bpmn:ScriptTask"],
+ "properties": [
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Process",
+ "isAbstract": true,
+ "extends": ["bpmn:Process"],
+ "properties": [
+ {
+ "name": "candidateStarterGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStarterUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "versionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "historyTimeToLive",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "isStartableInTasklist",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "EscalationEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:EscalationEventDefinition"],
+ "properties": [
+ {
+ "name": "escalationCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FormalExpression",
+ "isAbstract": true,
+ "extends": ["bpmn:FormalExpression"],
+ "properties": [
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Assignable",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "assignee",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "dueDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "followUpDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "priority",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStrategy",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateParam",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "CallActivity",
+ "extends": ["bpmn:CallActivity"],
+ "properties": [
+ {
+ "name": "calledElementBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "calledElementVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementVersionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "caseVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingDelegateExpression",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ServiceTaskLike",
+ "extends": [
+ "bpmn:ServiceTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:SendTask",
+ "bpmn:MessageEventDefinition"
+ ],
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "DmnCapable",
+ "extends": ["bpmn:BusinessRuleTask"],
+ "properties": [
+ {
+ "name": "decisionRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "decisionRefBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "decisionRefVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "mapDecisionResult",
+ "isAttr": true,
+ "type": "String",
+ "default": "resultList"
+ },
+ {
+ "name": "decisionRefTenantId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExternalCapable",
+ "extends": ["camunda:ServiceTaskLike"],
+ "properties": [
+ {
+ "name": "type",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "topic",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TaskPriorized",
+ "extends": ["bpmn:Process", "camunda:ExternalCapable"],
+ "properties": [
+ {
+ "name": "taskPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Properties",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "values",
+ "type": "Property",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Property",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Connector",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["camunda:ServiceTaskLike"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputOutput",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:FlowNode", "camunda:Connector"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ },
+ {
+ "name": "inputParameters",
+ "isMany": true,
+ "type": "InputParameter"
+ },
+ {
+ "name": "outputParameters",
+ "isMany": true,
+ "type": "OutputParameter"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameter",
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameterDefinition",
+ "isAbstract": true
+ },
+ {
+ "name": "List",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "items",
+ "isMany": true,
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Map",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "entries",
+ "isMany": true,
+ "type": "Entry"
+ }
+ ]
+ },
+ {
+ "name": "Entry",
+ "properties": [
+ {
+ "name": "key",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Value",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "id",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Script",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "scriptFormat",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Field",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "camunda:ServiceTaskLike",
+ "camunda:ExecutionListener",
+ "camunda:TaskListener"
+ ]
+ },
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "expression",
+ "type": "String"
+ },
+ {
+ "name": "stringValue",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "string",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "OutputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "Collectable",
+ "isAbstract": true,
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+ "superClass": ["camunda:AsyncCapable"],
+ "properties": [
+ {
+ "name": "collection",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "elementVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FailedJobRetryTimeCycle",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "camunda:AsyncCapable",
+ "bpmn:MultiInstanceLoopCharacteristics"
+ ]
+ },
+ "properties": [
+ {
+ "name": "body",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExecutionListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "bpmn:Task",
+ "bpmn:ServiceTask",
+ "bpmn:UserTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:ScriptTask",
+ "bpmn:ReceiveTask",
+ "bpmn:ManualTask",
+ "bpmn:ExclusiveGateway",
+ "bpmn:SequenceFlow",
+ "bpmn:ParallelGateway",
+ "bpmn:InclusiveGateway",
+ "bpmn:EventBasedGateway",
+ "bpmn:StartEvent",
+ "bpmn:IntermediateCatchEvent",
+ "bpmn:IntermediateThrowEvent",
+ "bpmn:EndEvent",
+ "bpmn:BoundaryEvent",
+ "bpmn:CallActivity",
+ "bpmn:SubProcess",
+ "bpmn:Process"
+ ]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "TaskListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ },
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "eventDefinitions",
+ "type": "bpmn:TimerEventDefinition",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormData",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "fields",
+ "type": "FormField",
+ "isMany": true
+ },
+ {
+ "name": "businessKey",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "FormField",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "defaultValue",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "properties",
+ "type": "Properties"
+ },
+ {
+ "name": "validation",
+ "type": "Validation"
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Validation",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "constraints",
+ "type": "Constraint",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Constraint",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "config",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "ConditionalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ConditionalEventDefinition"],
+ "properties": [
+ {
+ "name": "variableName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableEvents",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ }
+ ],
+ "emumerations": []
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/flowableDescriptor.json b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/flowableDescriptor.json
new file mode 100644
index 000000000..2a929bd2d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/descriptor/flowableDescriptor.json
@@ -0,0 +1,1496 @@
+{
+ "name": "Flowable",
+ "uri": "http://flowable.org/bpmn",
+ "prefix": "flowable",
+ "xml": {
+ "tagAlias": "lowerCase"
+ },
+ "associations": [],
+ "types": [
+ {
+ "name": "InOutBinding",
+ "superClass": ["Element"],
+ "isAbstract": true,
+ "properties": [
+ {
+ "name": "source",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "sourceExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "target",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "local",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "variables",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "In",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "Out",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "AsyncCapable",
+ "isAbstract": true,
+ "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncBefore",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncAfter",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "exclusive",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "JobPriorized",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "flowable:AsyncCapable"],
+ "properties": [
+ {
+ "name": "jobPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "SignalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:SignalEventDefinition"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ }
+ ]
+ },
+ {
+ "name": "ErrorEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ErrorEventDefinition"],
+ "properties": [
+ {
+ "name": "errorCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "errorMessageVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Error",
+ "isAbstract": true,
+ "extends": ["bpmn:Error"],
+ "properties": [
+ {
+ "name": "flowable:errorMessage",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "PotentialStarter",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "resourceAssignmentExpression",
+ "type": "bpmn:ResourceAssignmentExpression"
+ }
+ ]
+ },
+ {
+ "name": "FormSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "formHandlerClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formType",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formReadOnly",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "formInit",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "TemplateSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "bpmn:FlowElement"],
+ "properties": [
+ {
+ "name": "modelerTemplate",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Initiator",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent"],
+ "properties": [
+ {
+ "name": "initiator",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ScriptTask",
+ "isAbstract": true,
+ "extends": ["bpmn:ScriptTask"],
+ "properties": [
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Process",
+ "isAbstract": true,
+ "extends": ["bpmn:Process"],
+ "properties": [
+ {
+ "name": "candidateStarterGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStarterUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "versionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "historyTimeToLive",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "isStartableInTasklist",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "EscalationEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:EscalationEventDefinition"],
+ "properties": [
+ {
+ "name": "escalationCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FormalExpression",
+ "isAbstract": true,
+ "extends": ["bpmn:FormalExpression"],
+ "properties": [
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Assignable",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "assignee",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "dueDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "followUpDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "priority",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStrategy",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateParam",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Assignee",
+ "supperClass": "Element",
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "viewId",
+ "type": "Number",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "CallActivity",
+ "extends": ["bpmn:CallActivity"],
+ "properties": [
+ {
+ "name": "calledElementBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "calledElementVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementVersionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "caseVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingDelegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementType",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "processInstanceName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "inheritBusinessKey",
+ "isAttr": true,
+ "type": "Boolean"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "inheritVariables",
+ "isAttr": true,
+ "type": "Boolean"
+ }
+ ]
+ },
+ {
+ "name": "ServiceTaskLike",
+ "extends": [
+ "bpmn:ServiceTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:SendTask",
+ "bpmn:MessageEventDefinition"
+ ],
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "DmnCapable",
+ "extends": ["bpmn:BusinessRuleTask"],
+ "properties": [
+ {
+ "name": "decisionRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "decisionRefBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "decisionRefVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "mapDecisionResult",
+ "isAttr": true,
+ "type": "String",
+ "default": "resultList"
+ },
+ {
+ "name": "decisionRefTenantId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExternalCapable",
+ "extends": ["flowable:ServiceTaskLike"],
+ "properties": [
+ {
+ "name": "type",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "topic",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TaskPriorized",
+ "extends": ["bpmn:Process", "flowable:ExternalCapable"],
+ "properties": [
+ {
+ "name": "taskPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Properties",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "values",
+ "type": "Property",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Property",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Button",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "code",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "isHide",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "next",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "sort",
+ "type": "Integer",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Assignee",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "condition",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "operationType",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "sort",
+ "type": "Integer",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Connector",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["flowable:ServiceTaskLike"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputOutput",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:FlowNode", "flowable:Connector"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ },
+ {
+ "name": "inputParameters",
+ "isMany": true,
+ "type": "InputParameter"
+ },
+ {
+ "name": "outputParameters",
+ "isMany": true,
+ "type": "OutputParameter"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameter",
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameterDefinition",
+ "isAbstract": true
+ },
+ {
+ "name": "List",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "items",
+ "isMany": true,
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Map",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "entries",
+ "isMany": true,
+ "type": "Entry"
+ }
+ ]
+ },
+ {
+ "name": "Entry",
+ "properties": [
+ {
+ "name": "key",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Value",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "id",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Script",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "scriptFormat",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Field",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "flowable:ServiceTaskLike",
+ "flowable:ExecutionListener",
+ "flowable:TaskListener"
+ ]
+ },
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "expression",
+ "type": "String"
+ },
+ {
+ "name": "stringValue",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "string",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ChildField",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "InputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "OutputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "Collectable",
+ "isAbstract": true,
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+ "superClass": ["flowable:AsyncCapable"],
+ "properties": [
+ {
+ "name": "collection",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "elementVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FailedJobRetryTimeCycle",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "flowable:AsyncCapable",
+ "bpmn:MultiInstanceLoopCharacteristics"
+ ]
+ },
+ "properties": [
+ {
+ "name": "body",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExecutionListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "bpmn:Task",
+ "bpmn:ServiceTask",
+ "bpmn:UserTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:ScriptTask",
+ "bpmn:ReceiveTask",
+ "bpmn:ManualTask",
+ "bpmn:ExclusiveGateway",
+ "bpmn:SequenceFlow",
+ "bpmn:ParallelGateway",
+ "bpmn:InclusiveGateway",
+ "bpmn:EventBasedGateway",
+ "bpmn:StartEvent",
+ "bpmn:IntermediateCatchEvent",
+ "bpmn:IntermediateThrowEvent",
+ "bpmn:EndEvent",
+ "bpmn:BoundaryEvent",
+ "bpmn:CallActivity",
+ "bpmn:SubProcess",
+ "bpmn:Process"
+ ]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "TaskListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ },
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "eventDefinitions",
+ "type": "bpmn:TimerEventDefinition",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ },
+ {
+ "name": "children",
+ "type": "ChildField",
+ "isMany": true
+ },
+ {
+ "name": "extensionElements",
+ "type": "bpmn:ExtensionElements",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormData",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "fields",
+ "type": "FormField",
+ "isMany": true
+ },
+ {
+ "name": "businessKey",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "FormField",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "defaultValue",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "properties",
+ "type": "Properties"
+ },
+ {
+ "name": "validation",
+ "type": "Validation"
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Validation",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "constraints",
+ "type": "Constraint",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Constraint",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "config",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "ConditionalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ConditionalEventDefinition"],
+ "properties": [
+ {
+ "name": "variableName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableEvent",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Condition",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:SequenceFlow"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "field",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "compare",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "logic",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "sort",
+ "type": "Integer",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "AssignStartUserHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "RejectHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "RejectReturnTaskId",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "String",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "AssignEmptyHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "AssignEmptyUserIds",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "String",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "ButtonsSetting",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "flowable:id",
+ "type": "Integer",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:enable",
+ "type": "Boolean",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:displayName",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "FieldsPermission",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "flowable:field",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:title",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:permission",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "BoundaryEventType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:BoundaryEvent"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "TimeoutHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:BoundaryEvent"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "ApproveType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "ApproveMethod",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "CandidateStrategy",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "CandidateParam",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "String",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "SignEnable",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Boolean",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "SkipExpression",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "skipExpression",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ReasonRequire",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Boolean",
+ "isBody": true
+ }
+ ]
+ }
+ ],
+ "emumerations": []
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/activiti/activitiExtension.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/activiti/activitiExtension.js
new file mode 100644
index 000000000..54d506019
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/activiti/activitiExtension.js
@@ -0,0 +1,94 @@
+const ALLOWED_TYPES = {
+ FailedJobRetryTimeCycle: [
+ 'bpmn:StartEvent',
+ 'bpmn:BoundaryEvent',
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn:Activity',
+ ],
+ Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+ Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+};
+
+function is(element, type) {
+ return (
+ element &&
+ typeof element.$instanceOf === 'function' &&
+ element.$instanceOf(type)
+ );
+}
+
+function exists(element) {
+ return element && element.length > 0;
+}
+
+function includesType(collection, type) {
+ return (
+ exists(collection) &&
+ collection.some((element) => {
+ return is(element, type);
+ })
+ );
+}
+
+function anyType(element, types) {
+ return types.some((type) => {
+ return is(element, type);
+ });
+}
+
+function isAllowed(propName, propDescriptor, newElement) {
+ const name = propDescriptor.name;
+ const types = ALLOWED_TYPES[name.replace(/activiti:/, '')];
+
+ return name === propName && anyType(newElement, types);
+}
+
+function ActivitiModdleExtension(eventBus) {
+ eventBus.on(
+ 'property.clone',
+ function (context) {
+ const newElement = context.newElement;
+ const propDescriptor = context.propertyDescriptor;
+
+ this.canCloneProperty(newElement, propDescriptor);
+ },
+ this,
+ );
+}
+
+ActivitiModdleExtension.$inject = ['eventBus'];
+
+ActivitiModdleExtension.prototype.canCloneProperty = function (
+ newElement,
+ propDescriptor,
+) {
+ if (
+ isAllowed('activiti:FailedJobRetryTimeCycle', propDescriptor, newElement)
+ ) {
+ return (
+ includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
+ includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
+ is(
+ newElement.loopCharacteristics,
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ )
+ );
+ }
+
+ if (isAllowed('activiti:Connector', propDescriptor, newElement)) {
+ return includesType(
+ newElement.eventDefinitions,
+ 'bpmn:MessageEventDefinition',
+ );
+ }
+
+ if (isAllowed('activiti:Field', propDescriptor, newElement)) {
+ return includesType(
+ newElement.eventDefinitions,
+ 'bpmn:MessageEventDefinition',
+ );
+ }
+};
+
+// module.exports = ActivitiModdleExtension;
+export default ActivitiModdleExtension;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/activiti/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/activiti/index.js
new file mode 100644
index 000000000..7c38ff4e5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/activiti/index.js
@@ -0,0 +1,11 @@
+/*
+ * @author igdianov
+ * address https://github.com/igdianov/activiti-bpmn-moddle
+ * */
+
+import activitiExtension from './activitiExtension';
+
+export default {
+ __init__: ['ActivitiModdleExtension'],
+ ActivitiModdleExtension: ['type', activitiExtension],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/camunda/extension.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/camunda/extension.js
new file mode 100644
index 000000000..3956be680
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/camunda/extension.js
@@ -0,0 +1,156 @@
+import { isFunction, isObject } from '@vben/utils';
+
+const WILDCARD = '*';
+
+function CamundaModdleExtension(eventBus) {
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
+ const self = this;
+
+ eventBus.on('moddleCopy.canCopyProperty', (context) => {
+ const parent = context.parent;
+ const property = context.property;
+
+ return self.canCopyProperty(property, parent);
+ });
+}
+
+CamundaModdleExtension.$inject = ['eventBus'];
+
+/**
+ * Check wether to disallow copying property.
+ */
+CamundaModdleExtension.prototype.canCopyProperty = function (property, parent) {
+ // (1) check wether property is allowed in parent
+ if (isObject(property) && !isAllowedInParent(property, parent)) {
+ return false;
+ }
+
+ // (2) check more complex scenarios
+
+ if (is(property, 'camunda:InputOutput') && !this.canHostInputOutput(parent)) {
+ return false;
+ }
+
+ if (
+ isAny(property, ['camunda:Connector', 'camunda:Field']) &&
+ !this.canHostConnector(parent)
+ ) {
+ return false;
+ }
+
+ if (is(property, 'camunda:In') && !this.canHostIn(parent)) {
+ return false;
+ }
+};
+
+CamundaModdleExtension.prototype.canHostInputOutput = function (parent) {
+ // allowed in camunda:Connector
+ const connector = getParent(parent, 'camunda:Connector');
+
+ if (connector) {
+ return true;
+ }
+
+ // special rules inside bpmn:FlowNode
+ const flowNode = getParent(parent, 'bpmn:FlowNode');
+
+ if (!flowNode) {
+ return false;
+ }
+
+ if (
+ isAny(flowNode, ['bpmn:StartEvent', 'bpmn:Gateway', 'bpmn:BoundaryEvent'])
+ ) {
+ return false;
+ }
+
+ return !(is(flowNode, 'bpmn:SubProcess') && flowNode.get('triggeredByEvent'));
+};
+
+CamundaModdleExtension.prototype.canHostConnector = function (parent) {
+ const serviceTaskLike = getParent(parent, 'camunda:ServiceTaskLike');
+
+ if (is(serviceTaskLike, 'bpmn:MessageEventDefinition')) {
+ // only allow on throw and end events
+ return (
+ getParent(parent, 'bpmn:IntermediateThrowEvent') ||
+ getParent(parent, 'bpmn:EndEvent')
+ );
+ }
+
+ return true;
+};
+
+CamundaModdleExtension.prototype.canHostIn = function (parent) {
+ const callActivity = getParent(parent, 'bpmn:CallActivity');
+
+ if (callActivity) {
+ return true;
+ }
+
+ const signalEventDefinition = getParent(parent, 'bpmn:SignalEventDefinition');
+
+ if (signalEventDefinition) {
+ // only allow on throw and end events
+ return (
+ getParent(parent, 'bpmn:IntermediateThrowEvent') ||
+ getParent(parent, 'bpmn:EndEvent')
+ );
+ }
+
+ return true;
+};
+
+// module.exports = CamundaModdleExtension;
+export default CamundaModdleExtension;
+
+// helpers //////////
+
+function is(element, type) {
+ return (
+ element && isFunction(element.$instanceOf) && element.$instanceOf(type)
+ );
+}
+
+function isAny(element, types) {
+ return types.some((t) => {
+ return is(element, t);
+ });
+}
+
+function getParent(element, type) {
+ if (!type) {
+ return element.$parent;
+ }
+
+ if (is(element, type)) {
+ return element;
+ }
+
+ if (!element.$parent) {
+ return;
+ }
+
+ return getParent(element.$parent, type);
+}
+
+function isAllowedInParent(property, parent) {
+ // (1) find property descriptor
+ const descriptor =
+ property.$type && property.$model.getTypeDescriptor(property.$type);
+
+ const allowedIn = descriptor && descriptor.meta && descriptor.meta.allowedIn;
+
+ if (!allowedIn || isWildcard(allowedIn)) {
+ return true;
+ }
+
+ // (2) check wether property has parent of allowed type
+ return allowedIn.some((type) => {
+ return getParent(parent, type);
+ });
+}
+
+function isWildcard(allowedIn) {
+ return allowedIn.includes(WILDCARD);
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/camunda/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/camunda/index.js
new file mode 100644
index 000000000..f20d5eb8e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/camunda/index.js
@@ -0,0 +1,6 @@
+import extension from './extension';
+
+export default {
+ __init__: ['camundaModdleExtension'],
+ camundaModdleExtension: ['type', extension],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/flowable/flowableExtension.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/flowable/flowableExtension.js
new file mode 100644
index 000000000..25fa1cc8e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/flowable/flowableExtension.js
@@ -0,0 +1,94 @@
+const ALLOWED_TYPES = {
+ FailedJobRetryTimeCycle: [
+ 'bpmn:StartEvent',
+ 'bpmn:BoundaryEvent',
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn:Activity',
+ ],
+ Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+ Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+};
+
+function is(element, type) {
+ return (
+ element &&
+ typeof element.$instanceOf === 'function' &&
+ element.$instanceOf(type)
+ );
+}
+
+function exists(element) {
+ return element && element.length > 0;
+}
+
+function includesType(collection, type) {
+ return (
+ exists(collection) &&
+ collection.some((element) => {
+ return is(element, type);
+ })
+ );
+}
+
+function anyType(element, types) {
+ return types.some((type) => {
+ return is(element, type);
+ });
+}
+
+function isAllowed(propName, propDescriptor, newElement) {
+ const name = propDescriptor.name;
+ const types = ALLOWED_TYPES[name.replace(/flowable:/, '')];
+
+ return name === propName && anyType(newElement, types);
+}
+
+function FlowableModdleExtension(eventBus) {
+ eventBus.on(
+ 'property.clone',
+ function (context) {
+ const newElement = context.newElement;
+ const propDescriptor = context.propertyDescriptor;
+
+ this.canCloneProperty(newElement, propDescriptor);
+ },
+ this,
+ );
+}
+
+FlowableModdleExtension.$inject = ['eventBus'];
+
+FlowableModdleExtension.prototype.canCloneProperty = function (
+ newElement,
+ propDescriptor,
+) {
+ if (
+ isAllowed('flowable:FailedJobRetryTimeCycle', propDescriptor, newElement)
+ ) {
+ return (
+ includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
+ includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
+ is(
+ newElement.loopCharacteristics,
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ )
+ );
+ }
+
+ if (isAllowed('flowable:Connector', propDescriptor, newElement)) {
+ return includesType(
+ newElement.eventDefinitions,
+ 'bpmn:MessageEventDefinition',
+ );
+ }
+
+ if (isAllowed('flowable:Field', propDescriptor, newElement)) {
+ return includesType(
+ newElement.eventDefinitions,
+ 'bpmn:MessageEventDefinition',
+ );
+ }
+};
+
+// module.exports = FlowableModdleExtension;
+export default FlowableModdleExtension;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/flowable/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/flowable/index.js
new file mode 100644
index 000000000..86474675b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/extension-moddle/flowable/index.js
@@ -0,0 +1,10 @@
+/*
+ * @author igdianov
+ * address https://github.com/igdianov/activiti-bpmn-moddle
+ * */
+import flowableExtension from './flowableExtension';
+
+export default {
+ __init__: ['FlowableModdleExtension'],
+ FlowableModdleExtension: ['type', flowableExtension],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/CustomPalette.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/CustomPalette.js
new file mode 100644
index 000000000..aa682e7db
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/CustomPalette.js
@@ -0,0 +1,231 @@
+import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
+
+function CustomPalette(
+ palette,
+ create,
+ elementFactory,
+ spaceTool,
+ lassoTool,
+ handTool,
+ globalConnect,
+ translate,
+) {
+ PaletteProvider.call(
+ this,
+ palette,
+ create,
+ elementFactory,
+ spaceTool,
+ lassoTool,
+ handTool,
+ globalConnect,
+ translate,
+ 2000,
+ );
+}
+
+CustomPalette.$inject = [
+ 'palette',
+ 'create',
+ 'elementFactory',
+ 'spaceTool',
+ 'lassoTool',
+ 'handTool',
+ 'globalConnect',
+ 'translate',
+];
+
+CustomPalette.prototype = Object.create(PaletteProvider.prototype);
+CustomPalette.prototype.constructor = CustomPalette;
+
+CustomPalette.prototype.getPaletteEntries = function () {
+ const actions = {};
+ const create = this._create;
+ const elementFactory = this._elementFactory;
+ const spaceTool = this._spaceTool;
+ const lassoTool = this._lassoTool;
+ const handTool = this._handTool;
+ const globalConnect = this._globalConnect;
+ const translate = this._translate;
+
+ function createAction(type, group, className, title, options) {
+ function createListener(event) {
+ const shape = Object.assign(
+ elementFactory.createShape({ type }, options),
+ );
+
+ if (options) {
+ shape.businessObject.di.isExpanded = options.isExpanded;
+ }
+
+ create.start(event, shape);
+ }
+
+ const shortType = type.replace(/^bpmn:/, '');
+
+ return {
+ group,
+ className,
+ title: title || translate('Create {type}', { type: shortType }),
+ action: {
+ dragstart: createListener,
+ click: createListener,
+ },
+ };
+ }
+
+ function createSubprocess(event) {
+ const subProcess = elementFactory.createShape({
+ type: 'bpmn:SubProcess',
+ x: 0,
+ y: 0,
+ isExpanded: true,
+ });
+
+ const startEvent = elementFactory.createShape({
+ type: 'bpmn:StartEvent',
+ x: 40,
+ y: 82,
+ parent: subProcess,
+ });
+
+ create.start(event, [subProcess, startEvent], {
+ hints: {
+ autoSelect: [startEvent],
+ },
+ });
+ }
+
+ function createParticipant(event) {
+ create.start(event, elementFactory.createParticipantShape());
+ }
+
+ Object.assign(actions, {
+ 'hand-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-hand-tool',
+ title: translate('Activate the hand tool'),
+ action: {
+ click(event) {
+ handTool.activateHand(event);
+ },
+ },
+ },
+ 'lasso-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-lasso-tool',
+ title: translate('Activate the lasso tool'),
+ action: {
+ click(event) {
+ lassoTool.activateSelection(event);
+ },
+ },
+ },
+ 'space-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-space-tool',
+ title: translate('Activate the create/remove space tool'),
+ action: {
+ click(event) {
+ spaceTool.activateSelection(event);
+ },
+ },
+ },
+ 'global-connect-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Activate the global connect tool'),
+ action: {
+ click(event) {
+ globalConnect.toggle(event);
+ },
+ },
+ },
+ 'tool-separator': {
+ group: 'tools',
+ separator: true,
+ },
+ 'create.start-event': createAction(
+ 'bpmn:StartEvent',
+ 'event',
+ 'bpmn-icon-start-event-none',
+ translate('Create StartEvent'),
+ ),
+ 'create.intermediate-event': createAction(
+ 'bpmn:IntermediateThrowEvent',
+ 'event',
+ 'bpmn-icon-intermediate-event-none',
+ translate('Create Intermediate/Boundary Event'),
+ ),
+ 'create.end-event': createAction(
+ 'bpmn:EndEvent',
+ 'event',
+ 'bpmn-icon-end-event-none',
+ translate('Create EndEvent'),
+ ),
+ 'create.exclusive-gateway': createAction(
+ 'bpmn:ExclusiveGateway',
+ 'gateway',
+ 'bpmn-icon-gateway-none',
+ translate('Create Gateway'),
+ ),
+ 'create.user-task': createAction(
+ 'bpmn:UserTask',
+ 'activity',
+ 'bpmn-icon-user-task',
+ translate('Create User Task'),
+ ),
+ 'create.call-activity': createAction(
+ 'bpmn:CallActivity',
+ 'activity',
+ 'bpmn-icon-call-activity',
+ translate('Create Call Activity'),
+ ),
+ 'create.service-task': createAction(
+ 'bpmn:ServiceTask',
+ 'activity',
+ 'bpmn-icon-service',
+ translate('Create Service Task'),
+ ),
+ 'create.data-object': createAction(
+ 'bpmn:DataObjectReference',
+ 'data-object',
+ 'bpmn-icon-data-object',
+ translate('Create DataObjectReference'),
+ ),
+ 'create.data-store': createAction(
+ 'bpmn:DataStoreReference',
+ 'data-store',
+ 'bpmn-icon-data-store',
+ translate('Create DataStoreReference'),
+ ),
+ 'create.subprocess-expanded': {
+ group: 'activity',
+ className: 'bpmn-icon-subprocess-expanded',
+ title: translate('Create expanded SubProcess'),
+ action: {
+ dragstart: createSubprocess,
+ click: createSubprocess,
+ },
+ },
+ 'create.participant-expanded': {
+ group: 'collaboration',
+ className: 'bpmn-icon-participant',
+ title: translate('Create Pool/Participant'),
+ action: {
+ dragstart: createParticipant,
+ click: createParticipant,
+ },
+ },
+ 'create.group': createAction(
+ 'bpmn:Group',
+ 'artifact',
+ 'bpmn-icon-group',
+ translate('Create Group'),
+ ),
+ });
+
+ return actions;
+};
+
+export default CustomPalette;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/index.js
new file mode 100644
index 000000000..afe1367c8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/index.js
@@ -0,0 +1,22 @@
+// import PaletteModule from "diagram-js/lib/features/palette";
+// import CreateModule from "diagram-js/lib/features/create";
+// import SpaceToolModule from "diagram-js/lib/features/space-tool";
+// import LassoToolModule from "diagram-js/lib/features/lasso-tool";
+// import HandToolModule from "diagram-js/lib/features/hand-tool";
+// import GlobalConnectModule from "diagram-js/lib/features/global-connect";
+// import translate from "diagram-js/lib/i18n/translate";
+//
+// import PaletteProvider from "./paletteProvider";
+//
+// export default {
+// __depends__: [PaletteModule, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate],
+// __init__: ["paletteProvider"],
+// paletteProvider: ["type", PaletteProvider]
+// };
+
+import CustomPalette from './CustomPalette';
+
+export default {
+ __init__: ['paletteProvider'],
+ paletteProvider: ['type', CustomPalette],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/paletteProvider.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/paletteProvider.js
new file mode 100644
index 000000000..9faa09737
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/palette/paletteProvider.js
@@ -0,0 +1,221 @@
+/**
+ * A palette provider for BPMN 2.0 elements.
+ */
+function PaletteProvider(
+ palette,
+ create,
+ elementFactory,
+ spaceTool,
+ lassoTool,
+ handTool,
+ globalConnect,
+ translate,
+) {
+ this._palette = palette;
+ this._create = create;
+ this._elementFactory = elementFactory;
+ this._spaceTool = spaceTool;
+ this._lassoTool = lassoTool;
+ this._handTool = handTool;
+ this._globalConnect = globalConnect;
+ this._translate = translate;
+
+ palette.registerProvider(this);
+}
+
+export default PaletteProvider;
+
+PaletteProvider.$inject = [
+ 'palette',
+ 'create',
+ 'elementFactory',
+ 'spaceTool',
+ 'lassoTool',
+ 'handTool',
+ 'globalConnect',
+ 'translate',
+];
+
+PaletteProvider.prototype.getPaletteEntries = function () {
+ const actions = {};
+ const create = this._create;
+ const elementFactory = this._elementFactory;
+ const spaceTool = this._spaceTool;
+ const lassoTool = this._lassoTool;
+ const handTool = this._handTool;
+ const globalConnect = this._globalConnect;
+ const translate = this._translate;
+
+ function createAction(type, group, className, title, options) {
+ function createListener(event) {
+ const shape = elementFactory.createShape(
+ Object.assign({ type }, options),
+ );
+
+ if (options) {
+ shape.businessObject.di.isExpanded = options.isExpanded;
+ }
+
+ create.start(event, shape);
+ }
+
+ const shortType = type.replace(/^bpmn:/, '');
+
+ return {
+ group,
+ className,
+ title: title || translate('Create {type}', { type: shortType }),
+ action: {
+ dragstart: createListener,
+ click: createListener,
+ },
+ };
+ }
+
+ function createSubprocess(event) {
+ const subProcess = elementFactory.createShape({
+ type: 'bpmn:SubProcess',
+ x: 0,
+ y: 0,
+ isExpanded: true,
+ });
+
+ const startEvent = elementFactory.createShape({
+ type: 'bpmn:StartEvent',
+ x: 40,
+ y: 82,
+ parent: subProcess,
+ });
+
+ create.start(event, [subProcess, startEvent], {
+ hints: {
+ autoSelect: [startEvent],
+ },
+ });
+ }
+
+ function createParticipant(event) {
+ create.start(event, elementFactory.createParticipantShape());
+ }
+
+ Object.assign(actions, {
+ 'hand-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-hand-tool',
+ title: translate('Activate the hand tool'),
+ action: {
+ click(event) {
+ handTool.activateHand(event);
+ },
+ },
+ },
+ 'lasso-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-lasso-tool',
+ title: translate('Activate the lasso tool'),
+ action: {
+ click(event) {
+ lassoTool.activateSelection(event);
+ },
+ },
+ },
+ 'space-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-space-tool',
+ title: translate('Activate the create/remove space tool'),
+ action: {
+ click(event) {
+ spaceTool.activateSelection(event);
+ },
+ },
+ },
+ 'global-connect-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Activate the global connect tool'),
+ action: {
+ click(event) {
+ globalConnect.toggle(event);
+ },
+ },
+ },
+ 'tool-separator': {
+ group: 'tools',
+ separator: true,
+ },
+ 'create.start-event': createAction(
+ 'bpmn:StartEvent',
+ 'event',
+ 'bpmn-icon-start-event-none',
+ translate('Create StartEvent'),
+ ),
+ 'create.intermediate-event': createAction(
+ 'bpmn:IntermediateThrowEvent',
+ 'event',
+ 'bpmn-icon-intermediate-event-none',
+ translate('Create Intermediate/Boundary Event'),
+ ),
+ 'create.end-event': createAction(
+ 'bpmn:EndEvent',
+ 'event',
+ 'bpmn-icon-end-event-none',
+ translate('Create EndEvent'),
+ ),
+ 'create.exclusive-gateway': createAction(
+ 'bpmn:ExclusiveGateway',
+ 'gateway',
+ 'bpmn-icon-gateway-none',
+ translate('Create Gateway'),
+ ),
+ 'create.user-task': createAction(
+ 'bpmn:UserTask',
+ 'activity',
+ 'bpmn-icon-user-task',
+ translate('Create User Task'),
+ ),
+ 'create.service-task': createAction(
+ 'bpmn:ServiceTask',
+ 'activity',
+ 'bpmn-icon-service',
+ translate('Create Service Task'),
+ ),
+ 'create.data-object': createAction(
+ 'bpmn:DataObjectReference',
+ 'data-object',
+ 'bpmn-icon-data-object',
+ translate('Create DataObjectReference'),
+ ),
+ 'create.data-store': createAction(
+ 'bpmn:DataStoreReference',
+ 'data-store',
+ 'bpmn-icon-data-store',
+ translate('Create DataStoreReference'),
+ ),
+ 'create.subprocess-expanded': {
+ group: 'activity',
+ className: 'bpmn-icon-subprocess-expanded',
+ title: translate('Create expanded SubProcess'),
+ action: {
+ dragstart: createSubprocess,
+ click: createSubprocess,
+ },
+ },
+ 'create.participant-expanded': {
+ group: 'collaboration',
+ className: 'bpmn-icon-participant',
+ title: translate('Create Pool/Participant'),
+ action: {
+ dragstart: createParticipant,
+ click: createParticipant,
+ },
+ },
+ 'create.group': createAction(
+ 'bpmn:Group',
+ 'artifact',
+ 'bpmn-icon-group',
+ translate('Create Group'),
+ ),
+ });
+
+ return actions;
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/translate/customTranslate.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/translate/customTranslate.js
new file mode 100644
index 000000000..cc6ad6e9a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/translate/customTranslate.js
@@ -0,0 +1,44 @@
+// import translations from "./zh";
+//
+// export default function customTranslate(template, replacements) {
+// replacements = replacements || {};
+//
+// // Translate
+// template = translations[template] || template;
+//
+// // Replace
+// return template.replace(/{([^}]+)}/g, function(_, key) {
+// let str = replacements[key];
+// if (
+// translations[replacements[key]] !== null &&
+// translations[replacements[key]] !== "undefined"
+// ) {
+// // eslint-disable-next-line no-mixed-spaces-and-tabs
+// str = translations[replacements[key]];
+// // eslint-disable-next-line no-mixed-spaces-and-tabs
+// }
+// return str || "{" + key + "}";
+// });
+// }
+
+export default function customTranslate(translations) {
+ return function (template, replacements) {
+ replacements = replacements || {};
+ // 将模板和翻译字典的键统一转换为小写进行匹配
+ const lowerTemplate = template.toLowerCase();
+ const translation = Object.keys(translations).find(
+ (key) => key.toLowerCase() === lowerTemplate,
+ );
+
+ // 如果找到匹配的翻译,使用翻译后的模板
+ if (translation) {
+ template = translations[translation];
+ }
+
+ // 替换模板中的占位符
+ return template.replaceAll(/\{([^}]+)\}/g, (_, key) => {
+ // 如果替换值存在,返回替换值;否则返回原始占位符
+ return replacements[key] === undefined ? `{${key}}` : replacements[key];
+ });
+ };
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/translate/zh.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/translate/zh.js
new file mode 100644
index 000000000..a573e5f2b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/designer/plugins/translate/zh.js
@@ -0,0 +1,253 @@
+/**
+ * This is a sample file that should be replaced with the actual translation.
+ *
+ * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
+ * translations and labels to translate.
+ */
+export default {
+ // 添加部分
+ 'Append EndEvent': '追加结束事件',
+ 'Append Gateway': '追加网关',
+ 'Append Task': '追加任务',
+ 'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
+ TextAnnotation: '文本注释',
+ 'Activate the global connect tool': '激活全局连接工具',
+ 'Append {type}': '添加 {type}',
+ 'Add Lane above': '在上面添加道',
+ 'Divide into two Lanes': '分割成两个道',
+ 'Divide into three Lanes': '分割成三个道',
+ 'Add Lane below': '在下面添加道',
+ 'Append compensation activity': '追加补偿活动',
+ 'Change type': '修改类型',
+ 'Connect using Association': '使用关联连接',
+ 'Connect using Sequence/MessageFlow or Association':
+ '使用顺序/消息流或者关联连接',
+ 'Connect using DataInputAssociation': '使用数据输入关联连接',
+ Remove: '移除',
+ 'Activate the hand tool': '激活抓手工具',
+ 'Activate the lasso tool': '激活套索工具',
+ 'Activate the create/remove space tool': '激活创建/删除空间工具',
+ 'Create expanded SubProcess': '创建扩展子过程',
+ 'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
+ 'Create Pool/Participant': '创建池/参与者',
+ 'Participant Multiplicity': '参与者多重性',
+ 'Empty pool/participant (removes content)': '清空池/参与者(移除内容)',
+ 'Empty pool/participant': '收缩池/参与者',
+ 'Expanded pool/participant': '展开池/参与者',
+ 'Parallel Multi-Instance': '并行多重事件',
+ 'Sequential Multi-Instance': '时序多重事件',
+ DataObjectReference: '数据对象参考',
+ DataStoreReference: '数据存储参考',
+ 'Data object reference': '数据对象引用 ',
+ 'Data store reference': '数据存储引用 ',
+ Loop: '循环',
+ 'Ad-hoc': '即席',
+ 'Create {type}': '创建 {type}',
+ Task: '任务',
+ 'Send Task': '发送任务',
+ 'Receive Task': '接收任务',
+ 'User Task': '用户任务',
+ 'Manual Task': '手工任务',
+ 'Business Rule Task': '业务规则任务',
+ 'Service Task': '服务任务',
+ 'Script Task': '脚本任务',
+ 'Call Activity': '调用活动',
+ 'Sub-Process (collapsed)': '子流程(折叠的)',
+ 'Sub-Process (expanded)': '子流程(展开的)',
+ 'Ad-hoc sub-process': '即席子流程',
+ 'Ad-hoc sub-process (collapsed)': '即席子流程(折叠的)',
+ 'Ad-hoc sub-process (expanded)': '即席子流程(展开的)',
+ 'Start Event': '开始事件',
+ StartEvent: '开始事件',
+ 'Intermediate Throw Event': '中间事件',
+ 'End Event': '结束事件',
+ EndEvent: '结束事件',
+ 'Create StartEvent': '创建开始事件',
+ 'Create EndEvent': '创建结束事件',
+ 'Create Task': '创建任务',
+ 'Create User Task': '创建用户任务',
+ 'Create Call Activity': '创建调用活动',
+ 'Create Service Task': '创建服务任务',
+ 'Create Gateway': '创建网关',
+ 'Create DataObjectReference': '创建数据对象',
+ 'Create DataStoreReference': '创建数据存储',
+ 'Create Group': '创建分组',
+ 'Create Intermediate/Boundary Event': '创建中间/边界事件',
+ 'Message Start Event': '消息开始事件',
+ 'Timer Start Event': '定时开始事件',
+ 'Conditional Start Event': '条件开始事件',
+ 'Signal Start Event': '信号开始事件',
+ 'Error Start Event': '错误开始事件',
+ 'Escalation Start Event': '升级开始事件',
+ 'Compensation Start Event': '补偿开始事件',
+ 'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
+ 'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
+ 'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
+ 'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
+ 'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
+ 'Message Intermediate Catch Event': '消息中间捕获事件',
+ 'Message Intermediate Throw Event': '消息中间抛出事件',
+ 'Timer Intermediate Catch Event': '定时中间捕获事件',
+ 'Escalation Intermediate Throw Event': '升级中间抛出事件',
+ 'Conditional Intermediate Catch Event': '条件中间捕获事件',
+ 'Link Intermediate Catch Event': '链接中间捕获事件',
+ 'Link Intermediate Throw Event': '链接中间抛出事件',
+ 'Compensation Intermediate Throw Event': '补偿中间抛出事件',
+ 'Signal Intermediate Catch Event': '信号中间捕获事件',
+ 'Signal Intermediate Throw Event': '信号中间抛出事件',
+ 'Message End Event': '消息结束事件',
+ 'Escalation End Event': '定时结束事件',
+ 'Error End Event': '错误结束事件',
+ 'Cancel End Event': '取消结束事件',
+ 'Compensation End Event': '补偿结束事件',
+ 'Signal End Event': '信号结束事件',
+ 'Terminate End Event': '终止结束事件',
+ 'Message Boundary Event': '消息边界事件',
+ 'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
+ 'Timer Boundary Event': '定时边界事件',
+ 'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
+ 'Escalation Boundary Event': '升级边界事件',
+ 'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
+ 'Conditional Boundary Event': '条件边界事件',
+ 'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
+ 'Error Boundary Event': '错误边界事件',
+ 'Cancel Boundary Event': '取消边界事件',
+ 'Signal Boundary Event': '信号边界事件',
+ 'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
+ 'Compensation Boundary Event': '补偿边界事件',
+ 'Exclusive Gateway': '互斥网关',
+ 'Parallel Gateway': '并行网关',
+ 'Inclusive Gateway': '相容网关',
+ 'Complex Gateway': '复杂网关',
+ 'Event-based Gateway': '事件网关',
+ Transaction: '转运',
+ 'sub-process': '子流程',
+ 'Event sub-process': '事件子流程',
+ 'Collapsed Pool': '折叠池',
+ 'Expanded Pool': '展开池',
+
+ // Errors
+ 'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
+ 'no shape type specified': '没有指定的形状类型',
+ 'flow elements must be children of pools/participants':
+ '流元素必须是池/参与者的子类',
+ 'out of bounds release': 'out of bounds release',
+ 'more than {count} child lanes': '子道大于{count} ',
+ 'element required': '元素不能为空',
+ 'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
+ 'no diagram to display': '没有可展示的流程图',
+ 'no process or collaboration to display': '没有可展示的流程/协作',
+ 'element {element} referenced by {referenced}#{property} not yet drawn':
+ '由{referenced}#{property}引用的{element}元素仍未绘制',
+ 'already rendered {element}': '{element} 已被渲染',
+ 'failed to import {element}': '导入{element}失败',
+ // 属性面板的参数
+ Id: '编号',
+ Name: '名称',
+ General: '常规',
+ Details: '详情',
+ 'Message Name': '消息名称',
+ Message: '消息',
+ Initiator: '创建者',
+ 'Asynchronous Continuations': '持续异步',
+ 'Asynchronous Before': '异步前',
+ 'Asynchronous After': '异步后',
+ 'Job Configuration': '工作配置',
+ Exclusive: '排除',
+ 'Job Priority': '工作优先级',
+ 'Retry Time Cycle': '重试时间周期',
+ Documentation: '文档',
+ 'Element Documentation': '元素文档',
+ 'History Configuration': '历史配置',
+ 'History Time To Live': '历史的生存时间',
+ Forms: '表单',
+ 'Form Key': '表单key',
+ 'Form Fields': '表单字段',
+ 'Business Key': '业务key',
+ 'Form Field': '表单字段',
+ ID: '编号',
+ Type: '类型',
+ Label: '名称',
+ 'Default Value': '默认值',
+ 'Default Flow': '默认流转路径',
+ 'Conditional Flow': '条件流转路径',
+ 'Sequence Flow': '普通流转路径',
+ Validation: '校验',
+ 'Add Constraint': '添加约束',
+ Config: '配置',
+ Properties: '属性',
+ 'Add Property': '添加属性',
+ Value: '值',
+ Listeners: '监听器',
+ 'Execution Listener': '执行监听',
+ 'Event Type': '事件类型',
+ 'Listener Type': '监听器类型',
+ 'Java Class': 'Java类',
+ Expression: '表达式',
+ 'Must provide a value': '必须提供一个值',
+ 'Delegate Expression': '代理表达式',
+ Script: '脚本',
+ 'Script Format': '脚本格式',
+ 'Script Type': '脚本类型',
+ 'Inline Script': '内联脚本',
+ 'External Script': '外部脚本',
+ Resource: '资源',
+ 'Field Injection': '字段注入',
+ Extensions: '扩展',
+ 'Input/Output': '输入/输出',
+ 'Input Parameters': '输入参数',
+ 'Output Parameters': '输出参数',
+ Parameters: '参数',
+ 'Output Parameter': '输出参数',
+ 'Timer Definition Type': '定时器定义类型',
+ 'Timer Definition': '定时器定义',
+ Date: '日期',
+ Duration: '持续',
+ Cycle: '循环',
+ Signal: '信号',
+ 'Signal Name': '信号名称',
+ Escalation: '升级',
+ Error: '错误',
+ 'Link Name': '链接名称',
+ Condition: '条件名称',
+ 'Variable Name': '变量名称',
+ 'Variable Event': '变量事件',
+ 'Specify more than one variable change event as a comma separated list.':
+ '多个变量事件以逗号隔开',
+ 'Wait for Completion': '等待完成',
+ 'Activity Ref': '活动参考',
+ 'Version Tag': '版本标签',
+ Executable: '可执行文件',
+ 'External Task Configuration': '扩展任务配置',
+ 'Task Priority': '任务优先级',
+ External: '外部',
+ Connector: '连接器',
+ 'Must configure Connector': '必须配置连接器',
+ 'Connector Id': '连接器编号',
+ Implementation: '实现方式',
+ 'Field Injections': '字段注入',
+ Fields: '字段',
+ 'Result Variable': '结果变量',
+ Topic: '主题',
+ 'Configure Connector': '配置连接器',
+ 'Input Parameter': '输入参数',
+ Assignee: '代理人',
+ 'Candidate Users': '候选用户',
+ 'Candidate Groups': '候选组',
+ 'Due Date': '到期时间',
+ 'Follow Up Date': '跟踪日期',
+ Priority: '优先级',
+ [`The follow up date as an EL expression (e.g. \${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)`]: `跟踪日期必须符合EL表达式,如: \${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00`,
+ [`The due date as an EL expression (e.g. \${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)`]: `跟踪日期必须符合EL表达式,如: \${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00`,
+ Variables: '变量',
+ 'Candidate Starter Configuration': '候选人起动器配置',
+ 'Candidate Starter Groups': '候选人起动器组',
+ 'This maps to the process definition key.': '这映射到流程定义键。',
+ 'Candidate Starter Users': '候选人起动器的用户',
+ 'Specify more than one user as a comma separated list.':
+ '指定多个用户作为逗号分隔的列表。',
+ 'Tasklist Configuration': 'Tasklist配置',
+ Startable: '启动',
+ 'Specify more than one group as a comma separated list.':
+ '指定多个组作为逗号分隔的列表。',
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/index.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/index.ts
new file mode 100644
index 000000000..f27319abb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/index.ts
@@ -0,0 +1,9 @@
+import './theme/index.scss';
+import 'bpmn-js/dist/assets/diagram-js.css';
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
+
+export { default as MyProcessDesigner } from './designer';
+export { default as MyProcessViewer } from './designer/index2';
+export { default as MyProcessPenal } from './penal';
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/palette/ProcessPalette.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/palette/ProcessPalette.vue
new file mode 100644
index 000000000..1bb79de35
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/palette/ProcessPalette.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/PropertiesPanel.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/PropertiesPanel.vue
new file mode 100644
index 000000000..97ad28d99
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/PropertiesPanel.vue
@@ -0,0 +1,401 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/base/ElementBaseInfo.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/base/ElementBaseInfo.vue
new file mode 100644
index 000000000..9b16a5245
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/base/ElementBaseInfo.vue
@@ -0,0 +1,225 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/ElementCustomConfig.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/ElementCustomConfig.vue
new file mode 100644
index 000000000..656249b25
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/ElementCustomConfig.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/components/BoundaryEventTimer.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/components/BoundaryEventTimer.vue
new file mode 100644
index 000000000..25a6ebd38
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/components/BoundaryEventTimer.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
审批人超时未处理时
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+ 当超过
+
+
+
+
+ {
+ updateTimeModdle();
+ updateElementExtensions();
+ }
+ "
+ />
+
+
+
+
+
+ 未处理
+
+
+
+
+
+ {
+ updateTimeModdle();
+ updateElementExtensions();
+ }
+ "
+ />
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/components/UserTaskCustomConfig.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/components/UserTaskCustomConfig.vue
new file mode 100644
index 000000000..93d04013b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/components/UserTaskCustomConfig.vue
@@ -0,0 +1,699 @@
+
+
+
+
+
+
审批类型
+
+
+
+ {{ item.label }}
+
+
+
+
+
审批人拒绝时
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
审批人为空时
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
审批人与提交人为同一人时
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
操作按钮
+
+
+
+
+
+
+
+ {{ OPERATION_BUTTON_NAME.get(item.id) }}
+
+
+
+
+
+
+
+
+
+
+
+
字段权限
+
+
+
+
字段名称
+
+
+ 只读
+
+
+ 可编辑
+
+
+ 隐藏
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
是否需要签名
+
+
+
+
+
审批意见
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/data.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/data.ts
new file mode 100644
index 000000000..cd8830003
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/custom-config/data.ts
@@ -0,0 +1,13 @@
+import BoundaryEventTimer from './components/BoundaryEventTimer.vue';
+import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue';
+
+export const CustomConfigMap = {
+ UserTask: {
+ name: '用户任务',
+ component: UserTaskCustomConfig,
+ },
+ BoundaryEventTimerEventDefinition: {
+ name: '定时边界事件(非中断)',
+ component: BoundaryEventTimer,
+ },
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/flow-condition/FlowCondition.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/flow-condition/FlowCondition.vue
new file mode 100644
index 000000000..e135c428f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/flow-condition/FlowCondition.vue
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/form/ElementForm.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/form/ElementForm.vue
new file mode 100644
index 000000000..dce969d76
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/form/ElementForm.vue
@@ -0,0 +1,539 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/index.js
new file mode 100644
index 000000000..1688cf7d1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/index.js
@@ -0,0 +1,7 @@
+import MyPropertiesPanel from './PropertiesPanel.vue';
+
+MyPropertiesPanel.install = function (Vue) {
+ Vue.component(MyPropertiesPanel.name, MyPropertiesPanel);
+};
+
+export default MyPropertiesPanel;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/ElementListeners.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/ElementListeners.vue
new file mode 100644
index 000000000..94cf1c8eb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/ElementListeners.vue
@@ -0,0 +1,576 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/ListenerFieldModal.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/ListenerFieldModal.vue
new file mode 100644
index 000000000..4ce87247b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/ListenerFieldModal.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/UserTaskListeners.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/UserTaskListeners.vue
new file mode 100644
index 000000000..709bd2884
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/UserTaskListeners.vue
@@ -0,0 +1,562 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/utilSelf.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/utilSelf.ts
new file mode 100644
index 000000000..2cfd78dfa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/listeners/utilSelf.ts
@@ -0,0 +1,101 @@
+// 初始化表单数据
+import { cloneDeep } from '@vben/utils';
+
+export function initListenerForm(listener: any) {
+ let self = {
+ ...listener,
+ };
+ if (listener.script) {
+ self = {
+ ...listener,
+ ...listener.script,
+ scriptType: listener.script.resource ? 'externalScript' : 'inlineScript',
+ };
+ }
+ if (
+ listener.event === 'timeout' &&
+ listener.eventDefinitions &&
+ listener.eventDefinitions.length > 0
+ ) {
+ let k = '';
+ for (const key in listener.eventDefinitions[0]) {
+ // console.log(listener.eventDefinitions, key);
+ if (key.includes('time')) {
+ k = key;
+ self.eventDefinitionType = key.replace('time', '').toLowerCase();
+ }
+ }
+ // console.log(k);
+ self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
+ }
+ return self;
+}
+
+export function initListenerType(listener: any) {
+ let listenerType;
+ if (listener.class) listenerType = 'classListener';
+ if (listener.expression) listenerType = 'expressionListener';
+ if (listener.delegateExpression) listenerType = 'delegateExpressionListener';
+ if (listener.script) listenerType = 'scriptListener';
+ return {
+ ...cloneDeep(listener),
+ ...listener.script,
+ listenerType,
+ };
+}
+
+/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
+export function initListenerForm2(processListener: any) {
+ switch (processListener.valueType) {
+ case 'class': {
+ return {
+ listenerType: 'classListener',
+ class: processListener.value,
+ event: processListener.event,
+ fields: [],
+ id: undefined,
+ };
+ }
+ case 'delegateExpression': {
+ return {
+ listenerType: 'delegateExpressionListener',
+ delegateExpression: processListener.value,
+ event: processListener.event,
+ fields: [],
+ id: undefined,
+ };
+ }
+ case 'expression': {
+ return {
+ listenerType: 'expressionListener',
+ expression: processListener.value,
+ event: processListener.event,
+ fields: [],
+ id: undefined,
+ };
+ }
+ // No default
+ }
+ throw new Error('未知的监听器类型');
+}
+
+export const listenerType = {
+ classListener: 'Java 类',
+ expressionListener: '表达式',
+ delegateExpressionListener: '代理表达式',
+ scriptListener: '脚本',
+};
+
+export const eventType = {
+ create: '创建',
+ assignment: '指派',
+ complete: '完成',
+ delete: '删除',
+ update: '更新',
+ timeout: '超时',
+};
+
+export const fieldType = {
+ string: '字符串',
+ expression: '表达式',
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/multi-instance/ElementMultiInstance.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/multi-instance/ElementMultiInstance.vue
new file mode 100644
index 000000000..2883834d3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/multi-instance/ElementMultiInstance.vue
@@ -0,0 +1,540 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/other/ElementOtherConfig.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/other/ElementOtherConfig.vue
new file mode 100644
index 000000000..019d3829e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/other/ElementOtherConfig.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/properties/ElementProperties.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/properties/ElementProperties.vue
new file mode 100644
index 000000000..beae93e1b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/properties/ElementProperties.vue
@@ -0,0 +1,253 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/signal-message/SignalAndMessage.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/signal-message/SignalAndMessage.vue
new file mode 100644
index 000000000..57414380d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/signal-message/SignalAndMessage.vue
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+ 消息列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 信号列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/signal-message/SignalMessageModal.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/signal-message/SignalMessageModal.vue
new file mode 100644
index 000000000..ee6f72280
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/signal-message/SignalMessageModal.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/ElementTask.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/ElementTask.vue
new file mode 100644
index 000000000..2967742e4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/ElementTask.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/data.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/data.ts
new file mode 100644
index 000000000..d453b382c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/data.ts
@@ -0,0 +1,40 @@
+import CallActivity from './task-components/CallActivity.vue';
+import ReceiveTask from './task-components/ReceiveTask.vue';
+import ScriptTask from './task-components/ScriptTask.vue';
+import ServiceTask from './task-components/ServiceTask.vue';
+import UserTask from './task-components/UserTask.vue';
+
+export const installedComponent = {
+ UserTask: {
+ name: '用户任务',
+ component: UserTask,
+ },
+ ServiceTask: {
+ name: '服务任务',
+ component: ServiceTask,
+ },
+ ScriptTask: {
+ name: '脚本任务',
+ component: ScriptTask,
+ },
+ ReceiveTask: {
+ name: '接收任务',
+ component: ReceiveTask,
+ },
+ CallActivity: {
+ name: '调用活动',
+ component: CallActivity,
+ },
+};
+
+export const getTaskCollapseItemName = (
+ elementType: keyof typeof installedComponent,
+) => {
+ return installedComponent[elementType].name;
+};
+
+export const isTaskCollapseItemShow = (
+ elementType: keyof typeof installedComponent,
+) => {
+ return installedComponent[elementType];
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/CallActivity.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/CallActivity.vue
new file mode 100644
index 000000000..778268f6d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/CallActivity.vue
@@ -0,0 +1,418 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/HttpHeaderEditor.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/HttpHeaderEditor.vue
new file mode 100644
index 000000000..4ee5d2707
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/HttpHeaderEditor.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ReceiveTask.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ReceiveTask.vue
new file mode 100644
index 000000000..a4d53b718
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ReceiveTask.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+ 消息实例:
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ScriptTask.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ScriptTask.vue
new file mode 100644
index 000000000..5dcf6041a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ScriptTask.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ServiceTask.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ServiceTask.vue
new file mode 100644
index 000000000..d15451301
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/ServiceTask.vue
@@ -0,0 +1,491 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/UserTask.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/UserTask.vue
new file mode 100644
index 000000000..24997687d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/task/task-components/UserTask.vue
@@ -0,0 +1,576 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/CycleConfig.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/CycleConfig.vue
new file mode 100644
index 000000000..5d35687d4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/CycleConfig.vue
@@ -0,0 +1,390 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 每{{ f.label }}
+
+
+ 从
+
+ 到
+
+ 之间每{{ f.label }}
+
+
+ 从第
+
+ 开始每
+
+ {{ f.label }}
+
+ 指定
+
+
+
+
+
+ {{ pad(n - 1) }}
+
+
+
+
+
+
+
+
+
+
+
+ 循环次数:
+
+
+ 开始时间:
+
+
+ 间隔时长:
+
+
+
+ {{ unit.label }}:
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/DurationConfig.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/DurationConfig.vue
new file mode 100644
index 000000000..61026cb99
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/DurationConfig.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/TimeEventConfig.vue b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/TimeEventConfig.vue
new file mode 100644
index 000000000..f754f2200
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/penal/time-event-config/TimeEventConfig.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+ 类型:
+
+
+
+
+
+
+
+
+ 条件:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/index.scss b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/index.scss
new file mode 100644
index 000000000..a2d32d4f3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/index.scss
@@ -0,0 +1,120 @@
+@use './process-designer';
+@use './process-panel';
+
+$success-color: #4eb819;
+$primary-color: #409eff;
+$danger-color: #f56c6c;
+$cancel-color: #909399;
+
+.process-viewer {
+ position: relative;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
+ repeat !important;
+ border: 1px solid #efefef;
+
+ .success-arrow {
+ fill: $success-color;
+ stroke: $success-color;
+ }
+
+ .success-conditional {
+ fill: white;
+ stroke: $success-color;
+ }
+
+ .success.djs-connection {
+ .djs-visual path {
+ stroke: $success-color !important;
+ //marker-end: url(#sequenceflow-end-white-success)!important;
+ }
+ }
+
+ .success.djs-connection.condition-expression {
+ .djs-visual path {
+ //marker-start: url(#conditional-flow-marker-white-success)!important;
+ }
+ }
+
+ .success.djs-shape {
+ .djs-visual rect {
+ fill: $success-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $success-color !important;
+ }
+
+ .djs-visual polygon {
+ stroke: $success-color !important;
+ }
+
+ .djs-visual path:nth-child(2) {
+ fill: $success-color !important;
+ stroke: $success-color !important;
+ }
+
+ .djs-visual circle {
+ fill: $success-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $success-color !important;
+ }
+ }
+
+ .primary.djs-shape {
+ .djs-visual rect {
+ fill: $primary-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $primary-color !important;
+ }
+
+ .djs-visual polygon {
+ stroke: $primary-color !important;
+ }
+
+ .djs-visual circle {
+ fill: $primary-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $primary-color !important;
+ }
+ }
+
+ .danger.djs-shape {
+ .djs-visual rect {
+ fill: $danger-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $danger-color !important;
+ }
+
+ .djs-visual polygon {
+ stroke: $danger-color !important;
+ }
+
+ .djs-visual circle {
+ fill: $danger-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $danger-color !important;
+ }
+ }
+
+ .cancel.djs-shape {
+ .djs-visual rect {
+ fill: $cancel-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $cancel-color !important;
+ }
+
+ .djs-visual polygon {
+ stroke: $cancel-color !important;
+ }
+
+ .djs-visual circle {
+ fill: $cancel-color !important;
+ fill-opacity: 0.15 !important;
+ stroke: $cancel-color !important;
+ }
+ }
+}
+
+.process-viewer .djs-tooltip-container,
+.process-viewer .djs-overlay-container,
+.process-viewer .djs-palette {
+ display: none;
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/process-designer.scss b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/process-designer.scss
new file mode 100644
index 000000000..f35e1ee5b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/process-designer.scss
@@ -0,0 +1,183 @@
+@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
+
+// 边框被 token-simulation 样式覆盖了
+.djs-palette {
+ background: var(--palette-background-color);
+ border: solid 1px var(--palette-border-color) !important;
+ border-radius: 2px;
+}
+
+.my-process-designer {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+
+ .my-process-designer__header {
+ width: 100%;
+ min-height: 36px;
+
+ .ant-button {
+ text-align: center;
+ }
+
+ .ant-button-group {
+ margin: 4px;
+ }
+
+ .ant-tooltip__popper {
+ .ant-button {
+ width: 100%;
+ padding-right: 8px;
+ padding-left: 8px;
+ text-align: left;
+ }
+
+ .ant-button:hover {
+ color: #fff;
+ background: rgb(64 158 255 / 80%);
+ }
+ }
+
+ .align {
+ position: relative;
+
+ i {
+ &::after {
+ position: absolute;
+ content: '|';
+ // transform: rotate(90deg) translate(200%, 60%);
+ transform: rotate(180deg) translate(271%, -10%);
+ }
+ }
+ }
+
+ .align.align-left i {
+ transform: rotate(90deg);
+ }
+
+ .align.align-right i {
+ transform: rotate(-90deg);
+ }
+
+ .align.align-top i {
+ transform: rotate(180deg);
+ }
+
+ .align.align-bottom i {
+ transform: rotate(0deg);
+ }
+
+ .align.align-center i {
+ transform: rotate(0deg);
+
+ &::after {
+ // transform: rotate(90deg) translate(0, 60%);
+ transform: rotate(0deg) translate(-0%, -5%);
+ }
+ }
+
+ .align.align-middle i {
+ transform: rotate(-90deg);
+
+ &::after {
+ // transform: rotate(90deg) translate(0, 60%);
+ transform: rotate(0deg) translate(0, -10%);
+ }
+ }
+ }
+
+ .my-process-designer__container {
+ display: inline-flex;
+ flex: 1;
+ width: 100%;
+
+ .my-process-designer__canvas {
+ position: relative;
+ flex: 1;
+ height: 100%;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
+ repeat !important;
+
+ div.toggle-mode {
+ display: none;
+ }
+ }
+
+ .my-process-designer__property-panel {
+ z-index: 10;
+ height: 100%;
+ overflow: scroll;
+ overflow-y: auto;
+
+ * {
+ box-sizing: border-box;
+ }
+ }
+ // svg {
+ // width: 100%;
+ // height: 100%;
+ // min-height: 100%;
+ // overflow: hidden;
+ // }
+ }
+}
+
+//侧边栏配置
+// .djs-palette .two-column .open {
+.open {
+ // .djs-palette.open {
+ .djs-palette-entries {
+ div[class^='bpmn-icon-']::before,
+ div[class*='bpmn-icon-']::before {
+ line-height: unset;
+ }
+
+ div.entry {
+ position: relative;
+ }
+
+ div.entry:hover {
+ &::after {
+ position: absolute;
+ top: 0;
+ right: -10px;
+ bottom: 0;
+ z-index: 100;
+ box-sizing: border-box;
+ display: inline-block;
+ width: max-content;
+ padding: 0 16px;
+ overflow: hidden;
+ font-size: 0.5em;
+ font-variant: normal;
+ vertical-align: text-bottom;
+ text-transform: none;
+ text-decoration: inherit;
+ content: attr(title);
+ background: #fafafa;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: 0 0 6px #eee;
+ transform: translateX(100%);
+ }
+ }
+ }
+}
+
+pre {
+ height: 100%;
+ max-height: calc(80vh - 32px);
+ margin: 0;
+ overflow: hidden;
+ overflow-y: auto;
+}
+
+.hljs {
+ white-space: pre-wrap;
+}
+
+.hljs * {
+ font-family: Consolas, Monaco, monospace;
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/process-panel.scss b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/process-panel.scss
new file mode 100644
index 000000000..ab4f05d3a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/theme/process-panel.scss
@@ -0,0 +1,127 @@
+.process-panel__container {
+ box-sizing: border-box;
+ max-height: 100%;
+ padding: 0 8px;
+ overflow-y: scroll;
+ border-left: 1px solid #eee;
+ box-shadow: 0 0 8px #ccc;
+}
+
+.panel-tab__title {
+ padding: 0 8px;
+ font-size: 1.1em;
+ font-weight: 600;
+ line-height: 1.2em;
+
+ i {
+ margin-right: 8px;
+ font-size: 1.2em;
+ }
+}
+
+.panel-tab__content {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 8px 16px;
+ border-top: 1px solid #eee;
+
+ .panel-tab__content--title {
+ display: flex;
+ justify-content: space-between;
+ padding-bottom: 8px;
+
+ span {
+ flex: 1;
+ text-align: left;
+ }
+ }
+}
+
+.element-property {
+ display: flex;
+ align-items: flex-start;
+ width: 100%;
+ margin: 8px 0;
+
+ .element-property__label {
+ box-sizing: border-box;
+ display: block;
+ width: 90px;
+ padding-right: 12px;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 32px;
+ text-align: right;
+ }
+
+ .element-property__value {
+ flex: 1;
+ line-height: 32px;
+ }
+
+ .ant-form-item {
+ width: 100%;
+ padding-bottom: 18px;
+ margin-bottom: 0;
+ }
+}
+
+.list-property {
+ flex-direction: column;
+
+ .element-listener-item {
+ display: inline-grid;
+ grid-template-columns: 16px auto 32px 32px;
+ column-gap: 8px;
+ width: 100%;
+ }
+
+ .element-listener-item + .element-listener-item {
+ margin-top: 8px;
+ }
+}
+
+.listener-filed__title {
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ margin-top: 0;
+
+ span {
+ width: 200px;
+ font-size: 14px;
+ text-align: left;
+ }
+
+ i {
+ margin-right: 8px;
+ }
+}
+
+.element-drawer__button {
+ display: inline-flex;
+ justify-content: space-around;
+ width: 100%;
+ margin-top: 8px;
+}
+
+.element-drawer__button > .ant-button {
+ width: 100%;
+}
+
+.ant-collapse-item__content {
+ padding-bottom: 0;
+}
+
+.ant-input.is-disabled .ant-input__inner {
+ color: #999;
+}
+
+.ant-form-item.ant-form-item--mini {
+ margin-bottom: 0;
+
+ & + .ant-form-item {
+ margin-top: 16px;
+ }
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/utils.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/utils.ts
new file mode 100644
index 000000000..b8bf53277
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/package/utils.ts
@@ -0,0 +1,94 @@
+const bpmnInstances = () => (window as any)?.bpmnInstances;
+// 创建监听器实例
+export function createListenerObject(options, isTask, prefix) {
+ const listenerObj = Object.create(null);
+ listenerObj.event = options.event;
+ isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
+ switch (options.listenerType) {
+ case 'delegateExpressionListener': {
+ listenerObj.delegateExpression = options.delegateExpression;
+ break;
+ }
+ case 'expressionListener': {
+ listenerObj.expression = options.expression;
+ break;
+ }
+ case 'scriptListener': {
+ listenerObj.script = createScriptObject(options, prefix);
+ break;
+ }
+ default: {
+ listenerObj.class = options.class;
+ }
+ }
+ // 注入字段
+ if (options.fields) {
+ listenerObj.fields = options.fields.map((field) => {
+ return createFieldObject(field, prefix);
+ });
+ }
+ // 任务监听器的 定时器 设置
+ if (isTask && options.event === 'timeout' && !!options.eventDefinitionType) {
+ const timeDefinition = bpmnInstances().moddle.create(
+ 'bpmn:FormalExpression',
+ {
+ body: options.eventTimeDefinitions,
+ },
+ );
+ const TimerEventDefinition = bpmnInstances().moddle.create(
+ 'bpmn:TimerEventDefinition',
+ {
+ id: `TimerEventDefinition_${uuid(8)}`,
+ [`time${options.eventDefinitionType.replace(/^\S/, (s) => s.toUpperCase())}`]:
+ timeDefinition,
+ },
+ );
+ listenerObj.eventDefinitions = [TimerEventDefinition];
+ }
+ return bpmnInstances().moddle.create(
+ `${prefix}:${isTask ? 'TaskListener' : 'ExecutionListener'}`,
+ listenerObj,
+ );
+}
+
+// 创建 监听器的注入字段 实例
+export function createFieldObject(option, prefix) {
+ const { name, fieldType, string, expression } = option;
+ const fieldConfig =
+ fieldType === 'string' ? { name, string } : { name, expression };
+ return bpmnInstances().moddle.create(`${prefix}:Field`, fieldConfig);
+}
+
+// 创建脚本实例
+export function createScriptObject(options, prefix) {
+ const { scriptType, scriptFormat, value, resource } = options;
+ const scriptConfig =
+ scriptType === 'inlineScript'
+ ? { scriptFormat, value }
+ : { scriptFormat, resource };
+ return bpmnInstances().moddle.create(`${prefix}:Script`, scriptConfig);
+}
+
+// 更新元素扩展属性
+export function updateElementExtensions(element, extensionList) {
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: extensionList,
+ });
+ // 直接使用原始元素对象,不需要toRaw包装
+ bpmnInstances().modeling.updateProperties(element, {
+ extensionElements: extensions,
+ });
+}
+
+// 创建一个id
+export function uuid(
+ length = 8,
+ charsString = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
+) {
+ let result = '';
+
+ for (let i = length; i > 0; --i) {
+ result += charsString[Math.floor(Math.random() * charsString.length)];
+ }
+ return result;
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/highlight/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/highlight/index.js
new file mode 100644
index 000000000..c8d70960d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/highlight/index.js
@@ -0,0 +1,8 @@
+import hljs from 'highlight.js/lib/core';
+import jsonLanguage from 'highlight.js/lib/languages/json';
+import xmlLanguage from 'highlight.js/lib/languages/xml';
+
+hljs.registerLanguage('xml', xmlLanguage);
+hljs.registerLanguage('json', jsonLanguage);
+
+export default hljs;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/custom-renderer/CustomRenderer.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/custom-renderer/CustomRenderer.js
new file mode 100644
index 000000000..c83615c4d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/custom-renderer/CustomRenderer.js
@@ -0,0 +1,28 @@
+import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer';
+
+function CustomRenderer(
+ config,
+ eventBus,
+ styles,
+ pathMap,
+ canvas,
+ textRenderer,
+) {
+ BpmnRenderer.call(
+ this,
+ config,
+ eventBus,
+ styles,
+ pathMap,
+ canvas,
+ textRenderer,
+ 2000,
+ );
+
+ this.handlers.label = () => null;
+}
+
+CustomRenderer.prototype = Object.create(BpmnRenderer.prototype);
+CustomRenderer.prototype.constructor = CustomRenderer;
+
+export default CustomRenderer;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/custom-renderer/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/custom-renderer/index.js
new file mode 100644
index 000000000..a1842ec76
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/custom-renderer/index.js
@@ -0,0 +1,6 @@
+import CustomRenderer from './CustomRenderer';
+
+export default {
+ __init__: ['customRenderer'],
+ customRenderer: ['type', CustomRenderer],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
new file mode 100644
index 000000000..f343a787d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
@@ -0,0 +1,18 @@
+import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
+import inherits from 'inherits';
+
+function CustomRules(eventBus) {
+ BpmnRules.call(this, eventBus);
+}
+
+inherits(CustomRules, BpmnRules);
+
+CustomRules.prototype.canDrop = function () {
+ return false;
+};
+
+CustomRules.prototype.canMove = function () {
+ return false;
+};
+
+export default CustomRules;
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/rules/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/rules/index.js
new file mode 100644
index 000000000..838b93ea6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/modules/rules/index.js
@@ -0,0 +1,6 @@
+import CustomRules from './CustomRules';
+
+export default {
+ __init__: ['customRules'],
+ customRules: ['type', CustomRules],
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/translations.ts b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/translations.ts
new file mode 100644
index 000000000..75c615592
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/translations.ts
@@ -0,0 +1,25 @@
+/**
+ * This is a sample file that should be replaced with the actual translation.
+ *
+ * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
+ * translations and labels to translate.
+ */
+export default {
+ 'Exclusive Gateway': 'Exklusives Gateway',
+ 'Parallel Gateway': 'Paralleles Gateway',
+ 'Inclusive Gateway': 'Inklusives Gateway',
+ 'Complex Gateway': 'Komplexes Gateway',
+ 'Event based Gateway': 'Ereignis-basiertes Gateway',
+ 'Message Start Event': '消息启动事件',
+ 'Timer Start Event': '定时启动事件',
+ 'Conditional Start Event': '条件启动事件',
+ 'Signal Start Event': '信号启动事件',
+ 'Error Start Event': '错误启动事件',
+ 'Escalation Start Event': '升级启动事件',
+ 'Compensation Start Event': '补偿启动事件',
+ 'Message Start Event (non-interrupting)': '消息启动事件 (非中断)',
+ 'Timer Start Event (non-interrupting)': '定时启动事件 (非中断)',
+ 'Conditional Start Event (non-interrupting)': '条件启动事件 (非中断)',
+ 'Signal Start Event (non-interrupting)': '信号启动事件 (非中断)',
+ 'Escalation Start Event (non-interrupting)': '升级启动事件 (非中断)',
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/directive/clickOutSide.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/directive/clickOutSide.js
new file mode 100644
index 000000000..e8ea772ba
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/directive/clickOutSide.js
@@ -0,0 +1,40 @@
+// outside.js
+
+const ctx = '@@clickoutsideContext';
+
+export default {
+ bind(el, binding, vnode) {
+ const ele = el;
+ const documentHandler = (e) => {
+ if (!vnode.context || ele.contains(e.target)) {
+ return false;
+ }
+ // 调用指令回调
+ if (binding.expression) {
+ vnode.context[el[ctx].methodName](e);
+ } else {
+ el[ctx].bindingFn(e);
+ }
+ };
+ // 将方法添加到ele
+ ele[ctx] = {
+ documentHandler,
+ methodName: binding.expression,
+ bindingFn: binding.value,
+ };
+
+ setTimeout(() => {
+ document.addEventListener('touchstart', documentHandler); // 为document绑定事件
+ });
+ },
+ update(el, binding) {
+ const ele = el;
+ ele[ctx].methodName = binding.expression;
+ ele[ctx].bindingFn = binding.value;
+ },
+ unbind(el) {
+ document.removeEventListener('touchstart', el[ctx].documentHandler); // 解绑
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete el[ctx];
+ },
+};
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/index.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/index.js
new file mode 100644
index 000000000..973685a0b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/index.js
@@ -0,0 +1,10 @@
+export function debounce(fn, delay = 500) {
+ let timer;
+ return function (...args) {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ timer = setTimeout(fn.bind(this, ...args), delay);
+ };
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/xml2json.js b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/xml2json.js
new file mode 100644
index 000000000..76858999d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/bpmn-process-designer/src/utils/xml2json.js
@@ -0,0 +1,51 @@
+function xmlStr2XmlObj(xmlStr) {
+ // eslint-disable-next-line no-useless-assignment
+ let xmlObj = {};
+ if (document.all) {
+ const xmlDom = new window.ActiveXObject('Microsoft.XMLDOM');
+ xmlDom.loadXML(xmlStr);
+ xmlObj = xmlDom;
+ } else {
+ xmlObj = new DOMParser().parseFromString(xmlStr, 'text/xml');
+ }
+ return xmlObj;
+}
+
+function xml2json(xml) {
+ try {
+ let obj = {};
+ if (xml.children.length > 0) {
+ for (let i = 0; i < xml.children.length; i++) {
+ const item = xml.children.item(i);
+ const nodeName = item.nodeName;
+ if (obj[nodeName] === undefined) {
+ obj[nodeName] = xml2json(item);
+ } else {
+ if (obj[nodeName].push === undefined) {
+ const old = obj[nodeName];
+ obj[nodeName] = [];
+ obj[nodeName].push(old);
+ }
+ obj[nodeName].push(xml2json(item));
+ }
+ }
+ } else {
+ obj = xml.textContent;
+ }
+ return obj;
+ } catch (error) {
+ console.warn(error.message);
+ }
+}
+
+function xmlObj2json(xml) {
+ const xmlObj = xmlStr2XmlObj(xml);
+ console.warn(xmlObj);
+ let jsonObj = {};
+ if (xmlObj.childNodes.length > 0) {
+ jsonObj = xml2json(xmlObj);
+ }
+ return jsonObj;
+}
+
+export default xmlObj2json;
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/child-process-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/child-process-node-config.vue
new file mode 100644
index 000000000..1ac758a6d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/child-process-node-config.vue
@@ -0,0 +1,870 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/condition-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/condition-node-config.vue
new file mode 100644
index 000000000..bd7c9e80b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/condition-node-config.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+
+
+
+ 未满足其它条件时,将进入此分支(该分支不可编辑和删除)
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/copy-task-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/copy-task-node-config.vue
new file mode 100644
index 000000000..4a0feab8b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/copy-task-node-config.vue
@@ -0,0 +1,523 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
字段权限
+
+
+
+ 字段名称
+
+
+
+
+ 只读
+
+
+
+
+ 可编辑
+
+
+
+
+ 隐藏
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/delay-timer-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/delay-timer-node-config.vue
new file mode 100644
index 000000000..0e0ac4cc3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/delay-timer-node-config.vue
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+ {{ nodeName }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/condition-dialog.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/condition-dialog.vue
new file mode 100644
index 000000000..5d8eb78d5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/condition-dialog.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/condition.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/condition.vue
new file mode 100644
index 000000000..37df5a8ce
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/condition.vue
@@ -0,0 +1,328 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/http-request-param-setting.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/http-request-param-setting.vue
new file mode 100644
index 000000000..4799dea6b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/http-request-param-setting.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/http-request-setting.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/http-request-setting.vue
new file mode 100644
index 000000000..0ebc1a9c3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/http-request-setting.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/user-task-listener.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/user-task-listener.vue
new file mode 100644
index 000000000..328d1662b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/modules/user-task-listener.vue
@@ -0,0 +1,111 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/router-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/router-node-config.vue
new file mode 100644
index 000000000..a5a3c02a4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/router-node-config.vue
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+ {{ nodeName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/start-user-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/start-user-node-config.vue
new file mode 100644
index 000000000..acd02d03e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/start-user-node-config.vue
@@ -0,0 +1,290 @@
+
+
+
+
+
+
+
+
+
+ 全部成员可以发起流程
+
+
+
+ {{ getUserNicknames(startUserIds) }} 可发起流程
+
+
+
+ {{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
+ {{ startUserIds.length }} 人可发起流程
+
+
+
+
+
+ {{ getDeptNames(startDeptIds) }} 可发起流程
+
+
+
+ {{ getDeptNames(startDeptIds.slice(0, 2)) }} 等
+ {{ startDeptIds.length }} 个部门可发起流程
+
+
+
+
+
+
+
字段权限
+
+
+
+ 字段名称
+
+
+
+
+ 只读
+
+
+
+
+ 可编辑
+
+
+
+
+ 隐藏
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/trigger-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/trigger-node-config.vue
new file mode 100644
index 000000000..403a7932c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/trigger-node-config.vue
@@ -0,0 +1,687 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/user-task-node-config.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/user-task-node-config.vue
new file mode 100644
index 000000000..2afdd6f97
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/user-task-node-config.vue
@@ -0,0 +1,1299 @@
+
+
+
+
+
+
+
+ 审批类型 :
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
操作按钮
+
+
+
+ 操作按钮
+ 显示名称
+
+ 启用
+
+
+
+
+
+
+
+ {{ OPERATION_BUTTON_NAME.get(item.id) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
字段权限
+
+
+
+ 字段名称
+
+
+
+
+ 只读
+
+
+
+
+ 可编辑
+
+
+
+
+ 隐藏
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/utils.ts b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/utils.ts
new file mode 100644
index 000000000..ee5e06809
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes-config/utils.ts
@@ -0,0 +1,48 @@
+import { APPROVE_TYPE, ApproveType, TimeUnitType } from '../../consts';
+
+/** 获取条件节点默认的名称 */
+export function getDefaultConditionNodeName(
+ index: number,
+ defaultFlow: boolean | undefined,
+): string {
+ if (defaultFlow) {
+ return '其它情况';
+ }
+ return `条件${index + 1}`;
+}
+
+/** 获取包容分支条件节点默认的名称 */
+export function getDefaultInclusiveConditionNodeName(
+ index: number,
+ defaultFlow: boolean | undefined,
+): string {
+ if (defaultFlow) {
+ return '其它情况';
+ }
+ return `包容条件${index + 1}`;
+}
+
+/** 转换时间单位字符串为枚举值 */
+export function convertTimeUnit(strTimeUnit: string) {
+ if (strTimeUnit === 'M') {
+ return TimeUnitType.MINUTE;
+ }
+ if (strTimeUnit === 'H') {
+ return TimeUnitType.HOUR;
+ }
+ if (strTimeUnit === 'D') {
+ return TimeUnitType.DAY;
+ }
+ return TimeUnitType.HOUR;
+}
+
+/** 根据审批类型获取对应的文本描述 */
+export function getApproveTypeText(approveType: ApproveType): string {
+ let approveTypeText = '';
+ APPROVE_TYPE.forEach((item) => {
+ if (item.value === approveType) {
+ approveTypeText = item.label;
+ }
+ });
+ return approveTypeText;
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/child-process-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/child-process-node.vue
new file mode 100644
index 000000000..584dd6924
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/child-process-node.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CHILD_PROCESS_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/copy-task-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/copy-task-node.vue
new file mode 100644
index 000000000..60a06010b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/copy-task-node.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.COPY_TASK_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/delay-timer-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/delay-timer-node.vue
new file mode 100644
index 000000000..89cecc48e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/delay-timer-node.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.DELAY_TIMER_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/end-event-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/end-event-node.vue
new file mode 100644
index 000000000..b979e3497
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/end-event-node.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/exclusive-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/exclusive-node.vue
new file mode 100644
index 000000000..e4ded6bd6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/exclusive-node.vue
@@ -0,0 +1,307 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
优先级{{ index + 1 }}
+
+
+
+ {{ item.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CONDITION_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/inclusive-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/inclusive-node.vue
new file mode 100644
index 000000000..aa61be155
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/inclusive-node.vue
@@ -0,0 +1,309 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+ {{ item.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CONDITION_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/process-instance-data.ts b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/process-instance-data.ts
new file mode 100644
index 000000000..a9f2a06bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/process-instance-data.ts
@@ -0,0 +1,56 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 流程实例列表字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'startUser',
+ title: '发起人',
+ slots: {
+ default: ({ row }: { row: any }) => {
+ return row.startUser?.nickname;
+ },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ slots: {
+ default: ({ row }: { row: any }) => {
+ return row.startUser?.deptName;
+ },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '开始时间',
+ formatter: 'formatDateTime',
+ minWidth: 140,
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ formatter: 'formatDateTime',
+ minWidth: 140,
+ },
+ {
+ field: 'status',
+ title: '流程状态',
+ minWidth: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
+ },
+ },
+ {
+ field: 'durationInMillis',
+ title: '耗时',
+ minWidth: 100,
+ formatter: 'formatPast2',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/process-instance-modal.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/process-instance-modal.vue
new file mode 100644
index 000000000..c66bcef38
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/process-instance-modal.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/task-list-data.ts b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/task-list-data.ts
new file mode 100644
index 000000000..fbb2d0ea5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/task-list-data.ts
@@ -0,0 +1,61 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 审批记录列表字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'assigneeUser',
+ title: '审批人',
+ slots: {
+ default: ({ row }: { row: any }) => {
+ return row.assigneeUser?.nickname || row.ownerUser?.nickname;
+ },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ slots: {
+ default: ({ row }: { row: any }) => {
+ return row.assigneeUser?.deptName || row.ownerUser?.deptName;
+ },
+ },
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '开始时间',
+ formatter: 'formatDateTime',
+ minWidth: 140,
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ formatter: 'formatDateTime',
+ minWidth: 140,
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_TASK_STATUS },
+ },
+ },
+ {
+ field: 'reason',
+ title: '审批建议',
+ minWidth: 160,
+ },
+ {
+ field: 'durationInMillis',
+ title: '耗时',
+ minWidth: 100,
+ formatter: 'formatPast2',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/task-list-modal.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/task-list-modal.vue
new file mode 100644
index 000000000..baa4cc7ed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/modules/task-list-modal.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/node-handler.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/node-handler.vue
new file mode 100644
index 000000000..00d13f8d8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/node-handler.vue
@@ -0,0 +1,350 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/parallel-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/parallel-node.vue
new file mode 100644
index 000000000..82b134024
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/parallel-node.vue
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
无优先级
+
+
+
+ {{ item.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CONDITION_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/router-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/router-node.vue
new file mode 100644
index 000000000..fb815cf20
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/router-node.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.ROUTER_BRANCH_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/start-user-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/start-user-node.vue
new file mode 100644
index 000000000..f79a4a304
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/start-user-node.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.START_USER_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/trigger-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/trigger-node.vue
new file mode 100644
index 000000000..bd1c2f6ef
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/trigger-node.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.TRIGGER_NODE) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/user-task-node.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/user-task-node.vue
new file mode 100644
index 000000000..a0691a0a6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/nodes/user-task-node.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNode.name }}
+
+
+
+
+ {{ currentNode.showText }}
+
+
+ {{ NODE_DEFAULT_TEXT.get(currentNode.type) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/process-node-tree.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/process-node-tree.vue
new file mode 100644
index 000000000..677370030
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/process-node-tree.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-designer.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-designer.vue
new file mode 100644
index 000000000..659f0a332
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-designer.vue
@@ -0,0 +1,253 @@
+
+
+
+
+
+ 以下节点配置不完善,请修改相关配置
+
+ {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-model.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-model.vue
new file mode 100644
index 000000000..7679c9316
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-model.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 以下节点内容不完善,请修改后保存
+
+ {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-viewer.vue b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-viewer.vue
new file mode 100644
index 000000000..95cf3f321
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/components/simple-process-viewer.vue
@@ -0,0 +1,45 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/consts.ts b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/consts.ts
new file mode 100644
index 000000000..f521f25f4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/consts.ts
@@ -0,0 +1,896 @@
+import { BpmNodeTypeEnum, BpmTaskStatusEnum } from '@vben/constants';
+
+interface DictDataType {
+ label: string;
+ value: number | string;
+}
+
+// 用户任务的审批类型。 【参考飞书】
+export enum ApproveType {
+ /**
+ * 人工审批
+ */
+ USER = 1,
+ /**
+ * 自动通过
+ */
+ AUTO_APPROVE = 2,
+ /**
+ * 自动拒绝
+ */
+ AUTO_REJECT = 3,
+}
+
+// 多人审批方式类型枚举 ( 用于审批节点 )
+export enum ApproveMethodType {
+ /**
+ * 随机挑选一人审批
+ */
+ RANDOM_SELECT_ONE_APPROVE = 1,
+
+ /**
+ * 多人会签(按通过比例)
+ */
+ APPROVE_BY_RATIO = 2,
+
+ /**
+ * 多人或签(通过只需一人,拒绝只需一人)
+ */
+ ANY_APPROVE = 3,
+ /**
+ * 多人依次审批
+ */
+ SEQUENTIAL_APPROVE = 4,
+}
+
+export enum NodeId {
+ /**
+ * 发起人节点 Id
+ */
+ END_EVENT_NODE_ID = 'EndEvent',
+
+ /**
+ * 发起人节点 Id
+ */
+ START_USER_NODE_ID = 'StartUserNode',
+}
+
+// 条件配置类型 ( 用于条件节点配置 )
+export enum ConditionType {
+ /**
+ * 条件表达式
+ */
+ EXPRESSION = 1,
+
+ /**
+ * 条件规则
+ */
+ RULE = 2,
+}
+
+// 操作按钮类型枚举 (用于审批节点)
+export enum OperationButtonType {
+ /**
+ * 通过
+ */
+ APPROVE = 1,
+ /**
+ * 拒绝
+ */
+ REJECT = 2,
+ /**
+ * 转办
+ */
+ TRANSFER = 3,
+ /**
+ * 委派
+ */
+ DELEGATE = 4,
+ /**
+ * 加签
+ */
+ ADD_SIGN = 5,
+ /**
+ * 退回
+ */
+ RETURN = 6,
+ /**
+ * 抄送
+ */
+ COPY = 7,
+}
+
+// 审批拒绝类型枚举
+export enum RejectHandlerType {
+ /**
+ * 结束流程
+ */
+ FINISH_PROCESS = 1,
+ /**
+ * 驳回到指定节点
+ */
+ RETURN_USER_TASK = 2,
+}
+
+// 用户任务超时处理类型枚举
+export enum TimeoutHandlerType {
+ /**
+ * 自动提醒
+ */
+ REMINDER = 1,
+ /**
+ * 自动同意
+ */
+ APPROVE = 2,
+ /**
+ * 自动拒绝
+ */
+ REJECT = 3,
+}
+
+// 用户任务的审批人为空时,处理类型枚举
+export enum AssignEmptyHandlerType {
+ /**
+ * 自动通过
+ */
+ APPROVE = 1,
+ /**
+ * 自动拒绝
+ */
+ REJECT = 2,
+ /**
+ * 指定人员审批
+ */
+ ASSIGN_USER = 3,
+ /**
+ * 转交给流程管理员
+ */
+ ASSIGN_ADMIN = 4,
+}
+
+// 用户任务的审批人与发起人相同时,处理类型枚举
+export enum AssignStartUserHandlerType {
+ /**
+ * 由发起人对自己审批
+ */
+ START_USER_AUDIT = 1,
+ /**
+ * 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
+ */
+ SKIP = 2,
+ /**
+ * 转交给部门负责人审批
+ */
+ ASSIGN_DEPT_LEADER = 3,
+}
+
+// 时间单位枚举
+export enum TimeUnitType {
+ /**
+ * 分钟
+ */
+ MINUTE = 1,
+ /**
+ * 小时
+ */
+ HOUR = 2,
+ /**
+ * 天
+ */
+ DAY = 3,
+}
+
+/**
+ * 表单权限的枚举
+ */
+export enum FieldPermissionType {
+ /**
+ * 隐藏
+ */
+ NONE = '3',
+ /**
+ * 只读
+ */
+ READ = '1',
+ /**
+ * 编辑
+ */
+ WRITE = '2',
+}
+
+/**
+ * 延迟类型
+ */
+export enum DelayTypeEnum {
+ /**
+ * 固定时长
+ */
+ FIXED_TIME_DURATION = 1,
+ /**
+ * 固定日期时间
+ */
+ FIXED_DATE_TIME = 2,
+}
+
+/**
+ * 触发器类型枚举
+ */
+export enum TriggerTypeEnum {
+ /**
+ * 发送 HTTP 请求触发器
+ */
+ HTTP_REQUEST = 1,
+ /**
+ * 接收 HTTP 回调请求触发器
+ */
+ HTTP_CALLBACK = 2,
+ /**
+ * 表单数据更新触发器
+ */
+ FORM_UPDATE = 10,
+ /**
+ * 表单数据删除触发器
+ */
+ FORM_DELETE = 11,
+}
+
+export enum ChildProcessStartUserTypeEnum {
+ /**
+ * 同主流程发起人
+ */
+ MAIN_PROCESS_START_USER = 1,
+ /**
+ * 表单
+ */
+ FROM_FORM = 2,
+}
+
+export enum ChildProcessStartUserEmptyTypeEnum {
+ /**
+ * 同主流程发起人
+ */
+ MAIN_PROCESS_START_USER = 1,
+ /**
+ * 子流程管理员
+ */
+ CHILD_PROCESS_ADMIN = 2,
+ /**
+ * 主流程管理员
+ */
+ MAIN_PROCESS_ADMIN = 3,
+}
+
+export enum ChildProcessMultiInstanceSourceTypeEnum {
+ /**
+ * 固定数量
+ */
+ FIXED_QUANTITY = 1,
+ /**
+ * 数字表单
+ */
+ NUMBER_FORM = 2,
+ /**
+ * 多选表单
+ */
+ MULTIPLE_FORM = 3,
+}
+
+// 候选人策略枚举 ( 用于审批节点。抄送节点 )
+export enum CandidateStrategy {
+ /**
+ * 指定角色
+ */
+ ROLE = 10,
+ /**
+ * 部门成员
+ */
+ DEPT_MEMBER = 20,
+ /**
+ * 部门的负责人
+ */
+ DEPT_LEADER = 21,
+ /**
+ * 指定岗位
+ */
+ POST = 22,
+ /**
+ * 连续多级部门的负责人
+ */
+ MULTI_LEVEL_DEPT_LEADER = 23,
+ /**
+ * 指定用户
+ */
+ USER = 30,
+ /**
+ * 审批人自选
+ */
+ APPROVE_USER_SELECT = 34,
+ /**
+ * 发起人自选
+ */
+ START_USER_SELECT = 35,
+ /**
+ * 发起人自己
+ */
+ START_USER = 36,
+ /**
+ * 发起人部门负责人
+ */
+ START_USER_DEPT_LEADER = 37,
+ /**
+ * 发起人连续多级部门的负责人
+ */
+ START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
+ /**
+ * 指定用户组
+ */
+ USER_GROUP = 40,
+ /**
+ * 表单内用户字段
+ */
+ FORM_USER = 50,
+ /**
+ * 表单内部门负责人
+ */
+ FORM_DEPT_LEADER = 51,
+ /**
+ * 流程表达式
+ */
+ EXPRESSION = 60,
+}
+
+export enum BpmHttpRequestParamTypeEnum {
+ /**
+ * 固定值
+ */
+ FIXED_VALUE = 1,
+ /**
+ * 表单
+ */
+ FROM_FORM = 2,
+}
+
+// 这里定义 HTTP 请求参数类型
+export type HttpRequestParam = {
+ key: string;
+ type: number;
+ value: string;
+};
+
+// 监听器结构定义
+export type ListenerHandler = {
+ body?: HttpRequestParam[];
+ enable: boolean;
+ header?: HttpRequestParam[];
+ path?: string;
+};
+
+/**
+ * 条件规则结构定义
+ */
+export type ConditionRule = {
+ leftSide: string | undefined;
+ opCode: string;
+ rightSide: string | undefined;
+};
+
+/**
+ * 条件结构定义
+ */
+export type Condition = {
+ // 条件规则的逻辑关系是否为且
+ and: boolean;
+ rules: ConditionRule[];
+};
+
+/**
+ * 条件组结构定义
+ */
+export type ConditionGroup = {
+ // 条件组的逻辑关系是否为且
+ and: boolean;
+ // 条件数组
+ conditions: Condition[];
+};
+
+/**
+ * 条件节点设置结构定义,用于条件节点
+ */
+export type ConditionSetting = {
+ // 条件表达式
+ conditionExpression?: string;
+ // 条件组
+ conditionGroups?: ConditionGroup;
+ // 条件类型
+ conditionType?: ConditionType;
+ // 是否默认的条件
+ defaultFlow?: boolean;
+};
+
+/**
+ * 审批拒绝结构定义
+ */
+export type RejectHandler = {
+ // 退回节点 Id
+ returnNodeId?: string;
+ // 审批拒绝类型
+ type: RejectHandlerType;
+};
+
+/**
+ * 审批超时结构定义
+ */
+export type TimeoutHandler = {
+ // 是否开启超时处理
+ enable: boolean;
+ // 执行动作是自动提醒, 最大提醒次数
+ maxRemindCount?: number;
+ // 超时时间设置
+ timeDuration?: string;
+ // 超时执行的动作
+ type?: number;
+};
+
+/**
+ * 审批人为空的结构定义
+ */
+export type AssignEmptyHandler = {
+ // 审批人为空的处理类型
+ type: AssignEmptyHandlerType;
+ // 指定用户的编号数组
+ userIds?: number[];
+};
+
+/**
+ * 延迟设置
+ */
+export type DelaySetting = {
+ // 延迟时间表达式
+ delayTime: string;
+ // 延迟类型
+ delayType: number;
+};
+
+/**
+ * 路由分支结构定义
+ */
+export type RouterSetting = {
+ conditionExpression: string;
+ conditionGroups: ConditionGroup;
+ conditionType: ConditionType;
+ nodeId: string | undefined;
+};
+
+/**
+ * 操作按钮权限结构定义
+ */
+export type ButtonSetting = {
+ displayName: string;
+ enable: boolean;
+ id: OperationButtonType;
+};
+
+/**
+ * HTTP 请求触发器结构定义
+ */
+export type HttpRequestTriggerSetting = {
+ // 请求体参数设置
+ body?: HttpRequestParam[];
+ // 请求头参数设置
+ header?: HttpRequestParam[];
+ // 请求响应设置
+ response?: Record[];
+ // 请求 URL
+ url: string;
+};
+
+/**
+ * 流程表单触发器配置结构定义
+ */
+export type FormTriggerSetting = {
+ // 条件表达式
+ conditionExpression?: string;
+ // 条件组
+ conditionGroups?: ConditionGroup;
+ // 条件类型
+ conditionType?: ConditionType;
+ // 删除表单字段配置
+ deleteFields?: string[];
+ // 更新表单字段配置
+ updateFormFields?: Record;
+};
+
+/**
+ * 触发器节点结构定义
+ */
+export type TriggerSetting = {
+ formSettings?: FormTriggerSetting[];
+ httpRequestSetting?: HttpRequestTriggerSetting;
+ type: TriggerTypeEnum;
+};
+
+export type IOParameter = {
+ source: string;
+ target: string;
+};
+
+export type StartUserSetting = {
+ emptyType?: ChildProcessStartUserEmptyTypeEnum;
+ formField?: string;
+ type: ChildProcessStartUserTypeEnum;
+};
+
+export type TimeoutSetting = {
+ enable: boolean;
+ timeExpression?: string;
+ type?: DelayTypeEnum;
+};
+
+export type MultiInstanceSetting = {
+ approveRatio?: number;
+ enable: boolean;
+ sequential?: boolean;
+ source?: string;
+ sourceType?: ChildProcessMultiInstanceSourceTypeEnum;
+};
+
+/**
+ * 子流程节点结构定义
+ */
+export type ChildProcessSetting = {
+ async: boolean;
+ calledProcessDefinitionKey: string;
+ calledProcessDefinitionName: string;
+ inVariables?: IOParameter[];
+ multiInstanceSetting: MultiInstanceSetting;
+ outVariables?: IOParameter[];
+ skipStartUserNode: boolean;
+ startUserSetting: StartUserSetting;
+ timeoutSetting: TimeoutSetting;
+};
+
+/**
+ * 节点结构定义
+ */
+export interface SimpleFlowNode {
+ id: string;
+ type: BpmNodeTypeEnum;
+ name: string;
+ showText?: string;
+ // 孩子节点
+ childNode?: SimpleFlowNode;
+ // 条件节点
+ conditionNodes?: SimpleFlowNode[];
+ // 审批类型
+ approveType?: ApproveType;
+ // 候选人策略
+ candidateStrategy?: number;
+ // 候选人参数
+ candidateParam?: string;
+ // 多人审批方式
+ approveMethod?: ApproveMethodType;
+ // 通过比例
+ approveRatio?: number;
+ // 审批按钮设置
+ buttonsSetting?: any[];
+ // 表单权限
+ fieldsPermission?: Array>;
+ // 审批任务超时处理
+ timeoutHandler?: TimeoutHandler;
+ // 审批任务拒绝处理
+ rejectHandler?: RejectHandler;
+ // 审批人为空的处理
+ assignEmptyHandler?: AssignEmptyHandler;
+ // 审批节点的审批人与发起人相同时,对应的处理类型
+ assignStartUserHandlerType?: number;
+ // 创建任务监听器
+ taskCreateListener?: ListenerHandler;
+ // 创建任务监听器
+ taskAssignListener?: ListenerHandler;
+ // 创建任务监听器
+ taskCompleteListener?: ListenerHandler;
+ // 条件设置
+ conditionSetting?: ConditionSetting;
+ // 活动的状态,用于前端节点状态展示
+ activityStatus?: BpmTaskStatusEnum;
+ // 延迟设置
+ delaySetting?: DelaySetting;
+ // 路由分支
+ routerGroups?: RouterSetting[];
+ defaultFlowId?: string;
+ // 签名
+ signEnable?: boolean;
+ // 审批意见
+ reasonRequire?: boolean;
+ // 跳过表达式
+ skipExpression?: string;
+ // 触发器设置
+ triggerSetting?: TriggerSetting;
+ // 子流程
+ childProcessSetting?: ChildProcessSetting;
+}
+
+/**
+ * 条件组默认值
+ */
+export const DEFAULT_CONDITION_GROUP_VALUE = {
+ and: true,
+ conditions: [
+ {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: undefined,
+ rightSide: '',
+ },
+ ],
+ },
+ ],
+};
+
+export const NODE_DEFAULT_TEXT = new Map();
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.USER_TASK_NODE, '请配置审批人');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.COPY_TASK_NODE, '请配置抄送人');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.CONDITION_NODE, '请设置条件');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.START_USER_NODE, '请设置发起人');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.DELAY_TIMER_NODE, '请设置延迟器');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.ROUTER_BRANCH_NODE, '请设置路由节点');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.TRIGGER_NODE, '请设置触发器');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.TRANSACTOR_NODE, '请设置办理人');
+NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.CHILD_PROCESS_NODE, '请设置子流程');
+
+export const NODE_DEFAULT_NAME = new Map();
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.USER_TASK_NODE, '审批人');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.COPY_TASK_NODE, '抄送人');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.CONDITION_NODE, '条件');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.START_USER_NODE, '发起人');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.DELAY_TIMER_NODE, '延迟器');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.ROUTER_BRANCH_NODE, '路由分支');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.TRIGGER_NODE, '触发器');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.TRANSACTOR_NODE, '办理人');
+NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.CHILD_PROCESS_NODE, '子流程');
+
+// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
+export const CANDIDATE_STRATEGY: DictDataType[] = [
+ { label: '指定成员', value: CandidateStrategy.USER as any },
+ { label: '指定角色', value: CandidateStrategy.ROLE as any },
+ { label: '指定岗位', value: CandidateStrategy.POST as any },
+ { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER as any },
+ { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER as any },
+ {
+ label: '连续多级部门负责人',
+ value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER as any,
+ },
+ { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT as any },
+ { label: '审批人自选', value: CandidateStrategy.APPROVE_USER_SELECT as any },
+ { label: '发起人本人', value: CandidateStrategy.START_USER as any },
+ {
+ label: '发起人部门负责人',
+ value: CandidateStrategy.START_USER_DEPT_LEADER as any,
+ },
+ {
+ label: '发起人连续部门负责人',
+ value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER as any,
+ },
+ { label: '用户组', value: CandidateStrategy.USER_GROUP as any },
+ { label: '表单内用户字段', value: CandidateStrategy.FORM_USER as any },
+ {
+ label: '表单内部门负责人',
+ value: CandidateStrategy.FORM_DEPT_LEADER as any,
+ },
+ { label: '流程表达式', value: CandidateStrategy.EXPRESSION as any },
+];
+// 审批节点 的审批类型
+export const APPROVE_TYPE: DictDataType[] = [
+ { label: '人工审批', value: ApproveType.USER as any },
+ { label: '自动通过', value: ApproveType.AUTO_APPROVE as any },
+ { label: '自动拒绝', value: ApproveType.AUTO_REJECT as any },
+];
+
+export const APPROVE_METHODS: DictDataType[] = [
+ {
+ label: '按顺序依次审批',
+ value: ApproveMethodType.SEQUENTIAL_APPROVE as any,
+ },
+ {
+ label: '会签(可同时审批,至少 % 人必须审批通过)',
+ value: ApproveMethodType.APPROVE_BY_RATIO as any,
+ },
+ {
+ label: '或签(可同时审批,有一人通过即可)',
+ value: ApproveMethodType.ANY_APPROVE as any,
+ },
+ {
+ label: '随机挑选一人审批',
+ value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE as any,
+ },
+];
+
+export const CONDITION_CONFIG_TYPES: DictDataType[] = [
+ { label: '条件规则', value: ConditionType.RULE as any },
+ { label: '条件表达式', value: ConditionType.EXPRESSION as any },
+];
+
+// 时间单位类型
+export const TIME_UNIT_TYPES: DictDataType[] = [
+ { label: '分钟', value: TimeUnitType.MINUTE as any },
+ { label: '小时', value: TimeUnitType.HOUR as any },
+ { label: '天', value: TimeUnitType.DAY as any },
+];
+// 超时处理执行动作类型
+export const TIMEOUT_HANDLER_TYPES: DictDataType[] = [
+ { label: '自动提醒', value: 1 },
+ { label: '自动同意', value: 2 },
+ { label: '自动拒绝', value: 3 },
+];
+export const REJECT_HANDLER_TYPES: DictDataType[] = [
+ { label: '终止流程', value: RejectHandlerType.FINISH_PROCESS as any },
+ { label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK as any },
+ // { label: '结束任务', value: RejectHandlerType.FINISH_TASK }
+];
+export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataType[] = [
+ { label: '自动通过', value: 1 },
+ { label: '自动拒绝', value: 2 },
+ { label: '指定成员审批', value: 3 },
+ { label: '转交给流程管理员', value: 4 },
+];
+export const ASSIGN_START_USER_HANDLER_TYPES: DictDataType[] = [
+ { label: '由发起人对自己审批', value: 1 },
+ { label: '自动跳过', value: 2 },
+ { label: '转交给部门负责人审批', value: 3 },
+];
+
+// 比较运算符
+export const COMPARISON_OPERATORS: DictDataType[] = [
+ {
+ value: '==',
+ label: '等于',
+ },
+ {
+ value: '!=',
+ label: '不等于',
+ },
+ {
+ value: '>',
+ label: '大于',
+ },
+ {
+ value: '>=',
+ label: '大于等于',
+ },
+ {
+ value: '<',
+ label: '小于',
+ },
+ {
+ value: '<=',
+ label: '小于等于',
+ },
+ {
+ value: 'contain',
+ label: '包含',
+ },
+ {
+ value: '!contain',
+ label: '不包含',
+ },
+];
+// 审批操作按钮名称
+export const OPERATION_BUTTON_NAME = new Map();
+OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过');
+OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝');
+OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办');
+OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派');
+OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签');
+OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回');
+OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送');
+
+// 默认的按钮权限设置
+export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
+ { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true },
+ { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true },
+ { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true },
+ { id: OperationButtonType.RETURN, displayName: '退回', enable: true },
+];
+
+// 办理人默认的按钮权限设置
+export const TRANSACTOR_DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '办理', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
+ { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
+ { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
+ { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
+ { id: OperationButtonType.RETURN, displayName: '退回', enable: false },
+];
+
+// 发起人的按钮权限。暂时定死,不可以编辑
+export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '提交', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
+ { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
+ { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
+ { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
+ { id: OperationButtonType.RETURN, displayName: '退回', enable: false },
+];
+
+export const MULTI_LEVEL_DEPT: DictDataType[] = [
+ { label: '第 1 级部门', value: 1 },
+ { label: '第 2 级部门', value: 2 },
+ { label: '第 3 级部门', value: 3 },
+ { label: '第 4 级部门', value: 4 },
+ { label: '第 5 级部门', value: 5 },
+ { label: '第 6 级部门', value: 6 },
+ { label: '第 7 级部门', value: 7 },
+ { label: '第 8 级部门', value: 8 },
+ { label: '第 9 级部门', value: 9 },
+ { label: '第 10 级部门', value: 10 },
+ { label: '第 11 级部门', value: 11 },
+ { label: '第 12 级部门', value: 12 },
+ { label: '第 13 级部门', value: 13 },
+ { label: '第 14 级部门', value: 14 },
+ { label: '第 15 级部门', value: 15 },
+];
+
+export const DELAY_TYPE = [
+ { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
+ { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME },
+];
+
+export const BPM_HTTP_REQUEST_PARAM_TYPES = [
+ {
+ value: 1,
+ label: '固定值',
+ },
+ {
+ value: 2,
+ label: '表单',
+ },
+];
+
+export const TRIGGER_TYPES: DictDataType[] = [
+ { label: '发送 HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST as any },
+ { label: '接收 HTTP 回调', value: TriggerTypeEnum.HTTP_CALLBACK as any },
+ { label: '修改表单数据', value: TriggerTypeEnum.FORM_UPDATE as any },
+ { label: '删除表单数据', value: TriggerTypeEnum.FORM_DELETE as any },
+];
+
+export const CHILD_PROCESS_START_USER_TYPE = [
+ {
+ label: '同主流程发起人',
+ value: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
+ },
+ { label: '从表单中获取', value: ChildProcessStartUserTypeEnum.FROM_FORM },
+];
+
+export const CHILD_PROCESS_START_USER_EMPTY_TYPE = [
+ {
+ label: '同主流程发起人',
+ value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER,
+ },
+ {
+ label: '子流程管理员',
+ value: ChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN,
+ },
+ {
+ label: '主流程管理员',
+ value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN,
+ },
+];
+
+export const CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = [
+ {
+ label: '固定数量',
+ value: ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY,
+ },
+ {
+ label: '数字表单',
+ value: ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM,
+ },
+ {
+ label: '多选表单',
+ value: ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM,
+ },
+];
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/helpers.ts b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/helpers.ts
new file mode 100644
index 000000000..a873ec664
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/helpers.ts
@@ -0,0 +1,751 @@
+import type { Ref } from 'vue';
+
+import type {
+ ConditionGroup,
+ HttpRequestParam,
+ SimpleFlowNode,
+} from './consts';
+
+import type { BpmUserGroupApi } from '#/api/bpm/userGroup';
+import type { SystemDeptApi } from '#/api/system/dept';
+import type { SystemPostApi } from '#/api/system/post';
+import type { SystemRoleApi } from '#/api/system/role';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { inject, nextTick, ref, toRaw, unref, watch } from 'vue';
+
+import {
+ BpmNodeTypeEnum,
+ BpmTaskStatusEnum,
+ ProcessVariableEnum,
+} from '@vben/constants';
+
+import { parseFormFields } from '#/components/form-create';
+
+import {
+ ApproveMethodType,
+ AssignEmptyHandlerType,
+ AssignStartUserHandlerType,
+ CandidateStrategy,
+ COMPARISON_OPERATORS,
+ ConditionType,
+ FieldPermissionType,
+ NODE_DEFAULT_NAME,
+ RejectHandlerType,
+} from './consts';
+
+export function useWatchNode(props: {
+ flowNode: SimpleFlowNode;
+}): Ref {
+ const node = ref(props.flowNode);
+ watch(
+ () => props.flowNode,
+ (newValue) => {
+ node.value = newValue;
+ },
+ );
+ return node;
+}
+
+// 解析 formCreate 所有表单字段, 并返回
+function parseFormCreateFields(formFields?: string[]) {
+ const result: Array> = [];
+ if (formFields) {
+ formFields.forEach((fieldStr: string) => {
+ parseFormFields(JSON.parse(fieldStr), result);
+ });
+ }
+ return result;
+}
+
+/**
+ * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
+ */
+export function useFormFieldsPermission(
+ defaultPermission: FieldPermissionType,
+) {
+ // 字段权限配置. 需要有 field, title, permissioin 属性
+ const fieldsPermissionConfig = ref>>([]);
+
+ const formType = inject[>('formType', ref()); // 表单类型
+
+ const formFields = inject][>('formFields', ref([])); // 流程表单字段
+
+ function getNodeConfigFormFields(
+ nodeFormFields?: Array>,
+ ) {
+ nodeFormFields = toRaw(nodeFormFields);
+ fieldsPermissionConfig.value =
+ !nodeFormFields || nodeFormFields.length === 0
+ ? getDefaultFieldsPermission(unref(formFields))
+ : mergeFieldsPermission(nodeFormFields, unref(formFields));
+ }
+ // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段)
+ function mergeFieldsPermission(
+ formFieldsPermisson: Array>,
+ formFields?: string[],
+ ) {
+ let mergedFieldsPermission: Array> = [];
+ if (formFields) {
+ mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => {
+ const found = formFieldsPermisson.find(
+ (fieldPermission) => fieldPermission.field === item.field,
+ );
+ return {
+ field: item.field,
+ title: item.title,
+ permission: found ? found.permission : defaultPermission,
+ };
+ });
+ }
+ return mergedFieldsPermission;
+ }
+
+ // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
+ function getDefaultFieldsPermission(formFields?: string[]) {
+ let defaultFieldsPermission: Array> = [];
+ if (formFields) {
+ defaultFieldsPermission = parseFormCreateFields(formFields).map(
+ (item) => {
+ return {
+ field: item.field,
+ title: item.title,
+ permission: defaultPermission,
+ };
+ },
+ );
+ }
+ return defaultFieldsPermission;
+ }
+
+ // 获取表单的所有字段,作为下拉框选项
+ const formFieldOptions = parseFormCreateFields(unref(formFields));
+
+ return {
+ formType,
+ fieldsPermissionConfig,
+ formFieldOptions,
+ getNodeConfigFormFields,
+ };
+}
+
+/**
+ * @description 获取流程表单的字段
+ */
+export function useFormFields() {
+ const formFields = inject][>('formFields', ref([])); // 流程表单字段
+ return parseFormCreateFields(unref(formFields));
+}
+
+// TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
+/**
+ * @description 获取流程表单的字段和发起人字段
+ */
+export function useFormFieldsAndStartUser() {
+ const injectFormFields = inject][>('formFields', ref([])); // 流程表单字段
+ const formFields = parseFormCreateFields(unref(injectFormFields));
+ // 添加发起人
+ formFields.unshift({
+ field: ProcessVariableEnum.START_USER_ID,
+ title: '发起人',
+ required: true,
+ });
+ return formFields;
+}
+
+export type UserTaskFormType = {
+ approveMethod: ApproveMethodType;
+ approveRatio?: number;
+ assignEmptyHandlerType?: AssignEmptyHandlerType;
+ assignEmptyHandlerUserIds?: number[];
+ assignStartUserHandlerType?: AssignStartUserHandlerType;
+ buttonsSetting: any[];
+ candidateStrategy: CandidateStrategy;
+ deptIds?: number[]; // 部门
+ deptLevel?: number; // 部门层级
+ expression?: string; // 流程表达式
+ formDept?: string; // 表单内部门字段
+ formUser?: string; // 表单内用户字段
+ maxRemindCount?: number;
+ postIds?: number[]; // 岗位
+ reasonRequire: boolean;
+ rejectHandlerType?: RejectHandlerType;
+ returnNodeId?: string;
+ roleIds?: number[]; // 角色
+ signEnable: boolean;
+ skipExpression?: string; // 跳过表达式
+ taskAssignListener?: {
+ body: HttpRequestParam[];
+ header: HttpRequestParam[];
+ };
+ taskAssignListenerEnable?: boolean;
+ taskAssignListenerPath?: string;
+ taskCompleteListener?: {
+ body: HttpRequestParam[];
+ header: HttpRequestParam[];
+ };
+ taskCompleteListenerEnable?: boolean;
+ taskCompleteListenerPath?: string;
+ taskCreateListener?: {
+ body: HttpRequestParam[];
+ header: HttpRequestParam[];
+ };
+ taskCreateListenerEnable?: boolean;
+ taskCreateListenerPath?: string;
+ timeDuration?: number;
+ timeoutHandlerEnable?: boolean;
+ timeoutHandlerType?: number;
+ userGroups?: number[]; // 用户组
+ userIds?: number[]; // 用户
+};
+
+export type CopyTaskFormType = {
+ candidateStrategy: CandidateStrategy;
+ deptIds?: number[]; // 部门
+ deptLevel?: number; // 部门层级
+ expression?: string; // 流程表达式
+ formDept?: string; // 表单内部门字段
+ formUser?: string; // 表单内用户字段
+ postIds?: number[]; // 岗位
+ roleIds?: number[]; // 角色
+ userGroups?: number[]; // 用户组
+ userIds?: number[]; // 用户
+};
+
+/**
+ * @description 节点表单数据。 用于审批节点、抄送节点
+ */
+export function useNodeForm(nodeType: BpmNodeTypeEnum) {
+ const roleOptions = inject][>('roleList', ref([])); // 角色列表
+ const postOptions = inject][>('postList', ref([])); // 岗位列表
+ const userOptions = inject][>('userList', ref([])); // 用户列表
+ const deptOptions = inject][>('deptList', ref([])); // 部门列表
+ const userGroupOptions = inject][>(
+ 'userGroupList',
+ ref([]),
+ ); // 用户组列表
+ const deptTreeOptions = inject][>(
+ 'deptTree',
+ ref([]),
+ ); // 部门树
+ const formFields = inject][>('formFields', ref([])); // 流程表单字段
+ const configForm = ref();
+
+ if (
+ nodeType === BpmNodeTypeEnum.USER_TASK_NODE ||
+ nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE
+ ) {
+ configForm.value = {
+ candidateStrategy: CandidateStrategy.USER,
+ approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
+ approveRatio: 100,
+ rejectHandlerType: RejectHandlerType.FINISH_PROCESS,
+ assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
+ returnNodeId: '',
+ timeoutHandlerEnable: false,
+ timeoutHandlerType: 1,
+ timeDuration: 6, // 默认 6小时
+ maxRemindCount: 1, // 默认 提醒 1次
+ buttonsSetting: [],
+ };
+ }
+ configForm.value = {
+ candidateStrategy: CandidateStrategy.USER,
+ };
+
+ function getShowText(): string {
+ let showText = '';
+ // 指定成员
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.USER &&
+ configForm.value?.userIds?.length > 0
+ ) {
+ const candidateNames: string[] = [];
+ userOptions?.value.forEach((item: any) => {
+ if (configForm.value?.userIds?.includes(item.id)) {
+ candidateNames.push(item.nickname);
+ }
+ });
+ showText = `指定成员:${candidateNames.join(',')}`;
+ }
+ // 指定角色
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.ROLE &&
+ configForm.value.roleIds?.length > 0
+ ) {
+ const candidateNames: string[] = [];
+ roleOptions?.value.forEach((item: any) => {
+ if (configForm.value?.roleIds?.includes(item.id)) {
+ candidateNames.push(item.name);
+ }
+ });
+ showText = `指定角色:${candidateNames.join(',')}`;
+ }
+ // 指定部门
+ if (
+ (configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
+ configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
+ configForm.value?.candidateStrategy ===
+ CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) &&
+ configForm.value?.deptIds?.length > 0
+ ) {
+ const candidateNames: string[] = [];
+ deptOptions?.value.forEach((item) => {
+ if (configForm.value?.deptIds?.includes(item.id)) {
+ candidateNames.push(item.name);
+ }
+ });
+ if (
+ configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER
+ ) {
+ showText = `部门成员:${candidateNames.join(',')}`;
+ } else if (
+ configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER
+ ) {
+ showText = `部门的负责人:${candidateNames.join(',')}`;
+ } else {
+ showText = `多级部门的负责人:${candidateNames.join(',')}`;
+ }
+ }
+
+ // 指定岗位
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.POST &&
+ configForm.value.postIds?.length > 0
+ ) {
+ const candidateNames: string[] = [];
+ postOptions?.value.forEach((item) => {
+ if (configForm.value?.postIds?.includes(item.id)) {
+ candidateNames.push(item.name);
+ }
+ });
+ showText = `指定岗位: ${candidateNames.join(',')}`;
+ }
+ // 指定用户组
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP &&
+ configForm.value?.userGroups?.length > 0
+ ) {
+ const candidateNames: string[] = [];
+ userGroupOptions?.value.forEach((item) => {
+ if (configForm.value?.userGroups?.includes(item.id)) {
+ candidateNames.push(item.name);
+ }
+ });
+ showText = `指定用户组: ${candidateNames.join(',')}`;
+ }
+
+ // 表单内用户字段
+ if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) {
+ const formFieldOptions = parseFormCreateFields(unref(formFields));
+ const item = formFieldOptions.find(
+ (item) => item.field === configForm.value?.formUser,
+ );
+ showText = `表单用户:${item?.title}`;
+ }
+
+ // 表单内部门负责人
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
+ ) {
+ showText = `表单内部门负责人`;
+ }
+
+ // 审批人自选
+ if (
+ configForm.value?.candidateStrategy ===
+ CandidateStrategy.APPROVE_USER_SELECT
+ ) {
+ showText = `审批人自选`;
+ }
+
+ // 发起人自选
+ if (
+ configForm.value?.candidateStrategy ===
+ CandidateStrategy.START_USER_SELECT
+ ) {
+ showText = `发起人自选`;
+ }
+ // 发起人自己
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) {
+ showText = `发起人自己`;
+ }
+ // 发起人的部门负责人
+ if (
+ configForm.value?.candidateStrategy ===
+ CandidateStrategy.START_USER_DEPT_LEADER
+ ) {
+ showText = `发起人的部门负责人`;
+ }
+ // 发起人的部门负责人
+ if (
+ configForm.value?.candidateStrategy ===
+ CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+ ) {
+ showText = `发起人连续部门负责人`;
+ }
+ // 流程表达式
+ if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) {
+ showText = `流程表达式:${configForm.value.expression}`;
+ }
+ return showText;
+ }
+
+ /**
+ * 处理候选人参数的赋值
+ */
+ function handleCandidateParam() {
+ let candidateParam: string | undefined;
+ if (!configForm.value) {
+ return candidateParam;
+ }
+ switch (configForm.value.candidateStrategy) {
+ case CandidateStrategy.DEPT_LEADER:
+ case CandidateStrategy.DEPT_MEMBER: {
+ candidateParam = configForm.value.deptIds?.join(',');
+ break;
+ }
+ case CandidateStrategy.EXPRESSION: {
+ candidateParam = configForm.value.expression;
+ break;
+ }
+ // 表单内部门的负责人
+ case CandidateStrategy.FORM_DEPT_LEADER: {
+ // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
+ const deptFieldOnForm = configForm.value.formDept;
+ candidateParam = deptFieldOnForm?.concat(
+ `|${configForm.value.deptLevel}`,
+ );
+ break;
+ }
+ case CandidateStrategy.FORM_USER: {
+ candidateParam = configForm.value?.formUser;
+ break;
+ }
+ // 指定连续多级部门的负责人
+ case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+ // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
+ const deptIds = configForm.value.deptIds?.join(',');
+ candidateParam = deptIds?.concat(`|${configForm.value.deptLevel}`);
+ break;
+ }
+ case CandidateStrategy.POST: {
+ candidateParam = configForm.value.postIds?.join(',');
+ break;
+ }
+ case CandidateStrategy.ROLE: {
+ candidateParam = configForm.value.roleIds?.join(',');
+ break;
+ }
+ // 发起人部门负责人
+ case CandidateStrategy.START_USER_DEPT_LEADER:
+ case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: {
+ candidateParam = `${configForm.value.deptLevel}`;
+ break;
+ }
+ case CandidateStrategy.USER: {
+ candidateParam = configForm.value.userIds?.join(',');
+ break;
+ }
+ case CandidateStrategy.USER_GROUP: {
+ candidateParam = configForm.value.userGroups?.join(',');
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ return candidateParam;
+ }
+ /**
+ * 解析候选人参数
+ */
+ function parseCandidateParam(
+ candidateStrategy: CandidateStrategy,
+ candidateParam: string | undefined,
+ ) {
+ if (!configForm.value || !candidateParam) {
+ return;
+ }
+ switch (candidateStrategy) {
+ case CandidateStrategy.DEPT_LEADER:
+ case CandidateStrategy.DEPT_MEMBER: {
+ configForm.value.deptIds = candidateParam
+ .split(',')
+ .map((item) => +item);
+ break;
+ }
+ case CandidateStrategy.EXPRESSION: {
+ configForm.value.expression = candidateParam;
+ break;
+ }
+ // 表单内的部门负责人
+ case CandidateStrategy.FORM_DEPT_LEADER: {
+ // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
+ const paramArray = candidateParam.split('|');
+ if (paramArray.length > 1) {
+ configForm.value.formDept = paramArray[0];
+ if (paramArray[1]) configForm.value.deptLevel = +paramArray[1];
+ }
+ break;
+ }
+ case CandidateStrategy.FORM_USER: {
+ configForm.value.formUser = candidateParam;
+ break;
+ }
+ // 指定连续多级部门的负责人
+ case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+ // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
+ const paramArray = candidateParam.split('|') as string[];
+ if (paramArray.length > 1) {
+ configForm.value.deptIds = paramArray[0]
+ ?.split(',')
+ .map((item) => +item);
+ if (paramArray[1]) configForm.value.deptLevel = +paramArray[1];
+ }
+ break;
+ }
+ case CandidateStrategy.POST: {
+ configForm.value.postIds = candidateParam
+ .split(',')
+ .map((item) => +item);
+ break;
+ }
+ case CandidateStrategy.ROLE: {
+ configForm.value.roleIds = candidateParam
+ .split(',')
+ .map((item) => +item);
+ break;
+ }
+ // 发起人部门负责人
+ case CandidateStrategy.START_USER_DEPT_LEADER:
+ case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: {
+ configForm.value.deptLevel = +candidateParam;
+ break;
+ }
+ case CandidateStrategy.USER: {
+ configForm.value.userIds = candidateParam
+ .split(',')
+ .map((item) => +item);
+ break;
+ }
+ case CandidateStrategy.USER_GROUP: {
+ configForm.value.userGroups = candidateParam
+ .split(',')
+ .map((item) => +item);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+ return {
+ configForm,
+ roleOptions,
+ postOptions,
+ userOptions,
+ userGroupOptions,
+ deptTreeOptions,
+ handleCandidateParam,
+ parseCandidateParam,
+ getShowText,
+ };
+}
+
+/**
+ * @description 抽屉配置
+ */
+export function useDrawer() {
+ // 抽屉配置是否可见
+ const settingVisible = ref(false);
+ // 关闭配置抽屉
+ function closeDrawer() {
+ settingVisible.value = false;
+ }
+ // 打开配置抽屉
+ function openDrawer() {
+ settingVisible.value = true;
+ }
+ return {
+ settingVisible,
+ closeDrawer,
+ openDrawer,
+ };
+}
+
+/**
+ * @description 节点名称配置
+ */
+export function useNodeName(nodeType: BpmNodeTypeEnum) {
+ // 节点名称
+ const nodeName = ref();
+ // 节点名称输入框
+ const showInput = ref(false);
+ // 输入框的引用
+ const inputRef = ref(null);
+ // 点击节点名称编辑图标
+ function clickIcon() {
+ showInput.value = true;
+ }
+ // 修改节点名称
+ function changeNodeName() {
+ showInput.value = false;
+ nodeName.value =
+ nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string);
+ }
+ // 监听 showInput 的变化,当变为 true 时自动聚焦
+ watch(showInput, (value) => {
+ if (value) {
+ nextTick(() => {
+ inputRef.value?.focus();
+ });
+ }
+ });
+
+ return {
+ nodeName,
+ showInput,
+ inputRef,
+ clickIcon,
+ changeNodeName,
+ };
+}
+
+export function useNodeName2(
+ node: Ref,
+ nodeType: BpmNodeTypeEnum,
+) {
+ // 显示节点名称输入框
+ const showInput = ref(false);
+ // 输入框的引用
+ const inputRef = ref(null);
+
+ // 监听 showInput 的变化,当变为 true 时自动聚焦
+ watch(showInput, (value) => {
+ if (value) {
+ nextTick(() => {
+ inputRef.value?.focus();
+ });
+ }
+ });
+
+ // 修改节点名称
+ function changeNodeName() {
+ showInput.value = false;
+ node.value.name =
+ node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string);
+ console.warn('node.value.name===>', node.value.name);
+ }
+ // 点击节点标题进行输入
+ function clickTitle() {
+ showInput.value = true;
+ }
+ return {
+ showInput,
+ inputRef,
+ clickTitle,
+ changeNodeName,
+ };
+}
+
+/**
+ * @description 根据节点任务状态,获取节点任务状态样式
+ */
+export function useTaskStatusClass(
+ taskStatus: BpmTaskStatusEnum | undefined,
+): string {
+ if (!taskStatus) {
+ return '';
+ }
+ if (taskStatus === BpmTaskStatusEnum.APPROVE) {
+ return 'status-pass';
+ }
+ if (taskStatus === BpmTaskStatusEnum.RUNNING) {
+ return 'status-running';
+ }
+ if (taskStatus === BpmTaskStatusEnum.REJECT) {
+ return 'status-reject';
+ }
+ if (taskStatus === BpmTaskStatusEnum.CANCEL) {
+ return 'status-cancel';
+ }
+ return '';
+}
+
+/** 条件组件文字展示 */
+export function getConditionShowText(
+ conditionType: ConditionType | undefined,
+ conditionExpression: string | undefined,
+ conditionGroups: ConditionGroup | undefined,
+ fieldOptions: Array>,
+) {
+ let showText: string | undefined;
+ if (conditionType === ConditionType.EXPRESSION && conditionExpression) {
+ showText = `表达式:${conditionExpression}`;
+ }
+ if (conditionType === ConditionType.RULE) {
+ // 条件组是否为与关系
+ const groupAnd = conditionGroups?.and;
+ let warningMessage: string | undefined;
+ const conditionGroup = conditionGroups?.conditions.map((item) => {
+ return `(${item.rules
+ .map((rule) => {
+ if (rule.leftSide && rule.rightSide) {
+ return `${getFormFieldTitle(
+ fieldOptions,
+ rule.leftSide,
+ )} ${getOpName(rule.opCode)} ${rule.rightSide}`;
+ } else {
+ // 有一条规则不完善。提示错误
+ warningMessage = '请完善条件规则';
+ return '';
+ }
+ })
+ .join(item.and ? ' 且 ' : ' 或 ')} ) `;
+ });
+ showText = warningMessage
+ ? ''
+ : conditionGroup?.join(groupAnd ? ' 且 ' : ' 或 ');
+ }
+ return showText;
+}
+
+/** 获取表单字段名称*/
+function getFormFieldTitle(
+ fieldOptions: Array>,
+ field: string,
+) {
+ const item = fieldOptions.find((item) => item.field === field);
+ return item?.title;
+}
+
+/** 获取操作符名称 */
+function getOpName(opCode: string): string | undefined {
+ const opName = COMPARISON_OPERATORS.find(
+ (item: any) => item.value === opCode,
+ );
+ return opName?.label;
+}
+
+/** 获取条件节点默认的名称 */
+export function getDefaultConditionNodeName(
+ index: number,
+ defaultFlow: boolean | undefined,
+): string {
+ if (defaultFlow) {
+ return '其它情况';
+ }
+ return `条件${index + 1}`;
+}
+
+/** 获取包容分支条件节点默认的名称 */
+export function getDefaultInclusiveConditionNodeName(
+ index: number,
+ defaultFlow: boolean | undefined,
+): string {
+ if (defaultFlow) {
+ return '其它情况';
+ }
+ return `包容条件${index + 1}`;
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/index.ts b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/index.ts
new file mode 100644
index 000000000..5300fa0d9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/index.ts
@@ -0,0 +1,9 @@
+import './styles/simple-process-designer.scss';
+
+export { default as HttpRequestSetting } from './components/nodes-config/modules/http-request-setting.vue';
+
+export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue';
+
+export { default as SimpleProcessViewer } from './components/simple-process-viewer.vue';
+
+export type { SimpleFlowNode } from './consts';
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.ttf b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.ttf
new file mode 100644
index 000000000..06f4e31c4
Binary files /dev/null and b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.ttf differ
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.woff b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.woff
new file mode 100644
index 000000000..0724e7505
Binary files /dev/null and b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.woff differ
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.woff2 b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.woff2
new file mode 100644
index 000000000..c904bb67d
Binary files /dev/null and b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/iconfont.woff2 differ
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/simple-process-designer.scss b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/simple-process-designer.scss
new file mode 100644
index 000000000..8f6dc85b4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/simple-process-designer.scss
@@ -0,0 +1,758 @@
+// TODO 整个样式是不是要重新优化一下
+// iconfont 样式
+@font-face {
+ font-family: iconfont; /* Project id 4495938 */
+ src:
+ url('iconfont.woff2?t=1737639517142') format('woff2'),
+ url('iconfont.woff?t=1737639517142') format('woff'),
+ url('iconfont.ttf?t=1737639517142') format('truetype');
+}
+// 配置节点头部
+.config-header {
+ display: flex;
+ flex-direction: column;
+
+ .node-name {
+ display: flex;
+ align-items: center;
+ height: 24px;
+ font-size: 16px;
+ line-height: 24px;
+ cursor: pointer;
+ }
+
+ .divide-line {
+ width: 100%;
+ height: 1px;
+ margin-top: 16px;
+ background: #eee;
+ }
+
+ .config-editable-input {
+ max-width: 510px;
+ height: 24px;
+ font-size: 16px;
+ line-height: 24px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ &:focus {
+ outline: 0;
+ border-color: #40a9ff;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+}
+// 节点连线气泡卡片样式
+.handler-item-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ width: 320px;
+ cursor: pointer;
+
+ .handler-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 12px;
+ }
+
+ .handler-item-icon {
+ width: 50px;
+ height: 50px;
+ text-align: center;
+ user-select: none;
+ background: #fff;
+ border: 1px solid #e2e2e2;
+ border-radius: 50%;
+
+ &:hover {
+ background: #e2e2e2;
+ box-shadow: 0 2px 4px 0 rgb(0 0 0 / 10%);
+ }
+
+ .icon-size {
+ font-size: 25px;
+ line-height: 50px;
+ }
+ }
+
+ .approve {
+ color: #ff943e;
+ }
+
+ .copy {
+ color: #3296fa;
+ }
+
+ .condition {
+ color: #67c23a;
+ }
+
+ .parallel {
+ color: #626aef;
+ }
+
+ .inclusive {
+ color: #345da2;
+ }
+
+ .delay {
+ color: #e47470;
+ }
+
+ .trigger {
+ color: #3373d2;
+ }
+
+ .router {
+ color: #ca3a31;
+ }
+
+ .transactor {
+ color: #309;
+ }
+
+ .child-process {
+ color: #963;
+ }
+
+ .async-child-process {
+ color: #066;
+ }
+
+ .handler-item-text {
+ width: 80px;
+ margin-top: 4px;
+ font-size: 13px;
+ text-align: center;
+ }
+}
+// Simple 流程模型样式
+.simple-process-model-container {
+ width: 100%;
+ height: 100%;
+ padding-top: 32px;
+ overflow-x: auto;
+ background-color: #fafafa;
+
+ .simple-process-model {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-width: fit-content;
+ background: url('./svg/simple-process-bg.svg') 0 0 repeat;
+ transform: scale(1);
+ transform-origin: 50% 0 0;
+ // 节点容器 定义节点宽度
+ .node-container {
+ width: 200px;
+ }
+ // 节点
+ .node-box {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-height: 70px;
+ padding: 5px 10px 8px;
+ cursor: pointer;
+ background-color: #fff;
+ border: 2px solid transparent;
+ border-radius: 8px;
+ box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%);
+ transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+ &.status-pass {
+ background-color: #a9da90;
+ border-color: #67c23a;
+ }
+
+ &.status-pass:hover {
+ border-color: #67c23a;
+ }
+
+ &.status-running {
+ background-color: #e7f0fe;
+ border-color: #5a9cf8;
+ }
+
+ &.status-running:hover {
+ border-color: #5a9cf8;
+ }
+
+ &.status-reject {
+ background-color: #f6e5e5;
+ border-color: #e47470;
+ }
+
+ &.status-reject:hover {
+ border-color: #e47470;
+ }
+
+ &:hover {
+ border-color: #0089ff;
+
+ .node-toolbar {
+ opacity: 1;
+ }
+
+ .branch-node-move {
+ display: flex;
+ }
+ }
+
+ // 普通节点标题
+ .node-title-container {
+ display: flex;
+ align-items: center;
+ padding: 4px;
+ cursor: pointer;
+ border-radius: 4px 4px 0 0;
+
+ .node-title-icon {
+ display: flex;
+ align-items: center;
+
+ &.user-task {
+ color: #ff943e;
+ }
+
+ &.copy-task {
+ color: #3296fa;
+ }
+
+ &.start-user {
+ color: #676565;
+ }
+
+ &.delay-node {
+ color: #e47470;
+ }
+
+ &.trigger-node {
+ color: #3373d2;
+ }
+
+ &.router-node {
+ color: #ca3a31;
+ }
+
+ &.transactor-task {
+ color: #309;
+ }
+
+ &.child-process {
+ color: #963;
+ }
+
+ &.async-child-process {
+ color: #066;
+ }
+ }
+
+ .node-title {
+ margin-left: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 18px;
+ color: #1f1f1f;
+ white-space: nowrap;
+
+ &:hover {
+ border-bottom: 1px dashed #f60;
+ }
+ }
+ }
+
+ // 条件节点标题
+ .branch-node-title-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 0;
+ cursor: pointer;
+ border-radius: 4px 4px 0 0;
+
+ .input-max-width {
+ max-width: 115px !important;
+ }
+
+ .branch-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 13px;
+ font-weight: 600;
+ color: #f60;
+ white-space: nowrap;
+
+ &:hover {
+ border-bottom: 1px dashed #000;
+ }
+ }
+
+ .branch-priority {
+ min-width: 50px;
+ font-size: 12px;
+ }
+ }
+
+ .node-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 32px;
+ padding: 4px 8px;
+ margin-top: 4px;
+ line-height: 32px;
+ color: #111f2c;
+ background: rgb(0 0 0 / 3%);
+ border-radius: 4px;
+
+ .node-text {
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+ font-size: 14px;
+ line-height: 24px;
+ word-break: break-all;
+ -webkit-box-orient: vertical;
+ }
+ }
+
+ //条件节点内容
+ .branch-node-content {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ padding: 4px 0;
+ margin-top: 4px;
+ line-height: 32px;
+ color: #111f2c;
+ background: rgb(0 0 0 / 3%);
+ border-radius: 4px;
+
+ .branch-node-text {
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 1; /* 这将限制文本显示为一行 */
+ font-size: 12px;
+ line-height: 24px;
+ word-break: break-all;
+ -webkit-box-orient: vertical;
+ }
+ }
+
+ // 节点操作 :删除
+ .node-toolbar {
+ position: absolute;
+ top: -20px;
+ right: 0;
+ display: flex;
+ opacity: 0;
+
+ .toolbar-icon {
+ vertical-align: middle;
+ text-align: center;
+ }
+ }
+
+ // 条件节点左右移动
+ .branch-node-move {
+ position: absolute;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ width: 10px;
+ height: 100%;
+ cursor: pointer;
+ }
+
+ .move-node-left {
+ top: 0;
+ left: -2px;
+ background: rgb(126 134 142 / 8%);
+ border-top-left-radius: 8px;
+ border-bottom-left-radius: 8px;
+ }
+
+ .move-node-right {
+ top: 0;
+ right: -2px;
+ background: rgb(126 134 142 / 8%);
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ }
+ }
+
+ .node-config-error {
+ border-color: #ff5219 !important;
+ }
+ // 普通节点包装
+ .node-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+ // 节点连线处理
+ .node-handler-wrapper {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 70px;
+ user-select: none;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ z-index: 0;
+ width: 2px;
+ height: 100%;
+ margin: auto;
+ content: '';
+ background-color: #dedede;
+ }
+
+ .node-handler {
+ .add-icon {
+ position: relative;
+ top: -5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 25px;
+ height: 25px;
+ color: #fff;
+ cursor: pointer;
+ background-color: #0089ff;
+ border-radius: 50%;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+ }
+
+ .node-handler-arrow {
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ display: flex;
+ transform: translateX(-50%);
+ }
+ }
+
+ // 条件节点包装
+ .branch-node-wrapper {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-top: 16px;
+
+ .branch-node-container {
+ position: relative;
+ display: flex;
+ min-width: fit-content;
+
+ &::before {
+ position: absolute;
+ left: 50%;
+ width: 4px;
+ height: 100%;
+ content: '';
+ background-color: #fafafa;
+ transform: translate(-50%);
+ }
+
+ .branch-node-add {
+ position: absolute;
+ top: -18px;
+ left: 50%;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ height: 36px;
+ padding: 0 10px;
+ font-size: 12px;
+ line-height: 36px;
+ border: 2px solid #dedede;
+ border-radius: 18px;
+ transform: translateX(-50%);
+ transform-origin: center center;
+ }
+
+ .branch-node-readonly {
+ position: absolute;
+ top: -18px;
+ left: 50%;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ background-color: #fff;
+ border: 2px solid #dedede;
+ border-radius: 50%;
+ transform: translateX(-50%);
+ transform-origin: center center;
+
+ &.status-pass {
+ background-color: #e9f4e2;
+ border-color: #6bb63c;
+ }
+
+ &.status-pass:hover {
+ border-color: #6bb63c;
+ }
+
+ .icon-size {
+ font-size: 22px;
+
+ &.condition {
+ color: #67c23a;
+ }
+
+ &.parallel {
+ color: #626aef;
+ }
+
+ &.inclusive {
+ color: #345da2;
+ }
+ }
+ }
+
+ .branch-node-item {
+ position: relative;
+ display: flex;
+ flex-shrink: 0;
+ flex-direction: column;
+ align-items: center;
+ min-width: 280px;
+ padding: 40px 40px 0;
+ background: transparent;
+ border-top: 2px solid #dedede;
+ border-bottom: 2px solid #dedede;
+
+ &::before {
+ position: absolute;
+ inset: 0;
+ width: 2px;
+ height: 100%;
+ margin: auto;
+ content: '';
+ background-color: #dedede;
+ }
+ }
+ // 覆盖条件节点第一个节点左上角的线
+ .branch-line-first-top {
+ position: absolute;
+ top: -5px;
+ left: -1px;
+ width: 50%;
+ height: 7px;
+ content: '';
+ background-color: #fafafa;
+ }
+ // 覆盖条件节点第一个节点左下角的线
+ .branch-line-first-bottom {
+ position: absolute;
+ bottom: -5px;
+ left: -1px;
+ width: 50%;
+ height: 7px;
+ content: '';
+ background-color: #fafafa;
+ }
+ // 覆盖条件节点最后一个节点右上角的线
+ .branch-line-last-top {
+ position: absolute;
+ top: -5px;
+ right: -1px;
+ width: 50%;
+ height: 7px;
+ content: '';
+ background-color: #fafafa;
+ }
+ // 覆盖条件节点最后一个节点右下角的线
+ .branch-line-last-bottom {
+ position: absolute;
+ right: -1px;
+ bottom: -5px;
+ width: 50%;
+ height: 7px;
+ content: '';
+ background-color: #fafafa;
+ }
+ }
+ }
+
+ .node-fixed-name {
+ display: inline-block;
+ width: auto;
+ padding: 0 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+ white-space: nowrap;
+ }
+ // 开始节点包装
+ .start-node-wrapper {
+ position: relative;
+ margin-top: 16px;
+
+ .start-node-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .start-node-box {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 90px;
+ height: 36px;
+ padding: 3px 4px;
+ color: #212121;
+ cursor: pointer;
+ background: #fafafa;
+ border-radius: 30px;
+ box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+ }
+ }
+ }
+
+ // 结束节点包装
+ .end-node-wrapper {
+ margin-bottom: 16px;
+
+ .end-node-box {
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 36px;
+ color: #212121;
+ background-color: #fff;
+ border: 2px solid transparent;
+ border-radius: 30px;
+ box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+
+ &.status-pass {
+ background-color: #a9da90;
+ border-color: #6bb63c;
+ }
+
+ &.status-pass:hover {
+ border-color: #6bb63c;
+ }
+
+ &.status-reject {
+ background-color: #f6e5e5;
+ border-color: #e47470;
+ }
+
+ &.status-reject:hover {
+ border-color: #e47470;
+ }
+
+ &.status-cancel {
+ background-color: #eaeaeb;
+ border-color: #919398;
+ }
+
+ &.status-cancel:hover {
+ border-color: #919398;
+ }
+ }
+ }
+
+ // 可编辑的 title 输入框
+ .editable-title-input {
+ max-width: 145px;
+ height: 20px;
+ margin-left: 4px;
+ font-size: 12px;
+ line-height: 20px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ &:focus {
+ outline: 0;
+ border-color: #40a9ff;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ }
+}
+
+.iconfont {
+ font-family: iconfont !important;
+ font-size: 16px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-trigger::before {
+ content: '\e6d3';
+}
+
+.icon-router::before {
+ content: '\e6b2';
+}
+
+.icon-delay::before {
+ content: '\e600';
+}
+
+.icon-start-user::before {
+ content: '\e679';
+}
+
+.icon-inclusive::before {
+ content: '\e602';
+}
+
+.icon-copy::before {
+ content: '\e7eb';
+}
+
+.icon-transactor::before {
+ content: '\e61c';
+}
+
+.icon-exclusive::before {
+ content: '\e717';
+}
+
+.icon-approve::before {
+ content: '\e715';
+}
+
+.icon-parallel::before {
+ content: '\e688';
+}
+
+.icon-async-child-process::before {
+ content: '\e6f2';
+}
+
+.icon-child-process::before {
+ content: '\e6c1';
+}
diff --git a/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/svg/simple-process-bg.svg b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/svg/simple-process-bg.svg
new file mode 100644
index 000000000..eb23ab5ae
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/components/simple-process-design/styles/svg/simple-process-bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/web-antdv-next/src/views/bpm/form/data.ts b/apps/web-antdv-next/src/views/bpm/form/data.ts
new file mode 100644
index 000000000..20ed60414
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/data.ts
@@ -0,0 +1,61 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '表单名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入表单名称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '表单名称',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 200,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/form/designer/data.ts b/apps/web-antdv-next/src/views/bpm/form/designer/data.ts
new file mode 100644
index 000000000..36a993ca7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/designer/data.ts
@@ -0,0 +1,48 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '表单名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入表单名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/form/designer/index.vue b/apps/web-antdv-next/src/views/bpm/form/designer/index.vue
new file mode 100644
index 000000000..60c3a3bbd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/designer/index.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/form/designer/modules/form.vue b/apps/web-antdv-next/src/views/bpm/form/designer/modules/form.vue
new file mode 100644
index 000000000..42766c63d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/designer/modules/form.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/form/index.vue b/apps/web-antdv-next/src/views/bpm/form/index.vue
new file mode 100644
index 000000000..f3b9d26c8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/index.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/form/mobile/index.vue b/apps/web-antdv-next/src/views/bpm/form/mobile/index.vue
new file mode 100644
index 000000000..d0e199ba7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/mobile/index.vue
@@ -0,0 +1,405 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/form/modules/detail.vue b/apps/web-antdv-next/src/views/bpm/form/modules/detail.vue
new file mode 100644
index 000000000..f0b612b8a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/form/modules/detail.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/group/data.ts b/apps/web-antdv-next/src/views/bpm/group/data.ts
new file mode 100644
index 000000000..01cc0b23d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/group/data.ts
@@ -0,0 +1,169 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { h } from 'vue';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { Tag } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let userList: SystemUserApi.User[] = [];
+getSimpleUserList().then((data) => (userList = data));
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '组名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入组名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入描述',
+ },
+ },
+ {
+ fieldName: 'userIds',
+ label: '成员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择成员',
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ mode: 'tags',
+ },
+ rules: z.array(z.number()).min(1, '请选择成员').default([]),
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '组名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入组名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '组名',
+ minWidth: 200,
+ },
+ {
+ field: 'description',
+ title: '描述',
+ minWidth: 200,
+ },
+ {
+ field: 'userIds',
+ title: '成员',
+ minWidth: 200,
+ slots: {
+ default: ({ row }) => {
+ const userIds = row.userIds || [];
+ return userIds.map((userId: number) =>
+ h(
+ Tag,
+ {
+ color: 'blue',
+ class: 'mr-1',
+ },
+ () => userList.find((u) => u.id === userId)?.nickname,
+ ),
+ );
+ },
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/group/index.vue b/apps/web-antdv-next/src/views/bpm/group/index.vue
new file mode 100644
index 000000000..fbe93289e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/group/index.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/group/modules/form.vue b/apps/web-antdv-next/src/views/bpm/group/modules/form.vue
new file mode 100644
index 000000000..849d0b900
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/group/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/data.ts b/apps/web-antdv-next/src/views/bpm/model/data.ts
new file mode 100644
index 000000000..59405e77a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/data.ts
@@ -0,0 +1,49 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { BpmModelApi } from '#/api/bpm/model';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '流程名称',
+ minWidth: 200,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'startUserIds',
+ title: '可见范围',
+ minWidth: 150,
+ slots: { default: 'startUserIds' },
+ },
+ {
+ field: 'type',
+ title: '流程类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_MODEL_TYPE },
+ },
+ },
+ {
+ field: 'formType',
+ title: '表单信息',
+ minWidth: 150,
+ slots: { default: 'formInfo' },
+ },
+ {
+ field: 'deploymentTime',
+ title: '最后发布',
+ minWidth: 280,
+ slots: { default: 'deploymentTime' },
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/model/definition/data.ts b/apps/web-antdv-next/src/views/bpm/model/definition/data.ts
new file mode 100644
index 000000000..1e33ac84a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/definition/data.ts
@@ -0,0 +1,72 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '定义编号',
+ minWidth: 250,
+ },
+ {
+ field: 'name',
+ title: '流程名称',
+ minWidth: 150,
+ },
+ {
+ field: 'icon',
+ title: '流程图标',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ width: 24,
+ height: 24,
+ },
+ },
+ },
+ {
+ field: 'startUsers',
+ title: '可见范围',
+ minWidth: 100,
+ slots: { default: 'startUsers' },
+ },
+ {
+ field: 'modelType',
+ title: '流程类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_MODEL_TYPE },
+ },
+ },
+ {
+ field: 'formType',
+ title: '表单信息',
+ minWidth: 150,
+ slots: { default: 'formInfo' },
+ },
+ {
+ field: 'version',
+ title: '流程版本',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellTag',
+ },
+ },
+ {
+ field: 'deploymentTime',
+ title: '部署时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/model/definition/index.vue b/apps/web-antdv-next/src/views/bpm/model/definition/index.vue
new file mode 100644
index 000000000..d1c8bb7fb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/definition/index.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+ 全部可见
+
+
+ {{ row.startUsers[0]!.nickname }}
+
+
+
+ {{ row.startUsers[0]!.nickname }}等
+ {{ row.startUsers.length }} 人可见
+
+
+
+
+
+
+ 暂无表单
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/index.vue b/apps/web-antdv-next/src/views/bpm/model/form/index.vue
new file mode 100644
index 000000000..8ac2c8dd3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/index.vue
@@ -0,0 +1,507 @@
+
+
+
+
+ ]
+
+
+
+
+
+
+ {{ formData.name || '创建流程' }}
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+ {{ step.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/basic-info.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/basic-info.vue
new file mode 100644
index 000000000..a297e253e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/basic-info.vue
@@ -0,0 +1,450 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+ {{ user.nickname?.substring(0, 1) }}
+
+
+ {{ user.nickname }}
+
+
+
+
+
+
+
+
+
+ {{ dept.name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ user.nickname?.substring(0, 1) }}
+
+
+ {{ user.nickname }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/bpm-model-editor.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/bpm-model-editor.vue
new file mode 100644
index 000000000..de34e0334
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/bpm-model-editor.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/custom-print-template.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/custom-print-template.vue
new file mode 100644
index 000000000..7133eb252
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/custom-print-template.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/extra-setting.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/extra-setting.vue
new file mode 100644
index 000000000..18ae5c1bd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/extra-setting.vue
@@ -0,0 +1,621 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/form-design.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/form-design.vue
new file mode 100644
index 000000000..60e242e29
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/form-design.vue
@@ -0,0 +1,187 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/process-design.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/process-design.vue
new file mode 100644
index 000000000..8f2495b75
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/process-design.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/simple-model-design.vue b/apps/web-antdv-next/src/views/bpm/model/form/modules/simple-model-design.vue
new file mode 100644
index 000000000..ba8ae33a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/simple-model-design.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/form/modules/tinymce-plugin.ts b/apps/web-antdv-next/src/views/bpm/model/form/modules/tinymce-plugin.ts
new file mode 100644
index 000000000..fac692ec7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/form/modules/tinymce-plugin.ts
@@ -0,0 +1,78 @@
+/** TinyMCE 自定义功能:
+ * - processrecord 按钮:插入流程记录占位元素
+ * - @ 自动补全:插入 mention 占位元素
+ */
+
+// TinyMCE 全局或通过打包器提供
+import type { Editor } from 'tinymce';
+
+export interface MentionItem {
+ id: string;
+ name: string;
+}
+
+/** 在编辑器 setup 回调中注册流程记录按钮和 @ 自动补全 */
+export function setupTinyPlugins(
+ editor: Editor,
+ getMentionList: () => MentionItem[],
+) {
+ // 按钮:流程记录
+ editor.ui.registry.addButton('processrecord', {
+ text: '流程记录',
+ tooltip: '插入流程记录占位',
+ onAction: () => {
+ // 流程记录占位显示, 仅用于显示。process-print.vue 组件中会替换掉
+ editor.insertContent(
+ [
+ '',
+ '
',
+ '| 流程记录 |
',
+ '',
+ '| 节点 | ',
+ '操作 | ',
+ '
',
+ '
',
+ '
',
+ ].join(''),
+ );
+ },
+ });
+
+ // @ 自动补全
+ editor.ui.registry.addAutocompleter('bpmMention', {
+ trigger: '@',
+ minChars: 0,
+ columns: 1,
+ fetch: (
+ pattern: string,
+ _maxResults: number,
+ _fetchOptions: Record,
+ ) => {
+ const list = getMentionList();
+ const keyword = (pattern || '').toLowerCase().trim();
+ const data = list
+ .filter((i) => i.name.toLowerCase().includes(keyword))
+ .map((i) => ({
+ value: i.id,
+ text: i.name,
+ }));
+ return Promise.resolve(data);
+ },
+ onAction: (
+ autocompleteApi: any,
+ rng: Range,
+ value: string,
+ _meta: Record,
+ ) => {
+ const list = getMentionList();
+ const item = list.find((i) => i.id === value);
+ const name = item ? item.name : value;
+ const info = encodeURIComponent(JSON.stringify({ id: value }));
+ editor.selection.setRng(rng);
+ editor.insertContent(
+ `@${name}`,
+ );
+ autocompleteApi.hide();
+ },
+ });
+}
diff --git a/apps/web-antdv-next/src/views/bpm/model/index.vue b/apps/web-antdv-next/src/views/bpm/model/index.vue
new file mode 100644
index 000000000..91c504f9e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/index.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/model/modules/category-draggable-model.vue b/apps/web-antdv-next/src/views/bpm/model/modules/category-draggable-model.vue
new file mode 100644
index 000000000..4071c1e16
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/model/modules/category-draggable-model.vue
@@ -0,0 +1,747 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ categoryInfo.name }}
+
+
+ ({{ categoryInfo.modelList?.length || 0 }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name.substring(0, 2) }}
+
+
+
![图标]()
+
+
+ {{ row.name }}
+
+
+
+
+
+
+ 全部可见
+
+
+ {{ row.startUsers[0].nickname }}
+
+
+ {{ row.startDepts[0].name }}
+
+
+
+ {{ row.startDepts[0].name }}等
+ {{ row.startDepts.length }} 个部门可见
+
+
+
+
+ {{ row.startUsers[0].nickname }}等
+ {{ row.startUsers.length }} 人可见
+
+
+
+
+
+
+ 暂无表单
+
+
+
+
+ {{ formatDateTime(row.processDefinition.deploymentTime) }}
+
+
+ v{{ row.processDefinition.version }}
+
+ 未部署
+
+ 已停用
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/oa/leave/create.vue b/apps/web-antdv-next/src/views/bpm/oa/leave/create.vue
new file mode 100644
index 000000000..9363959f3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/oa/leave/create.vue
@@ -0,0 +1,269 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/oa/leave/data.ts b/apps/web-antdv-next/src/views/bpm/oa/leave/data.ts
new file mode 100644
index 000000000..9570f422d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/oa/leave/data.ts
@@ -0,0 +1,205 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDate } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '请假类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择请假类型',
+ options: getDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE, 'number'),
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+
+ {
+ fieldName: 'startTime',
+ label: '开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择开始时间',
+ showTime: true,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择结束时间',
+ showTime: true,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'reason',
+ label: '原因',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入原因',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'type',
+ label: '请假类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择请假类型',
+ options: getDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE, 'number'),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '审批结果',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择审批结果',
+ allowClear: true,
+ options: getDictOptions(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ 'number',
+ ),
+ },
+ },
+ {
+ fieldName: 'reason',
+ label: '原因',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入原因',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '申请编号',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
+ },
+ },
+ {
+ field: 'startTime',
+ title: '开始时间',
+ minWidth: 180,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'type',
+ title: '请假类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_OA_LEAVE_TYPE },
+ },
+ },
+ {
+ field: 'reason',
+ title: '原因',
+ minWidth: 150,
+ },
+ {
+ field: 'createTime',
+ title: '申请时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情 */
+export function useDetailFormSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ label: '请假类型',
+ field: 'type',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.BPM_OA_LEAVE_TYPE,
+ value: val,
+ }),
+ },
+ {
+ label: '开始时间',
+ field: 'startTime',
+ render: (val) => formatDate(val) as string,
+ },
+ {
+ label: '结束时间',
+ field: 'endTime',
+ render: (val) => formatDate(val) as string,
+ },
+ {
+ label: '原因',
+ field: 'reason',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/oa/leave/detail.vue b/apps/web-antdv-next/src/views/bpm/oa/leave/detail.vue
new file mode 100644
index 000000000..9f2d0aa1a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/oa/leave/detail.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/oa/leave/index.vue b/apps/web-antdv-next/src/views/bpm/oa/leave/index.vue
new file mode 100644
index 000000000..484b21efd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/oa/leave/index.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processExpression/components/index.ts b/apps/web-antdv-next/src/views/bpm/processExpression/components/index.ts
new file mode 100644
index 000000000..4047767df
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processExpression/components/index.ts
@@ -0,0 +1 @@
+export { default as ProcessExpressionSelectModal } from './select-modal.vue';
diff --git a/apps/web-antdv-next/src/views/bpm/processExpression/components/select-modal.vue b/apps/web-antdv-next/src/views/bpm/processExpression/components/select-modal.vue
new file mode 100644
index 000000000..ebecfe5a5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processExpression/components/select-modal.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processExpression/data.ts b/apps/web-antdv-next/src/views/bpm/processExpression/data.ts
new file mode 100644
index 000000000..a2679571e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processExpression/data.ts
@@ -0,0 +1,128 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'expression',
+ label: '表达式',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入表达式',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 200,
+ },
+
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'expression',
+ title: '表达式',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/processExpression/index.vue b/apps/web-antdv-next/src/views/bpm/processExpression/index.vue
new file mode 100644
index 000000000..6cb421177
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processExpression/index.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processExpression/modules/form.vue b/apps/web-antdv-next/src/views/bpm/processExpression/modules/form.vue
new file mode 100644
index 000000000..a621c3cba
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processExpression/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/create/index.vue b/apps/web-antdv-next/src/views/bpm/processInstance/create/index.vue
new file mode 100644
index 000000000..638fed593
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/create/index.vue
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![流程图标]()
+
+
+ {{ definition.name?.slice(0, 2) }}
+
+
+
+
+ {{ definition.name }}
+
+
+
+
+
+
+
+
+
+
+
+ 没有找到搜索结果
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/create/modules/form.vue b/apps/web-antdv-next/src/views/bpm/processInstance/create/modules/form.vue
new file mode 100644
index 000000000..bd4ff8353
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/create/modules/form.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/data.ts b/apps/web-antdv-next/src/views/bpm/processInstance/data.ts
new file mode 100644
index 000000000..883cc97e2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/data.ts
@@ -0,0 +1,122 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getCategorySimpleList } from '#/api/bpm/category';
+import { getSimpleProcessDefinitionList } from '#/api/bpm/definition';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '流程名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入流程名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'processDefinitionId',
+ label: '所属流程',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择流程定义',
+ allowClear: true,
+ api: getSimpleProcessDefinitionList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '流程分类',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请输入流程分类',
+ allowClear: true,
+ api: getCategorySimpleList,
+ labelField: 'name',
+ valueField: 'code',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '流程状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ 'number',
+ ),
+ placeholder: '请选择流程状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '发起时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '流程名称',
+ minWidth: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'summary',
+ title: '摘要',
+ minWidth: 200,
+ slots: {
+ default: 'slot-summary',
+ },
+ },
+ {
+ field: 'categoryName',
+ title: '流程分类',
+ minWidth: 120,
+ fixed: 'left',
+ },
+ {
+ field: 'status',
+ title: '流程状态',
+ minWidth: 250,
+ slots: {
+ default: 'slot-status',
+ },
+ },
+ {
+ field: 'startTime',
+ title: '发起时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/index.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/index.vue
new file mode 100644
index 000000000..f4d8288a7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/index.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+ 编号:{{ id || '-' }}
+
+
+
+
+
+
+
+
+
+ {{ processInstance?.name }}
+
+
+
+
+
+
+
+
+ {{ processInstance?.startUser?.nickname.substring(0, 1) }}
+
+
+ {{ processInstance?.startUser?.nickname }}
+
+
+
+ {{ formatDateTime(processInstance?.startTime) }} 提交
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 待开发
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/bpm-viewer.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/bpm-viewer.vue
new file mode 100644
index 000000000..4ffbb62c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/bpm-viewer.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue
new file mode 100644
index 000000000..ed0d81325
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/operation-button.vue
@@ -0,0 +1,1447 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/process-print.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/process-print.vue
new file mode 100644
index 000000000..0680c5a9e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/process-print.vue
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+
+
+
+ {{ printData.processInstance.name }}
+
+
+
+ {{ `流程编号: ${printData.processInstance.id}` }}
+
+
+ {{ `打印人员: ${userName}` }}
+
+
+
+
+
+ | 发起人 |
+
+ {{ printData.processInstance.startUser?.nickname }}
+ |
+ 发起时间 |
+
+ {{ formatDate(printData.processInstance.startTime) }}
+ |
+
+
+ | 所属部门 |
+
+ {{ printData.processInstance.startUser?.deptName }}
+ |
+ 流程状态 |
+
+ {{
+ getDictLabel(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ printData.processInstance.status,
+ )
+ }}
+ |
+
+
+
+ 表单内容
+ |
+
+
+ |
+ {{ item.name }}
+ |
+
+
+ |
+
+
+
+ 流程记录
+ |
+
+
+ |
+ {{ item.name }}
+ |
+
+ {{ item.description }}
+
+ ![]()
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/signature.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/signature.vue
new file mode 100644
index 000000000..c3f1a86c9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/signature.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/simple-bpm-viewer.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/simple-bpm-viewer.vue
new file mode 100644
index 000000000..751f390ca
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/simple-bpm-viewer.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/task-list.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/task-list.vue
new file mode 100644
index 000000000..0565fcb9b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/task-list.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+ {{ row.reason }}
+ -
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue
new file mode 100644
index 000000000..68fde46df
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/detail/modules/time-line.vue
@@ -0,0 +1,483 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activity.name }}
+
+ 【跳过】
+
+
+
+
+ {{ getApprovalNodeTime(activity) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ user.nickname.substring(0, 1) }}
+
+
{{ user.nickname }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ task.assigneeUser?.nickname.substring(0, 1) }}
+
+ {{ task.assigneeUser?.nickname }}
+
+
+
+
+ {{ task.ownerUser?.nickname.substring(0, 1) }}
+
+ {{ task.ownerUser?.nickname }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 审批意见:{{ task.reason }}
+
+
+ 签名:
+
+
+
+
+
+
+
+
+
+ {{ user.nickname.substring(0, 1) }}
+
+
+ {{ user.nickname }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/index.vue b/apps/web-antdv-next/src/views/bpm/processInstance/index.vue
new file mode 100644
index 000000000..ddb721467
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/index.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.key }} : {{ item.value }}
+
+
+
+ -
+
+
+
+
+
+
+
+ ({{ row.tasks![0]!.name }}) 审批中
+
+
+
+
+
+
+ 等 {{ row.tasks!.length }} 人 ({{ row.tasks![0]!.name }})审批中
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/manager/data.ts b/apps/web-antdv-next/src/views/bpm/processInstance/manager/data.ts
new file mode 100644
index 000000000..a356cd076
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/manager/data.ts
@@ -0,0 +1,156 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getCategorySimpleList } from '#/api/bpm/category';
+import { getSimpleProcessDefinitionList } from '#/api/bpm/definition';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'startUserId',
+ label: '发起人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择发起人',
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '流程名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入流程名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'processDefinitionId',
+ label: '所属流程',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择流程定义',
+ allowClear: true,
+ api: getSimpleProcessDefinitionList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '流程分类',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请输入流程分类',
+ allowClear: true,
+ api: getCategorySimpleList,
+ labelField: 'name',
+ valueField: 'code',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '流程状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ 'number',
+ ),
+ placeholder: '请选择流程状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '发起时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '流程编号',
+ minWidth: 320,
+ fixed: 'left',
+ },
+ {
+ field: 'name',
+ title: '流程名称',
+ minWidth: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'categoryName',
+ title: '流程分类',
+ minWidth: 120,
+ fixed: 'left',
+ },
+ {
+ field: 'startUser.nickname',
+ title: '流程发起人',
+ minWidth: 120,
+ },
+ {
+ field: 'startUser.deptName',
+ title: '发起部门',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '流程状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
+ },
+ },
+ {
+ field: 'startTime',
+ title: '发起时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'durationInMillis',
+ title: '流程耗时',
+ minWidth: 180,
+ formatter: 'formatPast2',
+ },
+ {
+ field: 'tasks',
+ title: '当前审批任务',
+ minWidth: 320,
+ slots: { default: 'tasks' },
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/manager/index.vue b/apps/web-antdv-next/src/views/bpm/processInstance/manager/index.vue
new file mode 100644
index 000000000..592b7ee8c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/manager/index.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/report/data.ts b/apps/web-antdv-next/src/views/bpm/processInstance/report/data.ts
new file mode 100644
index 000000000..dc099b5bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/report/data.ts
@@ -0,0 +1,157 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type {
+ VxeGridPropTypes,
+ VxeTableGridOptions,
+} from '#/adapter/vxe-table';
+import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+interface FormField {
+ field: string;
+ title: string;
+ type: string;
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(
+ formFields: FormField[] = [],
+): VbenFormSchema[] {
+ // 基础搜索字段配置
+ const baseFormSchema: VbenFormSchema[] = [
+ {
+ fieldName: 'startUserId',
+ label: '发起人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择发起人',
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '流程名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入流程名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '流程状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择流程状态',
+ allowClear: true,
+ options: getDictOptions(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ 'number',
+ ),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '发起时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+
+ // 动态表单字段配置:目前支持 input 和 textarea 类型
+ const dynamicFormSchema: VbenFormSchema[] = formFields
+ .filter((item) => ['input', 'textarea'].includes(item.type))
+ .map((item) => ({
+ fieldName: `formFieldsParams.${item.field}`,
+ label: item.title,
+ component: 'Input',
+ componentProps: {
+ placeholder: `请输入${item.title}`,
+ allowClear: true,
+ },
+ }));
+
+ return [...baseFormSchema, ...dynamicFormSchema];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ formFields: FormField[] = [],
+): VxeTableGridOptions['columns'] {
+ // 基础列配置
+ const baseColumns: VxeGridPropTypes.Columns =
+ [
+ {
+ field: 'name',
+ title: '流程名称',
+ minWidth: 250,
+ fixed: 'left',
+ },
+ {
+ field: 'startUser.nickname',
+ title: '流程发起人',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '流程状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
+ },
+ },
+ {
+ field: 'startTime',
+ title: '发起时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+
+ // 动态表单字段列配置:根据表单字段生成对应的列,从 formVariables 中获取值
+ const formFieldColumns = formFields.map((item) => ({
+ field: `formVariables.${item.field}`,
+ title: item.title,
+ minWidth: 120,
+ formatter: ({ row }: any) => {
+ return row.formVariables?.[item.field] ?? '';
+ },
+ }));
+
+ return [
+ ...baseColumns,
+ ...formFieldColumns,
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/processInstance/report/index.vue b/apps/web-antdv-next/src/views/bpm/processInstance/report/index.vue
new file mode 100644
index 000000000..0792fdf07
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processInstance/report/index.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processListener/components/data.ts b/apps/web-antdv-next/src/views/bpm/processListener/components/data.ts
new file mode 100644
index 000000000..eb9c62e93
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processListener/components/data.ts
@@ -0,0 +1,73 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 选择监听器弹窗的列表字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'name', title: '名字', minWidth: 160 },
+ {
+ field: 'type',
+ title: '类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_LISTENER_TYPE },
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ { field: 'event', title: '事件', minWidth: 120 },
+ {
+ field: 'valueType',
+ title: '值类型',
+ minWidth: 200,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE },
+ },
+ },
+ { field: 'value', title: '值', minWidth: 150 },
+ {
+ title: '操作',
+ width: 100,
+ slots: { default: 'action' },
+ fixed: 'right',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ defaultValue: CommonStatusEnum.ENABLE,
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ disabled: true,
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/processListener/components/index.ts b/apps/web-antdv-next/src/views/bpm/processListener/components/index.ts
new file mode 100644
index 000000000..04dbccf3d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processListener/components/index.ts
@@ -0,0 +1 @@
+export { default as ProcessListenerSelectModal } from './select-modal.vue';
diff --git a/apps/web-antdv-next/src/views/bpm/processListener/components/select-modal.vue b/apps/web-antdv-next/src/views/bpm/processListener/components/select-modal.vue
new file mode 100644
index 000000000..0b85d24e2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processListener/components/select-modal.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processListener/data.ts b/apps/web-antdv-next/src/views/bpm/processListener/data.ts
new file mode 100644
index 000000000..b8fdd6f32
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processListener/data.ts
@@ -0,0 +1,212 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+export const EVENT_EXECUTION_OPTIONS = [
+ {
+ label: '开始',
+ value: 'start',
+ },
+ {
+ label: '结束',
+ value: 'end',
+ },
+];
+
+export const EVENT_OPTIONS = [
+ { label: '创建', value: 'create' },
+ { label: '指派', value: 'assignment' },
+ { label: '完成', value: 'complete' },
+ { label: '删除', value: 'delete' },
+ { label: '更新', value: 'update' },
+ { label: '超时', value: 'timeout' },
+];
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'type',
+ label: '类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE, 'string'),
+ placeholder: '请选择类型',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'event',
+ label: '事件',
+ component: 'Select',
+ componentProps: {
+ options: EVENT_OPTIONS,
+ placeholder: '请选择事件',
+ allowClear: true,
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ trigger: (values) => (values.event = undefined),
+ componentProps: (values) => ({
+ options:
+ values.type === 'execution'
+ ? EVENT_EXECUTION_OPTIONS
+ : EVENT_OPTIONS,
+ }),
+ },
+ },
+ {
+ fieldName: 'valueType',
+ label: '值类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE,
+ 'string',
+ ),
+ placeholder: '请选择值类型',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'value',
+ label: '类路径|表达式',
+ component: 'Input',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['valueType'],
+ trigger: (values) => (values.value = undefined),
+ componentProps: (values) => ({
+ placeholder:
+ values.valueType === 'class' ? '请输入类路径' : '请输入表达式',
+ }),
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择类型',
+ options: getDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE, 'string'),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 200,
+ },
+ {
+ field: 'type',
+ title: '类型',
+ minWidth: 200,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_LISTENER_TYPE },
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'event',
+ title: '事件',
+ minWidth: 200,
+ },
+ {
+ field: 'valueType',
+ title: '值类型',
+ minWidth: 200,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE },
+ },
+ },
+ {
+ field: 'value',
+ title: '值',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/processListener/index.vue b/apps/web-antdv-next/src/views/bpm/processListener/index.vue
new file mode 100644
index 000000000..a624f2948
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processListener/index.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/processListener/modules/form.vue b/apps/web-antdv-next/src/views/bpm/processListener/modules/form.vue
new file mode 100644
index 000000000..dc0cee273
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/processListener/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/task/copy/data.ts b/apps/web-antdv-next/src/views/bpm/task/copy/data.ts
new file mode 100644
index 000000000..202887d73
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/copy/data.ts
@@ -0,0 +1,92 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '流程名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入流程名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '抄送时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'processInstanceName',
+ title: '流程名称',
+ minWidth: 200,
+ },
+ {
+ field: 'summary',
+ title: '摘要',
+ minWidth: 200,
+ formatter: ({ cellValue }) => {
+ return cellValue && cellValue.length > 0
+ ? cellValue
+ .map((item: any) => `${item.key} : ${item.value}`)
+ .join('\n')
+ : '-';
+ },
+ },
+ {
+ field: 'startUser.nickname',
+ title: '流程发起人',
+ minWidth: 120,
+ },
+ {
+ field: 'processInstanceStartTime',
+ title: '流程发起时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'activityName',
+ title: '抄送节点',
+ minWidth: 120,
+ },
+ {
+ field: 'createUser.nickname',
+ title: '抄送人',
+ minWidth: 120,
+ formatter: ({ cellValue }) => {
+ return cellValue || '-';
+ },
+ },
+ {
+ field: 'reason',
+ title: '抄送意见',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '抄送时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/task/copy/index.vue b/apps/web-antdv-next/src/views/bpm/task/copy/index.vue
new file mode 100644
index 000000000..f567452fc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/copy/index.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/task/done/data.ts b/apps/web-antdv-next/src/views/bpm/task/done/data.ts
new file mode 100644
index 000000000..3c5e9f678
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/done/data.ts
@@ -0,0 +1,154 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getCategorySimpleList } from '#/api/bpm/category';
+import { getSimpleProcessDefinitionList } from '#/api/bpm/definition';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '任务名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入任务名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'processDefinitionKey',
+ label: '所属流程',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择流程定义',
+ allowClear: true,
+ api: getSimpleProcessDefinitionList,
+ labelField: 'name',
+ valueField: 'key',
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '流程分类',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请输入流程分类',
+ allowClear: true,
+ api: getCategorySimpleList,
+ labelField: 'name',
+ valueField: 'code',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '审批状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.BPM_TASK_STATUS, 'number'),
+ placeholder: '请选择审批状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '发起时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'processInstance.name',
+ title: '流程',
+ minWidth: 200,
+ },
+ {
+ field: 'processInstance.summary',
+ title: '摘要',
+ minWidth: 200,
+ formatter: ({ cellValue }) => {
+ return cellValue && cellValue.length > 0
+ ? cellValue
+ .map((item: any) => `${item.key} : ${item.value}`)
+ .join('\n')
+ : '-';
+ },
+ },
+ {
+ field: 'processInstance.startUser.nickname',
+ title: '发起人',
+ minWidth: 120,
+ },
+ {
+ field: 'processInstance.createTime',
+ title: '发起时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'name',
+ title: '当前任务',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '任务开始时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '任务结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 180,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_TASK_STATUS },
+ },
+ },
+ {
+ field: 'reason',
+ title: '审批建议',
+ minWidth: 180,
+ },
+ {
+ field: 'durationInMillis',
+ title: '耗时',
+ minWidth: 180,
+ formatter: 'formatPast2',
+ },
+ {
+ field: 'processInstanceId',
+ title: '流程编号',
+ minWidth: 280,
+ },
+ {
+ field: 'id',
+ title: '任务编号',
+ minWidth: 280,
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/task/done/index.vue b/apps/web-antdv-next/src/views/bpm/task/done/index.vue
new file mode 100644
index 000000000..33811a2f2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/done/index.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/task/manager/data.ts b/apps/web-antdv-next/src/views/bpm/task/manager/data.ts
new file mode 100644
index 000000000..947982b23
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/manager/data.ts
@@ -0,0 +1,104 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '任务名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入任务名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'processInstance.name',
+ title: '流程',
+ minWidth: 200,
+ },
+ {
+ field: 'processInstance.startUser.nickname',
+ title: '发起人',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '当前任务',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '任务开始时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '任务结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'assigneeUser.nickname',
+ title: '审批人',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 180,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BPM_TASK_STATUS },
+ },
+ },
+ {
+ field: 'reason',
+ title: '审批建议',
+ minWidth: 180,
+ },
+ {
+ field: 'durationInMillis',
+ title: '耗时',
+ minWidth: 180,
+ formatter: 'formatPast2',
+ },
+ {
+ field: 'processInstanceId',
+ title: '流程编号',
+ minWidth: 280,
+ },
+ {
+ field: 'id',
+ title: '任务编号',
+ minWidth: 280,
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/task/manager/index.vue b/apps/web-antdv-next/src/views/bpm/task/manager/index.vue
new file mode 100644
index 000000000..829570b9a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/manager/index.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/bpm/task/todo/data.ts b/apps/web-antdv-next/src/views/bpm/task/todo/data.ts
new file mode 100644
index 000000000..0b1efd71c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/todo/data.ts
@@ -0,0 +1,131 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getCategorySimpleList } from '#/api/bpm/category';
+import { getSimpleProcessDefinitionList } from '#/api/bpm/definition';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '任务名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入任务名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'processDefinitionKey',
+ label: '所属流程',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择流程定义',
+ allowClear: true,
+ api: getSimpleProcessDefinitionList,
+ labelField: 'name',
+ valueField: 'key',
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '流程分类',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请输入流程分类',
+ allowClear: true,
+ api: getCategorySimpleList,
+ labelField: 'name',
+ valueField: 'code',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '流程状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ 'number',
+ ),
+ placeholder: '请选择流程状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '发起时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'processInstance.name',
+ title: '流程',
+ minWidth: 200,
+ },
+ {
+ field: 'processInstance.summary',
+ title: '摘要',
+ minWidth: 200,
+ formatter: ({ cellValue }) => {
+ return cellValue && cellValue.length > 0
+ ? cellValue
+ .map((item: any) => `${item.key} : ${item.value}`)
+ .join('\n')
+ : '-';
+ },
+ },
+ {
+ field: 'processInstance.startUser.nickname',
+ title: '发起人',
+ minWidth: 120,
+ },
+ {
+ field: 'processInstance.createTime',
+ title: '发起时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'name',
+ title: '当前任务',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '任务时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'processInstanceId',
+ title: '流程编号',
+ minWidth: 280,
+ },
+ {
+ field: 'id',
+ title: '任务编号',
+ minWidth: 280,
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/bpm/task/todo/index.vue b/apps/web-antdv-next/src/views/bpm/task/todo/index.vue
new file mode 100644
index 000000000..089e6662f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/bpm/task/todo/index.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/data.ts b/apps/web-antdv-next/src/views/crm/backlog/data.ts
new file mode 100644
index 000000000..d0b66d13e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/data.ts
@@ -0,0 +1,102 @@
+import type { Ref } from 'vue';
+
+export interface LeftSideItem {
+ name: string;
+ menu: string;
+ count: Ref;
+}
+
+/** 跟进状态 */
+export const FOLLOWUP_STATUS = [
+ { label: '待跟进', value: false },
+ { label: '已跟进', value: true },
+];
+
+/** 归属范围 */
+export const SCENE_TYPES = [
+ { label: '我负责的', value: 1 },
+ { label: '我参与的', value: 2 },
+ { label: '下属负责的', value: 3 },
+];
+
+/** 联系状态 */
+export const CONTACT_STATUS = [
+ { label: '今日需联系', value: 1 },
+ { label: '已逾期', value: 2 },
+ { label: '已联系', value: 3 },
+];
+
+/** 审批状态 */
+export const AUDIT_STATUS = [
+ { label: '待审批', value: 10 },
+ { label: '审核通过', value: 20 },
+ { label: '审核不通过', value: 30 },
+];
+
+/** 回款提醒类型 */
+export const RECEIVABLE_REMIND_TYPE = [
+ { label: '待回款', value: 1 },
+ { label: '已逾期', value: 2 },
+ { label: '已回款', value: 3 },
+];
+
+/** 合同过期状态 */
+export const CONTRACT_EXPIRY_TYPE = [
+ { label: '即将过期', value: 1 },
+ { label: '已过期', value: 2 },
+];
+
+/** 左侧菜单 */
+export const useLeftSides = (
+ customerTodayContactCount: Ref,
+ clueFollowCount: Ref,
+ customerFollowCount: Ref,
+ customerPutPoolRemindCount: Ref,
+ contractAuditCount: Ref,
+ contractRemindCount: Ref,
+ receivableAuditCount: Ref,
+ receivablePlanRemindCount: Ref,
+): LeftSideItem[] => {
+ return [
+ {
+ name: '今日需联系客户',
+ menu: 'customerTodayContact',
+ count: customerTodayContactCount,
+ },
+ {
+ name: '分配给我的线索',
+ menu: 'clueFollow',
+ count: clueFollowCount,
+ },
+ {
+ name: '分配给我的客户',
+ menu: 'customerFollow',
+ count: customerFollowCount,
+ },
+ {
+ name: '待进入公海的客户',
+ menu: 'customerPutPoolRemind',
+ count: customerPutPoolRemindCount,
+ },
+ {
+ name: '待审核合同',
+ menu: 'contractAudit',
+ count: contractAuditCount,
+ },
+ {
+ name: '待审核回款',
+ menu: 'receivableAudit',
+ count: receivableAuditCount,
+ },
+ {
+ name: '待回款提醒',
+ menu: 'receivablePlanRemind',
+ count: receivablePlanRemindCount,
+ },
+ {
+ name: '即将到期的合同',
+ menu: 'contractRemind',
+ count: contractRemindCount,
+ },
+ ];
+};
diff --git a/apps/web-antdv-next/src/views/crm/backlog/index.vue b/apps/web-antdv-next/src/views/crm/backlog/index.vue
new file mode 100644
index 000000000..b461f18a7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/index.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/clue-follow-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/clue-follow-list.vue
new file mode 100644
index 000000000..64776fd3f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/clue-follow-list.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/contract-audit-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/contract-audit-list.vue
new file mode 100644
index 000000000..98f944c66
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/contract-audit-list.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/contract-remind-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/contract-remind-list.vue
new file mode 100644
index 000000000..a394b3b6a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/contract-remind-list.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/customer-follow-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/customer-follow-list.vue
new file mode 100644
index 000000000..6e344d072
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/customer-follow-list.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/customer-put-pool-remind-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/customer-put-pool-remind-list.vue
new file mode 100644
index 000000000..fe7824574
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/customer-put-pool-remind-list.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/customer-today-contact-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/customer-today-contact-list.vue
new file mode 100644
index 000000000..03d0b25c3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/customer-today-contact-list.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/receivable-audit-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/receivable-audit-list.vue
new file mode 100644
index 000000000..83aeff594
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/receivable-audit-list.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/backlog/modules/receivable-plan-remind-list.vue b/apps/web-antdv-next/src/views/crm/backlog/modules/receivable-plan-remind-list.vue
new file mode 100644
index 000000000..4bafa4628
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/backlog/modules/receivable-plan-remind-list.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/components/data.ts b/apps/web-antdv-next/src/views/crm/business/components/data.ts
new file mode 100644
index 000000000..10ecb5a0f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/components/data.ts
@@ -0,0 +1,52 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+/** 商机关联列表列定义 */
+export function useBusinessDetailListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'name',
+ title: '商机名称',
+ fixed: 'left',
+ slots: { default: 'name' },
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ fixed: 'left',
+ slots: { default: 'customerName' },
+ },
+ {
+ field: 'totalPrice',
+ title: '商机金额(元)',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'dealTime',
+ title: '预计成交日期',
+ formatter: 'formatDate',
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ },
+ {
+ field: 'statusTypeName',
+ title: '商机状态组',
+ fixed: 'right',
+ },
+ {
+ field: 'statusName',
+ title: '商机阶段',
+ fixed: 'right',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/business/components/detail-list-modal.vue b/apps/web-antdv-next/src/views/crm/business/components/detail-list-modal.vue
new file mode 100644
index 000000000..5184a938f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/components/detail-list-modal.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/components/detail-list.vue b/apps/web-antdv-next/src/views/crm/business/components/detail-list.vue
new file mode 100644
index 000000000..712b55e82
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/components/detail-list.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/components/index.ts b/apps/web-antdv-next/src/views/crm/business/components/index.ts
new file mode 100644
index 000000000..a63de4701
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/components/index.ts
@@ -0,0 +1 @@
+export { default as BusinessDetailsList } from './detail-list.vue';
diff --git a/apps/web-antdv-next/src/views/crm/business/data.ts b/apps/web-antdv-next/src/views/crm/business/data.ts
new file mode 100644
index 000000000..ab71445ec
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/data.ts
@@ -0,0 +1,274 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { useUserStore } from '@vben/stores';
+import { erpPriceMultiply } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status';
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '商机名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入商机名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ allowClear: true,
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户名称',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.customerDefault,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'contactId',
+ label: '合同名称',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'statusTypeId',
+ label: '商机状态组',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getBusinessStatusTypeSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择商机状态组',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'dealTime',
+ label: '预计成交日期',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: false,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择预计成交日期',
+ },
+ },
+ {
+ fieldName: 'product',
+ label: '产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '请输入产品清单',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'totalProductPrice',
+ label: '产品总金额',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ disabled: true,
+ placeholder: '请输入产品总金额',
+ },
+ rules: z.number().min(0).optional().default(0),
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '整单折扣(%)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入整单折扣',
+ },
+ rules: z.number().min(0).max(100).optional().default(0),
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '折扣后金额',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ disabled: true,
+ placeholder: '请输入折扣后金额',
+ },
+ dependencies: {
+ triggerFields: ['totalProductPrice', 'discountPercent'],
+ trigger(values, form) {
+ const discountPrice =
+ erpPriceMultiply(
+ values.totalProductPrice,
+ values.discountPercent / 100,
+ ) ?? 0;
+ form.setFieldValue(
+ 'totalPrice',
+ values.totalProductPrice - discountPrice,
+ );
+ },
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '商机名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入商机名称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '商机名称',
+ fixed: 'left',
+ width: 160,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ fixed: 'left',
+ width: 120,
+ slots: { default: 'customerName' },
+ },
+ {
+ field: 'totalPrice',
+ title: '商机金额(元)',
+ width: 140,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'dealTime',
+ title: '预计成交日期',
+ formatter: 'formatDate',
+ width: 180,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ width: 200,
+ },
+ {
+ field: 'contactNextTime',
+ title: '下次联系时间',
+ formatter: 'formatDateTime',
+ width: 180,
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ width: 100,
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ width: 100,
+ },
+ {
+ field: 'contactLastTime',
+ title: '最后跟进时间',
+ formatter: 'formatDateTime',
+ width: 180,
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ formatter: 'formatDateTime',
+ width: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ width: 180,
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ width: 100,
+ },
+ {
+ field: 'statusTypeName',
+ title: '商机状态组',
+ fixed: 'right',
+ width: 140,
+ },
+ {
+ field: 'statusName',
+ title: '商机阶段',
+ fixed: 'right',
+ width: 120,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/business/detail/data.ts b/apps/web-antdv-next/src/views/crm/business/detail/data.ts
new file mode 100644
index 000000000..1cb1c4949
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/detail/data.ts
@@ -0,0 +1,141 @@
+import type { Ref } from 'vue';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { CrmBusinessApi } from '#/api/crm/business';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import {
+ DEFAULT_STATUSES,
+ getBusinessStatusSimpleList,
+} from '#/api/crm/business/status';
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'totalPrice',
+ label: '商机金额(元)',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'statusTypeName',
+ label: '商机组',
+ },
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
+
+/** 详情页的基础字段 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'name',
+ label: '商机名称',
+ },
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'totalPrice',
+ label: '商机金额(元)',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'dealTime',
+ label: '预计成交日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'contactNextTime',
+ label: '下次联系时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'statusTypeName',
+ label: '商机状态组',
+ },
+ {
+ field: 'statusName',
+ label: '商机阶段',
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ ];
+}
+
+/** 商机状态更新表单 */
+export function useStatusFormSchema(
+ formData: Ref,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'statusId',
+ label: '商机状态',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'endStatus',
+ label: '商机状态',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '商机阶段',
+ component: 'Select',
+ dependencies: {
+ triggerFields: [''],
+ async componentProps() {
+ const statusList = await getBusinessStatusSimpleList(
+ formData.value?.statusTypeId ?? 0,
+ );
+ const statusOptions = statusList.map((item) => ({
+ label: `${item.name}(赢单率:${item.percent}%)`,
+ value: item.id,
+ }));
+ const options = DEFAULT_STATUSES.map((item) => ({
+ label: `${item.name}(赢单率:${item.percent}%)`,
+ value: item.endStatus,
+ }));
+ statusOptions.push(...options);
+ return {
+ options: statusOptions,
+ };
+ },
+ },
+ rules: 'required',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/business/detail/index.vue b/apps/web-antdv-next/src/views/crm/business/detail/index.vue
new file mode 100644
index 000000000..92e5f44e2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/detail/index.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/business/detail/modules/info.vue
new file mode 100644
index 000000000..b025b0544
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/detail/modules/info.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/detail/modules/status-form.vue b/apps/web-antdv-next/src/views/crm/business/detail/modules/status-form.vue
new file mode 100644
index 000000000..fefb07e03
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/detail/modules/status-form.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/index.vue b/apps/web-antdv-next/src/views/crm/business/index.vue
new file mode 100644
index 000000000..235e7b41b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/index.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/modules/form.vue b/apps/web-antdv-next/src/views/crm/business/modules/form.vue
new file mode 100644
index 000000000..97d7493e0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/modules/form.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/status/data.ts b/apps/web-antdv-next/src/views/crm/business/status/data.ts
new file mode 100644
index 000000000..86f20d819
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/status/data.ts
@@ -0,0 +1,112 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { handleTree } from '@vben/utils';
+
+import { getDeptList } from '#/api/system/dept';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '状态组名',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入状态组名',
+ },
+ },
+ {
+ fieldName: 'deptIds',
+ label: '应用部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getDeptList();
+ return handleTree(data);
+ },
+ multiple: true,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择应用部门',
+ treeDefaultExpandAll: true,
+ },
+ help: '不选择部门时,默认全公司生效',
+ },
+ {
+ fieldName: 'statuses',
+ label: '阶段设置',
+ component: 'Input',
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '状态组名',
+ },
+ {
+ field: 'deptNames',
+ title: '应用部门',
+ formatter: ({ cellValue }) =>
+ cellValue?.length > 0 ? cellValue.join(' ') : '全公司',
+ },
+ {
+ field: 'creator',
+ title: '创建人',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 商机状态阶段列表列配置 */
+export function useFormColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'defaultStatus',
+ title: '阶段',
+ minWidth: 100,
+ slots: { default: 'defaultStatus' },
+ },
+ {
+ field: 'name',
+ title: '阶段名称',
+ minWidth: 100,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'percent',
+ title: '赢单率(%)',
+ minWidth: 100,
+ slots: { default: 'percent' },
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/business/status/index.vue b/apps/web-antdv-next/src/views/crm/business/status/index.vue
new file mode 100644
index 000000000..71dfecea8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/status/index.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/business/status/modules/form.vue b/apps/web-antdv-next/src/views/crm/business/status/modules/form.vue
new file mode 100644
index 000000000..54c146eb0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/business/status/modules/form.vue
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/clue/data.ts b/apps/web-antdv-next/src/views/crm/clue/data.ts
new file mode 100644
index 000000000..2cd328dde
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/clue/data.ts
@@ -0,0 +1,326 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+
+import { getAreaTree } from '#/api/system/area';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '线索名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入线索名称',
+ },
+ },
+ {
+ fieldName: 'source',
+ label: '客户来源',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
+ placeholder: '请选择客户来源',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择负责人',
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电话',
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ },
+ },
+ {
+ fieldName: 'wechat',
+ label: '微信',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入微信',
+ },
+ },
+ {
+ fieldName: 'qq',
+ label: 'QQ',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 QQ',
+ },
+ },
+ {
+ fieldName: 'industryId',
+ label: '客户行业',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
+ placeholder: '请选择客户行业',
+ },
+ },
+ {
+ fieldName: 'level',
+ label: '客户级别',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
+ placeholder: '请选择客户级别',
+ },
+ },
+ {
+ fieldName: 'areaId',
+ label: '地址',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: getAreaTree,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择地址',
+ },
+ },
+ {
+ fieldName: 'detailAddress',
+ label: '详细地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入详细地址',
+ },
+ },
+ {
+ fieldName: 'contactNextTime',
+ label: '下次联系时间',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择下次联系时间',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '线索名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入线索名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'transformStatus',
+ label: '转化状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '未转化', value: false },
+ { label: '已转化', value: true },
+ ],
+ placeholder: '请选择转化状态',
+ allowClear: true,
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电话',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ placeholder: ['开始日期', '结束日期'],
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '线索名称',
+ fixed: 'left',
+ minWidth: 160,
+ slots: {
+ default: 'name',
+ },
+ },
+ {
+ field: 'source',
+ title: '线索来源',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
+ },
+ },
+ {
+ field: 'mobile',
+ title: '手机',
+ minWidth: 120,
+ },
+ {
+ field: 'telephone',
+ title: '电话',
+ minWidth: 130,
+ },
+ {
+ field: 'email',
+ title: '邮箱',
+ minWidth: 180,
+ },
+ {
+ field: 'detailAddress',
+ title: '地址',
+ minWidth: 180,
+ },
+ {
+ field: 'industryId',
+ title: '客户行业',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
+ },
+ },
+ {
+ field: 'level',
+ title: '客户级别',
+ minWidth: 135,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
+ },
+ },
+ {
+ field: 'contactNextTime',
+ title: '下次联系时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'contactLastTime',
+ title: '最后跟进时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'contactLastContent',
+ title: '最后跟进记录',
+ minWidth: 200,
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 100,
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ minWidth: 100,
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 140,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/clue/detail/data.ts b/apps/web-antdv-next/src/views/crm/clue/detail/data.ts
new file mode 100644
index 000000000..9536eb79b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/clue/detail/data.ts
@@ -0,0 +1,111 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情头部的配置 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'source',
+ label: '线索来源',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
+ value: val,
+ }),
+ },
+ {
+ field: 'mobile',
+ label: '手机',
+ },
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
+
+/** 详情基本信息的配置 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'name',
+ label: '线索名称',
+ },
+ {
+ field: 'source',
+ label: '客户来源',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
+ value: val,
+ }),
+ },
+ {
+ field: 'mobile',
+ label: '手机',
+ },
+ {
+ field: 'telephone',
+ label: '电话',
+ },
+ {
+ field: 'email',
+ label: '邮箱',
+ },
+ {
+ field: 'areaName',
+ label: '地址',
+ render: (val, data) => {
+ const areaName = val ?? '';
+ const detailAddress = data?.detailAddress ?? '';
+ return [areaName, detailAddress].filter((item) => !!item).join(' ');
+ },
+ },
+ {
+ field: 'qq',
+ label: 'QQ',
+ },
+ {
+ field: 'wechat',
+ label: '微信',
+ },
+ {
+ field: 'industryId',
+ label: '客户行业',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
+ value: val,
+ }),
+ },
+ {
+ field: 'level',
+ label: '客户级别',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_CUSTOMER_LEVEL,
+ value: val,
+ }),
+ },
+ {
+ field: 'contactNextTime',
+ label: '下次联系时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/clue/detail/index.vue b/apps/web-antdv-next/src/views/crm/clue/detail/index.vue
new file mode 100644
index 000000000..02c356400
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/clue/detail/index.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/clue/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/clue/detail/modules/info.vue
new file mode 100644
index 000000000..08ef91fc8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/clue/detail/modules/info.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/clue/index.vue b/apps/web-antdv-next/src/views/crm/clue/index.vue
new file mode 100644
index 000000000..2b96fd237
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/clue/index.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/clue/modules/form.vue b/apps/web-antdv-next/src/views/crm/clue/modules/form.vue
new file mode 100644
index 000000000..f1a9de6ba
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/clue/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contact/components/data.ts b/apps/web-antdv-next/src/views/crm/contact/components/data.ts
new file mode 100644
index 000000000..b2e63f76e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/components/data.ts
@@ -0,0 +1,62 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 联系人明细列表列配置 */
+export function useDetailListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'name',
+ title: '姓名',
+ fixed: 'left',
+ slots: { default: 'name' },
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ fixed: 'left',
+ slots: { default: 'customerName' },
+ },
+ {
+ field: 'sex',
+ title: '性别',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_USER_SEX },
+ },
+ },
+ {
+ field: 'mobile',
+ title: '手机',
+ },
+ {
+ field: 'telephone',
+ title: '电话',
+ },
+ {
+ field: 'email',
+ title: '邮箱',
+ },
+ {
+ field: 'post',
+ title: '职位',
+ },
+ {
+ field: 'detailAddress',
+ title: '地址',
+ },
+ {
+ field: 'master',
+ title: '关键决策人',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/contact/components/detail-list-modal.vue b/apps/web-antdv-next/src/views/crm/contact/components/detail-list-modal.vue
new file mode 100644
index 000000000..4214096a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/components/detail-list-modal.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contact/components/detail-list.vue b/apps/web-antdv-next/src/views/crm/contact/components/detail-list.vue
new file mode 100644
index 000000000..832f80473
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/components/detail-list.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contact/components/index.ts b/apps/web-antdv-next/src/views/crm/contact/components/index.ts
new file mode 100644
index 000000000..d16cd533d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/components/index.ts
@@ -0,0 +1 @@
+export { default as ContactDetailsList } from './detail-list.vue';
diff --git a/apps/web-antdv-next/src/views/crm/contact/data.ts b/apps/web-antdv-next/src/views/crm/contact/data.ts
new file mode 100644
index 000000000..61a68c44c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/data.ts
@@ -0,0 +1,367 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+
+import { getSimpleContactList } from '#/api/crm/contact';
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import { getAreaTree } from '#/api/system/area';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '联系人姓名',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入联系人姓名',
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户名称',
+ component: 'ApiSelect',
+ rules: 'required',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电话',
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ },
+ },
+ {
+ fieldName: 'wechat',
+ label: '微信',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入微信',
+ },
+ },
+ {
+ fieldName: 'qq',
+ label: 'QQ',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入QQ',
+ },
+ },
+ {
+ fieldName: 'post',
+ label: '职位',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入职位',
+ },
+ },
+ {
+ fieldName: 'master',
+ label: '关键决策人',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ placeholder: '请选择是否关键决策人',
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ placeholder: '请选择性别',
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '直属上级',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleContactList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择直属上级',
+ },
+ },
+ {
+ fieldName: 'areaId',
+ label: '地址',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: getAreaTree,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择地址',
+ },
+ },
+ {
+ fieldName: 'detailAddress',
+ label: '详细地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入详细地址',
+ },
+ },
+ {
+ fieldName: 'contactNextTime',
+ label: '下次联系时间',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择下次联系时间',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '姓名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系人姓名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电话',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'wechat',
+ label: '微信',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入微信',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '电子邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电子邮箱',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '联系人姓名',
+ fixed: 'left',
+ minWidth: 240,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ fixed: 'left',
+ minWidth: 240,
+ slots: { default: 'customerName' },
+ },
+ {
+ field: 'mobile',
+ title: '手机',
+ minWidth: 120,
+ },
+ {
+ field: 'telephone',
+ title: '电话',
+ minWidth: 130,
+ },
+ {
+ field: 'email',
+ title: '邮箱',
+ minWidth: 180,
+ },
+ {
+ field: 'post',
+ title: '职位',
+ minWidth: 120,
+ },
+ {
+ field: 'areaName',
+ title: '地址',
+ minWidth: 120,
+ },
+ {
+ field: 'detailAddress',
+ title: '详细地址',
+ minWidth: 180,
+ },
+ {
+ field: 'master',
+ title: '关键决策人',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'parentId',
+ title: '直属上级',
+ minWidth: 120,
+ slots: { default: 'parentId' },
+ },
+ {
+ field: 'contactNextTime',
+ title: '下次联系时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'sex',
+ title: '性别',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_USER_SEX },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'contactLastTime',
+ title: '最后跟进时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 120,
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ minWidth: 120,
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/contact/detail/data.ts b/apps/web-antdv-next/src/views/crm/contact/detail/data.ts
new file mode 100644
index 000000000..3f1019325
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/detail/data.ts
@@ -0,0 +1,106 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情页的基础字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'post',
+ label: '职务',
+ },
+ {
+ field: 'mobile',
+ label: '手机',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
+
+/** 详情页的基础字段 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'name',
+ label: '姓名',
+ },
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'mobile',
+ label: '手机',
+ },
+ {
+ field: 'telephone',
+ label: '电话',
+ },
+ {
+ field: 'email',
+ label: '邮箱',
+ },
+ {
+ field: 'qq',
+ label: 'QQ',
+ },
+ {
+ field: 'wechat',
+ label: '微信',
+ },
+ {
+ field: 'areaName',
+ label: '地址',
+ render: (val, data) => {
+ const areaName = val ?? '';
+ const detailAddress = data?.detailAddress ?? '';
+ return [areaName, detailAddress].filter((item) => !!item).join(' ');
+ },
+ },
+ {
+ field: 'post',
+ label: '职务',
+ },
+ {
+ field: 'parentName',
+ label: '直属上级',
+ },
+ {
+ field: 'master',
+ label: '关键决策人',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.INFRA_BOOLEAN_STRING,
+ value: val,
+ }),
+ },
+ {
+ field: 'sex',
+ label: '性别',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.SYSTEM_USER_SEX, value: val }),
+ },
+ {
+ field: 'contactNextTime',
+ label: '下次联系时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/contact/detail/index.vue b/apps/web-antdv-next/src/views/crm/contact/detail/index.vue
new file mode 100644
index 000000000..1513e6e2c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/detail/index.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contact/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/contact/detail/modules/info.vue
new file mode 100644
index 000000000..9050e4cee
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/detail/modules/info.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contact/index.vue b/apps/web-antdv-next/src/views/crm/contact/index.vue
new file mode 100644
index 000000000..1d2e03ad5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/index.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contact/modules/form.vue b/apps/web-antdv-next/src/views/crm/contact/modules/form.vue
new file mode 100644
index 000000000..a70060cf0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contact/modules/form.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contract/components/data.ts b/apps/web-antdv-next/src/views/crm/contract/components/data.ts
new file mode 100644
index 000000000..c314faa99
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/components/data.ts
@@ -0,0 +1,92 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+export function useDetailListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '合同编号',
+ field: 'no',
+ minWidth: 150,
+ fixed: 'left',
+ },
+ {
+ title: '合同名称',
+ field: 'name',
+ minWidth: 150,
+ fixed: 'left',
+ slots: { default: 'name' },
+ },
+ {
+ title: '合同金额(元)',
+ field: 'totalPrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '合同开始时间',
+ field: 'startTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '合同结束时间',
+ field: 'endTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '已回款金额(元)',
+ field: 'totalReceivablePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '未回款金额(元)',
+ field: 'unpaidPrice',
+ minWidth: 150,
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(
+ row.totalPrice - row.totalReceivablePrice,
+ );
+ },
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 150,
+ },
+ {
+ title: '所属部门',
+ field: 'ownerUserDeptName',
+ minWidth: 150,
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建人',
+ field: 'creatorName',
+ minWidth: 150,
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '合同状态',
+ field: 'auditStatus',
+ fixed: 'right',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/contract/components/detail-list.vue b/apps/web-antdv-next/src/views/crm/contract/components/detail-list.vue
new file mode 100644
index 000000000..69770c802
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/components/detail-list.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contract/components/index.ts b/apps/web-antdv-next/src/views/crm/contract/components/index.ts
new file mode 100644
index 000000000..d9f61793c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/components/index.ts
@@ -0,0 +1 @@
+export { default as ContractDetailsList } from './detail-list.vue';
diff --git a/apps/web-antdv-next/src/views/crm/contract/config/data.ts b/apps/web-antdv-next/src/views/crm/contract/config/data.ts
new file mode 100644
index 000000000..2792b3601
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/config/data.ts
@@ -0,0 +1,37 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+export const schema: VbenFormSchema[] = [
+ {
+ component: 'RadioGroup',
+ fieldName: 'notifyEnabled',
+ label: '提前提醒设置',
+ componentProps: {
+ options: [
+ { label: '提醒', value: true },
+ { label: '不提醒', value: false },
+ ],
+ },
+ defaultValue: true,
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'notifyDays',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ },
+ renderComponentContent: () => ({
+ addonBefore: () => '提前',
+ addonAfter: () => '天提醒',
+ }),
+ dependencies: {
+ triggerFields: ['notifyEnabled'],
+ show: (values) => values.notifyEnabled,
+ trigger(values) {
+ if (!values.notifyEnabled) {
+ values.notifyDays = undefined;
+ }
+ },
+ },
+ },
+];
diff --git a/apps/web-antdv-next/src/views/crm/contract/config/index.vue b/apps/web-antdv-next/src/views/crm/contract/config/index.vue
new file mode 100644
index 000000000..e88af4b69
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/config/index.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contract/data.ts b/apps/web-antdv-next/src/views/crm/contract/data.ts
new file mode 100644
index 000000000..4289c6d1d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/data.ts
@@ -0,0 +1,415 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { useUserStore } from '@vben/stores';
+import { erpPriceInputFormatter, erpPriceMultiply } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getSimpleBusinessList } from '#/api/crm/business';
+import { getSimpleContactList } from '#/api/crm/contact';
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '合同编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '保存时自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '合同名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入合同名称',
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户名称',
+ component: 'ApiSelect',
+ rules: 'required',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ },
+ },
+ {
+ fieldName: 'businessId',
+ label: '商机名称',
+ component: 'Select',
+ componentProps: {
+ options: [],
+ placeholder: '请选择商机',
+ },
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId,
+ async componentProps(values) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '请选择客户',
+ };
+ }
+ const res = await getSimpleBusinessList();
+ const list = res.filter(
+ (item) => item.customerId === values.customerId,
+ );
+ return {
+ options: list.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择商机',
+ };
+ },
+ },
+ },
+ {
+ fieldName: 'orderDate',
+ label: '下单日期',
+ component: 'DatePicker',
+ rules: 'required',
+ componentProps: {
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ placeholder: '请选择下单日期',
+ },
+ },
+ {
+ fieldName: 'startTime',
+ label: '合同开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ placeholder: '请选择合同开始时间',
+ },
+ },
+ {
+ fieldName: 'endTime',
+ label: '合同结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ placeholder: '请选择合同结束时间',
+ },
+ },
+ {
+ fieldName: 'signUserId',
+ label: '公司签约人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ fieldName: 'signContactId',
+ label: '客户签约人',
+ component: 'Select',
+ componentProps: {
+ options: [],
+ placeholder: '请选择客户签约人',
+ },
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId,
+ async componentProps(values) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '请选择客户',
+ };
+ }
+ const res = await getSimpleContactList();
+ const list = res.filter(
+ (item) => item.customerId === values.customerId,
+ );
+ return {
+ options: list.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择客户签约人',
+ };
+ },
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ },
+ {
+ fieldName: 'product',
+ label: '产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'totalProductPrice',
+ label: '产品总金额',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入产品总金额',
+ },
+ rules: z.number().min(0).optional().default(0),
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '整单折扣(%)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入整单折扣',
+ },
+ rules: z.number().min(0).max(100).optional().default(0),
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '折扣后金额',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalProductPrice', 'discountPercent'],
+ trigger(values, form) {
+ const discountPrice =
+ erpPriceMultiply(
+ values.totalProductPrice,
+ values.discountPercent / 100,
+ ) ?? 0;
+ form.setFieldValue(
+ 'totalPrice',
+ values.totalProductPrice - discountPrice,
+ );
+ },
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '合同编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入合同编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '合同名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入合同名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '合同编号',
+ field: 'no',
+ minWidth: 180,
+ fixed: 'left',
+ },
+ {
+ title: '合同名称',
+ field: 'name',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'name' },
+ },
+ {
+ title: '客户名称',
+ field: 'customerName',
+ minWidth: 120,
+ slots: { default: 'customerName' },
+ },
+ {
+ title: '商机名称',
+ field: 'businessName',
+ minWidth: 130,
+ slots: { default: 'businessName' },
+ },
+ {
+ title: '合同金额(元)',
+ field: 'totalPrice',
+ minWidth: 140,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '下单时间',
+ field: 'orderDate',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '合同开始时间',
+ field: 'startTime',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '合同结束时间',
+ field: 'endTime',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '客户签约人',
+ field: 'signContactName',
+ minWidth: 130,
+ slots: { default: 'signContactName' },
+ },
+ {
+ title: '公司签约人',
+ field: 'signUserName',
+ minWidth: 130,
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 200,
+ },
+ {
+ title: '已回款金额(元)',
+ field: 'totalReceivablePrice',
+ minWidth: 140,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '未回款金额(元)',
+ field: 'unReceivablePrice',
+ minWidth: 140,
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(
+ row.totalPrice - row.totalReceivablePrice,
+ );
+ },
+ },
+ {
+ title: '最后跟进时间',
+ field: 'contactLastTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 120,
+ },
+ {
+ title: '所属部门',
+ field: 'ownerUserDeptName',
+ minWidth: 100,
+ },
+ {
+ title: '更新时间',
+ field: 'updateTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建人',
+ field: 'creatorName',
+ minWidth: 120,
+ },
+ {
+ title: '合同状态',
+ field: 'auditStatus',
+ fixed: 'right',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ fixed: 'right',
+ minWidth: 130,
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/contract/detail/data.ts b/apps/web-antdv-next/src/views/crm/contract/detail/data.ts
new file mode 100644
index 000000000..bfe7b5606
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/detail/data.ts
@@ -0,0 +1,100 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情头部的配置 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'totalPrice',
+ label: '合同金额(元)',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'orderDate',
+ label: '下单时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'totalReceivablePrice',
+ label: '回款金额(元)',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ ];
+}
+
+/** 详情基本信息的配置 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'no',
+ label: '合同编号',
+ },
+ {
+ field: 'name',
+ label: '合同名称',
+ },
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'businessName',
+ label: '商机名称',
+ },
+ {
+ field: 'totalPrice',
+ label: '合同金额(元)',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'orderDate',
+ label: '下单时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'startTime',
+ label: '合同开始时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'endTime',
+ label: '合同结束时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'signContactName',
+ label: '客户签约人',
+ },
+ {
+ field: 'signUserName',
+ label: '公司签约人',
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ {
+ field: 'auditStatus',
+ label: '合同状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_AUDIT_STATUS,
+ value: val,
+ }),
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/contract/detail/index.vue b/apps/web-antdv-next/src/views/crm/contract/detail/index.vue
new file mode 100644
index 000000000..fca3dd725
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/detail/index.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contract/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/contract/detail/modules/info.vue
new file mode 100644
index 000000000..da23e2323
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/detail/modules/info.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contract/index.vue b/apps/web-antdv-next/src/views/crm/contract/index.vue
new file mode 100644
index 000000000..7fecd90b2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/index.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/contract/modules/form.vue b/apps/web-antdv-next/src/views/crm/contract/modules/form.vue
new file mode 100644
index 000000000..f244dea23
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/contract/modules/form.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/data.ts b/apps/web-antdv-next/src/views/crm/customer/data.ts
new file mode 100644
index 000000000..baabb5685
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/data.ts
@@ -0,0 +1,395 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { z } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+
+import { getAreaTree } from '#/api/system/area';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '客户名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入客户名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'source',
+ label: '客户来源',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
+ placeholder: '请选择客户来源',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ allowClear: true,
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ },
+ {
+ fieldName: 'telephone',
+ label: '电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电话',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'wechat',
+ label: '微信',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入微信',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'qq',
+ label: 'QQ',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入QQ',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'industryId',
+ label: '客户行业',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
+ placeholder: '请选择客户行业',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'level',
+ label: '客户级别',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
+ placeholder: '请选择客户级别',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'areaId',
+ label: '地址',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: getAreaTree,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择地址',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'detailAddress',
+ label: '详细地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入详细地址',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'contactNextTime',
+ label: '下次联系时间',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择下次联系时间',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '客户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电话',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ placeholder: ['开始日期', '结束日期'],
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 导入客户的表单 */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ allowClear: true,
+ class: 'w-full',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'file',
+ label: '客户数据',
+ component: 'Upload',
+ rules: 'required',
+ help: '仅允许导入 xls、xlsx 格式文件',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '是否覆盖',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '是',
+ unCheckedChildren: '否',
+ },
+ rules: z.boolean().default(false),
+ help: '是否更新已经存在的客户数据',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '客户名称',
+ fixed: 'left',
+ minWidth: 160,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'source',
+ title: '客户来源',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
+ },
+ },
+ {
+ field: 'mobile',
+ title: '手机',
+ minWidth: 120,
+ },
+ {
+ field: 'telephone',
+ title: '电话',
+ minWidth: 130,
+ },
+ {
+ field: 'email',
+ title: '邮箱',
+ minWidth: 180,
+ },
+ {
+ field: 'level',
+ title: '客户级别',
+ minWidth: 135,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
+ },
+ },
+ {
+ field: 'industryId',
+ title: '客户行业',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
+ },
+ },
+ {
+ field: 'contactNextTime',
+ title: '下次联系时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'lockStatus',
+ title: '锁定状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'dealStatus',
+ title: '成交状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'contactLastTime',
+ title: '最后跟进时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'contactLastContent',
+ title: '最后跟进记录',
+ minWidth: 200,
+ },
+ {
+ field: 'detailAddress',
+ title: '地址',
+ minWidth: 180,
+ },
+ {
+ field: 'poolDay',
+ title: '距离进入公海天数',
+ minWidth: 140,
+ formatter: ({ cellValue }) =>
+ cellValue === null ? '-' : `${cellValue} 天`,
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 100,
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ minWidth: 100,
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 100,
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/customer/detail/data.ts b/apps/web-antdv-next/src/views/crm/customer/detail/data.ts
new file mode 100644
index 000000000..9d55b4077
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/detail/data.ts
@@ -0,0 +1,130 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { useUserStore } from '@vben/stores';
+import { formatDateTime } from '@vben/utils';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { DictTag } from '#/components/dict-tag';
+
+/** 分配客户表单 */
+export function useDistributeFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'level',
+ label: '客户级别',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: val }),
+ },
+ {
+ field: 'dealStatus',
+ label: '成交状态',
+ render: (val) => (val ? '已成交' : '未成交'),
+ },
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
+
+/** 详情页的基础字段 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'name',
+ label: '客户名称',
+ },
+ {
+ field: 'source',
+ label: '客户来源',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
+ value: val,
+ }),
+ },
+ {
+ field: 'mobile',
+ label: '手机',
+ },
+ {
+ field: 'telephone',
+ label: '电话',
+ },
+ {
+ field: 'email',
+ label: '邮箱',
+ },
+ {
+ field: 'areaName',
+ label: '地址',
+ render: (val, data) => {
+ const areaName = val ?? '';
+ const detailAddress = data?.detailAddress ?? '';
+ return [areaName, detailAddress].filter(Boolean).join(' ');
+ },
+ },
+ {
+ field: 'qq',
+ label: 'QQ',
+ },
+ {
+ field: 'wechat',
+ label: '微信',
+ },
+ {
+ field: 'industryId',
+ label: '客户行业',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
+ value: val,
+ }),
+ },
+ {
+ field: 'contactNextTime',
+ label: '下次联系时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/customer/detail/index.vue b/apps/web-antdv-next/src/views/crm/customer/detail/index.vue
new file mode 100644
index 000000000..925d1a5ac
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/detail/index.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/detail/modules/distribute-form.vue b/apps/web-antdv-next/src/views/crm/customer/detail/modules/distribute-form.vue
new file mode 100644
index 000000000..53787ca78
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/detail/modules/distribute-form.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/customer/detail/modules/info.vue
new file mode 100644
index 000000000..c98491177
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/detail/modules/info.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/index.vue b/apps/web-antdv-next/src/views/crm/customer/index.vue
new file mode 100644
index 000000000..ece7a224c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/index.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/limitConfig/data.ts b/apps/web-antdv-next/src/views/crm/customer/limitConfig/data.ts
new file mode 100644
index 000000000..914116b16
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/limitConfig/data.ts
@@ -0,0 +1,154 @@
+import type { VbenFormSchema } from '@vben/common-ui';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { LimitConfType } from '#/api/crm/customer/limitConfig';
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'type',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'userIds',
+ label: '规则适用人群',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ mode: 'multiple',
+ allowClear: true,
+ placeholder: '请选择规则适用人群',
+ },
+ },
+ {
+ fieldName: 'deptIds',
+ label: '规则适用部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ multiple: true,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择规则适用部门',
+ treeDefaultExpandAll: true,
+ },
+ },
+ {
+ fieldName: 'maxCount',
+ label:
+ confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT
+ ? '拥有客户数上限'
+ : '锁定客户数上限',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: `请输入${
+ LimitConfType.CUSTOMER_QUANTITY_LIMIT === confType
+ ? '拥有客户数上限'
+ : '锁定客户数上限'
+ }`,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'dealCountEnabled',
+ label: '成交客户是否占用拥有客户数',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ dependencies: {
+ triggerFields: [''],
+ show: () => confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT,
+ },
+ defaultValue: false,
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ confType: LimitConfType,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ fixed: 'left',
+ },
+ {
+ field: 'users',
+ title: '规则适用人群',
+ formatter: ({ cellValue }) => {
+ return cellValue
+ .map((user: any) => {
+ return user.nickname;
+ })
+ .join(',');
+ },
+ },
+ {
+ field: 'depts',
+ title: '规则适用部门',
+ formatter: ({ cellValue }) => {
+ return cellValue
+ .map((dept: any) => {
+ return dept.name;
+ })
+ .join(',');
+ },
+ },
+ {
+ field: 'maxCount',
+ title:
+ confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT
+ ? '拥有客户数上限'
+ : '锁定客户数上限',
+ },
+ {
+ field: 'dealCountEnabled',
+ title: '成交客户是否占用拥有客户数',
+ visible: confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/customer/limitConfig/index.vue b/apps/web-antdv-next/src/views/crm/customer/limitConfig/index.vue
new file mode 100644
index 000000000..e2f3ec545
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/limitConfig/index.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/limitConfig/modules/form.vue b/apps/web-antdv-next/src/views/crm/customer/limitConfig/modules/form.vue
new file mode 100644
index 000000000..392a85273
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/limitConfig/modules/form.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/modules/form.vue b/apps/web-antdv-next/src/views/crm/customer/modules/form.vue
new file mode 100644
index 000000000..fd0799276
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/modules/form.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/modules/import-form.vue b/apps/web-antdv-next/src/views/crm/customer/modules/import-form.vue
new file mode 100644
index 000000000..a943b4b51
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/modules/import-form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/pool/data.ts b/apps/web-antdv-next/src/views/crm/customer/pool/data.ts
new file mode 100644
index 000000000..d057b67ed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/pool/data.ts
@@ -0,0 +1,161 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '客户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'industryId',
+ label: '所属行业',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, 'number'),
+ placeholder: '请选择所属行业',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'level',
+ label: '客户级别',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL, 'number'),
+ placeholder: '请选择客户级别',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'source',
+ label: '客户来源',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE, 'number'),
+ placeholder: '请选择客户来源',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '客户名称',
+ field: 'name',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'name' },
+ },
+ {
+ title: '客户来源',
+ field: 'source',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
+ },
+ },
+ {
+ title: '手机',
+ field: 'mobile',
+ minWidth: 120,
+ },
+ {
+ title: '电话',
+ field: 'telephone',
+ minWidth: 120,
+ },
+ {
+ title: '邮箱',
+ field: 'email',
+ minWidth: 140,
+ },
+ {
+ title: '客户级别',
+ field: 'level',
+ minWidth: 135,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
+ },
+ },
+ {
+ title: '客户行业',
+ field: 'industryId',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
+ },
+ },
+ {
+ title: '下次联系时间',
+ field: 'contactNextTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 200,
+ },
+ {
+ title: '成交状态',
+ field: 'dealStatus',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ title: '最后跟进时间',
+ field: 'contactLastTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '最后跟进记录',
+ field: 'contactLastContent',
+ minWidth: 200,
+ },
+ {
+ title: '更新时间',
+ field: 'updateTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建人',
+ field: 'creatorName',
+ minWidth: 100,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/customer/pool/index.vue b/apps/web-antdv-next/src/views/crm/customer/pool/index.vue
new file mode 100644
index 000000000..27ac45840
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/pool/index.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/customer/poolConfig/data.ts b/apps/web-antdv-next/src/views/crm/customer/poolConfig/data.ts
new file mode 100644
index 000000000..86fd9f82f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/poolConfig/data.ts
@@ -0,0 +1,78 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+export const schema: VbenFormSchema[] = [
+ {
+ component: 'RadioGroup',
+ fieldName: 'enabled',
+ label: '客户公海规则设置',
+ componentProps: {
+ options: [
+ { label: '开启', value: true },
+ { label: '关闭', value: false },
+ ],
+ },
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'contactExpireDays',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ },
+ renderComponentContent: () => ({
+ addonAfter: () => '天不跟进或',
+ }),
+ dependencies: {
+ triggerFields: ['enabled'],
+ show: (value) => value.enabled,
+ },
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'dealExpireDays',
+ renderComponentContent: () => ({
+ addonBefore: () => '或',
+ addonAfter: () => '天未成交',
+ }),
+ componentProps: {
+ min: 0,
+ precision: 0,
+ },
+ dependencies: {
+ triggerFields: ['enabled'],
+ show: (value) => value.enabled,
+ },
+ },
+ {
+ component: 'RadioGroup',
+ fieldName: 'notifyEnabled',
+ label: '提前提醒设置',
+ componentProps: {
+ options: [
+ { label: '开启', value: true },
+ { label: '关闭', value: false },
+ ],
+ },
+ dependencies: {
+ triggerFields: ['enabled'],
+ show: (value) => value.enabled,
+ },
+ defaultValue: false,
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'notifyDays',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ },
+ renderComponentContent: () => ({
+ addonBefore: () => '提前',
+ addonAfter: () => '天提醒',
+ }),
+ dependencies: {
+ triggerFields: ['notifyEnabled'],
+ show: (value) => value.enabled && value.notifyEnabled,
+ },
+ },
+];
diff --git a/apps/web-antdv-next/src/views/crm/customer/poolConfig/index.vue b/apps/web-antdv-next/src/views/crm/customer/poolConfig/index.vue
new file mode 100644
index 000000000..50b33be1a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/customer/poolConfig/index.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/followup/data.ts b/apps/web-antdv-next/src/views/crm/followup/data.ts
new file mode 100644
index 000000000..1cf26690c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/followup/data.ts
@@ -0,0 +1,194 @@
+import type { Ref } from 'vue';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { getBusinessPageByCustomer } from '#/api/crm/business';
+import { getContactPageByCustomer } from '#/api/crm/contact';
+import { BizTypeEnum } from '#/api/crm/permission';
+
+/** 新增/修改的表单 */
+export function useFormSchema(
+ bizId: Ref,
+): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'bizId',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'bizType',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '跟进类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_FOLLOW_UP_TYPE, 'number'),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'nextTime',
+ label: '下次联系时间',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择下次联系时间',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'content',
+ label: '跟进内容',
+ component: 'Textarea',
+ rules: 'required',
+ },
+ {
+ fieldName: 'picUrls',
+ label: '图片',
+ component: 'ImageUpload',
+ },
+ {
+ fieldName: 'fileUrls',
+ label: '附件',
+ component: 'FileUpload',
+ },
+ {
+ fieldName: 'contactIds',
+ label: '关联联系人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: async () => {
+ if (!bizId.value) {
+ return [];
+ }
+ const res = await getContactPageByCustomer({
+ pageNo: 1,
+ pageSize: 100,
+ customerId: bizId.value,
+ });
+ return res.list;
+ },
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ },
+ },
+ {
+ fieldName: 'businessIds',
+ label: '关联商机',
+ component: 'ApiSelect',
+ componentProps: {
+ api: async () => {
+ if (!bizId.value) {
+ return [];
+ }
+ const res = await getBusinessPageByCustomer({
+ pageNo: 1,
+ pageSize: 100,
+ customerId: bizId.value,
+ });
+ return res.list;
+ },
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ bizType: number,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ },
+ { field: 'creatorName', title: '跟进人' },
+ {
+ field: 'type',
+ title: '跟进类型',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_FOLLOW_UP_TYPE },
+ },
+ },
+ { field: 'content', title: '跟进内容' },
+ {
+ field: 'nextTime',
+ title: '下次联系时间',
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'contacts',
+ title: '关联联系人',
+ visible: bizType === BizTypeEnum.CRM_CUSTOMER,
+ slots: { default: 'contacts' },
+ },
+ {
+ field: 'businesses',
+ title: '关联商机',
+ visible: bizType === BizTypeEnum.CRM_CUSTOMER,
+ slots: { default: 'businesses' },
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的系统字段 */
+export function useFollowUpDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ {
+ field: 'contactLastContent',
+ label: '最后跟进记录',
+ },
+ {
+ field: 'contactLastTime',
+ label: '最后跟进时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'creatorName',
+ label: '创建人',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '更新时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/followup/index.ts b/apps/web-antdv-next/src/views/crm/followup/index.ts
new file mode 100644
index 000000000..911cbed51
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/followup/index.ts
@@ -0,0 +1 @@
+export { default as FollowUp } from './index.vue';
diff --git a/apps/web-antdv-next/src/views/crm/followup/index.vue b/apps/web-antdv-next/src/views/crm/followup/index.vue
new file mode 100644
index 000000000..1467db44c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/followup/index.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/followup/modules/form.vue b/apps/web-antdv-next/src/views/crm/followup/modules/form.vue
new file mode 100644
index 000000000..b373555b6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/followup/modules/form.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/permission/index.ts b/apps/web-antdv-next/src/views/crm/permission/index.ts
new file mode 100644
index 000000000..2988df4dd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/permission/index.ts
@@ -0,0 +1,2 @@
+export { default as PermissionList } from './modules/list.vue';
+export { default as TransferForm } from './modules/transfer-form.vue';
diff --git a/apps/web-antdv-next/src/views/crm/permission/modules/data.ts b/apps/web-antdv-next/src/views/crm/permission/modules/data.ts
new file mode 100644
index 000000000..fdbe9bcbd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/permission/modules/data.ts
@@ -0,0 +1,233 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { BizTypeEnum, PermissionLevelEnum } from '#/api/crm/permission';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useTransferFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'newOwnerUserId',
+ label: '选择新负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'oldOwnerHandler',
+ label: '老负责人',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ {
+ label: '加入团队',
+ value: true,
+ },
+ {
+ label: '移除',
+ value: false,
+ },
+ ],
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'oldOwnerPermissionLevel',
+ label: '老负责人权限级别',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.CRM_PERMISSION_LEVEL,
+ 'number',
+ ).filter((dict) => dict.value !== PermissionLevelEnum.OWNER),
+ },
+ dependencies: {
+ triggerFields: ['oldOwnerHandler'],
+ show: (values) => values.oldOwnerHandler,
+ trigger(values) {
+ if (!values.oldOwnerHandler) {
+ values.oldOwnerPermissionLevel = undefined;
+ }
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'toBizTypes',
+ label: '同时转移',
+ component: 'CheckboxGroup',
+ componentProps: {
+ options: [
+ {
+ label: '联系人',
+ value: BizTypeEnum.CRM_CONTACT,
+ },
+ {
+ label: '商机',
+ value: BizTypeEnum.CRM_BUSINESS,
+ },
+ {
+ label: '合同',
+ value: BizTypeEnum.CRM_CONTRACT,
+ },
+ ],
+ },
+ },
+ ];
+}
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'bizId',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'ids',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '选择人员',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ dependencies: {
+ triggerFields: ['ids'],
+ show: (values) => {
+ return values.ids === undefined;
+ },
+ },
+ },
+ {
+ fieldName: 'level',
+ label: '权限级别',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.CRM_PERMISSION_LEVEL,
+ 'number',
+ ).filter((dict) => dict.value !== PermissionLevelEnum.OWNER),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'bizType',
+ label: 'Crm 类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ {
+ label: '联系人',
+ value: BizTypeEnum.CRM_CONTACT,
+ },
+ {
+ label: '商机',
+ value: BizTypeEnum.CRM_BUSINESS,
+ },
+ {
+ label: '合同',
+ value: BizTypeEnum.CRM_CONTRACT,
+ },
+ ],
+ },
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'toBizTypes',
+ label: '同时添加至',
+ component: 'CheckboxGroup',
+ componentProps: {
+ options: [
+ {
+ label: '联系人',
+ value: BizTypeEnum.CRM_CONTACT,
+ },
+ {
+ label: '商机',
+ value: BizTypeEnum.CRM_BUSINESS,
+ },
+ {
+ label: '合同',
+ value: BizTypeEnum.CRM_CONTRACT,
+ },
+ ],
+ },
+ dependencies: {
+ triggerFields: ['ids', 'bizType'],
+ show: (values) => {
+ return (
+ values.ids === undefined &&
+ values.bizType === BizTypeEnum.CRM_CUSTOMER
+ );
+ },
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ },
+ {
+ field: 'nickname',
+ title: '姓名',
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ },
+ {
+ field: 'postNames',
+ title: '岗位',
+ },
+ {
+ field: 'level',
+ title: '权限级别',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_PERMISSION_LEVEL },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '加入时间',
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/permission/modules/form.vue b/apps/web-antdv-next/src/views/crm/permission/modules/form.vue
new file mode 100644
index 000000000..27f494513
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/permission/modules/form.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/permission/modules/list.vue b/apps/web-antdv-next/src/views/crm/permission/modules/list.vue
new file mode 100644
index 000000000..13f0fb723
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/permission/modules/list.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/permission/modules/transfer-form.vue b/apps/web-antdv-next/src/views/crm/permission/modules/transfer-form.vue
new file mode 100644
index 000000000..21883543f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/permission/modules/transfer-form.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/category/data.ts b/apps/web-antdv-next/src/views/crm/product/category/data.ts
new file mode 100644
index 000000000..f7802e840
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/category/data.ts
@@ -0,0 +1,95 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmProductCategoryApi } from '#/api/crm/product/category';
+
+import { handleTree } from '@vben/utils';
+
+import { getProductCategoryList } from '#/api/crm/product/category';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '上级分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getProductCategoryList();
+ data.unshift({
+ id: 0,
+ name: '顶级分类',
+ } as CrmProductCategoryApi.ProductCategory);
+ return handleTree(data);
+ },
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择上级分类',
+ showSearch: true,
+ treeDefaultExpandAll: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入分类名称',
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '分类名称',
+ treeNode: true,
+ },
+ {
+ field: 'id',
+ title: '分类编号',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ width: 250,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/product/category/index.vue b/apps/web-antdv-next/src/views/crm/product/category/index.vue
new file mode 100644
index 000000000..1a5c780ea
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/category/index.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/category/modules/form.vue b/apps/web-antdv-next/src/views/crm/product/category/modules/form.vue
new file mode 100644
index 000000000..af578b51d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/category/modules/form.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/components/data.ts b/apps/web-antdv-next/src/views/crm/product/components/data.ts
new file mode 100644
index 000000000..266a91291
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/components/data.ts
@@ -0,0 +1,111 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 产品详情列表的列定义 */
+export function useDetailListColumns(
+ showBusinessPrice: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'productName',
+ title: '产品名称',
+ },
+ {
+ field: 'productNo',
+ title: '产品条码',
+ },
+ {
+ field: 'productUnit',
+ title: '产品单位',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
+ },
+ },
+ {
+ field: 'productPrice',
+ title: '产品价格(元)',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'businessPrice',
+ title: '商机价格(元)',
+ formatter: 'formatAmount2',
+ visible: showBusinessPrice,
+ },
+ {
+ field: 'contractPrice',
+ title: '合同价格(元)',
+ formatter: 'formatAmount2',
+ visible: !showBusinessPrice,
+ },
+ {
+ field: 'count',
+ title: '数量',
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'totalPrice',
+ title: '合计金额(元)',
+ formatter: 'formatAmount2',
+ },
+ ];
+}
+
+/** 产品编辑表格的列定义 */
+export function useProductEditTableColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50 },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 100,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'productNo',
+ title: '条码',
+ minWidth: 150,
+ },
+ {
+ field: 'productUnit',
+ title: '单位',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
+ },
+ },
+ {
+ field: 'productPrice',
+ title: '价格(元)',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'sellingPrice',
+ title: '售价(元)',
+ minWidth: 100,
+ slots: { default: 'sellingPrice' },
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 100,
+ slots: { default: 'count' },
+ },
+ {
+ field: 'totalPrice',
+ title: '合计',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/product/components/detail-list.vue b/apps/web-antdv-next/src/views/crm/product/components/detail-list.vue
new file mode 100644
index 000000000..5ce23bc0d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/components/detail-list.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+ {{ `产品总金额:${erpPriceInputFormatter(totalProductPrice)}元` }}
+
+
+ {{ `整单折扣:${erpPriceInputFormatter(discountPercent)}%` }}
+
+
+ {{
+ `实际金额:${erpPriceInputFormatter(totalProductPrice * (1 - discountPercent / 100))}元`
+ }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/components/edit-table.vue b/apps/web-antdv-next/src/views/crm/product/components/edit-table.vue
new file mode 100644
index 000000000..0d15de46a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/components/edit-table.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/components/index.ts b/apps/web-antdv-next/src/views/crm/product/components/index.ts
new file mode 100644
index 000000000..dc527926a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/components/index.ts
@@ -0,0 +1,2 @@
+export { default as ProductDetailsList } from './detail-list.vue';
+export { default as ProductEditTable } from './edit-table.vue';
diff --git a/apps/web-antdv-next/src/views/crm/product/data.ts b/apps/web-antdv-next/src/views/crm/product/data.ts
new file mode 100644
index 000000000..858b16908
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/data.ts
@@ -0,0 +1,231 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getProductCategoryList } from '#/api/crm/product/category';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '产品名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入产品名称',
+ allowClear: true,
+ },
+ },
+ {
+ component: 'ApiSelect',
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ allowClear: true,
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ component: 'Input',
+ fieldName: 'no',
+ label: '产品编码',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入产品编码',
+ allowClear: true,
+ },
+ },
+ {
+ component: 'ApiTreeSelect',
+ fieldName: 'categoryId',
+ label: '产品类型',
+ rules: 'required',
+ componentProps: {
+ api: async () => {
+ const data = await getProductCategoryList();
+ return handleTree(data);
+ },
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择产品类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'unit',
+ label: '产品单位',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_PRODUCT_UNIT, 'number'),
+ placeholder: '请选择产品单位',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'price',
+ label: '价格(元)',
+ rules: 'required',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.1,
+ placeholder: '请输入产品价格',
+ },
+ },
+ {
+ component: 'Textarea',
+ fieldName: 'description',
+ label: '产品描述',
+ componentProps: {
+ placeholder: '请输入产品描述',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '上架状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_PRODUCT_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '产品名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入产品名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '上架状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择上架状态',
+ options: getDictOptions(DICT_TYPE.CRM_PRODUCT_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '产品编号',
+ visible: false,
+ },
+ {
+ field: 'name',
+ title: '产品名称',
+ minWidth: 240,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'categoryName',
+ title: '产品类型',
+ minWidth: 120,
+ },
+ {
+ field: 'unit',
+ title: '产品单位',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
+ },
+ },
+ {
+ field: 'no',
+ title: '产品编码',
+ minWidth: 120,
+ },
+ {
+ field: 'price',
+ title: '价格(元)',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'description',
+ title: '产品描述',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '上架状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_PRODUCT_STATUS },
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 120,
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/product/detail/data.ts b/apps/web-antdv-next/src/views/crm/product/detail/data.ts
new file mode 100644
index 000000000..0adf792cb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/detail/data.ts
@@ -0,0 +1,72 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'categoryName',
+ label: '产品类别',
+ },
+ {
+ field: 'unit',
+ label: '产品单位',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_UNIT, value: val }),
+ },
+ {
+ field: 'price',
+ label: '产品价格(元)',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'no',
+ label: '产品编码',
+ },
+ ];
+}
+
+/** 详情页的基础字段 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'name',
+ label: '产品名称',
+ },
+ {
+ field: 'no',
+ label: '产品编码',
+ },
+ {
+ field: 'price',
+ label: '价格(元)',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'description',
+ label: '产品描述',
+ },
+ {
+ field: 'categoryName',
+ label: '产品类型',
+ },
+ {
+ field: 'status',
+ label: '是否上下架',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_STATUS, value: val }),
+ },
+ {
+ field: 'unit',
+ label: '产品单位',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_UNIT, value: val }),
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/product/detail/index.vue b/apps/web-antdv-next/src/views/crm/product/detail/index.vue
new file mode 100644
index 000000000..c73f7a7b3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/detail/index.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/product/detail/modules/info.vue
new file mode 100644
index 000000000..4e557455d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/detail/modules/info.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/index.vue b/apps/web-antdv-next/src/views/crm/product/index.vue
new file mode 100644
index 000000000..41aae7fb7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/index.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/product/modules/form.vue b/apps/web-antdv-next/src/views/crm/product/modules/form.vue
new file mode 100644
index 000000000..7887d9e8a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/product/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/components/data.ts b/apps/web-antdv-next/src/views/crm/receivable/components/data.ts
new file mode 100644
index 000000000..4f3f0e134
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/components/data.ts
@@ -0,0 +1,73 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 详情列表的字段 */
+export function useDetailListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '回款编号',
+ field: 'no',
+ minWidth: 150,
+ fixed: 'left',
+ },
+ {
+ title: '客户名称',
+ field: 'customerName',
+ minWidth: 150,
+ },
+ {
+ title: '合同编号',
+ field: 'contract.no',
+ minWidth: 150,
+ },
+ {
+ title: '回款日期',
+ field: 'returnTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '回款金额(元)',
+ field: 'price',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '回款方式',
+ field: 'returnType',
+ minWidth: 150,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
+ },
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 150,
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '回款状态',
+ field: 'auditStatus',
+ minWidth: 100,
+ fixed: 'right',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/receivable/components/detail-list.vue b/apps/web-antdv-next/src/views/crm/receivable/components/detail-list.vue
new file mode 100644
index 000000000..979f5521d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/components/detail-list.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/components/index.ts b/apps/web-antdv-next/src/views/crm/receivable/components/index.ts
new file mode 100644
index 000000000..971ab650b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/components/index.ts
@@ -0,0 +1 @@
+export { default as ReceivableDetailsList } from './detail-list.vue';
diff --git a/apps/web-antdv-next/src/views/crm/receivable/data.ts b/apps/web-antdv-next/src/views/crm/receivable/data.ts
new file mode 100644
index 000000000..f7f9c95d2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/data.ts
@@ -0,0 +1,299 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+
+import { getContractSimpleList } from '#/api/crm/contract';
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import {
+ getReceivablePlan,
+ getReceivablePlanSimpleList,
+} from '#/api/crm/receivable/plan';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '回款编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '保存时自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ allowClear: true,
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户名称',
+ component: 'ApiSelect',
+ rules: 'required',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ },
+ {
+ fieldName: 'contractId',
+ label: '合同名称',
+ component: 'Select',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId || values.id,
+ async componentProps(values) {
+ if (values.customerId) {
+ if (!values.id) {
+ // 特殊:只有在【新增】时,才清空合同编号
+ values.contractId = undefined;
+ }
+ const contracts = await getContractSimpleList(values.customerId);
+ return {
+ options: contracts.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择合同',
+ } as any;
+ }
+ },
+ },
+ },
+ {
+ fieldName: 'planId',
+ label: '回款期数',
+ component: 'Select',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['contractId'],
+ disabled: (values) => !values.contractId,
+ async componentProps(values) {
+ if (values.contractId) {
+ values.planId = undefined;
+ const plans = await getReceivablePlanSimpleList(
+ values.customerId,
+ values.contractId,
+ );
+ return {
+ options: plans.map((item) => ({
+ label: item.period,
+ value: item.id,
+ })),
+ placeholder: '请选择回款期数',
+ onChange: async (value: any) => {
+ const plan = await getReceivablePlan(value);
+ values.returnTime = plan?.returnTime;
+ values.price = plan?.price;
+ values.returnType = plan?.returnType;
+ },
+ } as any;
+ }
+ },
+ },
+ },
+ {
+ fieldName: 'returnType',
+ label: '回款方式',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE, 'number'),
+ placeholder: '请选择回款方式',
+ },
+ },
+ {
+ fieldName: 'price',
+ label: '回款金额',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入回款金额',
+ min: 0,
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'returnTime',
+ label: '回款日期',
+ component: 'DatePicker',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请选择回款日期',
+ showTime: false,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '回款编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入回款编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '回款编号',
+ field: 'no',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'no' },
+ },
+ {
+ title: '客户名称',
+ field: 'customerName',
+ minWidth: 150,
+ slots: { default: 'customerName' },
+ },
+ {
+ title: '合同编号',
+ field: 'contract',
+ minWidth: 160,
+ slots: { default: 'contractNo' },
+ },
+ {
+ title: '回款日期',
+ field: 'returnTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '回款金额(元)',
+ field: 'price',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '回款方式',
+ field: 'returnType',
+ minWidth: 150,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
+ },
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '合同金额(元)',
+ field: 'contract.totalPrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 150,
+ },
+ {
+ title: '所属部门',
+ field: 'ownerUserDeptName',
+ minWidth: 150,
+ },
+ {
+ title: '更新时间',
+ field: 'updateTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建人',
+ field: 'creatorName',
+ minWidth: 150,
+ },
+ {
+ title: '回款状态',
+ field: 'auditStatus',
+ minWidth: 100,
+ fixed: 'right',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ minWidth: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/receivable/detail/data.ts b/apps/web-antdv-next/src/views/crm/receivable/detail/data.ts
new file mode 100644
index 000000000..dcceec53a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/detail/data.ts
@@ -0,0 +1,105 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'totalPrice',
+ label: '合同金额(元)',
+ render: (val, data) =>
+ erpPriceInputFormatter(val ?? data?.contract?.totalPrice ?? 0),
+ },
+ {
+ field: 'returnTime',
+ label: '回款日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'price',
+ label: '回款金额(元)',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ ];
+}
+
+/** 详情页的基础字段 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'no',
+ label: '回款编号',
+ },
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'contract',
+ label: '合同编号',
+ render: (val, data) =>
+ val && data?.contract?.no ? data?.contract?.no : '',
+ },
+ {
+ field: 'returnTime',
+ label: '回款日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'price',
+ label: '回款金额',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'returnType',
+ label: '回款方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ ];
+}
+
+/** 系统信息字段 */
+export function useDetailSystemSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ {
+ field: 'creatorName',
+ label: '创建人',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '更新时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/receivable/detail/index.vue b/apps/web-antdv-next/src/views/crm/receivable/detail/index.vue
new file mode 100644
index 000000000..39c84b9a0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/detail/index.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/receivable/detail/modules/info.vue
new file mode 100644
index 000000000..524b5f141
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/detail/modules/info.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/index.vue b/apps/web-antdv-next/src/views/crm/receivable/index.vue
new file mode 100644
index 000000000..97392936c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/index.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ --
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/modules/form.vue b/apps/web-antdv-next/src/views/crm/receivable/modules/form.vue
new file mode 100644
index 000000000..07f240dd7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/modules/form.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/components/data.ts b/apps/web-antdv-next/src/views/crm/receivable/plan/components/data.ts
new file mode 100644
index 000000000..9a67c0be2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/components/data.ts
@@ -0,0 +1,62 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+/** 详情列表的字段 */
+export function useDetailListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '客户名称',
+ field: 'customerName',
+ minWidth: 150,
+ },
+ {
+ title: '合同编号',
+ field: 'contractNo',
+ minWidth: 150,
+ },
+ {
+ title: '期数',
+ field: 'period',
+ minWidth: 150,
+ },
+ {
+ title: '计划回款(元)',
+ field: 'price',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '计划回款日期',
+ field: 'returnTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '提前几天提醒',
+ field: 'remindDays',
+ minWidth: 150,
+ },
+ {
+ title: '提醒日期',
+ field: 'remindTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 150,
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/components/detail-list.vue b/apps/web-antdv-next/src/views/crm/receivable/plan/components/detail-list.vue
new file mode 100644
index 000000000..1faa4f201
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/components/detail-list.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/components/index.ts b/apps/web-antdv-next/src/views/crm/receivable/plan/components/index.ts
new file mode 100644
index 000000000..cbb4b1216
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/components/index.ts
@@ -0,0 +1,2 @@
+export { default as ReceivablePlanDetailsInfo } from '../detail/modules/info.vue';
+export { default as ReceivablePlanDetailsList } from './detail-list.vue';
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/data.ts b/apps/web-antdv-next/src/views/crm/receivable/plan/data.ts
new file mode 100644
index 000000000..b8e848493
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/data.ts
@@ -0,0 +1,285 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { getContractSimpleList } from '#/api/crm/contract';
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'period',
+ label: '期数',
+ component: 'Input',
+ componentProps: {
+ placeholder: '保存时自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => values.id,
+ },
+ defaultValue: userStore.userInfo?.id,
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ rules: 'required',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'contractId',
+ label: '合同',
+ component: 'Select',
+ rules: 'required',
+ componentProps: {
+ options: [],
+ placeholder: '请选择合同',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId,
+ async componentProps(values) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '请选择客户',
+ };
+ }
+ const res = await getContractSimpleList(values.customerId);
+ return {
+ options: res.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择合同',
+ onChange: (value: number) => {
+ const contract = res.find((item) => item.id === value);
+ if (contract) {
+ values.price =
+ contract.totalPrice - contract.totalReceivablePrice;
+ }
+ },
+ };
+ },
+ },
+ },
+ {
+ fieldName: 'price',
+ label: '计划回款金额',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入计划回款金额',
+ min: 0,
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'returnTime',
+ label: '计划回款日期',
+ component: 'DatePicker',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请选择计划回款日期',
+ showTime: false,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD',
+ },
+ defaultValue: new Date(),
+ },
+ {
+ fieldName: 'remindDays',
+ label: '提前几天提醒',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入提前几天提醒',
+ min: 0,
+ },
+ },
+ {
+ fieldName: 'returnType',
+ label: '回款方式',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE, 'number'),
+ placeholder: '请选择回款方式',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择客户',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'contractNo',
+ label: '合同编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入合同编号',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '客户名称',
+ field: 'customerName',
+ minWidth: 150,
+ fixed: 'left',
+ slots: { default: 'customerName' },
+ },
+ {
+ title: '合同编号',
+ field: 'contractNo',
+ minWidth: 200,
+ },
+ {
+ title: '期数',
+ field: 'period',
+ minWidth: 150,
+ slots: { default: 'period' },
+ },
+ {
+ title: '计划回款金额(元)',
+ field: 'price',
+ minWidth: 160,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '计划回款日期',
+ field: 'returnTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '提前几天提醒',
+ field: 'remindDays',
+ minWidth: 150,
+ },
+ {
+ title: '提醒日期',
+ field: 'remindTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '回款方式',
+ field: 'returnType',
+ minWidth: 130,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
+ },
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 120,
+ },
+ {
+ title: '负责人',
+ field: 'ownerUserName',
+ minWidth: 120,
+ },
+ {
+ title: '实际回款金额(元)',
+ field: 'receivable.price',
+ minWidth: 160,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '实际回款日期',
+ field: 'receivable.returnTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '未回款金额(元)',
+ field: 'unpaidPrice',
+ minWidth: 160,
+ formatter: ({ row }) => {
+ if (row.receivable) {
+ return erpPriceInputFormatter(row.price - row.receivable.price);
+ }
+ return erpPriceInputFormatter(row.price);
+ },
+ },
+ {
+ title: '更新时间',
+ field: 'updateTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '创建人',
+ field: 'creatorName',
+ minWidth: 100,
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/detail/data.ts b/apps/web-antdv-next/src/views/crm/receivable/plan/detail/data.ts
new file mode 100644
index 000000000..5059f02ab
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/detail/data.ts
@@ -0,0 +1,124 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'contractNo',
+ label: '合同编号',
+ },
+ {
+ field: 'price',
+ label: '计划回款金额',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'returnTime',
+ label: '计划回款日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'receivable',
+ label: '实际回款金额',
+ render: (val) => erpPriceInputFormatter(val?.price ?? 0),
+ },
+ ];
+}
+
+/** 详情页的基础字段 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'period',
+ label: '期数',
+ },
+ {
+ field: 'customerName',
+ label: '客户名称',
+ },
+ {
+ field: 'contractNo',
+ label: '合同编号',
+ },
+ {
+ field: 'returnTime',
+ label: '计划回款日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'price',
+ label: '计划回款金额',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'returnType',
+ label: '计划回款方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'remindDays',
+ label: '提前几天提醒',
+ },
+ {
+ field: 'receivable',
+ label: '实际回款金额',
+ render: (val) => erpPriceInputFormatter(val ?? 0),
+ },
+ {
+ field: 'receivableRemain',
+ label: '未回款金额',
+ render: (val, data) => {
+ const paid = data?.receivable?.price ?? 0;
+ return erpPriceInputFormatter(Math.max(val - paid, 0));
+ },
+ },
+ {
+ field: 'receivable.returnTime',
+ label: '实际回款日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'remark',
+ label: '备注',
+ },
+ ];
+}
+
+/** 系统信息字段 */
+export function useDetailSystemSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'ownerUserName',
+ label: '负责人',
+ },
+ {
+ field: 'creatorName',
+ label: '创建人',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '更新时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/detail/index.vue b/apps/web-antdv-next/src/views/crm/receivable/plan/detail/index.vue
new file mode 100644
index 000000000..1e2e559bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/detail/index.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/detail/modules/info.vue b/apps/web-antdv-next/src/views/crm/receivable/plan/detail/modules/info.vue
new file mode 100644
index 000000000..2a77aa2a3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/detail/modules/info.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/index.vue b/apps/web-antdv-next/src/views/crm/receivable/plan/index.vue
new file mode 100644
index 000000000..93edff787
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/index.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/receivable/plan/modules/form.vue b/apps/web-antdv-next/src/views/crm/receivable/plan/modules/form.vue
new file mode 100644
index 000000000..69e420b94
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/receivable/plan/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/statistics/customer/chartOptions.ts b/apps/web-antdv-next/src/views/crm/statistics/customer/chartOptions.ts
new file mode 100644
index 000000000..383833832
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/customer/chartOptions.ts
@@ -0,0 +1,490 @@
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+
+const getLegend = (extra: Record = {}) => ({
+ top: 10,
+ ...extra,
+});
+
+const getGrid = (extra: Record = {}) => ({
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ ...extra,
+});
+
+const getTooltip = (extra: Record = {}) => ({
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+ ...extra,
+});
+
+export function getChartOptions(activeTabName: any, res: any): any {
+ switch (activeTabName) {
+ // 客户转化率分析
+ case 'conversionStat': {
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '客户转化率',
+ type: 'line',
+ data: res.map((item: any) => {
+ return {
+ name: item.time,
+ value: item.customerCreateCount
+ ? (
+ (item.customerDealCount / item.customerCreateCount) *
+ 100
+ ).toFixed(2)
+ : 0,
+ };
+ }),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '客户转化率分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: {
+ type: 'value',
+ name: '转化率(%)',
+ },
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ case 'customerSummary': {
+ return {
+ grid: getGrid({
+ bottom: '8%',
+ left: '5%',
+ right: '5%',
+ top: 80,
+ }),
+ legend: getLegend(),
+ series: [
+ {
+ name: '新增客户数',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: res.map((item: any) => item.customerCreateCount),
+ },
+ {
+ name: '成交客户数',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: res.map((item: any) => item.customerDealCount),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '新增客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '成交客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((item: any) => item.time),
+ },
+ };
+ }
+ case 'dealCycleByArea': {
+ const data = res.map((s: any) => {
+ return {
+ areaName: s.areaName,
+ customerDealCycle: s.customerDealCycle,
+ customerDealCount: s.customerDealCount,
+ };
+ });
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '成交周期(天)',
+ type: 'bar',
+ data: data.map((s: any) => s.customerDealCycle),
+ yAxisIndex: 0,
+ },
+ {
+ name: '成交客户数',
+ type: 'bar',
+ data: data.map((s: any) => s.customerDealCount),
+ yAxisIndex: 1,
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '成交周期(天)',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '成交客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '区域',
+ data: data.map((s: any) => s.areaName),
+ },
+ };
+ }
+ case 'dealCycleByProduct': {
+ const data = res.map((s: any) => {
+ return {
+ productName: s.productName ?? '未知',
+ customerDealCycle: s.customerDealCount,
+ customerDealCount: s.customerDealCount,
+ };
+ });
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '成交周期(天)',
+ type: 'bar',
+ data: data.map((s: any) => s.customerDealCycle),
+ yAxisIndex: 0,
+ },
+ {
+ name: '成交客户数',
+ type: 'bar',
+ data: data.map((s: any) => s.customerDealCount),
+ yAxisIndex: 1,
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '成交周期(天)',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '成交客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '产品名称',
+ data: data.map((s: any) => s.productName),
+ },
+ };
+ }
+ case 'dealCycleByUser': {
+ const customerDealCycleByDate = res.customerDealCycleByDate;
+ const customerDealCycleByUser = res.customerDealCycleByUser;
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '成交周期(天)',
+ type: 'bar',
+ data: customerDealCycleByDate.map((s: any) => s.customerDealCycle),
+ yAxisIndex: 0,
+ },
+ {
+ name: '成交客户数',
+ type: 'bar',
+ data: customerDealCycleByUser.map((s: any) => s.customerDealCount),
+ yAxisIndex: 1,
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '成交周期(天)',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '成交客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: customerDealCycleByDate.map((s: any) => s.time),
+ },
+ };
+ }
+ // 客户跟进次数分析
+ case 'followUpSummary': {
+ return {
+ grid: getGrid({
+ right: 30, // 让 X 轴右侧显示完整
+ }),
+ legend: getLegend(),
+ series: [
+ {
+ name: '跟进客户数',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: res.map((s: any) => s.followUpCustomerCount),
+ },
+ {
+ name: '跟进次数',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: res.map((s: any) => s.followUpRecordCount),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '客户跟进次数分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '跟进客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '跟进次数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ axisTick: {
+ alignWithLabel: true,
+ },
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ // 客户跟进方式分析
+ case 'followUpType': {
+ return {
+ title: {
+ text: '客户跟进方式分析',
+ left: 'center',
+ },
+ legend: getLegend({
+ left: 'left',
+ }),
+ tooltip: getTooltip({
+ trigger: 'item',
+ axisPointer: undefined,
+ formatter: '{b} : {c}% ',
+ }),
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '客户跟进方式分析' }, // 保存为图片
+ },
+ },
+ series: [
+ {
+ name: '跟进方式',
+ type: 'pie',
+ radius: '50%',
+ data: res.map((s: any) => {
+ return {
+ name: getDictLabel(
+ DICT_TYPE.CRM_FOLLOW_UP_TYPE,
+ s.followUpType,
+ ),
+ value: s.followUpRecordCount,
+ };
+ }),
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ },
+ },
+ ],
+ };
+ }
+ case 'poolSummary': {
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '进入公海客户数',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: res.map((s: any) => s.customerPutCount),
+ },
+ {
+ name: '公海领取客户数',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: res.map((s: any) => s.customerTakeCount),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '公海客户分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '进入公海客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '公海领取客户数',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ default: {
+ return {};
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/customer/data.ts b/apps/web-antdv-next/src/views/crm/statistics/customer/data.ts
new file mode 100644
index 000000000..a2ec49ecc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/customer/data.ts
@@ -0,0 +1,401 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+import {
+ beginOfDay,
+ endOfDay,
+ erpCalculatePercentage,
+ formatDateTime,
+ handleTree,
+} from '@vben/utils';
+
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+const userStore = useUserStore();
+
+export const customerSummaryTabs = [
+ {
+ tab: '客户总量分析',
+ key: 'customerSummary',
+ },
+ {
+ tab: '客户跟进次数分析',
+ key: 'followUpSummary',
+ },
+ {
+ tab: '客户跟进方式分析',
+ key: 'followUpType',
+ },
+ {
+ tab: '客户转化率分析',
+ key: 'conversionStat',
+ },
+ {
+ tab: '公海客户分析',
+ key: 'poolSummary',
+ },
+ {
+ tab: '员工客户成交周期分析',
+ key: 'dealCycleByUser',
+ },
+ {
+ tab: '地区客户成交周期分析',
+ key: 'dealCycleByArea',
+ },
+ {
+ tab: '产品客户成交周期分析',
+ key: 'dealCycleByProduct',
+ },
+];
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'times',
+ label: '时间范围',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ defaultValue: [
+ formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
+ formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
+ ],
+ },
+ {
+ fieldName: 'interval',
+ label: '时间间隔',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择时间间隔',
+ options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
+ },
+ defaultValue: 2,
+ },
+ {
+ fieldName: 'deptId',
+ label: '归属部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ treeDefaultExpandAll: true,
+ placeholder: '请选择归属部门',
+ },
+ defaultValue: userStore.userInfo?.deptId,
+ },
+ {
+ fieldName: 'userId',
+ label: '员工',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择员工',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ activeTabName: any,
+): VxeTableGridOptions['columns'] {
+ switch (activeTabName) {
+ case 'conversionStat': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ minWidth: 100,
+ },
+ {
+ field: 'contractName',
+ title: '合同名称',
+ minWidth: 200,
+ },
+ {
+ field: 'totalPrice',
+ title: '合同总金额',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'receivablePrice',
+ title: '回款金额',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'source',
+ title: '客户来源',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
+ },
+ },
+ {
+ field: 'industryId',
+ title: '客户行业',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
+ },
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 200,
+ },
+ {
+ field: 'creatorUserName',
+ title: '创建人',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'orderDate',
+ title: '下单日期',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ ];
+ }
+ case 'customerSummary': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'ownerUserName',
+ title: '员工姓名',
+ minWidth: 100,
+ },
+ {
+ field: 'customerCreateCount',
+ title: '新增客户数',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCount',
+ title: '成交客户数',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealRate',
+ title: '客户成交率(%)',
+ minWidth: 200,
+ formatter: ({ row }) => {
+ return erpCalculatePercentage(
+ row.customerDealCount,
+ row.customerCreateCount,
+ );
+ },
+ },
+ {
+ field: 'contractPrice',
+ title: '合同总金额',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'receivablePrice',
+ title: '回款金额',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'creceivablePrice',
+ title: '未回款金额',
+ minWidth: 200,
+ formatter: ({ row }) => {
+ return erpCalculatePercentage(
+ row.receivablePrice,
+ row.contractPrice,
+ );
+ },
+ },
+ {
+ field: 'ccreceivablePrice',
+ title: '回款完成率(%)',
+ formatter: ({ row }) => {
+ return erpCalculatePercentage(
+ row.receivablePrice,
+ row.contractPrice,
+ );
+ },
+ },
+ ];
+ }
+ case 'dealCycleByArea': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'areaName',
+ title: '区域',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCycle',
+ title: '成交周期(天)',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCount',
+ title: '成交客户数',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'dealCycleByProduct': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'productName',
+ title: '产品名称',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCycle',
+ title: '成交周期(天)',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCount',
+ title: '成交客户数',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'dealCycleByUser': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'ownerUserName',
+ title: '日期',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCycle',
+ title: '成交周期(天)',
+ minWidth: 200,
+ },
+ {
+ field: 'customerDealCount',
+ title: '成交客户数',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'followUpSummary': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'ownerUserName',
+ title: '员工姓名',
+ minWidth: 200,
+ },
+ {
+ field: 'followUpRecordCount',
+ title: '跟进次数',
+ minWidth: 200,
+ },
+ {
+ field: 'followUpCustomerCount',
+ title: '跟进客户数',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'followUpType': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'followUpType',
+ title: '跟进方式',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_FOLLOW_UP_TYPE },
+ },
+ },
+ {
+ field: 'followUpRecordCount',
+ title: '个数',
+ minWidth: 200,
+ },
+ {
+ field: 'portion',
+ title: '占比(%)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'poolSummary': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'ownerUserName',
+ title: '员工姓名',
+ minWidth: 200,
+ },
+ {
+ field: 'customerPutCount',
+ title: '进入公海客户数',
+ minWidth: 200,
+ },
+ {
+ field: 'customerTakeCount',
+ title: '公海领取客户数',
+ minWidth: 200,
+ },
+ ];
+ }
+ default: {
+ return [];
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/customer/index.vue b/apps/web-antdv-next/src/views/crm/statistics/customer/index.vue
new file mode 100644
index 000000000..a0c39c4d3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/customer/index.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/statistics/funnel/chartOptions.ts b/apps/web-antdv-next/src/views/crm/statistics/funnel/chartOptions.ts
new file mode 100644
index 000000000..e7ce47511
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/funnel/chartOptions.ts
@@ -0,0 +1,279 @@
+import { erpCalculatePercentage } from '@vben/utils';
+
+const getLegend = (extra: Record = {}) => ({
+ top: 10,
+ ...extra,
+});
+
+const getGrid = (extra: Record = {}) => ({
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ ...extra,
+});
+
+const getTooltip = (extra: Record = {}) => ({
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+ ...extra,
+});
+
+export function getChartOptions(
+ activeTabName: any,
+ active: boolean,
+ res: any,
+): any {
+ switch (activeTabName) {
+ case 'businessInversionRateSummary': {
+ return {
+ color: ['#6ca2ff', '#6ac9d7', '#ff7474'],
+ tooltip: getTooltip(),
+ legend: getLegend({
+ data: ['赢单转化率', '商机总数', '赢单商机数'],
+ bottom: '0px',
+ itemWidth: 14,
+ }),
+ grid: getGrid({
+ top: '40px',
+ left: '40px',
+ right: '40px',
+ bottom: '40px',
+ borderColor: '#fff',
+ }),
+ xAxis: [
+ {
+ type: 'category',
+ data: res.map((s: any) => s.time),
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: { width: 0 },
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: { color: '#BDBDBD' },
+ },
+ splitLine: {
+ show: false,
+ },
+ },
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ name: '赢单转化率',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: { width: 0 },
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: { color: '#BDBDBD' },
+ },
+ splitLine: {
+ show: false,
+ },
+ },
+ {
+ type: 'value',
+ name: '商机数',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: { width: 0 },
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}个',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: { color: '#BDBDBD' },
+ },
+ splitLine: {
+ show: false,
+ },
+ },
+ ],
+ series: [
+ {
+ name: '赢单转化率',
+ type: 'line',
+ yAxisIndex: 0,
+ data: res.map((s: any) =>
+ erpCalculatePercentage(s.businessWinCount, s.businessCount),
+ ),
+ },
+ {
+ name: '商机总数',
+ type: 'bar',
+ yAxisIndex: 1,
+ barWidth: 15,
+ data: res.map((s: any) => s.businessCount),
+ },
+ {
+ name: '赢单商机数',
+ type: 'bar',
+ yAxisIndex: 1,
+ barWidth: 15,
+ data: res.map((s: any) => s.businessWinCount),
+ },
+ ],
+ };
+ }
+ case 'businessSummary': {
+ return {
+ grid: getGrid({
+ left: 30,
+ right: 30, // 让 X 轴右侧显示完整
+ }),
+ legend: getLegend(),
+ series: [
+ {
+ name: '新增商机数量',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: res.map((s: any) => s.businessCreateCount),
+ },
+ {
+ name: '新增商机金额',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: res.map((s: any) => s.totalPrice),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '新增商机分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '新增商机数量',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ },
+ {
+ type: 'value',
+ name: '新增商机金额',
+ min: 0,
+ minInterval: 1, // 显示整数刻度
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 右侧网格线虚化, 减少混乱
+ opacity: 0.7,
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ case 'funnel': {
+ // tips:写死 value 值是为了保持漏斗顺序不变
+ const list: { name: string; value: number }[] = [];
+ if (active) {
+ list.push(
+ { value: 60, name: `客户-${res.customerCount || 0}个` },
+ { value: 40, name: `商机-${res.businessCount || 0}个` },
+ { value: 20, name: `赢单-${res.businessWinCount || 0}个` },
+ );
+ } else {
+ list.push(
+ {
+ value: res.customerCount || 0,
+ name: `客户-${res.customerCount || 0}个`,
+ },
+ {
+ value: res.businessCount || 0,
+ name: `商机-${res.businessCount || 0}个`,
+ },
+ {
+ value: res.businessWinCount || 0,
+ name: `赢单-${res.businessWinCount || 0}个`,
+ },
+ );
+ }
+ return {
+ title: {
+ text: '销售漏斗',
+ },
+ tooltip: getTooltip({
+ trigger: 'item',
+ axisPointer: undefined,
+ formatter: '{a}
{b}',
+ }),
+ toolbox: {
+ feature: {
+ dataView: { readOnly: false },
+ restore: {},
+ saveAsImage: {},
+ },
+ },
+ legend: getLegend({
+ data: ['客户', '商机', '赢单'],
+ }),
+ series: [
+ {
+ name: '销售漏斗',
+ type: 'funnel',
+ left: '10%',
+ top: 60,
+ bottom: 60,
+ width: '80%',
+ min: 0,
+ max: 100,
+ minSize: '0%',
+ maxSize: '100%',
+ sort: 'descending',
+ gap: 2,
+ label: {
+ show: true,
+ position: 'inside',
+ },
+ labelLine: {
+ length: 10,
+ lineStyle: {
+ width: 1,
+ type: 'solid',
+ },
+ },
+ itemStyle: {
+ borderColor: '#fff',
+ borderWidth: 1,
+ },
+ emphasis: {
+ label: {
+ fontSize: 20,
+ },
+ },
+ data: list,
+ },
+ ],
+ };
+ }
+ default: {
+ return {};
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/funnel/data.ts b/apps/web-antdv-next/src/views/crm/statistics/funnel/data.ts
new file mode 100644
index 000000000..5dd93db19
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/funnel/data.ts
@@ -0,0 +1,271 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
+
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+const userStore = useUserStore();
+
+export const customerSummaryTabs = [
+ {
+ tab: '销售漏斗分析',
+ key: 'funnel',
+ },
+ {
+ tab: '新增商机分析',
+ key: 'businessSummary',
+ },
+ {
+ tab: '商机转化率分析',
+ key: 'businessInversionRateSummary',
+ },
+];
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'times',
+ label: '时间范围',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ defaultValue: [
+ formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
+ formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
+ ],
+ },
+ {
+ fieldName: 'interval',
+ label: '时间间隔',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择时间间隔',
+ options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
+ },
+ defaultValue: 2,
+ },
+ {
+ fieldName: 'deptId',
+ label: '归属部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ treeDefaultExpandAll: true,
+ placeholder: '请选择归属部门',
+ },
+ defaultValue: userStore.userInfo?.deptId,
+ },
+ {
+ fieldName: 'userId',
+ label: '员工',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ allowClear: true,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择员工',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ activeTabName: any,
+): VxeTableGridOptions['columns'] {
+ switch (activeTabName) {
+ case 'businessInversionRateSummary': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'name',
+ title: '商机名称',
+ minWidth: 100,
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ minWidth: 200,
+ },
+ {
+ field: 'totalPrice',
+ title: '商机金额(元)',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'dealTime',
+ title: '预计成交日期',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 200,
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ minWidth: 200,
+ },
+ {
+ field: 'contactLastTime',
+ title: '最后跟进时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 100,
+ },
+ {
+ field: 'statusTypeName',
+ title: '商机状态组',
+ minWidth: 100,
+ },
+ {
+ field: 'statusName',
+ title: '商机阶段',
+ minWidth: 100,
+ },
+ ];
+ }
+ case 'businessSummary': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'name',
+ title: '商机名称',
+ minWidth: 100,
+ },
+ {
+ field: 'customerName',
+ title: '客户名称',
+ minWidth: 200,
+ },
+ {
+ field: 'totalPrice',
+ title: '商机金额(元)',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'dealTime',
+ title: '预计成交日期',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'ownerUserName',
+ title: '负责人',
+ minWidth: 200,
+ },
+ {
+ field: 'ownerUserDeptName',
+ title: '所属部门',
+ minWidth: 200,
+ },
+ {
+ field: 'contactLastTime',
+ title: '最后跟进时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 200,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 100,
+ },
+ {
+ field: 'statusTypeName',
+ title: '商机状态组',
+ minWidth: 100,
+ },
+ {
+ field: 'statusName',
+ title: '商机阶段',
+ minWidth: 100,
+ },
+ ];
+ }
+ case 'funnel': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'endStatus',
+ title: '阶段',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE },
+ },
+ },
+ {
+ field: 'businessCount',
+ title: '商机数',
+ minWidth: 200,
+ },
+ {
+ field: 'totalPrice',
+ title: '商机总金额(元)',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ ];
+ }
+ default: {
+ return [];
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/funnel/index.vue b/apps/web-antdv-next/src/views/crm/statistics/funnel/index.vue
new file mode 100644
index 000000000..72035f28f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/funnel/index.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/statistics/performance/chartOptions.ts b/apps/web-antdv-next/src/views/crm/statistics/performance/chartOptions.ts
new file mode 100644
index 000000000..073c625e4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/performance/chartOptions.ts
@@ -0,0 +1,395 @@
+const getLegend = (extra: Record = {}) => ({
+ top: 10,
+ ...extra,
+});
+
+const getGrid = (extra: Record = {}) => ({
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ ...extra,
+});
+
+const getTooltip = (extra: Record = {}) => ({
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+ ...extra,
+});
+
+export function getChartOptions(activeTabName: any, res: any): any {
+ switch (activeTabName) {
+ case 'ContractCountPerformance': {
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '当月合同数量(个)',
+ type: 'line',
+ data: res.map((s: any) => s.currentMonthCount),
+ },
+ {
+ name: '上月合同数量(个)',
+ type: 'line',
+ data: res.map((s: any) => s.lastMonthCount),
+ },
+ {
+ name: '去年同月合同数量(个)',
+ type: 'line',
+ data: res.map((s: any) => s.lastYearCount),
+ },
+ {
+ name: '环比增长率(%)',
+ type: 'line',
+ yAxisIndex: 1,
+ data: res.map((s: any) =>
+ s.lastMonthCount === 0
+ ? 'NULL'
+ : (
+ ((s.currentMonthCount - s.lastMonthCount) /
+ s.lastMonthCount) *
+ 100
+ ).toFixed(2),
+ ),
+ },
+ {
+ name: '同比增长率(%)',
+ type: 'line',
+ yAxisIndex: 1,
+ data: res.map((s: any) =>
+ s.lastYearCount === 0
+ ? 'NULL'
+ : (
+ ((s.currentMonthCount - s.lastYearCount) /
+ s.lastYearCount) *
+ 100
+ ).toFixed(2),
+ ),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ yAxis: [
+ {
+ type: 'value',
+ name: '数量(个)',
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD',
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6',
+ },
+ },
+ },
+ {
+ type: 'value',
+ name: '',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: {
+ width: 0,
+ },
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD',
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6',
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ case 'ContractPricePerformance': {
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '当月合同金额(元)',
+ type: 'line',
+ data: res.map((s: any) => s.currentMonthCount),
+ },
+ {
+ name: '上月合同金额(元)',
+ type: 'line',
+ data: res.map((s: any) => s.lastMonthCount),
+ },
+ {
+ name: '去年同月合同金额(元)',
+ type: 'line',
+ data: res.map((s: any) => s.lastYearCount),
+ },
+ {
+ name: '环比增长率(%)',
+ type: 'line',
+ yAxisIndex: 1,
+ data: res.map((s: any) =>
+ s.lastMonthCount === 0
+ ? 'NULL'
+ : (
+ ((s.currentMonthCount - s.lastMonthCount) /
+ s.lastMonthCount) *
+ 100
+ ).toFixed(2),
+ ),
+ },
+ {
+ name: '同比增长率(%)',
+ type: 'line',
+ yAxisIndex: 1,
+ data: res.map((s: any) =>
+ s.lastYearCount === 0
+ ? 'NULL'
+ : (
+ ((s.currentMonthCount - s.lastYearCount) /
+ s.lastYearCount) *
+ 100
+ ).toFixed(2),
+ ),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '金额(元)',
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD',
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6',
+ },
+ },
+ },
+ {
+ type: 'value',
+ name: '',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: {
+ width: 0,
+ },
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD',
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6',
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ case 'ReceivablePricePerformance': {
+ return {
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '当月回款金额(元)',
+ type: 'line',
+ data: res.map((s: any) => s.currentMonthCount),
+ },
+ {
+ name: '上月回款金额(元)',
+ type: 'line',
+ data: res.map((s: any) => s.lastMonthCount),
+ },
+ {
+ name: '去年同月回款金额(元)',
+ type: 'line',
+ data: res.map((s: any) => s.lastYearCount),
+ },
+ {
+ name: '环比增长率(%)',
+ type: 'line',
+ yAxisIndex: 1,
+ data: res.map((s: any) =>
+ s.lastMonthCount === 0
+ ? 'NULL'
+ : (
+ ((s.currentMonthCount - s.lastMonthCount) /
+ s.lastMonthCount) *
+ 100
+ ).toFixed(2),
+ ),
+ },
+ {
+ name: '同比增长率(%)',
+ type: 'line',
+ yAxisIndex: 1,
+ data: res.map((s: any) =>
+ s.lastYearCount === 0
+ ? 'NULL'
+ : (
+ ((s.currentMonthCount - s.lastYearCount) /
+ s.lastYearCount) *
+ 100
+ ).toFixed(2),
+ ),
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '金额(元)',
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD',
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6',
+ },
+ },
+ },
+ {
+ type: 'value',
+ name: '',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: {
+ width: 0,
+ },
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%',
+ },
+ /** 坐标轴轴线相关设置 */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD',
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6',
+ },
+ },
+ },
+ ],
+ xAxis: {
+ type: 'category',
+ name: '日期',
+ data: res.map((s: any) => s.time),
+ },
+ };
+ }
+ default: {
+ return {};
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/performance/data.ts b/apps/web-antdv-next/src/views/crm/statistics/performance/data.ts
new file mode 100644
index 000000000..d95e4c9c1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/performance/data.ts
@@ -0,0 +1,71 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { useUserStore } from '@vben/stores';
+import { handleTree } from '@vben/utils';
+
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+
+const userStore = useUserStore();
+
+export const customerSummaryTabs = [
+ {
+ tab: '员工合同数量统计',
+ key: 'ContractCountPerformance',
+ },
+ {
+ tab: '员工合同金额统计',
+ key: 'ContractPricePerformance',
+ },
+ {
+ tab: '员工回款金额统计',
+ key: 'ReceivablePricePerformance',
+ },
+];
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'time',
+ label: '选择年份',
+ component: 'DatePicker',
+ componentProps: {
+ picker: 'year',
+ format: 'YYYY',
+ valueFormat: 'YYYY',
+ placeholder: '请选择年份',
+ },
+ defaultValue: new Date().getFullYear().toString(),
+ },
+ {
+ fieldName: 'deptId',
+ label: '归属部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ treeDefaultExpandAll: true,
+ placeholder: '请选择归属部门',
+ },
+ defaultValue: userStore.userInfo?.deptId,
+ },
+ {
+ fieldName: 'userId',
+ label: '员工',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择员工',
+ allowClear: true,
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/performance/index.vue b/apps/web-antdv-next/src/views/crm/statistics/performance/index.vue
new file mode 100644
index 000000000..0e029aec1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/performance/index.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/statistics/portrait/chartOptions.ts b/apps/web-antdv-next/src/views/crm/statistics/portrait/chartOptions.ts
new file mode 100644
index 000000000..7088c22c4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/portrait/chartOptions.ts
@@ -0,0 +1,241 @@
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+
+function areaReplace(areaName: string) {
+ if (!areaName) {
+ return areaName;
+ }
+ return areaName
+ .replace('维吾尔自治区', '')
+ .replace('壮族自治区', '')
+ .replace('回族自治区', '')
+ .replace('自治区', '')
+ .replace('省', '');
+}
+
+const getPieTooltip = (extra: Record = {}) => ({
+ trigger: 'item',
+ ...extra,
+});
+
+const getPieLegend = (extra: Record = {}) => ({
+ orient: 'vertical',
+ left: 'left',
+ ...extra,
+});
+
+const getPieSeries = (name: string, data: any[]) => ({
+ name,
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2,
+ },
+ label: {
+ show: false,
+ position: 'center',
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold',
+ },
+ },
+ labelLine: {
+ show: false,
+ },
+ data,
+});
+
+const getPiePanel = ({
+ data,
+ legendExtra,
+ seriesName,
+ title,
+ tooltipExtra,
+}: {
+ data: any[];
+ legendExtra?: Record;
+ seriesName: string;
+ title: string;
+ tooltipExtra?: Record;
+}) => ({
+ title: {
+ text: title,
+ left: 'center',
+ },
+ tooltip: getPieTooltip(tooltipExtra),
+ legend: getPieLegend(legendExtra),
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: title },
+ },
+ },
+ series: [getPieSeries(seriesName, data)],
+});
+
+export function getChartOptions(activeTabName: any, res: any): any {
+ switch (activeTabName) {
+ case 'area': {
+ const data = res.map((item: any) => {
+ return {
+ ...item,
+ areaName: areaReplace(item.areaName),
+ };
+ });
+ let leftMin = 0;
+ let leftMax = 0;
+ let rightMin = 0;
+ let rightMax = 0;
+ data.forEach((item: any) => {
+ leftMin = Math.min(leftMin, item.customerCount || 0);
+ leftMax = Math.max(leftMax, item.customerCount || 0);
+ rightMin = Math.min(rightMin, item.dealCount || 0);
+ rightMax = Math.max(rightMax, item.dealCount || 0);
+ });
+ return {
+ left: {
+ title: {
+ text: '全部客户',
+ left: 'center',
+ },
+ tooltip: {
+ trigger: 'item',
+ showDelay: 0,
+ transitionDuration: 0.2,
+ },
+ visualMap: {
+ text: ['高', '低'],
+ realtime: false,
+ calculable: true,
+ top: 'middle',
+ inRange: {
+ color: ['yellow', 'lightskyblue', 'orangered'],
+ },
+ min: leftMin,
+ max: leftMax,
+ },
+ series: [
+ {
+ name: '客户地域分布',
+ type: 'map',
+ map: 'china',
+ roam: false,
+ selectedMode: false,
+ data: data.map((item: any) => {
+ return {
+ name: item.areaName,
+ value: item.customerCount || 0,
+ };
+ }),
+ },
+ ],
+ },
+ right: {
+ title: {
+ text: '成交客户',
+ left: 'center',
+ },
+ tooltip: {
+ trigger: 'item',
+ showDelay: 0,
+ transitionDuration: 0.2,
+ },
+ visualMap: {
+ text: ['高', '低'],
+ realtime: false,
+ calculable: true,
+ top: 'middle',
+ inRange: {
+ color: ['yellow', 'lightskyblue', 'orangered'],
+ },
+ min: rightMin,
+ max: rightMax,
+ },
+ series: [
+ {
+ name: '客户地域分布',
+ type: 'map',
+ map: 'china',
+ roam: false,
+ selectedMode: false,
+ data: data.map((item: any) => {
+ return {
+ name: item.areaName,
+ value: item.dealCount || 0,
+ };
+ }),
+ },
+ ],
+ },
+ };
+ }
+ case 'industry': {
+ return {
+ left: getPiePanel({
+ title: '全部客户',
+ seriesName: '全部客户',
+ data: res.map((r: any) => ({
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
+ value: r.customerCount,
+ })),
+ }),
+ right: getPiePanel({
+ title: '成交客户',
+ seriesName: '成交客户',
+ data: res.map((r: any) => ({
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
+ value: r.dealCount,
+ })),
+ }),
+ };
+ }
+ case 'level': {
+ return {
+ left: getPiePanel({
+ title: '全部客户',
+ seriesName: '全部客户',
+ data: res.map((r: any) => ({
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
+ value: r.customerCount,
+ })),
+ }),
+ right: getPiePanel({
+ title: '成交客户',
+ seriesName: '成交客户',
+ data: res.map((r: any) => ({
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
+ value: r.dealCount,
+ })),
+ }),
+ };
+ }
+ case 'source': {
+ return {
+ left: getPiePanel({
+ title: '全部客户',
+ seriesName: '全部客户',
+ data: res.map((r: any) => ({
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
+ value: r.customerCount,
+ })),
+ }),
+ right: getPiePanel({
+ title: '成交客户',
+ seriesName: '成交客户',
+ data: res.map((r: any) => ({
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
+ value: r.dealCount,
+ })),
+ }),
+ };
+ }
+ default: {
+ return {};
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/portrait/data.ts b/apps/web-antdv-next/src/views/crm/statistics/portrait/data.ts
new file mode 100644
index 000000000..18e22e767
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/portrait/data.ts
@@ -0,0 +1,200 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { useUserStore } from '@vben/stores';
+import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
+
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+const userStore = useUserStore();
+
+export const customerSummaryTabs = [
+ {
+ tab: '城市分布分析',
+ key: 'area',
+ },
+ {
+ tab: '客户级别分析',
+ key: 'level',
+ },
+ {
+ tab: '客户来源分析',
+ key: 'source',
+ },
+ {
+ tab: '客户行业分析',
+ key: 'industry',
+ },
+];
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'times',
+ label: '时间范围',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ defaultValue: [
+ formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
+ formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
+ ],
+ },
+ {
+ fieldName: 'deptId',
+ label: '归属部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ treeDefaultExpandAll: true,
+ placeholder: '请选择归属部门',
+ },
+ defaultValue: userStore.userInfo?.deptId,
+ },
+ {
+ fieldName: 'userId',
+ label: '员工',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择员工',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ activeTabName: any,
+): VxeTableGridOptions['columns'] {
+ switch (activeTabName) {
+ case 'industry': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'industryId',
+ title: '客户行业',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
+ },
+ },
+ {
+ field: 'customerCount',
+ title: '客户个数',
+ minWidth: 200,
+ },
+ {
+ field: 'dealCount',
+ title: '成交个数',
+ minWidth: 200,
+ },
+ {
+ field: 'industryPortion',
+ title: '行业占比(%)',
+ minWidth: 200,
+ },
+ {
+ field: 'dealPortion',
+ title: '成交占比(%)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'level': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'level',
+ title: '客户级别',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
+ },
+ },
+ {
+ field: 'customerCount',
+ title: '客户个数',
+ minWidth: 200,
+ },
+ {
+ field: 'dealCount',
+ title: '成交个数',
+ minWidth: 200,
+ },
+ {
+ field: 'industryPortion',
+ title: '行业占比(%)',
+ minWidth: 200,
+ },
+ {
+ field: 'dealPortion',
+ title: '成交占比(%)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'source': {
+ return [
+ {
+ type: 'seq',
+ title: '序号',
+ },
+ {
+ field: 'source',
+ title: '客户来源',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
+ },
+ },
+ {
+ field: 'customerCount',
+ title: '客户个数',
+ minWidth: 200,
+ },
+ {
+ field: 'dealCount',
+ title: '成交个数',
+ minWidth: 200,
+ },
+ {
+ field: 'industryPortion',
+ title: '行业占比(%)',
+ minWidth: 200,
+ },
+ {
+ field: 'dealPortion',
+ title: '成交占比(%)',
+ minWidth: 200,
+ },
+ ];
+ }
+ default: {
+ return [];
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/portrait/index.vue b/apps/web-antdv-next/src/views/crm/statistics/portrait/index.vue
new file mode 100644
index 000000000..b75518b6b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/portrait/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/crm/statistics/rank/chartOptions.ts b/apps/web-antdv-next/src/views/crm/statistics/rank/chartOptions.ts
new file mode 100644
index 000000000..aceffbc65
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/rank/chartOptions.ts
@@ -0,0 +1,346 @@
+import { cloneDeep } from '@vben/utils';
+
+const getLegend = (extra: Record = {}) => ({
+ top: 10,
+ ...extra,
+});
+
+const getGrid = (extra: Record = {}) => ({
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ ...extra,
+});
+
+const getTooltip = () => ({
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+});
+
+export function getChartOptions(activeTabName: any, res: any): any {
+ switch (activeTabName) {
+ case 'contactCountRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '新增联系人数排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '新增联系人数排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '新增联系人数(个)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '创建人',
+ },
+ };
+ }
+ case 'contractCountRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '签约合同排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '签约合同排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '签约合同数(个)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '签订人',
+ },
+ };
+ }
+ case 'contractPriceRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '合同金额排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '合同金额排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '合同金额(元)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '签订人',
+ },
+ };
+ }
+ case 'customerCountRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: getGrid(),
+ legend: getLegend(),
+ series: [
+ {
+ name: '新增客户数排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '新增客户数排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '新增客户数(个)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '创建人',
+ },
+ };
+ }
+ case 'followCountRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [
+ {
+ name: '跟进次数排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '跟进次数排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '跟进次数(次)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '员工',
+ },
+ };
+ }
+ case 'followCustomerCountRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [
+ {
+ name: '跟进客户数排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '跟进客户数排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '跟进客户数(个)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '员工',
+ },
+ };
+ }
+ case 'productSalesRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [
+ {
+ name: '产品销量排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '产品销量排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '产品销量',
+ },
+ yAxis: {
+ type: 'category',
+ name: '员工',
+ },
+ };
+ }
+ case 'receivablePriceRank': {
+ return {
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: cloneDeep(res).toReversed(),
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [
+ {
+ name: '回款金额排行',
+ type: 'bar',
+ },
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false, // 数据区域缩放:Y 轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '回款金额排行' }, // 保存为图片
+ },
+ },
+ tooltip: getTooltip(),
+ xAxis: {
+ type: 'value',
+ name: '回款金额(元)',
+ },
+ yAxis: {
+ type: 'category',
+ name: '签订人',
+ nameGap: 30,
+ },
+ };
+ }
+ default: {
+ return {};
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/rank/data.ts b/apps/web-antdv-next/src/views/crm/statistics/rank/data.ts
new file mode 100644
index 000000000..b60d90dce
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/rank/data.ts
@@ -0,0 +1,277 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { useUserStore } from '@vben/stores';
+import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
+
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getRangePickerDefaultProps } from '#/utils';
+
+const userStore = useUserStore();
+
+export const customerSummaryTabs = [
+ {
+ tab: '合同金额排行',
+ key: 'contractPriceRank',
+ },
+ {
+ tab: '回款金额排行',
+ key: 'receivablePriceRank',
+ },
+ {
+ tab: '签约合同排行',
+ key: 'contractCountRank',
+ },
+ {
+ tab: '产品销量排行',
+ key: 'productSalesRank',
+ },
+ {
+ tab: '新增客户数排行',
+ key: 'customerCountRank',
+ },
+ {
+ tab: '新增联系人数排行',
+ key: 'contactCountRank',
+ },
+ {
+ tab: '跟进次数排行',
+ key: 'followCountRank',
+ },
+ {
+ tab: '跟进客户数排行',
+ key: 'followCustomerCountRank',
+ },
+];
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'times',
+ label: '时间范围',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ defaultValue: [
+ formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
+ formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
+ ],
+ },
+ {
+ fieldName: 'deptId',
+ label: '归属部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ treeDefaultExpandAll: true,
+ placeholder: '请选择归属部门',
+ },
+ defaultValue: userStore.userInfo?.deptId,
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ activeTabName: any,
+): VxeTableGridOptions['columns'] {
+ switch (activeTabName) {
+ case 'contactCountRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '创建人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '新增联系人数(个)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'contractCountRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '签约合同数(个)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'contractPriceRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '合同金额(元)',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ ];
+ }
+ case 'customerCountRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '新增客户数(个)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'followCountRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '跟进次数(次)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'followCustomerCountRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '跟进客户数(个)',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'productSalesRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '产品销量',
+ minWidth: 200,
+ },
+ ];
+ }
+ case 'receivablePriceRank': {
+ return [
+ {
+ type: 'seq',
+ title: '公司排名',
+ },
+ {
+ field: 'nickname',
+ title: '签订人',
+ minWidth: 200,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 200,
+ },
+ {
+ field: 'count',
+ title: '回款金额(元)',
+ minWidth: 200,
+ formatter: 'formatAmount2',
+ },
+ ];
+ }
+ default: {
+ return [];
+ }
+ }
+}
diff --git a/apps/web-antdv-next/src/views/crm/statistics/rank/index.vue b/apps/web-antdv-next/src/views/crm/statistics/rank/index.vue
new file mode 100644
index 000000000..9c4bec2a6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/crm/statistics/rank/index.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/dashboard/workspace/index.vue b/apps/web-antdv-next/src/views/dashboard/workspace/index.vue
index b95d61381..8f6620310 100644
--- a/apps/web-antdv-next/src/views/dashboard/workspace/index.vue
+++ b/apps/web-antdv-next/src/views/dashboard/workspace/index.vue
@@ -30,58 +30,58 @@ const userStore = useUserStore();
// 例如:url: /dashboard/workspace
const projectItems: WorkbenchProjectItem[] = [
{
- color: '',
- content: '不要等待机会,而要创造机会。',
- date: '2021-04-01',
- group: '开源组',
- icon: 'carbon:logo-github',
- title: 'Github',
- url: 'https://github.com',
+ color: '#6DB33F',
+ content: 'github.com/YunaiV/ruoyi-vue-pro',
+ date: '2025-01-02',
+ group: 'Spring Boot 单体架构',
+ icon: 'simple-icons:springboot',
+ title: 'ruoyi-vue-pro',
+ url: 'https://github.com/YunaiV/ruoyi-vue-pro',
},
{
- color: '#3fb27f',
- content: '现在的你决定将来的你。',
- date: '2021-04-01',
- group: '算法组',
- icon: 'ion:logo-vue',
- title: 'Vue',
- url: 'https://vuejs.org',
+ color: '#409EFF',
+ content: 'github.com/yudaocode/yudao-ui-admin-vue3',
+ date: '2025-02-03',
+ group: 'Vue3 + element-plus 管理后台',
+ icon: 'ep:element-plus',
+ title: 'yudao-ui-admin-vue3',
+ url: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
+ },
+ {
+ color: '#ff4d4f',
+ content: 'github.com/yudaocode/yudao-mall-uniapp',
+ date: '2025-03-04',
+ group: 'Vue3 + uniapp 商城手机端',
+ icon: 'icon-park-outline:mall-bag',
+ title: 'yudao-mall-uniapp',
+ url: 'https://github.com/yudaocode/yudao-mall-uniapp',
+ },
+ {
+ color: '#1890ff',
+ content: 'github.com/YunaiV/yudao-cloud',
+ date: '2025-04-05',
+ group: 'Spring Cloud 微服务架构',
+ icon: 'material-symbols:cloud-outline',
+ title: 'yudao-cloud',
+ url: 'https://github.com/YunaiV/yudao-cloud',
},
{
color: '#e18525',
- content: '没有什么才能比努力更重要。',
- date: '2021-04-01',
- group: '上班摸鱼',
- icon: 'ion:logo-html5',
- title: 'Html5',
- url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
+ content: 'github.com/yudaocode/yudao-ui-admin-vben',
+ date: '2025-05-06',
+ group: 'Vue3 + vben5(antd) 管理后台',
+ icon: 'devicon:antdesign',
+ title: 'yudao-ui-admin-vben',
+ url: 'https://github.com/yudaocode/yudao-ui-admin-vben',
},
{
- color: '#bf0c2c',
- content: '热情和欲望可以突破一切难关。',
- date: '2021-04-01',
- group: 'UI',
- icon: 'ion:logo-angular',
- title: 'Angular',
- url: 'https://angular.io',
- },
- {
- color: '#00d8ff',
- content: '健康的身体是实现目标的基石。',
- date: '2021-04-01',
- group: '技术牛',
- icon: 'bx:bxl-react',
- title: 'React',
- url: 'https://reactjs.org',
- },
- {
- color: '#EBD94E',
- content: '路是走出来的,而不是空想出来的。',
- date: '2021-04-01',
- group: '架构组',
- icon: 'ion:logo-javascript',
- title: 'Js',
- url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
+ color: '#2979ff',
+ content: 'github.com/yudaocode/yudao-ui-admin-uniapp',
+ date: '2025-06-01',
+ group: 'Vue3 + uniapp 管理手机端',
+ icon: 'ant-design:mobile',
+ title: 'yudao-ui-admin-uniapp',
+ url: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
},
];
@@ -94,67 +94,61 @@ const quickNavItems: WorkbenchQuickNavItem[] = [
url: '/',
},
{
- color: '#bf0c2c',
- icon: 'ion:grid-outline',
- title: '仪表盘',
- url: '/dashboard',
+ color: '#ff6b6b',
+ icon: 'lucide:shopping-bag',
+ title: '商城中心',
+ url: '/mall',
},
{
- color: '#e18525',
- icon: 'ion:layers-outline',
- title: '组件',
- url: '/demos/features/icons',
+ color: '#7c3aed',
+ icon: 'tabler:ai',
+ title: 'AI 大模型',
+ url: '/ai',
},
{
color: '#3fb27f',
- icon: 'ion:settings-outline',
- title: '系统管理',
- url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
+ icon: 'simple-icons:erpnext',
+ title: 'ERP 系统',
+ url: '/erp',
},
{
color: '#4daf1bc9',
- icon: 'ion:key-outline',
- title: '权限管理',
- url: '/demos/access/page-control',
+ icon: 'simple-icons:civicrm',
+ title: 'CRM 系统',
+ url: '/crm',
},
{
- color: '#00d8ff',
- icon: 'ion:bar-chart-outline',
- title: '图表',
- url: '/analytics',
+ color: '#1a73e8',
+ icon: 'fa-solid:hdd',
+ title: 'IoT 物联网',
+ url: '/iot',
},
];
const todoItems = ref([
{
completed: false,
- content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
- date: '2024-07-30 11:00:00',
- title: '审查前端代码提交',
- },
- {
- completed: true,
- content: `检查并优化系统性能,降低CPU使用率。`,
- date: '2024-07-30 11:00:00',
- title: '系统性能优化',
+ content: `系统支持 JDK 8/17/21,Vue 2/3`,
+ date: '2024-07-15 09:30:00',
+ title: '技术兼容性',
},
{
completed: false,
- content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
- date: '2024-07-30 11:00:00',
- title: '安全检查',
+ content: `后端提供 Spring Boot 2.7/3.2 + Cloud 双架构`,
+ date: '2024-08-30 14:20:00',
+ title: '架构灵活性',
},
{
completed: false,
- content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
- date: '2024-07-30 11:00:00',
- title: '更新项目依赖',
+ content: `全部开源,个人与企业可 100% 直接使用,无需授权`,
+ date: '2024-07-25 16:45:00',
+ title: '开源免授权',
},
{
completed: false,
- content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
- date: '2024-07-30 11:00:00',
- title: '修复UI显示问题',
+ content: `国内使用最广泛的快速开发平台,远超 10w+ 企业使用`,
+ date: '2024-07-10 11:15:00',
+ title: '广泛企业认可',
},
]);
const trendItems: WorkbenchTrendItem[] = [
@@ -239,7 +233,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
>
- 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+ 早安, {{ userStore.userInfo?.nickname }}, 开始您一天的工作吧!
今日晴,20℃ - 32℃!
diff --git a/apps/web-antdv-next/src/views/demos/antd/index.vue b/apps/web-antdv-next/src/views/demos/antd/index.vue
deleted file mode 100644
index 6fb1998d6..000000000
--- a/apps/web-antdv-next/src/views/demos/antd/index.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/web-antdv-next/src/views/erp/finance/account/data.ts b/apps/web-antdv-next/src/views/erp/finance/account/data.ts
new file mode 100644
index 000000000..0b4495b1a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/account/data.ts
@@ -0,0 +1,190 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpAccountApi } from '#/api/erp/finance/account';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入名称',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入排序',
+ precision: 0,
+ },
+ rules: 'required',
+ defaultValue: 0,
+ },
+ {
+ fieldName: 'defaultStatus',
+ label: '是否默认',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ {
+ label: '是',
+ value: true,
+ },
+ {
+ label: '否',
+ value: false,
+ },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.boolean().default(false).optional(),
+ },
+ {
+ fieldName: 'no',
+ label: '编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入编码',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入编码',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onDefaultStatusChange?: (
+ newStatus: boolean,
+ row: ErpAccountApi.Account,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '名称',
+ minWidth: 150,
+ },
+ {
+ field: 'no',
+ title: '编码',
+ minWidth: 120,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'defaultStatus',
+ title: '是否默认',
+ minWidth: 100,
+ cellRender: {
+ attrs: { beforeChange: onDefaultStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: true,
+ unCheckedValue: false,
+ },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/finance/account/index.vue b/apps/web-antdv-next/src/views/erp/finance/account/index.vue
new file mode 100644
index 000000000..4ceb7abba
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/account/index.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/account/modules/form.vue b/apps/web-antdv-next/src/views/erp/finance/account/modules/form.vue
new file mode 100644
index 000000000..ef413e9ce
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/account/modules/form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/payment/data.ts b/apps/web-antdv-next/src/views/erp/finance/payment/data.ts
new file mode 100644
index 000000000..f4a9a61a9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/payment/data.ts
@@ -0,0 +1,586 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '付款单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'paymentTime',
+ label: '付款时间',
+ component: 'DatePicker',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '选择付款时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'financeUserId',
+ label: '财务人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择财务人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '采购入库、退货单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'accountId',
+ label: '付款账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择付款账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '合计付款',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '合计付款',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '优惠金额',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请输入优惠金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ },
+ },
+ {
+ fieldName: 'paymentPrice',
+ label: '实际付款',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '实际付款',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalPrice', 'discountPrice'],
+ componentProps: (values) => {
+ const totalPrice = values.totalPrice || 0;
+ const discountPrice = values.discountPrice || 0;
+ values.paymentPrice = totalPrice - discountPrice;
+ return {};
+ },
+ },
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'bizNo',
+ title: '采购单据编号',
+ minWidth: 200,
+ },
+ {
+ field: 'totalPrice',
+ title: '应付金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'paidPrice',
+ title: '已付金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'paymentPrice',
+ title: '本次付款',
+ minWidth: 115,
+ fixed: 'right',
+ slots: { default: 'paymentPrice' },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '付款单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入付款单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'paymentTime',
+ label: '付款时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'financeUserId',
+ label: '财务人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择财务人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '付款账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择付款账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bizNo',
+ label: '采购单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入采购单号',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '付款单号',
+ width: 180,
+ fixed: 'left',
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'paymentTime',
+ title: '付款时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'financeUserName',
+ title: '财务人员',
+ minWidth: 120,
+ },
+ {
+ field: 'accountName',
+ title: '付款账户',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '合计付款',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'discountPrice',
+ title: '优惠金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'paymentPrice',
+ title: '实际付款',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 采购入库单选择表单的配置项 */
+export function usePurchaseInGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '入库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入入库单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '已自动选择供应商',
+ },
+ },
+ {
+ fieldName: 'paymentStatus',
+ label: '付款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未付款', value: 0 },
+ { label: '部分付款', value: 1 },
+ { label: '全部付款', value: 2 },
+ ],
+ placeholder: '请选择付款状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 采购入库单选择列表的字段 */
+export function usePurchaseInGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '入库单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'inTime',
+ title: '入库时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'totalPrice',
+ title: '应付金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'paymentPrice',
+ title: '已付金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unPaymentPrice',
+ title: '未付金额',
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(row.totalPrice - row.paymentPrice || 0);
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ ];
+}
+
+/** 采购退货单选择表单的配置项 */
+export function useSaleReturnGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '退货单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入退货单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '已自动选择供应商',
+ },
+ },
+ {
+ fieldName: 'refundStatus',
+ label: '退款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未退款', value: 0 },
+ { label: '部分退款', value: 1 },
+ { label: '全部退款', value: 2 },
+ ],
+ placeholder: '请选择退款状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 采购退货单选择列表的字段 */
+export function useSaleReturnGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '退货单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'returnTime',
+ title: '退货时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'totalPrice',
+ title: '应退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'refundPrice',
+ title: '已退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unRefundPrice',
+ title: '未退金额',
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(row.totalPrice - row.refundPrice || 0);
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/finance/payment/index.vue b/apps/web-antdv-next/src/views/erp/finance/payment/index.vue
new file mode 100644
index 000000000..03c88b777
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/payment/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/payment/modules/form.vue b/apps/web-antdv-next/src/views/erp/finance/payment/modules/form.vue
new file mode 100644
index 000000000..767bd13b2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/payment/modules/form.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/payment/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/finance/payment/modules/item-form.vue
new file mode 100644
index 000000000..3b2289bb7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/payment/modules/item-form.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
合计:
+
+
+ 合计付款:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+ 已付金额:{{ erpPriceInputFormatter(summaries.paidPrice) }}
+
+
+ 本次付款:
+ {{ erpPriceInputFormatter(summaries.paymentPrice) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/payment/modules/purchase-in-select.vue b/apps/web-antdv-next/src/views/erp/finance/payment/modules/purchase-in-select.vue
new file mode 100644
index 000000000..ea4e118e8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/payment/modules/purchase-in-select.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/payment/modules/sale-return-select.vue b/apps/web-antdv-next/src/views/erp/finance/payment/modules/sale-return-select.vue
new file mode 100644
index 000000000..a68cb945c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/payment/modules/sale-return-select.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/receipt/data.ts b/apps/web-antdv-next/src/views/erp/finance/receipt/data.ts
new file mode 100644
index 000000000..774ddb0b1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/receipt/data.ts
@@ -0,0 +1,586 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '收款单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'receiptTime',
+ label: '收款时间',
+ component: 'DatePicker',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '选择收款时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'financeUserId',
+ label: '财务人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择财务人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '销售出库、退货单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'accountId',
+ label: '收款账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择收款账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '合计收款',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '合计收款',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '优惠金额',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请输入优惠金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ },
+ },
+ {
+ fieldName: 'receiptPrice',
+ label: '实际收款',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '实际收款',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalPrice', 'discountPrice'],
+ componentProps: (values) => {
+ const totalPrice = values.totalPrice || 0;
+ const discountPrice = values.discountPrice || 0;
+ values.receiptPrice = totalPrice - discountPrice;
+ return {};
+ },
+ },
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'bizNo',
+ title: '销售单据编号',
+ minWidth: 200,
+ },
+ {
+ field: 'totalPrice',
+ title: '应收金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'receiptedPrice',
+ title: '已收金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'receiptPrice',
+ title: '本次收款',
+ minWidth: 115,
+ fixed: 'right',
+ slots: { default: 'receiptPrice' },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '收款单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入收款单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'receiptTime',
+ label: '收款时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'financeUserId',
+ label: '财务人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择财务人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '收款账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择收款账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bizNo',
+ label: '销售单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入销售单号',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '收款单号',
+ width: 180,
+ fixed: 'left',
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'receiptTime',
+ title: '收款时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'financeUserName',
+ title: '财务人员',
+ minWidth: 120,
+ },
+ {
+ field: 'accountName',
+ title: '收款账户',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '合计收款',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'discountPrice',
+ title: '优惠金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'receiptPrice',
+ title: '实际收款',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 销售出库单选择表单的配置项 */
+export function useSaleOutGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '出库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入出库单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '已自动选择客户',
+ },
+ },
+ {
+ fieldName: 'receiptStatus',
+ label: '收款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未收款', value: 0 },
+ { label: '部分收款', value: 1 },
+ { label: '全部收款', value: 2 },
+ ],
+ placeholder: '请选择收款状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 销售出库单选择列表的字段 */
+export function useSaleOutGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '出库单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'outTime',
+ title: '出库时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'totalPrice',
+ title: '应收金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'receiptPrice',
+ title: '已收金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unReceiptPrice',
+ title: '未收金额',
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(row.totalPrice - row.receiptPrice || 0);
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ ];
+}
+
+/** 销售退货单选择表单的配置项 */
+export function useSaleReturnGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '退货单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入退货单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '已自动选择客户',
+ },
+ },
+ {
+ fieldName: 'refundStatus',
+ label: '退款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未退款', value: 0 },
+ { label: '部分退款', value: 1 },
+ { label: '全部退款', value: 2 },
+ ],
+ placeholder: '请选择退款状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 销售退货单选择列表的字段 */
+export function useSaleReturnGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '退货单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'returnTime',
+ title: '退货时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'totalPrice',
+ title: '应退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'refundPrice',
+ title: '已退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unRefundPrice',
+ title: '未退金额',
+ formatter: ({ row }) => {
+ return erpPriceInputFormatter(row.totalPrice - row.refundPrice || 0);
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/finance/receipt/index.vue b/apps/web-antdv-next/src/views/erp/finance/receipt/index.vue
new file mode 100644
index 000000000..258de73fb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/receipt/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/receipt/modules/form.vue b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/form.vue
new file mode 100644
index 000000000..f26f53ea5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/form.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/receipt/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/item-form.vue
new file mode 100644
index 000000000..5563f5252
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/item-form.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
合计:
+
+
+ 合计收款:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+ 已收金额:{{ erpPriceInputFormatter(summaries.receiptedPrice) }}
+
+
+ 本次收款:
+ {{ erpPriceInputFormatter(summaries.receiptPrice) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/receipt/modules/sale-out-select.vue b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/sale-out-select.vue
new file mode 100644
index 000000000..21e793c8c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/sale-out-select.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/finance/receipt/modules/sale-return-select.vue b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/sale-return-select.vue
new file mode 100644
index 000000000..3ebd3f2d3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/finance/receipt/modules/sale-return-select.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/home/index.vue b/apps/web-antdv-next/src/views/erp/home/index.vue
new file mode 100644
index 000000000..efb49045b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/home/index.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/home/modules/summary-card.vue b/apps/web-antdv-next/src/views/erp/home/modules/summary-card.vue
new file mode 100644
index 000000000..ff98e556a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/home/modules/summary-card.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/home/modules/time-summary-chart.vue b/apps/web-antdv-next/src/views/erp/home/modules/time-summary-chart.vue
new file mode 100644
index 000000000..ef15eaaf1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/home/modules/time-summary-chart.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/product/category/data.ts b/apps/web-antdv-next/src/views/erp/product/category/data.ts
new file mode 100644
index 000000000..283566a25
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/category/data.ts
@@ -0,0 +1,149 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpProductCategoryApi } from '#/api/erp/product/category';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getProductCategoryList } from '#/api/erp/product/category';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '上级分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getProductCategoryList();
+ data.unshift({
+ id: 0,
+ name: '顶级分类',
+ });
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择上级分类',
+ treeDefaultExpandAll: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'code',
+ label: '分类编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类编码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 查询表单 */
+export function useQueryFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '分类名称',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ allowClear: true,
+ },
+ },
+ {
+ component: 'Select',
+ fieldName: 'status',
+ label: '开启状态',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择开启状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '分类名称',
+ align: 'left',
+ treeNode: true,
+ },
+ {
+ field: 'code',
+ title: '分类编码',
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ },
+ {
+ field: 'status',
+ title: '分类状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/product/category/index.vue b/apps/web-antdv-next/src/views/erp/product/category/index.vue
new file mode 100644
index 000000000..cc22cb7a1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/category/index.vue
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/product/category/modules/form.vue b/apps/web-antdv-next/src/views/erp/product/category/modules/form.vue
new file mode 100644
index 000000000..a8c6be623
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/category/modules/form.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/product/product/data.ts b/apps/web-antdv-next/src/views/erp/product/product/data.ts
new file mode 100644
index 000000000..23664dd5e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/product/data.ts
@@ -0,0 +1,250 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getProductCategorySimpleList } from '#/api/erp/product/category';
+import { getProductUnitSimpleList } from '#/api/erp/product/unit';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入名称',
+ },
+ },
+ {
+ fieldName: 'barCode',
+ label: '条码',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入条码',
+ },
+ },
+ {
+ fieldName: 'categoryId',
+ label: '分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getProductCategorySimpleList();
+ return handleTree(data);
+ },
+
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择分类',
+ treeDefaultExpandAll: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'unitId',
+ label: '单位',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getProductUnitSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择单位',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'standard',
+ label: '规格',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入规格',
+ },
+ },
+ {
+ fieldName: 'expiryDay',
+ label: '保质期天数',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入保质期天数',
+ },
+ },
+ {
+ fieldName: 'weight',
+ label: '重量(kg)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入重量(kg)',
+ },
+ },
+ {
+ fieldName: 'purchasePrice',
+ label: '采购价格',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入采购价格,单位:元',
+ precision: 2,
+ min: 0,
+ step: 0.01,
+ },
+ },
+ {
+ fieldName: 'salePrice',
+ label: '销售价格',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入销售价格,单位:元',
+ precision: 2,
+ min: 0,
+ step: 0.01,
+ },
+ },
+ {
+ fieldName: 'minPrice',
+ label: '最低价格',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入最低价格,单位:元',
+ precision: 2,
+ min: 0,
+ step: 0.01,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'categoryId',
+ label: '分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getProductCategorySimpleList();
+ return handleTree(data);
+ },
+
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择分类',
+ treeDefaultExpandAll: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'barCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名称',
+ minWidth: 200,
+ },
+ {
+ field: 'standard',
+ title: '规格',
+ minWidth: 100,
+ },
+ {
+ field: 'categoryName',
+ title: '分类',
+ minWidth: 120,
+ },
+ {
+ field: 'unitName',
+ title: '单位',
+ minWidth: 100,
+ },
+ {
+ field: 'purchasePrice',
+ title: '采购价格',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'salePrice',
+ title: '销售价格',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'minPrice',
+ title: '最低价格',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/product/product/index.vue b/apps/web-antdv-next/src/views/erp/product/product/index.vue
new file mode 100644
index 000000000..4b96bc300
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/product/index.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/product/product/modules/form.vue b/apps/web-antdv-next/src/views/erp/product/product/modules/form.vue
new file mode 100644
index 000000000..5f39d9935
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/product/modules/form.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/product/unit/data.ts b/apps/web-antdv-next/src/views/erp/product/unit/data.ts
new file mode 100644
index 000000000..7b7b1a3a2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/unit/data.ts
@@ -0,0 +1,103 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '单位名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入单位名称',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '单位状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '单位名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入单位名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '单位状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择单位状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '单位编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '单位名称',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '单位状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/product/unit/index.vue b/apps/web-antdv-next/src/views/erp/product/unit/index.vue
new file mode 100644
index 000000000..3a3feb314
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/unit/index.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/product/unit/modules/form.vue b/apps/web-antdv-next/src/views/erp/product/unit/modules/form.vue
new file mode 100644
index 000000000..eac42a05a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/product/unit/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/in/data.ts b/apps/web-antdv-next/src/views/erp/purchase/in/data.ts
new file mode 100644
index 000000000..2334ad260
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/in/data.ts
@@ -0,0 +1,613 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpNumberFormatter, erpPriceInputFormatter } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '入库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'inTime',
+ label: '入库时间',
+ component: 'DatePicker',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '选择入库时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ formItemClass: 'col-span-1',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请选择关联订单',
+ disabled: formType === 'detail',
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ disabled: true,
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '入库产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入优惠率',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '付款优惠',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '付款优惠',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'discountedPrice',
+ label: '优惠后金额',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '优惠后金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalPrice', 'otherPrice'],
+ componentProps: (values) => {
+ const totalPrice = values.totalPrice || 0;
+ const otherPrice = values.otherPrice || 0;
+ values.discountedPrice = totalPrice - otherPrice;
+ return {};
+ },
+ },
+ },
+ {
+ fieldName: 'otherPrice',
+ label: '其他费用',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请输入其他费用',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '应付金额',
+ component: 'InputNumber',
+ componentProps: {
+ precision: 2,
+ min: 0,
+ disabled: true,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ formData?: any[],
+ disabled?: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 200,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'totalCount',
+ title: '原数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.inCount !== undefined,
+ },
+ {
+ field: 'inCount',
+ title: '已入库',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.returnCount !== undefined,
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ fixed: 'right',
+ minWidth: 120,
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalProductPrice',
+ fixed: 'right',
+ title: '产品金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ fixed: 'right',
+ field: 'taxPercent',
+ title: '税率(%)',
+ minWidth: 105,
+ slots: { default: 'taxPercent' },
+ },
+ {
+ fixed: 'right',
+ field: 'taxPrice',
+ title: '税额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'totalPrice',
+ fixed: 'right',
+ title: '合计金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '入库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入入库单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'inTime',
+ label: '入库时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入关联订单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'paymentStatus',
+ label: '付款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未付款', value: 0 },
+ { label: '部分付款', value: 1 },
+ { label: '全部付款', value: 2 },
+ ],
+ placeholder: '请选择付款状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '审批状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择审批状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '入库单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'inTime',
+ title: '入库时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '应付金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'paymentPrice',
+ title: '已付金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unPaymentPrice',
+ title: '未付金额',
+ formatter: ({ row }) => {
+ return `${erpNumberFormatter(row.totalPrice - row.paymentPrice, 2)}元`;
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useOrderGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useOrderGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'radio',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'orderTime',
+ title: '订单时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'inCount',
+ title: '入库数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额合计',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '含税金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/purchase/in/index.vue b/apps/web-antdv-next/src/views/erp/purchase/in/index.vue
new file mode 100644
index 000000000..fb87f1bfa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/in/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/in/modules/form.vue b/apps/web-antdv-next/src/views/erp/purchase/in/modules/form.vue
new file mode 100644
index 000000000..7aa3d5cf9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/in/modules/form.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/in/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/purchase/in/modules/item-form.vue
new file mode 100644
index 000000000..81f33b230
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/in/modules/item-form.vue
@@ -0,0 +1,294 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+ {{ row.taxPercent || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
+
+ 税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}
+
+ 税额合计:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/in/modules/purchase-order-select.vue b/apps/web-antdv-next/src/views/erp/purchase/in/modules/purchase-order-select.vue
new file mode 100644
index 000000000..5e24a9b4a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/in/modules/purchase-order-select.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
!disabled && (open = true)"
+ >
+
+
+ !disabled && (open = true)"
+ />
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/order/data.ts b/apps/web-antdv-next/src/views/erp/purchase/order/data.ts
new file mode 100644
index 000000000..a8cb484e7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/order/data.ts
@@ -0,0 +1,445 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '选择订单时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ label: '供应商',
+ fieldName: 'supplierId',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '采购产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入优惠率',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '付款优惠',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '付款优惠',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '优惠后金额',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '优惠后金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入支付订金',
+ precision: 2,
+ min: 0,
+ },
+ fieldName: 'depositPrice',
+ label: '支付订金',
+ rules: z.number().min(0).optional(),
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'taxPercent',
+ title: '税率(%)',
+ minWidth: 105,
+ fixed: 'right',
+ slots: { default: 'taxPercent' },
+ },
+ {
+ field: 'taxPrice',
+ title: '税额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'totalPrice',
+ title: '税额合计',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'inStatus',
+ label: '入库状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未入库', value: 0 },
+ { label: '部分入库', value: 1 },
+ { label: '全部入库', value: 2 },
+ ],
+ placeholder: '请选择入库状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'returnStatus',
+ label: '退货状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未退货', value: 0 },
+ { label: '部分退货', value: 1 },
+ { label: '全部退货', value: 2 },
+ ],
+ placeholder: '请选择退货状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'orderTime',
+ title: '订单时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'inCount',
+ title: '入库数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'returnCount',
+ title: '退货数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额合计',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '含税金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'depositPrice',
+ title: '支付订金',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/purchase/order/index.vue b/apps/web-antdv-next/src/views/erp/purchase/order/index.vue
new file mode 100644
index 000000000..0f34f55df
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/order/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/order/modules/form.vue b/apps/web-antdv-next/src/views/erp/purchase/order/modules/form.vue
new file mode 100644
index 000000000..cdd036f62
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/order/modules/form.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/order/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/purchase/order/modules/item-form.vue
new file mode 100644
index 000000000..106ebd322
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/order/modules/item-form.vue
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+ {{ row.taxPercent || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
+
+ 税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}
+
+ 税额合计:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/return/data.ts b/apps/web-antdv-next/src/views/erp/purchase/return/data.ts
new file mode 100644
index 000000000..ca3697d87
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/return/data.ts
@@ -0,0 +1,606 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpNumberFormatter, erpPriceInputFormatter } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '退货单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'returnTime',
+ label: '退货时间',
+ component: 'DatePicker',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '选择退货时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ formItemClass: 'col-span-1',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请选择关联订单',
+ disabled: formType === 'detail',
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ disabled: true,
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '退货产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入优惠率',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '退款优惠',
+ component: 'InputNumber',
+ componentProps: {
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'discountedPrice',
+ label: '优惠后金额',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '优惠后金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalPrice', 'otherPrice'],
+ componentProps: (values) => {
+ const totalPrice = values.totalPrice || 0;
+ const otherPrice = values.otherPrice || 0;
+ values.discountedPrice = totalPrice - otherPrice;
+ return {};
+ },
+ },
+ },
+ {
+ fieldName: 'otherPrice',
+ label: '其他费用',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请输入其他费用',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '应退金额',
+ component: 'InputNumber',
+ componentProps: {
+ precision: 2,
+ min: 0,
+ disabled: true,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ formData?: any[],
+ disabled?: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 200,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'inCount',
+ title: '已入库',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.inCount !== undefined,
+ },
+ {
+ field: 'returnCount',
+ title: '已退货',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.returnCount !== undefined,
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ fixed: 'right',
+ minWidth: 120,
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalProductPrice',
+ fixed: 'right',
+ title: '产品金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ fixed: 'right',
+ field: 'taxPercent',
+ title: '税率(%)',
+ minWidth: 105,
+ slots: { default: 'taxPercent' },
+ },
+ {
+ fixed: 'right',
+ field: 'taxPrice',
+ title: '税额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'totalPrice',
+ fixed: 'right',
+ title: '合计金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '退货单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入退货单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'returnTime',
+ label: '退货时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入关联订单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'refundStatus',
+ label: '退款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未退款', value: 0 },
+ { label: '部分退款', value: 1 },
+ { label: '全部退款', value: 2 },
+ ],
+ placeholder: '请选择退款状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '审批状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择审批状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '退货单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '退货产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'returnTime',
+ title: '退货时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '应退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'refundPrice',
+ title: '已退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unRefundPrice',
+ title: '未退金额',
+ formatter: ({ row }) => {
+ return `${erpNumberFormatter(row.totalPrice - row.refundPrice, 2)}元`;
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useOrderGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useOrderGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'radio',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'orderTime',
+ title: '订单时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'inCount',
+ title: '已入库数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'returnCount',
+ title: '已退货数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额合计',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '含税金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/purchase/return/index.vue b/apps/web-antdv-next/src/views/erp/purchase/return/index.vue
new file mode 100644
index 000000000..aa4c3cc53
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/return/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/return/modules/form.vue b/apps/web-antdv-next/src/views/erp/purchase/return/modules/form.vue
new file mode 100644
index 000000000..65d5834b3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/return/modules/form.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/return/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/purchase/return/modules/item-form.vue
new file mode 100644
index 000000000..afa352131
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/return/modules/item-form.vue
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+ {{ row.taxPercent || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
+
+ 税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}
+
+ 税额合计:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/return/modules/purchase-order-select.vue b/apps/web-antdv-next/src/views/erp/purchase/return/modules/purchase-order-select.vue
new file mode 100644
index 000000000..188ef9116
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/return/modules/purchase-order-select.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
!disabled && (open = true)"
+ >
+
+
+ !disabled && (open = true)"
+ />
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/supplier/data.ts b/apps/web-antdv-next/src/views/erp/purchase/supplier/data.ts
new file mode 100644
index 000000000..a5edc25ef
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/supplier/data.ts
@@ -0,0 +1,232 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '供应商名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入供应商名称',
+ },
+ },
+ {
+ fieldName: 'contact',
+ label: '联系人',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系人',
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '联系电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系电话',
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '电子邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电子邮箱',
+ },
+ },
+ {
+ fieldName: 'fax',
+ label: '传真',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入传真',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'taxNo',
+ label: '纳税人识别号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入纳税人识别号',
+ },
+ },
+ {
+ fieldName: 'taxPercent',
+ label: '税率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入税率',
+ min: 0,
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'bankName',
+ label: '开户行',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入开户行',
+ },
+ },
+ {
+ fieldName: 'bankAccount',
+ label: '开户账号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入开户账号',
+ },
+ },
+ {
+ fieldName: 'bankAddress',
+ label: '开户地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入开户地址',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 3,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '供应商名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入供应商名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '联系电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系电话',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '供应商名称',
+ minWidth: 150,
+ },
+ {
+ field: 'contact',
+ title: '联系人',
+ minWidth: 120,
+ },
+ {
+ field: 'mobile',
+ title: '手机号码',
+ minWidth: 130,
+ },
+ {
+ field: 'telephone',
+ title: '联系电话',
+ minWidth: 130,
+ },
+ {
+ field: 'email',
+ title: '电子邮箱',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ showOverflow: 'tooltip',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/purchase/supplier/index.vue b/apps/web-antdv-next/src/views/erp/purchase/supplier/index.vue
new file mode 100644
index 000000000..e012568b8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/supplier/index.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/purchase/supplier/modules/form.vue b/apps/web-antdv-next/src/views/erp/purchase/supplier/modules/form.vue
new file mode 100644
index 000000000..652c938bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/purchase/supplier/modules/form.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/customer/data.ts b/apps/web-antdv-next/src/views/erp/sale/customer/data.ts
new file mode 100644
index 000000000..b8327e0fb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/customer/data.ts
@@ -0,0 +1,239 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '客户名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入客户名称',
+ },
+ },
+ {
+ fieldName: 'contact',
+ label: '联系人',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系人',
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ },
+ },
+ {
+ fieldName: 'telephone',
+ label: '联系电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系电话',
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '电子邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入电子邮箱',
+ },
+ },
+ {
+ fieldName: 'fax',
+ label: '传真',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入传真',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入排序',
+ precision: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'taxNo',
+ label: '纳税人识别号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入纳税人识别号',
+ },
+ },
+ {
+ fieldName: 'taxPercent',
+ label: '税率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入税率',
+ precision: 2,
+ },
+ rules: z.number().min(0).max(100).optional(),
+ },
+ {
+ fieldName: 'bankName',
+ label: '开户行名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入开户行名称',
+ },
+ },
+ {
+ fieldName: 'bankAccount',
+ label: '开户行账号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入开户行账号',
+ },
+ },
+ {
+ fieldName: 'bankAddress',
+ label: '开户行地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入开户行地址',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 3,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '客户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '客户名称',
+ minWidth: 150,
+ },
+ {
+ field: 'contact',
+ title: '联系人',
+ minWidth: 120,
+ },
+ {
+ field: 'mobile',
+ title: '手机号码',
+ minWidth: 130,
+ },
+ {
+ field: 'telephone',
+ title: '联系电话',
+ minWidth: 130,
+ },
+ {
+ field: 'email',
+ title: '电子邮箱',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/sale/customer/index.vue b/apps/web-antdv-next/src/views/erp/sale/customer/index.vue
new file mode 100644
index 000000000..87b96ff9e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/customer/index.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/customer/modules/form.vue b/apps/web-antdv-next/src/views/erp/sale/customer/modules/form.vue
new file mode 100644
index 000000000..87c189f13
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/customer/modules/form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/order/data.ts b/apps/web-antdv-next/src/views/erp/sale/order/data.ts
new file mode 100644
index 000000000..8764ce452
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/order/data.ts
@@ -0,0 +1,459 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '选择订单时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ label: '客户',
+ fieldName: 'customerId',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'saleUserId',
+ label: '销售人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择销售人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '销售产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入优惠率',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '付款优惠',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '收款优惠',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '优惠后金额',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '优惠后金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入收取订金',
+ precision: 2,
+ min: 0,
+ },
+ fieldName: 'depositPrice',
+ label: '收取订金',
+ rules: z.number().min(0).optional(),
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'taxPercent',
+ title: '税率(%)',
+ minWidth: 105,
+ fixed: 'right',
+ slots: { default: 'taxPercent' },
+ },
+ {
+ field: 'taxPrice',
+ title: '税额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'totalPrice',
+ title: '税额合计',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'outStatus',
+ label: '出库状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未出库', value: 0 },
+ { label: '部分出库', value: 1 },
+ { label: '全部出库', value: 2 },
+ ],
+ placeholder: '请选择出库状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'returnStatus',
+ label: '退货状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未退货', value: 0 },
+ { label: '部分退货', value: 1 },
+ { label: '全部退货', value: 2 },
+ ],
+ placeholder: '请选择退货状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'orderTime',
+ title: '订单时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'outCount',
+ title: '出库数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'returnCount',
+ title: '退货数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额合计',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '含税金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'depositPrice',
+ title: '收取订金',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/sale/order/index.vue b/apps/web-antdv-next/src/views/erp/sale/order/index.vue
new file mode 100644
index 000000000..f0fa24c08
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/order/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/order/modules/form.vue b/apps/web-antdv-next/src/views/erp/sale/order/modules/form.vue
new file mode 100644
index 000000000..b5d306aed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/order/modules/form.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/order/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/sale/order/modules/item-form.vue
new file mode 100644
index 000000000..a9f9c8a27
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/order/modules/item-form.vue
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+ {{ row.taxPercent || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
+
+ 税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}
+
+ 税额合计:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/out/data.ts b/apps/web-antdv-next/src/views/erp/sale/out/data.ts
new file mode 100644
index 000000000..9ad4cb2bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/out/data.ts
@@ -0,0 +1,627 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpNumberFormatter, erpPriceInputFormatter } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '出库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'outTime',
+ label: '出库时间',
+ component: 'DatePicker',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '选择出库时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ formItemClass: 'col-span-1',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请选择关联订单',
+ disabled: formType === 'detail',
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ disabled: true,
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'saleUserId',
+ label: '销售人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择销售人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ fieldNames: {
+ label: 'nickname',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '出库产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入优惠率',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '收款优惠',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '付款优惠',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'discountedPrice',
+ label: '优惠后金额',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '优惠后金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalPrice', 'otherPrice'],
+ componentProps: (values) => {
+ const totalPrice = values.totalPrice || 0;
+ const otherPrice = values.otherPrice || 0;
+ values.discountedPrice = totalPrice - otherPrice;
+ return {};
+ },
+ },
+ },
+ {
+ fieldName: 'otherPrice',
+ label: '其他费用',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请输入其他费用',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ disabled: true,
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '应收金额',
+ component: 'InputNumber',
+ componentProps: {
+ precision: 2,
+ min: 0,
+ disabled: true,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ formData?: any[],
+ disabled?: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 200,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'totalCount',
+ title: '原数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.outCount !== undefined,
+ },
+ {
+ field: 'outCount',
+ title: '已出库',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.returnCount !== undefined,
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ fixed: 'right',
+ minWidth: 120,
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalProductPrice',
+ fixed: 'right',
+ title: '产品金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ fixed: 'right',
+ field: 'taxPercent',
+ title: '税率(%)',
+ minWidth: 105,
+ slots: { default: 'taxPercent' },
+ },
+ {
+ fixed: 'right',
+ field: 'taxPrice',
+ title: '税额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'totalPrice',
+ fixed: 'right',
+ title: '合计金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '出库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入出库单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'outTime',
+ label: '出库时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ fieldNames: {
+ label: 'nickname',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入关联订单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'receiptStatus',
+ label: '收款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未收款', value: 0 },
+ { label: '部分收款', value: 1 },
+ { label: '全部收款', value: 2 },
+ ],
+ placeholder: '请选择收款状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '审批状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择审批状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '出库单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'outTime',
+ title: '出库时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '应收金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'receiptPrice',
+ title: '已收金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unReceiptPrice',
+ title: '未收金额',
+ formatter: ({ row }) => {
+ return `${erpNumberFormatter(row.totalPrice - row.receiptPrice, 2)}元`;
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useOrderGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useOrderGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'radio',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'orderTime',
+ title: '订单时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'outCount',
+ title: '出库数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额合计',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '含税金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/sale/out/index.vue b/apps/web-antdv-next/src/views/erp/sale/out/index.vue
new file mode 100644
index 000000000..06f21645b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/out/index.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/out/modules/form.vue b/apps/web-antdv-next/src/views/erp/sale/out/modules/form.vue
new file mode 100644
index 000000000..adaf59c83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/out/modules/form.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/out/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/sale/out/modules/item-form.vue
new file mode 100644
index 000000000..5b03eeb69
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/out/modules/item-form.vue
@@ -0,0 +1,294 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+ {{ row.taxPercent || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
+
+ 税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}
+
+ 税额合计:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/out/modules/sale-order-select.vue b/apps/web-antdv-next/src/views/erp/sale/out/modules/sale-order-select.vue
new file mode 100644
index 000000000..9d6b40ba7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/out/modules/sale-order-select.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
!disabled && (open = true)"
+ >
+
+
+ !disabled && (open = true)"
+ />
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/return/data.ts b/apps/web-antdv-next/src/views/erp/sale/return/data.ts
new file mode 100644
index 000000000..d0ceeab47
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/return/data.ts
@@ -0,0 +1,614 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpNumberFormatter, erpPriceInputFormatter } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '退货单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'returnTime',
+ label: '退货时间',
+ component: 'DatePicker',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '选择退货时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ formItemClass: 'col-span-1',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请选择关联订单',
+ disabled: formType === 'detail',
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ disabled: true,
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'saleUserId',
+ label: '销售人员',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择销售人员',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '退货产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠率(%)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入优惠率',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '退款优惠',
+ component: 'InputNumber',
+ componentProps: {
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'discountedPrice',
+ label: '优惠后金额',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '优惠后金额',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['totalPrice', 'otherPrice'],
+ componentProps: (values) => {
+ const totalPrice = values.totalPrice || 0;
+ const otherPrice = values.otherPrice || 0;
+ values.discountedPrice = totalPrice - otherPrice;
+ return {};
+ },
+ },
+ },
+ {
+ fieldName: 'otherPrice',
+ label: '其他费用',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: formType === 'detail',
+ placeholder: '请输入其他费用',
+ precision: 2,
+ formatter: erpPriceInputFormatter,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '结算账户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择结算账户',
+ disabled: true,
+ allowClear: true,
+ showSearch: true,
+ api: getAccountSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'totalPrice',
+ label: '应收金额',
+ component: 'InputNumber',
+ componentProps: {
+ precision: 2,
+ min: 0,
+ disabled: true,
+ },
+ rules: z.number().min(0).optional(),
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ formData?: any[],
+ disabled?: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 200,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'totalCount',
+ title: '已出库',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.outCount !== undefined,
+ },
+ {
+ field: 'returnCount',
+ title: '已退货',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ fixed: 'right',
+ visible: formData && formData[0]?.returnCount !== undefined,
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ fixed: 'right',
+ minWidth: 120,
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalProductPrice',
+ fixed: 'right',
+ title: '产品金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ fixed: 'right',
+ field: 'taxPercent',
+ title: '税率(%)',
+ minWidth: 105,
+ slots: { default: 'taxPercent' },
+ },
+ {
+ fixed: 'right',
+ field: 'taxPrice',
+ title: '税额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'totalPrice',
+ fixed: 'right',
+ title: '合计金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '退货单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入退货单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'returnTime',
+ label: '退货时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderNo',
+ label: '关联订单',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入关联订单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'refundStatus',
+ label: '退款状态',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '未退款', value: 0 },
+ { label: '部分退款', value: 1 },
+ { label: '全部退款', value: 2 },
+ ],
+ placeholder: '请选择退款状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '审批状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择审批状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '退货单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '退货产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'returnTime',
+ title: '退货时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '应收金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'refundPrice',
+ title: '已退金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'unRefundPrice',
+ title: '未退金额',
+ formatter: ({ row }) => {
+ return `${erpNumberFormatter(row.totalPrice - row.refundPrice, 2)}元`;
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '审批状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useOrderGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '订单单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'orderTime',
+ label: '订单时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useOrderGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'radio',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'orderTime',
+ title: '订单时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'returnCount',
+ title: '已退货数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalProductPrice',
+ title: '金额合计',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '含税金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/sale/return/index.vue b/apps/web-antdv-next/src/views/erp/sale/return/index.vue
new file mode 100644
index 000000000..9d63d0dd0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/return/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/return/modules/form.vue b/apps/web-antdv-next/src/views/erp/sale/return/modules/form.vue
new file mode 100644
index 000000000..c8c12ecf0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/return/modules/form.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/return/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/sale/return/modules/item-form.vue
new file mode 100644
index 000000000..dcf559394
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/return/modules/item-form.vue
@@ -0,0 +1,294 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+ {{ row.taxPercent || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
+
+ 税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}
+
+ 税额合计:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/sale/return/modules/sale-order-select.vue b/apps/web-antdv-next/src/views/erp/sale/return/modules/sale-order-select.vue
new file mode 100644
index 000000000..8a8ade7a4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/sale/return/modules/sale-order-select.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
!disabled && (open = true)"
+ >
+
+
+ !disabled && (open = true)"
+ />
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/check/data.ts b/apps/web-antdv-next/src/views/erp/stock/check/data.ts
new file mode 100644
index 000000000..f78fa4a38
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/check/data.ts
@@ -0,0 +1,307 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '盘点单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'checkTime',
+ label: '盘点时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '选择盘点时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 150,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '账面库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'actualCount',
+ title: '实际库存',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'actualCount' },
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'count',
+ title: '盈亏数量',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalPrice',
+ title: '金额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '盘点单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入盘点单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'checkTime',
+ label: '盘点时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '盘点单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'checkTime',
+ title: '盘点时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '总金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/check/index.vue b/apps/web-antdv-next/src/views/erp/stock/check/index.vue
new file mode 100644
index 000000000..3edef5eb3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/check/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/check/modules/form.vue b/apps/web-antdv-next/src/views/erp/stock/check/modules/form.vue
new file mode 100644
index 000000000..2bdcec37b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/check/modules/form.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/check/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/stock/check/modules/item-form.vue
new file mode 100644
index 000000000..db56943d9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/check/modules/item-form.vue
@@ -0,0 +1,308 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.actualCount) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/in/data.ts b/apps/web-antdv-next/src/views/erp/stock/in/data.ts
new file mode 100644
index 000000000..5668018de
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/in/data.ts
@@ -0,0 +1,332 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '入库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'inTime',
+ label: '入库时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '选择入库时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ label: '供应商',
+ fieldName: 'supplierId',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '入库产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 150,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalPrice',
+ title: '金额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '入库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入入库单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'inTime',
+ label: '入库时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '供应商',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择供应商',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '入库单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '供应商',
+ minWidth: 120,
+ },
+ {
+ field: 'inTime',
+ title: '入库时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '总金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/in/index.vue b/apps/web-antdv-next/src/views/erp/stock/in/index.vue
new file mode 100644
index 000000000..4b362c25f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/in/index.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/in/modules/form.vue b/apps/web-antdv-next/src/views/erp/stock/in/modules/form.vue
new file mode 100644
index 000000000..06b614032
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/in/modules/form.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/in/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/stock/in/modules/item-form.vue
new file mode 100644
index 000000000..d1e1f7827
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/in/modules/item-form.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/move/data.ts b/apps/web-antdv-next/src/views/erp/stock/move/data.ts
new file mode 100644
index 000000000..5cda3ed99
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/move/data.ts
@@ -0,0 +1,318 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '调度单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'moveTime',
+ label: '调度时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '选择调度时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'fromWarehouseId',
+ title: '调出仓库',
+ minWidth: 150,
+ slots: { default: 'fromWarehouseId' },
+ },
+ {
+ field: 'toWarehouseId',
+ title: '调入仓库',
+ minWidth: 150,
+ slots: { default: 'toWarehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalPrice',
+ title: '金额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '调度单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入调度单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'moveTime',
+ label: '调度时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'fromWarehouseId',
+ label: '调出仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择调出仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'toWarehouseId',
+ label: '调入仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择调入仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '调度单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'moveTime',
+ title: '调度时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '总金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/move/index.vue b/apps/web-antdv-next/src/views/erp/stock/move/index.vue
new file mode 100644
index 000000000..f5da612cd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/move/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/move/modules/form.vue b/apps/web-antdv-next/src/views/erp/stock/move/modules/form.vue
new file mode 100644
index 000000000..772bc82f3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/move/modules/form.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/move/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/stock/move/modules/item-form.vue
new file mode 100644
index 000000000..fd85283ae
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/move/modules/item-form.vue
@@ -0,0 +1,318 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/out/data.ts b/apps/web-antdv-next/src/views/erp/stock/out/data.ts
new file mode 100644
index 000000000..7906b03f5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/out/data.ts
@@ -0,0 +1,342 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getCustomerSimpleList } from '#/api/erp/sale/customer';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的配置项 */
+export function useFormSchema(formType: string): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '出库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '系统自动生成',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'outTime',
+ label: '出库时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '选择出库时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ rules: 'required',
+ },
+ {
+ label: '客户',
+ fieldName: 'customerId',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '附件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ maxSize: 10,
+ accept: [
+ 'pdf',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+ 'txt',
+ 'jpg',
+ 'jpeg',
+ 'png',
+ ],
+ showDescription: formType !== 'detail',
+ disabled: formType === 'detail',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'items',
+ label: '出库产品清单',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 表单的明细表格列 */
+export function useFormItemColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
+ {
+ field: 'warehouseId',
+ title: '仓库名称',
+ minWidth: 150,
+ slots: { default: 'warehouseId' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 200,
+ slots: { default: 'productId' },
+ },
+ {
+ field: 'stockCount',
+ title: '库存',
+ minWidth: 80,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'productBarCode',
+ title: '条码',
+ minWidth: 120,
+ },
+ {
+ field: 'productUnitName',
+ title: '单位',
+ minWidth: 80,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ slots: { default: 'remark' },
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'count' },
+ },
+ {
+ field: 'productPrice',
+ title: '产品单价',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'productPrice' },
+ },
+ {
+ field: 'totalPrice',
+ title: '金额',
+ minWidth: 120,
+ fixed: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '操作',
+ width: 50,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '出库单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入出库单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'outTime',
+ label: '出库时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '客户',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择客户',
+ allowClear: true,
+ showSearch: true,
+ api: getCustomerSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '创建人',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择创建人',
+ allowClear: true,
+ showSearch: true,
+ api: getSimpleUserList,
+ fieldNames: {
+ label: 'nickname',
+ value: 'id',
+ },
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '出库单号',
+ width: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'productNames',
+ title: '产品信息',
+ showOverflow: 'tooltip',
+ minWidth: 120,
+ },
+ {
+ field: 'customerName',
+ title: '客户',
+ minWidth: 120,
+ },
+ {
+ field: 'outTime',
+ title: '出库时间',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'creatorName',
+ title: '创建人',
+ minWidth: 120,
+ },
+ {
+ field: 'totalCount',
+ title: '总数量',
+ formatter: 'formatAmount3',
+ minWidth: 120,
+ },
+ {
+ field: 'totalPrice',
+ title: '总金额',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/out/index.vue b/apps/web-antdv-next/src/views/erp/stock/out/index.vue
new file mode 100644
index 000000000..8cd238561
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/out/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/out/modules/form.vue b/apps/web-antdv-next/src/views/erp/stock/out/modules/form.vue
new file mode 100644
index 000000000..3c900af97
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/out/modules/form.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/out/modules/item-form.vue b/apps/web-antdv-next/src/views/erp/stock/out/modules/item-form.vue
new file mode 100644
index 000000000..1de84316a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/out/modules/item-form.vue
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ erpCountInputFormatter(row.count) || '-' }}
+
+
+
+ {{ erpPriceInputFormatter(row.productPrice) || '-' }}
+
+
+
+ {{ row.remark || '-' }}
+
+
+
+
+
+
+
+
+
合计:
+
+ 数量:{{ erpCountInputFormatter(summaries.count) }}
+
+ 金额:{{ erpPriceInputFormatter(summaries.totalPrice) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/record/data.ts b/apps/web-antdv-next/src/views/erp/stock/record/data.ts
new file mode 100644
index 000000000..e4e3d0ecd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/record/data.ts
@@ -0,0 +1,133 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'bizType',
+ label: '类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择类型',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.ERP_STOCK_RECORD_BIZ_TYPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'bizNo',
+ label: '业务单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入业务单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'productName',
+ title: '产品名称',
+ minWidth: 150,
+ },
+ {
+ field: 'categoryName',
+ title: '产品分类',
+ width: 120,
+ },
+ {
+ field: 'unitName',
+ title: '产品单位',
+ width: 100,
+ },
+ {
+ field: 'warehouseName',
+ title: '仓库',
+ width: 120,
+ },
+ {
+ field: 'bizType',
+ title: '类型',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_STOCK_RECORD_BIZ_TYPE },
+ },
+ },
+ {
+ field: 'bizNo',
+ title: '出入库单号',
+ width: 200,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'createTime',
+ title: '出入库日期',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'count',
+ title: '出入库数量',
+ width: 120,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'totalCount',
+ title: '库存量',
+ width: 100,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'creatorName',
+ title: '操作人',
+ width: 100,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/record/index.vue b/apps/web-antdv-next/src/views/erp/stock/record/index.vue
new file mode 100644
index 000000000..8c975031a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/record/index.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/stock/data.ts b/apps/web-antdv-next/src/views/erp/stock/stock/data.ts
new file mode 100644
index 000000000..037bb9c45
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/stock/data.ts
@@ -0,0 +1,69 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getProductSimpleList } from '#/api/erp/product/product';
+import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
+
+/** 搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ api: getProductSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'warehouseId',
+ label: '仓库',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择仓库',
+ allowClear: true,
+ showSearch: true,
+ api: getWarehouseSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'productName',
+ title: '产品名称',
+ minWidth: 150,
+ },
+ {
+ field: 'unitName',
+ title: '产品单位',
+ minWidth: 100,
+ },
+ {
+ field: 'categoryName',
+ title: '产品分类',
+ minWidth: 120,
+ },
+ {
+ field: 'count',
+ title: '库存量',
+ minWidth: 100,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'warehouseName',
+ title: '仓库',
+ minWidth: 120,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/stock/index.vue b/apps/web-antdv-next/src/views/erp/stock/stock/index.vue
new file mode 100644
index 000000000..2b20d7475
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/stock/index.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/warehouse/data.ts b/apps/web-antdv-next/src/views/erp/stock/warehouse/data.ts
new file mode 100644
index 000000000..ff8045b83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/warehouse/data.ts
@@ -0,0 +1,205 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpWarehouseApi } from '#/api/erp/stock/warehouse';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '仓库名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入仓库名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'address',
+ label: '仓库地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入仓库地址',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'warehousePrice',
+ label: '仓储费(元)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入仓储费,单位:元/天/KG',
+ min: 0,
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'truckagePrice',
+ label: '搬运费(元)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入搬运费,单位:元',
+ min: 0,
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'principal',
+ label: '负责人',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入负责人',
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入排序',
+ precision: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '仓库名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入仓库名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '仓库状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择仓库状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onDefaultStatusChange?: (
+ newStatus: boolean,
+ row: ErpWarehouseApi.Warehouse,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '仓库名称',
+ minWidth: 150,
+ },
+ {
+ field: 'address',
+ title: '仓库地址',
+ minWidth: 200,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'warehousePrice',
+ title: '仓储费',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'truckagePrice',
+ title: '搬运费',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'principal',
+ title: '负责人',
+ minWidth: 100,
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'defaultStatus',
+ title: '是否默认',
+ minWidth: 100,
+ cellRender: {
+ attrs: { beforeChange: onDefaultStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: true,
+ unCheckedValue: false,
+ },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/erp/stock/warehouse/index.vue b/apps/web-antdv-next/src/views/erp/stock/warehouse/index.vue
new file mode 100644
index 000000000..e567fd2ec
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/warehouse/index.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/erp/stock/warehouse/modules/form.vue b/apps/web-antdv-next/src/views/erp/stock/warehouse/modules/form.vue
new file mode 100644
index 000000000..f25d28cf7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/erp/stock/warehouse/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/apiAccessLog/data.ts b/apps/web-antdv-next/src/views/infra/apiAccessLog/data.ts
new file mode 100644
index 000000000..83b2977c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/apiAccessLog/data.ts
@@ -0,0 +1,273 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { JsonViewer } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入用户编号',
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ allowClear: true,
+ placeholder: '请选择用户类型',
+ },
+ },
+ {
+ fieldName: 'applicationName',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入应用名',
+ },
+ },
+ {
+ fieldName: 'beginTime',
+ label: '请求时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'duration',
+ label: '执行时长',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入执行时长',
+ },
+ },
+ {
+ fieldName: 'resultCode',
+ label: '结果码',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入结果码',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '日志编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userType',
+ title: '用户类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.USER_TYPE },
+ },
+ },
+ {
+ field: 'applicationName',
+ title: '应用名',
+ minWidth: 150,
+ },
+ {
+ field: 'requestMethod',
+ title: '请求方法',
+ minWidth: 80,
+ },
+ {
+ field: 'requestUrl',
+ title: '请求地址',
+ minWidth: 300,
+ },
+ {
+ field: 'beginTime',
+ title: '请求时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'duration',
+ title: '执行时长',
+ minWidth: 120,
+ formatter: ({ cellValue }) => `${cellValue} ms`,
+ },
+ {
+ field: 'resultCode',
+ title: '操作结果',
+ minWidth: 150,
+ formatter: ({ row }) => {
+ return row.resultCode === 0 ? '成功' : `失败(${row.resultMsg})`;
+ },
+ },
+ {
+ field: 'operateModule',
+ title: '操作模块',
+ minWidth: 150,
+ },
+ {
+ field: 'operateName',
+ title: '操作名',
+ minWidth: 220,
+ },
+ {
+ field: 'operateType',
+ title: '操作类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_OPERATE_TYPE },
+ },
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '日志编号',
+ },
+ {
+ field: 'traceId',
+ label: '链路追踪',
+ },
+ {
+ field: 'applicationName',
+ label: '应用名',
+ },
+ {
+ field: 'userId',
+ label: '用户Id',
+ },
+ {
+ field: 'userType',
+ label: '用户类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.USER_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'userIp',
+ label: '用户 IP',
+ },
+ {
+ field: 'userAgent',
+ label: '用户 UA',
+ },
+ {
+ field: 'requestMethod',
+ label: '请求信息',
+ render: (val, data) => {
+ if (val && data?.requestUrl) {
+ return `${val} ${data.requestUrl}`;
+ }
+ return '';
+ },
+ },
+ {
+ field: 'requestParams',
+ label: '请求参数',
+ render: (val) => {
+ if (val) {
+ return h(JsonViewer, {
+ value: JSON.parse(val),
+ previewMode: true,
+ });
+ }
+ return '';
+ },
+ },
+ {
+ field: 'responseBody',
+ label: '请求结果',
+ },
+ {
+ label: '请求时间',
+ field: 'beginTime',
+ render: (val, data) => {
+ if (val && data?.endTime) {
+ return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
+ }
+ return '';
+ },
+ },
+ {
+ label: '请求耗时',
+ field: 'duration',
+ render: (val) => {
+ return val ? `${val} ms` : '';
+ },
+ },
+ {
+ label: '操作结果',
+ field: 'resultCode',
+ render: (val, data) => {
+ if (val === 0) {
+ return '正常';
+ } else if (val > 0 && data?.resultMsg) {
+ return `失败 | ${val} | ${data.resultMsg}`;
+ }
+ return '';
+ },
+ },
+ {
+ field: 'operateModule',
+ label: '操作模块',
+ },
+ {
+ field: 'operateName',
+ label: '操作名',
+ },
+ {
+ field: 'operateType',
+ label: '操作类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.INFRA_OPERATE_TYPE,
+ value: val,
+ });
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/apiAccessLog/index.vue b/apps/web-antdv-next/src/views/infra/apiAccessLog/index.vue
new file mode 100644
index 000000000..4e4bc183e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/apiAccessLog/index.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/apiAccessLog/modules/detail.vue b/apps/web-antdv-next/src/views/infra/apiAccessLog/modules/detail.vue
new file mode 100644
index 000000000..badc9376c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/apiAccessLog/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/apiErrorLog/data.ts b/apps/web-antdv-next/src/views/infra/apiErrorLog/data.ts
new file mode 100644
index 000000000..1a8c545fe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/apiErrorLog/data.ts
@@ -0,0 +1,249 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { JsonViewer } from '@vben/common-ui';
+import { DICT_TYPE, InfraApiErrorLogProcessStatusEnum } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入用户编号',
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ allowClear: true,
+ placeholder: '请选择用户类型',
+ },
+ },
+ {
+ fieldName: 'applicationName',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入应用名',
+ },
+ },
+ {
+ fieldName: 'exceptionTime',
+ label: '异常时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'processStatus',
+ label: '处理状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
+ 'number',
+ ),
+ allowClear: true,
+ placeholder: '请选择处理状态',
+ },
+ defaultValue: InfraApiErrorLogProcessStatusEnum.INIT,
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '日志编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userType',
+ title: '用户类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.USER_TYPE },
+ },
+ },
+ {
+ field: 'applicationName',
+ title: '应用名',
+ minWidth: 150,
+ },
+ {
+ field: 'requestMethod',
+ title: '请求方法',
+ minWidth: 80,
+ },
+ {
+ field: 'requestUrl',
+ title: '请求地址',
+ minWidth: 200,
+ },
+ {
+ field: 'exceptionTime',
+ title: '异常发生时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'exceptionName',
+ title: '异常名',
+ minWidth: 180,
+ },
+ {
+ field: 'processStatus',
+ title: '处理状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ minWidth: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '日志编号',
+ },
+ {
+ field: 'traceId',
+ label: '链路追踪',
+ },
+ {
+ field: 'applicationName',
+ label: '应用名',
+ },
+ {
+ field: 'userId',
+ label: '用户Id',
+ },
+ {
+ field: 'userType',
+ label: '用户类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.USER_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'userIp',
+ label: '用户 IP',
+ },
+ {
+ field: 'userAgent',
+ label: '用户 UA',
+ },
+ {
+ field: 'requestMethod',
+ label: '请求信息',
+ render: (val, data) => {
+ if (val && data?.requestUrl) {
+ return `${val} ${data.requestUrl}`;
+ }
+ return '';
+ },
+ },
+ {
+ field: 'requestParams',
+ label: '请求参数',
+ render: (val) => {
+ if (val) {
+ return h(JsonViewer, {
+ value: JSON.parse(val),
+ previewMode: true,
+ });
+ }
+ return '';
+ },
+ },
+ {
+ field: 'exceptionTime',
+ label: '异常时间',
+ render: (val) => {
+ return formatDateTime(val) as string;
+ },
+ },
+ {
+ field: 'exceptionName',
+ label: '异常名',
+ },
+ {
+ field: 'exceptionStackTrace',
+ label: '异常堆栈',
+ show: (val) => !val,
+ render: (val) => {
+ if (val) {
+ return h('textarea', {
+ value: val,
+ style:
+ 'width: 100%; min-height: 200px; max-height: 400px; resize: vertical;',
+ readonly: true,
+ });
+ }
+ return '';
+ },
+ },
+ {
+ field: 'processStatus',
+ label: '处理状态',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'processUserId',
+ label: '处理人',
+ show: (val) => !val,
+ },
+ {
+ field: 'processTime',
+ label: '处理时间',
+ show: (val) => !val,
+ render: (val) => {
+ return formatDateTime(val) as string;
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/apiErrorLog/index.vue b/apps/web-antdv-next/src/views/infra/apiErrorLog/index.vue
new file mode 100644
index 000000000..c84d0d44e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/apiErrorLog/index.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/apiErrorLog/modules/detail.vue b/apps/web-antdv-next/src/views/infra/apiErrorLog/modules/detail.vue
new file mode 100644
index 000000000..da52c17fa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/apiErrorLog/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/build/index.vue b/apps/web-antdv-next/src/views/infra/build/index.vue
new file mode 100644
index 000000000..b98023120
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/build/index.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/data.ts b/apps/web-antdv-next/src/views/infra/codegen/data.ts
new file mode 100644
index 000000000..b07ea2a32
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/data.ts
@@ -0,0 +1,552 @@
+import type { Recordable } from '@vben/types';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { InfraCodegenApi } from '#/api/infra/codegen';
+import type { SystemMenuApi } from '#/api/system/menu';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { IconifyIcon } from '@vben/icons';
+import { handleTree } from '@vben/utils';
+
+import { getDataSourceConfigList } from '#/api/infra/data-source-config';
+import { getMenuList } from '#/api/system/menu';
+import { $t } from '#/locales';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 导入数据库表的表单 */
+export function useImportTableFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'dataSourceConfigId',
+ label: '数据源',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getDataSourceConfigList,
+ labelField: 'name',
+ valueField: 'id',
+ autoSelect: 'first',
+ placeholder: '请选择数据源',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '表名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入表名称',
+ },
+ },
+ {
+ fieldName: 'comment',
+ label: '表描述',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入表描述',
+ },
+ },
+ ];
+}
+
+/** 导入数据库表表格列定义 */
+export function useImportTableColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ { field: 'name', title: '表名称', minWidth: 200 },
+ { field: 'comment', title: '表描述', minWidth: 200 },
+ ];
+}
+
+/** 基本信息表单的 schema */
+export function useBasicInfoFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'tableName',
+ label: '表名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入仓库名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'tableComment',
+ label: '表描述',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入表描述',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'className',
+ label: '实体类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入实体类名称',
+ },
+ rules: 'required',
+ help: '默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。',
+ },
+ {
+ fieldName: 'author',
+ label: '作者',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入作者',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ rows: 3,
+ placeholder: '请输入备注',
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 生成信息表单基础 schema */
+export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Select',
+ fieldName: 'templateType',
+ label: '生成模板',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE,
+ 'number',
+ ),
+ class: 'w-full',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ component: 'Select',
+ fieldName: 'frontType',
+ label: '前端类型',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE, 'number'),
+ class: 'w-full',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ component: 'Select',
+ fieldName: 'scene',
+ label: '生成场景',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE, 'number'),
+ class: 'w-full',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'parentMenuId',
+ label: '上级菜单',
+ help: '分配到指定菜单下,例如 系统管理',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getMenuList();
+ data.unshift({
+ id: 0,
+ name: '顶级菜单',
+ } as SystemMenuApi.Menu);
+ return handleTree(data);
+ },
+ class: 'w-full',
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择上级菜单',
+ filterTreeNode(input: string, node: Recordable) {
+ if (!input || input.length === 0) {
+ return true;
+ }
+ const name: string = node.label ?? '';
+ if (!name) return false;
+ return name.includes(input) || $t(name).includes(input);
+ },
+ showSearch: true,
+ treeDefaultExpandedKeys: [0],
+ },
+ rules: 'selectRequired',
+ renderComponentContent() {
+ return {
+ title({ label, icon }: { icon: string; label: string }) {
+ const components = [];
+ if (!label) return '';
+ if (icon) {
+ components.push(h(IconifyIcon, { class: 'size-4', icon }));
+ }
+ components.push(h('span', { class: '' }, $t(label || '')));
+ return h('div', { class: 'flex items-center gap-1' }, components);
+ },
+ };
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'moduleName',
+ label: '模块名',
+ help: '模块名,即一级目录,例如 system、infra、tool 等等',
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'businessName',
+ label: '业务名',
+ help: '业务名,即二级目录,例如 user、permission、dict 等等',
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'className',
+ label: '类名称',
+ help: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'classComment',
+ label: '类描述',
+ help: '用作类描述,例如 用户',
+ rules: 'required',
+ },
+ ];
+}
+
+/** 树表信息 schema */
+export function useGenerationInfoTreeFormSchema(
+ columns: InfraCodegenApi.CodegenColumn[] = [],
+): VbenFormSchema[] {
+ return [
+ {
+ component: 'Divider',
+ fieldName: 'treeDivider',
+ label: '',
+ renderComponentContent: () => {
+ return {
+ default: () => ['树表信息'],
+ };
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ {
+ component: 'Select',
+ fieldName: 'treeParentColumnId',
+ label: '父编号字段',
+ help: '树显示的父编码字段名,例如 parent_Id',
+ componentProps: {
+ class: 'w-full',
+ allowClear: true,
+ placeholder: '请选择',
+ options: columns.map((column) => ({
+ label: column.columnName,
+ value: column.id,
+ })),
+ },
+ rules: 'selectRequired',
+ },
+ {
+ component: 'Select',
+ fieldName: 'treeNameColumnId',
+ label: '名称字段',
+ help: '树节点显示的名称字段,一般是 name',
+ componentProps: {
+ class: 'w-full',
+ allowClear: true,
+ placeholder: '请选择名称字段',
+ options: columns.map((column) => ({
+ label: column.columnName,
+ value: column.id,
+ })),
+ },
+ rules: 'selectRequired',
+ },
+ ];
+}
+
+/** 主子表信息 schema */
+export function useGenerationInfoSubTableFormSchema(
+ columns: InfraCodegenApi.CodegenColumn[] = [],
+ tables: InfraCodegenApi.CodegenTable[] = [],
+): VbenFormSchema[] {
+ return [
+ {
+ component: 'Divider',
+ fieldName: 'subDivider',
+ label: '',
+ renderComponentContent: () => {
+ return {
+ default: () => ['主子表信息'],
+ };
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ {
+ component: 'Select',
+ fieldName: 'masterTableId',
+ label: '关联的主表',
+ help: '关联主表(父表)的表名, 如:system_user',
+ componentProps: {
+ class: 'w-full',
+ allowClear: true,
+ placeholder: '请选择',
+ options: tables.map((table) => ({
+ label: `${table.tableName}:${table.tableComment}`,
+ value: table.id,
+ })),
+ },
+ rules: 'selectRequired',
+ },
+ {
+ component: 'Select',
+ fieldName: 'subJoinColumnId',
+ label: '子表关联的字段',
+ help: '子表关联的字段, 如:user_id',
+ componentProps: {
+ class: 'w-full',
+ allowClear: true,
+ placeholder: '请选择',
+ options: columns.map((column) => ({
+ label: `${column.columnName}:${column.columnComment}`,
+ value: column.id,
+ })),
+ },
+ rules: 'selectRequired',
+ },
+ {
+ component: 'RadioGroup',
+ fieldName: 'subJoinMany',
+ label: '关联关系',
+ help: '主表与子表的关联关系',
+ componentProps: {
+ class: 'w-full',
+ allowClear: true,
+ placeholder: '请选择',
+ options: [
+ {
+ label: '一对多',
+ value: true,
+ },
+ {
+ label: '一对一',
+ value: false,
+ },
+ ],
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'tableName',
+ label: '表名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入表名称',
+ },
+ },
+ {
+ fieldName: 'tableComment',
+ label: '表描述',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入表描述',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ getDataSourceConfigName?: (dataSourceConfigId: number) => string | undefined,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'dataSourceConfigId',
+ title: '数据源',
+ minWidth: 120,
+ formatter: ({ cellValue }) => getDataSourceConfigName?.(cellValue) || '-',
+ },
+ {
+ field: 'tableName',
+ title: '表名称',
+ minWidth: 200,
+ },
+ {
+ field: 'tableComment',
+ title: '表描述',
+ minWidth: 200,
+ },
+ {
+ field: 'className',
+ title: '实体',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 代码生成表格列定义 */
+export function useCodegenColumnTableColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'columnName', title: '字段列名', minWidth: 130 },
+ {
+ field: 'columnComment',
+ title: '字段描述',
+ minWidth: 100,
+ slots: { default: 'columnComment' },
+ },
+ { field: 'dataType', title: '物理类型', minWidth: 100 },
+ {
+ field: 'javaType',
+ title: 'Java 类型',
+ minWidth: 130,
+ slots: { default: 'javaType' },
+ params: {
+ options: [
+ { label: 'Long', value: 'Long' },
+ { label: 'String', value: 'String' },
+ { label: 'Integer', value: 'Integer' },
+ { label: 'Double', value: 'Double' },
+ { label: 'BigDecimal', value: 'BigDecimal' },
+ { label: 'LocalDateTime', value: 'LocalDateTime' },
+ { label: 'Boolean', value: 'Boolean' },
+ ],
+ },
+ },
+ {
+ field: 'javaField',
+ title: 'Java 属性',
+ minWidth: 100,
+ slots: { default: 'javaField' },
+ },
+ {
+ field: 'createOperation',
+ title: '插入',
+ width: 40,
+ slots: { default: 'createOperation' },
+ },
+ {
+ field: 'updateOperation',
+ title: '编辑',
+ width: 40,
+ slots: { default: 'updateOperation' },
+ },
+ {
+ field: 'listOperationResult',
+ title: '列表',
+ width: 40,
+ slots: { default: 'listOperationResult' },
+ },
+ {
+ field: 'listOperation',
+ title: '查询',
+ width: 40,
+ slots: { default: 'listOperation' },
+ },
+ {
+ field: 'listOperationCondition',
+ title: '查询方式',
+ minWidth: 100,
+ slots: { default: 'listOperationCondition' },
+ params: {
+ options: [
+ { label: '=', value: '=' },
+ { label: '!=', value: '!=' },
+ { label: '>', value: '>' },
+ { label: '>=', value: '>=' },
+ { label: '<', value: '<' },
+ { label: '<=', value: '<=' },
+ { label: 'LIKE', value: 'LIKE' },
+ { label: 'BETWEEN', value: 'BETWEEN' },
+ ],
+ },
+ },
+ {
+ field: 'nullable',
+ title: '允许空',
+ width: 60,
+ slots: { default: 'nullable' },
+ },
+ {
+ field: 'htmlType',
+ title: '显示类型',
+ width: 130,
+ slots: { default: 'htmlType' },
+ params: {
+ options: [
+ { label: '文本框', value: 'input' },
+ { label: '文本域', value: 'textarea' },
+ { label: '下拉框', value: 'select' },
+ { label: '单选框', value: 'radio' },
+ { label: '复选框', value: 'checkbox' },
+ { label: '日期控件', value: 'datetime' },
+ { label: '图片上传', value: 'imageUpload' },
+ { label: '文件上传', value: 'fileUpload' },
+ { label: '富文本控件', value: 'editor' },
+ ],
+ },
+ },
+ {
+ field: 'dictType',
+ title: '字典类型',
+ width: 120,
+ slots: { default: 'dictType' },
+ },
+ {
+ field: 'example',
+ title: '示例',
+ minWidth: 100,
+ slots: { default: 'example' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/codegen/edit/index.vue b/apps/web-antdv-next/src/views/infra/codegen/edit/index.vue
new file mode 100644
index 000000000..4f1098b7e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/edit/index.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/index.vue b/apps/web-antdv-next/src/views/infra/codegen/index.vue
new file mode 100644
index 000000000..e7e3f4f27
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/index.vue
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/modules/basic-info.vue b/apps/web-antdv-next/src/views/infra/codegen/modules/basic-info.vue
new file mode 100644
index 000000000..00c49911b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/modules/basic-info.vue
@@ -0,0 +1,45 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/modules/column-info.vue b/apps/web-antdv-next/src/views/infra/codegen/modules/column-info.vue
new file mode 100644
index 000000000..78f4e7101
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/modules/column-info.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/modules/generation-info.vue b/apps/web-antdv-next/src/views/infra/codegen/modules/generation-info.vue
new file mode 100644
index 000000000..56ffe616f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/modules/generation-info.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/modules/import-table.vue b/apps/web-antdv-next/src/views/infra/codegen/modules/import-table.vue
new file mode 100644
index 000000000..d7040011f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/modules/import-table.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/codegen/modules/preview-code.vue b/apps/web-antdv-next/src/views/infra/codegen/modules/preview-code.vue
new file mode 100644
index 000000000..e06888cdf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/codegen/modules/preview-code.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/config/data.ts b/apps/web-antdv-next/src/views/infra/config/data.ts
new file mode 100644
index 000000000..a365fbd78
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/config/data.ts
@@ -0,0 +1,187 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '参数分类',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入参数分类',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'name',
+ label: '参数名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入参数名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'key',
+ label: '参数键名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入参数键名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'value',
+ label: '参数键值',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入参数键值',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'visible',
+ label: '是否可见',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: true,
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '参数名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入参数名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'key',
+ label: '参数键名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入参数键名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '系统内置',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE, 'number'),
+ placeholder: '请选择系统内置',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '参数主键',
+ minWidth: 100,
+ },
+ {
+ field: 'category',
+ title: '参数分类',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '参数名称',
+ minWidth: 200,
+ },
+ {
+ field: 'key',
+ title: '参数键名',
+ minWidth: 200,
+ },
+ {
+ field: 'value',
+ title: '参数键值',
+ minWidth: 150,
+ },
+ {
+ field: 'visible',
+ title: '是否可见',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'type',
+ title: '系统内置',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_CONFIG_TYPE },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/config/index.vue b/apps/web-antdv-next/src/views/infra/config/index.vue
new file mode 100644
index 000000000..84047db8e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/config/index.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/config/modules/form.vue b/apps/web-antdv-next/src/views/infra/config/modules/form.vue
new file mode 100644
index 000000000..e31861fe6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/config/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/dataSourceConfig/data.ts b/apps/web-antdv-next/src/views/infra/dataSourceConfig/data.ts
new file mode 100644
index 000000000..6f4c6027f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/dataSourceConfig/data.ts
@@ -0,0 +1,92 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '数据源名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入数据源名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'url',
+ label: '数据源连接',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入数据源连接',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'username',
+ label: '用户名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'password',
+ label: '密码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入密码',
+ type: 'password',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '主键编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '数据源名称',
+ minWidth: 150,
+ },
+ {
+ field: 'url',
+ title: '数据源连接',
+ minWidth: 300,
+ },
+ {
+ field: 'username',
+ title: '用户名',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/dataSourceConfig/index.vue b/apps/web-antdv-next/src/views/infra/dataSourceConfig/index.vue
new file mode 100644
index 000000000..f6c45e81d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/dataSourceConfig/index.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/dataSourceConfig/modules/form.vue b/apps/web-antdv-next/src/views/infra/dataSourceConfig/modules/form.vue
new file mode 100644
index 000000000..5e6ec8509
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/dataSourceConfig/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo01/data.ts b/apps/web-antdv-next/src/views/infra/demo/demo01/data.ts
new file mode 100644
index 000000000..3656a96f8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo01/data.ts
@@ -0,0 +1,153 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ rules: 'required',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ fieldName: 'birthday',
+ label: '出生年',
+ rules: 'required',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择出生年',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ rules: 'required',
+ component: 'RichTextarea',
+ },
+ {
+ fieldName: 'avatar',
+ label: '头像',
+ component: 'ImageUpload',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ placeholder: '请选择性别',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'sex',
+ title: '性别',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_USER_SEX },
+ },
+ },
+ {
+ field: 'birthday',
+ title: '出生年',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'description',
+ title: '简介',
+ minWidth: 120,
+ },
+ {
+ field: 'avatar',
+ title: '头像',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo01/index.vue b/apps/web-antdv-next/src/views/infra/demo/demo01/index.vue
new file mode 100644
index 000000000..eaaa44b8e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo01/index.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo01/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/demo01/modules/form.vue
new file mode 100644
index 000000000..22614a609
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo01/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo02/data.ts b/apps/web-antdv-next/src/views/infra/demo/demo02/data.ts
new file mode 100644
index 000000000..74430fb28
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo02/data.ts
@@ -0,0 +1,120 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
+
+import { handleTree } from '@vben/utils';
+
+import { getDemo02CategoryList } from '#/api/infra/demo/demo02';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '上级示例分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getDemo02CategoryList({});
+ data.unshift({
+ id: 0,
+ name: '顶级示例分类',
+ });
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择上级示例分类',
+ treeDefaultExpandAll: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '父级编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入父级编号',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ treeNode: true,
+ },
+ {
+ field: 'parentId',
+ title: '父级编号',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo02/index.vue b/apps/web-antdv-next/src/views/infra/demo/demo02/index.vue
new file mode 100644
index 000000000..e30625ae5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo02/index.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo02/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/demo02/modules/form.vue
new file mode 100644
index 000000000..3f4e9f79c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo02/modules/form.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/data.ts b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/data.ts
new file mode 100644
index 000000000..f33d20c9e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/data.ts
@@ -0,0 +1,382 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ rules: 'required',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ fieldName: 'birthday',
+ label: '出生日期',
+ rules: 'required',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择出生日期',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ rules: 'required',
+ component: 'RichTextarea',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ placeholder: '请选择性别',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入简介',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'sex',
+ title: '性别',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_USER_SEX },
+ },
+ },
+ {
+ field: 'birthday',
+ title: '出生日期',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'description',
+ title: '简介',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ==================== 子表(学生课程) ====================
+
+/** 新增/修改的表单 */
+export function useDemo03CourseFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'score',
+ label: '分数',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分数',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useDemo03CourseGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'studentId',
+ label: '学生编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入学生编号',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'score',
+ label: '分数',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入分数',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useDemo03CourseGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'studentId',
+ title: '学生编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'score',
+ title: '分数',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ==================== 子表(学生班级) ====================
+
+/** 新增/修改的表单 */
+export function useDemo03GradeFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'teacher',
+ label: '班主任',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入班主任',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useDemo03GradeGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'studentId',
+ label: '学生编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入学生编号',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'teacher',
+ label: '班主任',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入班主任',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useDemo03GradeGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'studentId',
+ title: '学生编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'teacher',
+ title: '班主任',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/index.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/index.vue
new file mode 100644
index 000000000..610f11cc2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/index.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-course-form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-course-form.vue
new file mode 100644
index 000000000..c9431259a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-course-form.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue
new file mode 100644
index 000000000..7f42c143f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-grade-form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-grade-form.vue
new file mode 100644
index 000000000..18d39ad0d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-grade-form.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue
new file mode 100644
index 000000000..beee74977
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/form.vue
new file mode 100644
index 000000000..552d950ee
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/erp/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/data.ts b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/data.ts
new file mode 100644
index 000000000..0166ba1c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/data.ts
@@ -0,0 +1,276 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ rules: 'required',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ fieldName: 'birthday',
+ label: '出生日期',
+ rules: 'required',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ rules: 'required',
+ component: 'RichTextarea',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ placeholder: '请选择性别',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入简介',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ { type: 'expand', width: 80, slots: { content: 'expand_content' } },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'sex',
+ title: '性别',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_USER_SEX },
+ },
+ },
+ {
+ field: 'birthday',
+ title: '出生日期',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'description',
+ title: '简介',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ==================== 子表(学生课程) ====================
+
+/** 新增/修改列表的字段 */
+export function useDemo03CourseGridEditColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'score',
+ title: '分数',
+ minWidth: 120,
+ slots: { default: 'score' },
+ },
+ {
+ title: '操作',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useDemo03CourseGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'studentId',
+ title: '学生编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'score',
+ title: '分数',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
+
+// ==================== 子表(学生班级) ====================
+
+/** 新增/修改的表单 */
+export function useDemo03GradeFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'teacher',
+ label: '班主任',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入班主任',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useDemo03GradeGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'studentId',
+ title: '学生编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'teacher',
+ title: '班主任',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/index.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/index.vue
new file mode 100644
index 000000000..60909d5cd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/index.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-course-form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-course-form.vue
new file mode 100644
index 000000000..79ddf7f6e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-course-form.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-course-list.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-course-list.vue
new file mode 100644
index 000000000..50297db48
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-course-list.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-grade-form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-grade-form.vue
new file mode 100644
index 000000000..3e2854608
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-grade-form.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-grade-list.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-grade-list.vue
new file mode 100644
index 000000000..1ad2db740
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/demo03-grade-list.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/form.vue
new file mode 100644
index 000000000..38339201a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/inner/modules/form.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/normal/data.ts b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/data.ts
new file mode 100644
index 000000000..aaf2e5304
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/data.ts
@@ -0,0 +1,212 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ rules: 'required',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ fieldName: 'birthday',
+ label: '出生日期',
+ rules: 'required',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: '请选择出生日期',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ rules: 'required',
+ component: 'RichTextarea',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '性别',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ placeholder: '请选择性别',
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '简介',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入简介',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ },
+ {
+ field: 'sex',
+ title: '性别',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_USER_SEX },
+ },
+ },
+ {
+ field: 'birthday',
+ title: '出生日期',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'description',
+ title: '简介',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 120,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ==================== 子表(学生课程) ====================
+
+/** 新增/修改列表的字段 */
+export function useDemo03CourseGridEditColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 120,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'score',
+ title: '分数',
+ minWidth: 120,
+ slots: { default: 'score' },
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ==================== 子表(学生班级) ====================
+
+/** 新增/修改的表单 */
+export function useDemo03GradeFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名字',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名字',
+ },
+ },
+ {
+ fieldName: 'teacher',
+ label: '班主任',
+ rules: 'required',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入班主任',
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/normal/index.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/index.vue
new file mode 100644
index 000000000..d8564e236
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/index.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/demo03-course-form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/demo03-course-form.vue
new file mode 100644
index 000000000..3ca4398e7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/demo03-course-form.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/demo03-grade-form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/demo03-grade-form.vue
new file mode 100644
index 000000000..5d5f396bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/demo03-grade-form.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/form.vue
new file mode 100644
index 000000000..f6a7b7a4d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/demo03/normal/modules/form.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo01/index.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo01/index.vue
new file mode 100644
index 000000000..464ee1164
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo01/index.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.birthday) }}
+
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo01/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo01/modules/form.vue
new file mode 100644
index 000000000..88939ca47
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo01/modules/form.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo02/index.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo02/index.vue
new file mode 100644
index 000000000..d55c15654
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo02/index.vue
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo02/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo02/modules/form.vue
new file mode 100644
index 000000000..4d36264d7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo02/modules/form.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/index.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/index.vue
new file mode 100644
index 000000000..5a00d2d18
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/index.vue
@@ -0,0 +1,342 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.birthday) }}
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-course-form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-course-form.vue
new file mode 100644
index 000000000..6c4e43421
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-course-form.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue
new file mode 100644
index 000000000..e2f9d06f2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue
@@ -0,0 +1,294 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-form.vue
new file mode 100644
index 000000000..0080b6fff
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-form.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue
new file mode 100644
index 000000000..bb049732b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue
@@ -0,0 +1,294 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/form.vue
new file mode 100644
index 000000000..ae7e873b2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/erp/modules/form.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/index.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/index.vue
new file mode 100644
index 000000000..7dace39ac
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/index.vue
@@ -0,0 +1,331 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.birthday) }}
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-course-form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-course-form.vue
new file mode 100644
index 000000000..16568cb83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-course-form.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-course-list.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-course-list.vue
new file mode 100644
index 000000000..bb49eeb74
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-course-list.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-grade-form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-grade-form.vue
new file mode 100644
index 000000000..1c6cf36f3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-grade-form.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-grade-list.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-grade-list.vue
new file mode 100644
index 000000000..a08d67af3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/demo03-grade-list.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/form.vue
new file mode 100644
index 000000000..2527c0f12
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/inner/modules/form.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/index.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/index.vue
new file mode 100644
index 000000000..df62b67b6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/index.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.birthday) }}
+
+
+
+
+
+ {{ formatDateTime(row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/demo03-course-form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/demo03-course-form.vue
new file mode 100644
index 000000000..16568cb83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/demo03-course-form.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/demo03-grade-form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/demo03-grade-form.vue
new file mode 100644
index 000000000..1c6cf36f3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/demo03-grade-form.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/form.vue b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/form.vue
new file mode 100644
index 000000000..89f7cbe9d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/demo/general/demo03/normal/modules/form.vue
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/druid/index.vue b/apps/web-antdv-next/src/views/infra/druid/index.vue
new file mode 100644
index 000000000..01ec58d5f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/druid/index.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/file/data.ts b/apps/web-antdv-next/src/views/infra/file/data.ts
new file mode 100644
index 000000000..9ee70319b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/file/data.ts
@@ -0,0 +1,107 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单的字段 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '文件上传',
+ component: 'Upload',
+ componentProps: {
+ placeholder: '请选择要上传的文件',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'path',
+ label: '文件路径',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文件路径',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '文件类型',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文件类型',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ clearable: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'name',
+ title: '文件名',
+ minWidth: 150,
+ },
+ {
+ field: 'path',
+ title: '文件路径',
+ minWidth: 200,
+ showOverflow: true,
+ },
+ {
+ field: 'url',
+ title: 'URL',
+ minWidth: 200,
+ showOverflow: true,
+ },
+ {
+ field: 'size',
+ title: '文件大小',
+ minWidth: 80,
+ formatter: 'formatFileSize',
+ },
+ {
+ field: 'type',
+ title: '文件类型',
+ minWidth: 120,
+ },
+ {
+ field: 'file-content',
+ title: '文件内容',
+ minWidth: 120,
+ slots: {
+ default: 'file-content',
+ },
+ },
+ {
+ field: 'createTime',
+ title: '上传时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/file/index.vue b/apps/web-antdv-next/src/views/infra/file/index.vue
new file mode 100644
index 000000000..a12275dce
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/file/index.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/file/modules/form.vue b/apps/web-antdv-next/src/views/infra/file/modules/form.vue
new file mode 100644
index 000000000..bab6f0b6a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/file/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/fileConfig/data.ts b/apps/web-antdv-next/src/views/infra/fileConfig/data.ts
new file mode 100644
index 000000000..b34729dd4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/fileConfig/data.ts
@@ -0,0 +1,345 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '配置名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入配置名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'storage',
+ label: '存储器',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE, 'number'),
+ placeholder: '请选择存储器',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (formValues) => formValues.id,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ // DB / Local / FTP / SFTP
+ {
+ fieldName: 'config.basePath',
+ label: '基础路径',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入基础路径',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) =>
+ formValues.storage >= 10 && formValues.storage <= 12,
+ },
+ },
+ {
+ fieldName: 'config.host',
+ label: '主机地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入主机地址',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) =>
+ formValues.storage >= 11 && formValues.storage <= 12,
+ },
+ },
+ {
+ fieldName: 'config.port',
+ label: '主机端口',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入主机端口',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) =>
+ formValues.storage >= 11 && formValues.storage <= 12,
+ },
+ },
+ {
+ fieldName: 'config.username',
+ label: '用户名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) =>
+ formValues.storage >= 11 && formValues.storage <= 12,
+ },
+ },
+ {
+ fieldName: 'config.password',
+ label: '密码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入密码',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) =>
+ formValues.storage >= 11 && formValues.storage <= 12,
+ },
+ },
+ {
+ fieldName: 'config.mode',
+ label: '连接模式',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '主动模式', value: 'Active' },
+ { label: '被动模式', value: 'Passive' },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 11,
+ },
+ },
+ // S3
+ {
+ fieldName: 'config.endpoint',
+ label: '节点地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入节点地址',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ },
+ {
+ fieldName: 'config.bucket',
+ label: '存储 bucket',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 bucket',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ },
+ {
+ fieldName: 'config.accessKey',
+ label: 'accessKey',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 accessKey',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ },
+ {
+ fieldName: 'config.accessSecret',
+ label: 'accessSecret',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 accessSecret',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ },
+ {
+ fieldName: 'config.enablePathStyleAccess',
+ label: '是否 Path Style',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '启用', value: true },
+ { label: '禁用', value: false },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'config.enablePublicAccess',
+ label: '公开访问',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '公开', value: true },
+ { label: '私有', value: false },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'config.region',
+ label: '区域',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请填写区域,一般仅 AWS 需要填写',
+ },
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => formValues.storage === 20,
+ },
+ },
+ // 通用
+ {
+ fieldName: 'config.domain',
+ label: '自定义域名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入自定义域名',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['storage'],
+ show: (formValues) => !!formValues.storage,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '配置名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入配置名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'storage',
+ label: '存储器',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE, 'number'),
+ placeholder: '请选择存储器',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '配置名',
+ minWidth: 120,
+ },
+ {
+ field: 'storage',
+ title: '存储器',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_FILE_STORAGE },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 150,
+ },
+ {
+ field: 'master',
+ title: '主配置',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/fileConfig/index.vue b/apps/web-antdv-next/src/views/infra/fileConfig/index.vue
new file mode 100644
index 000000000..0568f820f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/fileConfig/index.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/fileConfig/modules/form.vue b/apps/web-antdv-next/src/views/infra/fileConfig/modules/form.vue
new file mode 100644
index 000000000..eaae98c17
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/fileConfig/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/job/data.ts b/apps/web-antdv-next/src/views/infra/job/data.ts
new file mode 100644
index 000000000..31811a055
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/data.ts
@@ -0,0 +1,245 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { Timeline } from 'ant-design-vue';
+
+import { CronTab } from '#/components/cron-tab';
+import { DictTag } from '#/components/dict-tag';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '任务名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入任务名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'handlerName',
+ label: '处理器的名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入处理器的名字',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => !!values.id,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'handlerParam',
+ label: '处理器的参数',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入处理器的参数',
+ },
+ },
+ {
+ fieldName: 'cronExpression',
+ label: 'CRON 表达式',
+ component: markRaw(CronTab),
+ componentProps: {
+ placeholder: '请输入 CRON 表达式',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'retryCount',
+ label: '重试次数',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入重试次数。设置为 0 时,不进行重试',
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'retryInterval',
+ label: '重试间隔',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔',
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'monitorTimeout',
+ label: '监控超时时间',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入监控超时时间,单位:毫秒',
+ min: 0,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '任务名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入任务名称',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '任务状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_JOB_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择任务状态',
+ },
+ },
+ {
+ fieldName: 'handlerName',
+ label: '处理器的名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入处理器的名字',
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '任务编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '任务名称',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '任务状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_JOB_STATUS },
+ },
+ },
+ {
+ field: 'handlerName',
+ title: '处理器的名字',
+ minWidth: 180,
+ },
+ {
+ field: 'handlerParam',
+ title: '处理器的参数',
+ minWidth: 140,
+ },
+ {
+ field: 'cronExpression',
+ title: 'CRON 表达式',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '任务编号',
+ },
+ {
+ field: 'name',
+ label: '任务名称',
+ },
+ {
+ field: 'status',
+ label: '任务状态',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.INFRA_JOB_STATUS,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'handlerName',
+ label: '处理器的名字',
+ },
+ {
+ field: 'handlerParam',
+ label: '处理器的参数',
+ },
+ {
+ field: 'cronExpression',
+ label: 'Cron 表达式',
+ },
+ {
+ field: 'retryCount',
+ label: '重试次数',
+ },
+ {
+ label: '重试间隔',
+ field: 'retryInterval',
+ render: (val) => {
+ return val ? `${val} 毫秒` : '无间隔';
+ },
+ },
+ {
+ label: '监控超时时间',
+ field: 'monitorTimeout',
+ render: (val) => {
+ return val && val > 0 ? `${val} 毫秒` : '未开启';
+ },
+ },
+ {
+ field: 'nextTimes',
+ label: '后续执行时间',
+ render: (val) => {
+ if (!val || val.length === 0) {
+ return '无后续执行时间';
+ }
+ return h(Timeline, {}, () =>
+ val?.map((time: Date) =>
+ h(Timeline.Item, {}, () => formatDateTime(time)),
+ ),
+ );
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/job/index.vue b/apps/web-antdv-next/src/views/infra/job/index.vue
new file mode 100644
index 000000000..f7514202f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/index.vue
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/job/logger/data.ts b/apps/web-antdv-next/src/views/infra/job/logger/data.ts
new file mode 100644
index 000000000..f17622cb9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/logger/data.ts
@@ -0,0 +1,185 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import dayjs from 'dayjs';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'handlerName',
+ label: '处理器的名字',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入处理器的名字',
+ },
+ },
+ {
+ fieldName: 'beginTime',
+ label: '开始执行时间',
+ component: 'DatePicker',
+ componentProps: {
+ allowClear: true,
+ placeholder: '选择开始执行时间',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ showTime: {
+ format: 'HH:mm:ss',
+ defaultValue: dayjs('00:00:00', 'HH:mm:ss'),
+ },
+ },
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束执行时间',
+ component: 'DatePicker',
+ componentProps: {
+ allowClear: true,
+ placeholder: '选择结束执行时间',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ showTime: {
+ format: 'HH:mm:ss',
+ defaultValue: dayjs('23:59:59', 'HH:mm:ss'),
+ },
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '任务状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择任务状态',
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '日志编号',
+ minWidth: 80,
+ },
+ {
+ field: 'jobId',
+ title: '任务编号',
+ minWidth: 80,
+ },
+ {
+ field: 'handlerName',
+ title: '处理器的名字',
+ minWidth: 180,
+ },
+ {
+ field: 'handlerParam',
+ title: '处理器的参数',
+ minWidth: 140,
+ },
+ {
+ field: 'executeIndex',
+ title: '第几次执行',
+ minWidth: 100,
+ },
+ {
+ field: 'beginTime',
+ title: '执行时间',
+ minWidth: 280,
+ formatter: ({ row }) => {
+ return `${formatDateTime(row.beginTime)} ~ ${formatDateTime(row.endTime)}`;
+ },
+ },
+ {
+ field: 'duration',
+ title: '执行时长',
+ minWidth: 120,
+ formatter: ({ row }) => {
+ return `${row.duration} 毫秒`;
+ },
+ },
+ {
+ field: 'status',
+ title: '任务状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_JOB_LOG_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '日志编号',
+ },
+ {
+ field: 'jobId',
+ label: '任务编号',
+ },
+ {
+ field: 'handlerName',
+ label: '处理器的名字',
+ },
+ {
+ field: 'handlerParam',
+ label: '处理器的参数',
+ },
+ {
+ field: 'executeIndex',
+ label: '第几次执行',
+ },
+ {
+ field: 'beginTime',
+ label: '执行时间',
+ render: (val, data) => {
+ if (val && data?.endTime) {
+ return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
+ }
+ return '';
+ },
+ },
+ {
+ field: 'duration',
+ label: '执行时长',
+ render: (val) => {
+ return val ? `${val} 毫秒` : '';
+ },
+ },
+ {
+ field: 'status',
+ label: '任务状态',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.INFRA_JOB_LOG_STATUS,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'result',
+ label: '执行结果',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/infra/job/logger/index.vue b/apps/web-antdv-next/src/views/infra/job/logger/index.vue
new file mode 100644
index 000000000..2dc65d3e6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/logger/index.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/job/logger/modules/detail.vue b/apps/web-antdv-next/src/views/infra/job/logger/modules/detail.vue
new file mode 100644
index 000000000..ceb1b5bf1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/logger/modules/detail.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/job/modules/detail.vue b/apps/web-antdv-next/src/views/infra/job/modules/detail.vue
new file mode 100644
index 000000000..f23149e2d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/modules/detail.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/job/modules/form.vue b/apps/web-antdv-next/src/views/infra/job/modules/form.vue
new file mode 100644
index 000000000..a66c66841
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/job/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/redis/index.vue b/apps/web-antdv-next/src/views/infra/redis/index.vue
new file mode 100644
index 000000000..fb063a1e6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/redis/index.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/redis/modules/commands.vue b/apps/web-antdv-next/src/views/infra/redis/modules/commands.vue
new file mode 100644
index 000000000..25c263e68
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/redis/modules/commands.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/redis/modules/info.vue b/apps/web-antdv-next/src/views/infra/redis/modules/info.vue
new file mode 100644
index 000000000..c832f1b04
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/redis/modules/info.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/redis/modules/memory.vue b/apps/web-antdv-next/src/views/infra/redis/modules/memory.vue
new file mode 100644
index 000000000..5220c4631
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/redis/modules/memory.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/server/index.vue b/apps/web-antdv-next/src/views/infra/server/index.vue
new file mode 100644
index 000000000..d8893bdbd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/server/index.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/skywalking/index.vue b/apps/web-antdv-next/src/views/infra/skywalking/index.vue
new file mode 100644
index 000000000..2ff01c3bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/skywalking/index.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/swagger/index.vue b/apps/web-antdv-next/src/views/infra/swagger/index.vue
new file mode 100644
index 000000000..c04365cdf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/swagger/index.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/infra/webSocket/index.vue b/apps/web-antdv-next/src/views/infra/webSocket/index.vue
new file mode 100644
index 000000000..bcb7ccc27
--- /dev/null
+++ b/apps/web-antdv-next/src/views/infra/webSocket/index.vue
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 连接管理
+
+
+
+ 连接状态:
+ {{ getStatusText }}
+
+
+
+
+ 服务地址
+
+
+
+
+
+
+ 消息发送
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 消息记录
+
+ {{ messageList.length }} 条
+
+
+
+
+
+
+
+
+
+
+
+ {{ getMessageTypeText(msg.type) }}
+
+
+ 用户 ID: {{ msg.userId }}
+
+
+
+ {{ formatDate(msg.time) }}
+
+
+
+ {{ msg.text }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/alert/config/data.ts b/apps/web-antdv-next/src/views/iot/alert/config/data.ts
new file mode 100644
index 000000000..c45a95a74
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/alert/config/data.ts
@@ -0,0 +1,199 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleRuleSceneList } from '#/api/iot/rule/scene';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改告警配置的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '配置名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入配置名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '配置描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入配置描述',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'level',
+ label: '告警级别',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_ALERT_LEVEL, 'number'),
+ placeholder: '请选择告警级别',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '配置状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sceneRuleIds',
+ label: '关联场景联动规则',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleRuleSceneList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择关联的场景联动规则',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'receiveUserIds',
+ label: '接收的用户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择接收的用户',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'receiveTypes',
+ label: '接收类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_ALERT_RECEIVE_TYPE, 'number'),
+ mode: 'multiple',
+ placeholder: '请选择接收类型',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '配置名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入配置名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '配置状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择配置状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '配置编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '配置名称',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '配置描述',
+ minWidth: 200,
+ },
+ {
+ field: 'level',
+ title: '告警级别',
+ minWidth: 100,
+ slots: { default: 'level' },
+ },
+ {
+ field: 'status',
+ title: '配置状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'sceneRuleIds',
+ title: '关联场景联动规则',
+ minWidth: 150,
+ slots: { default: 'sceneRules' },
+ },
+ {
+ field: 'receiveUserNames',
+ title: '接收人',
+ minWidth: 150,
+ },
+ {
+ field: 'receiveTypes',
+ title: '接收类型',
+ minWidth: 150,
+ slots: { default: 'receiveTypes' },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/alert/config/index.vue b/apps/web-antdv-next/src/views/iot/alert/config/index.vue
new file mode 100644
index 000000000..0f23562bc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/alert/config/index.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getLevelText(row.level) }}
+
+
+
+
+
+ {{ row.sceneRuleIds?.length || 0 }} 条
+
+
+
+
+
+ {{ getReceiveTypeText(type) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/alert/modules/alert-config-form.vue b/apps/web-antdv-next/src/views/iot/alert/modules/alert-config-form.vue
new file mode 100644
index 000000000..76172f8f5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/alert/modules/alert-config-form.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/alert/record/data.ts b/apps/web-antdv-next/src/views/iot/alert/record/data.ts
new file mode 100644
index 000000000..2c5ef9a84
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/alert/record/data.ts
@@ -0,0 +1,151 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleAlertConfigList } from '#/api/iot/alert/config';
+import { getSimpleDeviceList } from '#/api/iot/device/device';
+import { getSimpleProductList } from '#/api/iot/product/product';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'configId',
+ label: '告警配置',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleAlertConfigList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择告警配置',
+ allowClear: true,
+ showSearch: true,
+ },
+ },
+ {
+ fieldName: 'configLevel',
+ label: '告警级别',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_ALERT_LEVEL, 'number'),
+ placeholder: '请选择告警级别',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ allowClear: true,
+ showSearch: true,
+ },
+ },
+ {
+ fieldName: 'deviceId',
+ label: '设备',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeviceList,
+ labelField: 'deviceName',
+ valueField: 'id',
+ placeholder: '请选择设备',
+ allowClear: true,
+ showSearch: true,
+ },
+ },
+ {
+ fieldName: 'processStatus',
+ label: '是否处理',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING),
+ placeholder: '请选择是否处理',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '记录编号',
+ minWidth: 80,
+ },
+ {
+ field: 'configName',
+ title: '告警名称',
+ minWidth: 150,
+ },
+ {
+ field: 'configLevel',
+ title: '告警级别',
+ minWidth: 100,
+ slots: { default: 'configLevel' },
+ },
+ {
+ field: 'productId',
+ title: '产品名称',
+ minWidth: 120,
+ slots: { default: 'product' },
+ },
+ {
+ field: 'deviceId',
+ title: '设备名称',
+ minWidth: 120,
+ slots: { default: 'device' },
+ },
+ {
+ field: 'deviceMessage',
+ title: '触发的设备消息',
+ minWidth: 150,
+ slots: { default: 'deviceMessage' },
+ },
+ {
+ field: 'processStatus',
+ title: '是否处理',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'processRemark',
+ title: '处理结果',
+ minWidth: 150,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 100,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/alert/record/index.vue b/apps/web-antdv-next/src/views/iot/alert/record/index.vue
new file mode 100644
index 000000000..64ba6cfac
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/alert/record/index.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+ {{ getLevelText(row.configLevel) }}
+
+
+
+
+
+ {{ getProductName(row.productId) }}
+
+
+
+
+ {{ getDeviceName(row.deviceId) }}
+
+
+
+
+
+
+ {{ row.deviceMessage }}
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/data.ts b/apps/web-antdv-next/src/views/iot/device/device/data.ts
new file mode 100644
index 000000000..c53c0e207
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/data.ts
@@ -0,0 +1,335 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
+import { getSimpleProductList } from '#/api/iot/product/product';
+
+/** 基础表单字段 */
+export function useBasicFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values: any) => !!values?.id,
+ },
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'deviceType',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'deviceName',
+ label: 'DeviceName',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 DeviceName',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values: any) => !!values?.id,
+ },
+ rules: z
+ .string()
+ .min(4, 'DeviceName 长度不能少于 4 个字符')
+ .max(32, 'DeviceName 长度不能超过 32 个字符')
+ .regex(
+ /^[\w.\-:@]{4,32}$/,
+ '支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@',
+ ),
+ },
+ ];
+}
+
+/** 高级设置表单字段(更多设置) */
+export function useAdvancedFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'nickname',
+ label: '备注名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注名称',
+ },
+ rules: z
+ .string()
+ .min(4, '备注名称长度限制为 4~64 个字符')
+ .max(64, '备注名称长度限制为 4~64 个字符')
+ .regex(
+ /^[\u4E00-\u9FA5\u3040-\u30FF\w]+$/,
+ '备注名称只能包含中文、英文字母、日文、数字和下划线(_)',
+ )
+ .optional()
+ .or(z.literal('')),
+ },
+ {
+ fieldName: 'picUrl',
+ label: '设备图片',
+ component: 'ImageUpload',
+ },
+ {
+ fieldName: 'groupIds',
+ label: '设备分组',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeviceGroupList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择设备分组',
+ },
+ },
+ {
+ fieldName: 'serialNumber',
+ label: '设备序列号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入设备序列号',
+ },
+ rules: z
+ .string()
+ .regex(/^[\w-]+$/, '序列号只能包含字母、数字、中划线和下划线')
+ .optional()
+ .or(z.literal('')),
+ },
+ {
+ fieldName: 'longitude',
+ label: '设备经度',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入设备经度',
+ class: 'w-full',
+ min: -180,
+ max: 180,
+ precision: 6,
+ },
+ rules: z
+ .number()
+ .min(-180, '经度范围为 -180 到 180')
+ .max(180, '经度范围为 -180 到 180')
+ .optional()
+ .nullable(),
+ },
+ {
+ fieldName: 'latitude',
+ label: '设备纬度',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入设备纬度',
+ class: 'w-full',
+ min: -90,
+ max: 90,
+ precision: 6,
+ },
+ rules: z
+ .number()
+ .min(-90, '纬度范围为 -90 到 90')
+ .max(90, '纬度范围为 -90 到 90')
+ .optional()
+ .nullable(),
+ },
+ ];
+}
+
+/** 设备分组表单 */
+export function useGroupFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'groupIds',
+ label: '设备分组',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeviceGroupList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择设备分组',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 设备导入表单 */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '设备数据',
+ component: 'Upload',
+ rules: 'required',
+ help: '仅允许导入 xls、xlsx 格式文件',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '是否覆盖',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '是',
+ unCheckedChildren: '否',
+ },
+ rules: z.boolean().default(false),
+ help: '是否更新已经存在的设备数据',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'deviceName',
+ label: 'DeviceName',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 DeviceName',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '备注名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'deviceType',
+ label: '设备类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number'),
+ placeholder: '请选择设备类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '设备状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number'),
+ placeholder: '请选择设备状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'groupId',
+ label: '设备分组',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeviceGroupList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择设备分组',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'deviceName',
+ title: 'DeviceName',
+ minWidth: 150,
+ },
+ {
+ field: 'nickname',
+ title: '备注名称',
+ minWidth: 120,
+ },
+ {
+ field: 'picUrl',
+ title: '设备图片',
+ width: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'productId',
+ title: '所属产品',
+ minWidth: 120,
+ slots: { default: 'product' },
+ },
+ {
+ field: 'deviceType',
+ title: '设备类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE },
+ },
+ },
+ {
+ field: 'groupIds',
+ title: '所属分组',
+ minWidth: 150,
+ slots: { default: 'groups' },
+ },
+ {
+ field: 'state',
+ title: '设备状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.IOT_DEVICE_STATE },
+ },
+ },
+ {
+ field: 'onlineTime',
+ title: '最后上线时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/index.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/index.vue
new file mode 100644
index 000000000..cbe9319e7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/index.vue
@@ -0,0 +1,164 @@
+
+
+
+ getDeviceData(id)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getDeviceData(id)"
+ />
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/config.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/config.vue
new file mode 100644
index 000000000..8f263d28c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/config.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
{{ formattedConfig }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/header.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/header.vue
new file mode 100644
index 000000000..8a739a180
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/header.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
{{ device.deviceName }}
+
+
+
+
+
+
+
+
+
+
+ {{ product.name }}
+
+
+
+ {{ product.productKey }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/info.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/info.vue
new file mode 100644
index 000000000..ded227804
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/info.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/message.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/message.vue
new file mode 100644
index 000000000..04d07f3c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/message.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDateTime(row.ts) }}
+
+
+
+ {{ row.upstream ? '上行' : '下行' }}
+
+
+
+ {{ methodOptions.find((item) => item.value === row.method)?.label }}
+
+
+
+ {{ `{"code":${row.code},"msg":"${row.msg}","data":${row.data}\}` }}
+
+ {{ row.params }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-config-form.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-config-form.vue
new file mode 100644
index 000000000..fb792d4e7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-config-form.vue
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-config.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-config.vue
new file mode 100644
index 000000000..74f07130f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-config.vue
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-point-form.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-point-form.vue
new file mode 100644
index 000000000..db5fc0674
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/modbus-point-form.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/simulator.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/simulator.vue
new file mode 100644
index 000000000..ceb5b336a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/simulator.vue
@@ -0,0 +1,618 @@
+
+
+
+
+
+
+
+
+
+
+
+ 指令调试
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置属性值后,点击「发送属性上报」按钮
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ record.event?.dataType ?? '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置属性值后,点击「发送属性设置」按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设备消息
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/sub-device.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/sub-device.vue
new file mode 100644
index 000000000..3c1da17c6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/sub-device.vue
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-event.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-event.vue
new file mode 100644
index 000000000..77e533e7e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-event.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+ 标识符:
+
+
+
+ 时间范围:
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ row.request?.reportTime ? formatDateTime(row.request.reportTime) : '-'
+ }}
+
+
+
+ {{ row.request?.identifier }}
+
+
+
+ {{ getEventName(row.request?.identifier) }}
+
+
+ {{ getEventType(row.request?.identifier) }}
+
+
+ {{ parseParams(row.request?.params) }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-property-history.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-property-history.vue
new file mode 100644
index 000000000..1cb400c34
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-property-history.vue
@@ -0,0 +1,552 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDate(new Date(record.updateTime)) }}
+
+
+
+ {{ formatComplexValue(record.value) }}
+
+ {{ record.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-property.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-property.vue
new file mode 100644
index 000000000..b01765cff
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-property.vue
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.name }}
+
+
+
+ {{ item.identifier }}
+
+
+
+
+
+ {{ item.dataType }}
+
+
+
+
+
+
+
+
+
+
+
+ 属性值
+
+ {{ formatValueWithUnit(item) }}
+
+
+
+ 更新时间
+
+ {{
+ item.updateTime ? formatDateTime(item.updateTime) : '-'
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatValueWithUnit(row) }}
+
+
+ {{ row.updateTime ? formatDateTime(row.updateTime) : '-' }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-service.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-service.vue
new file mode 100644
index 000000000..3971d00d9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model-service.vue
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
+
+ 标识符:
+
+
+
+ 时间范围:
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ row.request?.reportTime ? formatDateTime(row.request.reportTime) : '-'
+ }}
+
+
+ {{ row.reply?.reportTime ? formatDateTime(row.reply.reportTime) : '-' }}
+
+
+
+ {{ row.request?.identifier }}
+
+
+
+ {{ getServiceName(row.request?.identifier) }}
+
+
+ {{ getCallType(row.request?.identifier) }}
+
+
+ {{ parseParams(row.request?.params) }}
+
+
+
+ {{
+ `{"code":${row.reply.code},"msg":"${row.reply.msg}","data":${row.reply.data}\}`
+ }}
+
+ -
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model.vue b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model.vue
new file mode 100644
index 000000000..3862c6620
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/detail/modules/thing-model.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/index.vue b/apps/web-antdv-next/src/views/iot/device/device/index.vue
new file mode 100644
index 000000000..9b1d53e58
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/index.vue
@@ -0,0 +1,523 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ products.find((p) => p.id === row.productId)?.name || '-' }}
+
+
+
+
+
+ {{ deviceGroups.find((g) => g.id === groupId)?.name }}
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/modules/card-view.vue b/apps/web-antdv-next/src/views/iot/device/device/modules/card-view.vue
new file mode 100644
index 000000000..dca9b5968
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/modules/card-view.vue
@@ -0,0 +1,428 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.deviceName }}
+
+
+
+
+
+
+
+
+ 设备类型
+
+
+
+ Deviceid
+
+
+ {{ item.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/modules/form.vue b/apps/web-antdv-next/src/views/iot/device/device/modules/form.vue
new file mode 100644
index 000000000..7e452f523
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/modules/form.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/modules/group-form.vue b/apps/web-antdv-next/src/views/iot/device/device/modules/group-form.vue
new file mode 100644
index 000000000..ee1ba20d6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/modules/group-form.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/device/modules/import-form.vue b/apps/web-antdv-next/src/views/iot/device/device/modules/import-form.vue
new file mode 100644
index 000000000..ca4c5d9ea
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/device/modules/import-form.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/group/data.ts b/apps/web-antdv-next/src/views/iot/device/group/data.ts
new file mode 100644
index 000000000..ca6a10a88
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/group/data.ts
@@ -0,0 +1,125 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '分组名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分组名称',
+ },
+ rules: z
+ .string()
+ .min(1, '分组名称不能为空')
+ .max(64, '分组名称长度不能超过 64 个字符'),
+ },
+ {
+ fieldName: 'status',
+ label: '分组状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'description',
+ label: '分组描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入分组描述',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分组名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分组名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: 'ID',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '分组名称',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'description',
+ title: '分组描述',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'deviceCount',
+ title: '设备数量',
+ minWidth: 100,
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/device/group/index.vue b/apps/web-antdv-next/src/views/iot/device/group/index.vue
new file mode 100644
index 000000000..2ee40a18b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/group/index.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/device/group/modules/form.vue b/apps/web-antdv-next/src/views/iot/device/group/modules/form.vue
new file mode 100644
index 000000000..3e1a2d521
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/device/group/modules/form.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/home/chart-options.ts b/apps/web-antdv-next/src/views/iot/home/chart-options.ts
new file mode 100644
index 000000000..902181505
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/chart-options.ts
@@ -0,0 +1,196 @@
+/** 消息趋势图表配置 */
+export function getMessageTrendChartOptions(
+ times: string[],
+ upstreamData: number[],
+ downstreamData: number[],
+): any {
+ return {
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ label: {
+ backgroundColor: '#6a7985',
+ },
+ },
+ },
+ legend: {
+ data: ['上行消息', '下行消息'],
+ top: '5%',
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ top: '15%',
+ containLabel: true,
+ },
+ xAxis: [
+ {
+ type: 'category',
+ boundaryGap: false,
+ data: times,
+ },
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ name: '消息数量',
+ },
+ ],
+ series: [
+ {
+ name: '上行消息',
+ type: 'line',
+ smooth: true,
+ areaStyle: {
+ opacity: 0.3,
+ },
+ emphasis: {
+ focus: 'series',
+ },
+ data: upstreamData,
+ itemStyle: {
+ color: '#1890ff',
+ },
+ },
+ {
+ name: '下行消息',
+ type: 'line',
+ smooth: true,
+ areaStyle: {
+ opacity: 0.3,
+ },
+ emphasis: {
+ focus: 'series',
+ },
+ data: downstreamData,
+ itemStyle: {
+ color: '#52c41a',
+ },
+ },
+ ],
+ };
+}
+
+/**
+ * 设备状态仪表盘图表配置
+ */
+export function getDeviceStateGaugeChartOptions(
+ value: number,
+ max: number,
+ color: string,
+ title: string,
+): any {
+ return {
+ series: [
+ {
+ type: 'gauge',
+ startAngle: 225,
+ endAngle: -45,
+ min: 0,
+ max,
+ center: ['50%', '50%'],
+ radius: '80%',
+ progress: {
+ show: true,
+ width: 12,
+ itemStyle: {
+ color,
+ },
+ },
+ axisLine: {
+ lineStyle: {
+ width: 12,
+ color: [[1, '#E5E7EB']] as [number, string][],
+ },
+ },
+ axisTick: { show: false },
+ splitLine: { show: false },
+ axisLabel: { show: false },
+ pointer: { show: false },
+ title: {
+ show: true,
+ offsetCenter: [0, '80%'],
+ fontSize: 14,
+ color: '#666',
+ },
+ detail: {
+ valueAnimation: true,
+ fontSize: 32,
+ fontWeight: 'bold',
+ color,
+ offsetCenter: [0, '10%'],
+ formatter: (val: number) => `${val} 个`,
+ },
+ data: [{ value, name: title }],
+ },
+ ],
+ };
+}
+
+/**
+ * 设备数量饼图配置
+ */
+export function getDeviceCountPieChartOptions(
+ data: Array<{ name: string; value: number }>,
+): any {
+ return {
+ tooltip: {
+ trigger: 'item',
+ formatter: '{b}: {c} 个 ({d}%)',
+ },
+ legend: {
+ type: 'scroll',
+ orient: 'horizontal',
+ bottom: '10px',
+ left: 'center',
+ icon: 'circle',
+ itemWidth: 10,
+ itemHeight: 10,
+ itemGap: 12,
+ textStyle: {
+ fontSize: 12,
+ },
+ pageButtonPosition: 'end',
+ pageIconSize: 12,
+ pageTextStyle: {
+ fontSize: 12,
+ },
+ pageFormatter: '{current}/{total}',
+ },
+ series: [
+ {
+ name: '设备数量',
+ type: 'pie',
+ radius: ['35%', '55%'],
+ center: ['50%', '40%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 8,
+ borderColor: '#fff',
+ borderWidth: 2,
+ },
+ label: {
+ show: false,
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ },
+ labelLine: {
+ show: false,
+ },
+ data,
+ },
+ ],
+ };
+}
diff --git a/apps/web-antdv-next/src/views/iot/home/data.ts b/apps/web-antdv-next/src/views/iot/home/data.ts
new file mode 100644
index 000000000..5a84bd8e9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/data.ts
@@ -0,0 +1,20 @@
+import type { IotStatisticsApi } from '#/api/iot/statistics';
+
+/** 统计数据 */
+export type StatsData = IotStatisticsApi.StatisticsSummaryRespVO;
+
+/** 默认统计数据 */
+export const defaultStatsData: StatsData = {
+ productCategoryCount: 0,
+ productCount: 0,
+ deviceCount: 0,
+ deviceMessageCount: 0,
+ productCategoryTodayCount: 0,
+ productTodayCount: 0,
+ deviceTodayCount: 0,
+ deviceMessageTodayCount: 0,
+ deviceOnlineCount: 0,
+ deviceOfflineCount: 0,
+ deviceInactiveCount: 0,
+ productCategoryDeviceCounts: {},
+};
diff --git a/apps/web-antdv-next/src/views/iot/home/index.vue b/apps/web-antdv-next/src/views/iot/home/index.vue
new file mode 100644
index 000000000..e05863fe9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/index.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/home/modules/device-count-card.vue b/apps/web-antdv-next/src/views/iot/home/modules/device-count-card.vue
new file mode 100644
index 000000000..422f6e758
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/modules/device-count-card.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/home/modules/device-map-card.vue b/apps/web-antdv-next/src/views/iot/home/modules/device-map-card.vue
new file mode 100644
index 000000000..baa2eec75
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/modules/device-map-card.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+ 在线
+
+
+
+ 离线
+
+
+
+ 待激活
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/home/modules/device-state-count-card.vue b/apps/web-antdv-next/src/views/iot/home/modules/device-state-count-card.vue
new file mode 100644
index 000000000..3a93f21f0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/modules/device-state-count-card.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/home/modules/message-trend-card.vue b/apps/web-antdv-next/src/views/iot/home/modules/message-trend-card.vue
new file mode 100644
index 000000000..f4a2a16cd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/home/modules/message-trend-card.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
消息量统计
+
+
+
+ 时间范围
+
+
+
+
+ 时间间隔
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/data.ts b/apps/web-antdv-next/src/views/iot/ota/data.ts
new file mode 100644
index 000000000..ff9ba6ee1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/data.ts
@@ -0,0 +1,173 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getSimpleProductList } from '#/api/iot/product/product';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改固件的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '固件名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入固件名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productId',
+ label: '所属产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'version',
+ label: '版本号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入版本号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '固件描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入固件描述',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '固件文件',
+ component: 'Upload',
+ componentProps: {
+ maxCount: 1,
+ accept: '.bin,.hex,.zip',
+ },
+ rules: 'required',
+ help: '支持上传 .bin、.hex、.zip 格式的固件文件',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '固件名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入固件名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ fixed: 'left',
+ },
+ {
+ field: 'id',
+ title: '固件编号',
+ width: 100,
+ },
+ {
+ field: 'name',
+ title: '固件名称',
+ minWidth: 150,
+ },
+ {
+ field: 'version',
+ title: '版本号',
+ width: 120,
+ },
+ {
+ field: 'productName',
+ title: '所属产品',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '固件描述',
+ minWidth: 200,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'fileSize',
+ title: '文件大小',
+ width: 120,
+ formatter: ({ cellValue }) => {
+ if (!cellValue) return '-';
+ const kb = cellValue / 1024;
+ if (kb < 1024) return `${kb.toFixed(2)} KB`;
+ return `${(kb / 1024).toFixed(2)} MB`;
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ width: 100,
+ formatter: ({ cellValue }) => {
+ return cellValue === 1 ? '启用' : '禁用';
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/ota/firmware/data.ts b/apps/web-antdv-next/src/views/iot/ota/firmware/data.ts
new file mode 100644
index 000000000..7b684dba9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/firmware/data.ts
@@ -0,0 +1,157 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getSimpleProductList } from '#/api/iot/product/product';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改固件的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '固件名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入固件名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productId',
+ label: '所属产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'version',
+ label: '版本号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入版本号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '固件描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入固件描述',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '固件文件',
+ component: 'FileUpload',
+ componentProps: {
+ maxNumber: 1,
+ accept: ['bin', 'hex', 'zip'],
+ maxSize: 50,
+ helpText: '支持上传 .bin、.hex、.zip 格式的固件文件,最大 50MB',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '固件名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入固件名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '固件编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '固件名称',
+ minWidth: 150,
+ },
+ {
+ field: 'version',
+ title: '版本号',
+ minWidth: 120,
+ },
+ {
+ field: 'description',
+ title: '固件描述',
+ minWidth: 200,
+ },
+ {
+ field: 'productId',
+ title: '所属产品',
+ minWidth: 150,
+ slots: { default: 'product' },
+ },
+ {
+ field: 'fileUrl',
+ title: '固件文件',
+ minWidth: 120,
+ slots: { default: 'fileUrl' },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/ota/firmware/index.vue b/apps/web-antdv-next/src/views/iot/ota/firmware/index.vue
new file mode 100644
index 000000000..8d19ad230
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/firmware/index.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.productName || '未知产品' }}
+
+
+
+
+
+ 无文件
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/index.vue b/apps/web-antdv-next/src/views/iot/ota/index.vue
new file mode 100644
index 000000000..6b0a148a6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/index.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/modules/detail/index.vue b/apps/web-antdv-next/src/views/iot/ota/modules/detail/index.vue
new file mode 100644
index 000000000..c1dabcb1c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/modules/detail/index.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+ {{ firmware?.name }}
+
+
+ {{ firmware?.productName }}
+
+
+ {{ firmware?.version }}
+
+
+ {{
+ firmware?.createTime
+ ? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
+ : '-'
+ }}
+
+
+ {{ firmware?.description }}
+
+
+
+
+
+
+
+
+
+
+ {{
+ Object.values(firmwareStatistics).reduce(
+ (sum: number, count) => sum + (count || 0),
+ 0,
+ ) || 0
+ }}
+
+
升级设备总数
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
+ 0
+ }}
+
+
待推送
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
+ }}
+
+
已推送
+
+
+
+
+
+ {{
+ firmwareStatistics[
+ IoTOtaTaskRecordStatusEnum.UPGRADING.value
+ ] || 0
+ }}
+
+
正在升级
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
+ 0
+ }}
+
+
升级成功
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
+ 0
+ }}
+
+
升级失败
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
+ 0
+ }}
+
+
升级取消
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/modules/firmware-detail/index.vue b/apps/web-antdv-next/src/views/iot/ota/modules/firmware-detail/index.vue
new file mode 100644
index 000000000..eb947ca89
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/modules/firmware-detail/index.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+ {{ firmware?.name }}
+
+
+ {{ firmware?.productName }}
+
+
+ {{ firmware?.version }}
+
+
+ {{
+ firmware?.createTime
+ ? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
+ : '-'
+ }}
+
+
+ {{ firmware?.description }}
+
+
+
+
+
+
+
+
+
+
+ {{
+ Object.values(firmwareStatistics).reduce(
+ (sum: number, count) => sum + (count || 0),
+ 0,
+ ) || 0
+ }}
+
+
升级设备总数
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
+ 0
+ }}
+
+
待推送
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
+ }}
+
+
已推送
+
+
+
+
+
+ {{
+ firmwareStatistics[
+ IoTOtaTaskRecordStatusEnum.UPGRADING.value
+ ] || 0
+ }}
+
+
正在升级
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
+ 0
+ }}
+
+
升级成功
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
+ 0
+ }}
+
+
升级失败
+
+
+
+
+
+ {{
+ firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
+ 0
+ }}
+
+
升级取消
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/modules/ota-firmware-form.vue b/apps/web-antdv-next/src/views/iot/ota/modules/ota-firmware-form.vue
new file mode 100644
index 000000000..896544c6f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/modules/ota-firmware-form.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-detail.vue b/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-detail.vue
new file mode 100644
index 000000000..8768c2bf6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-detail.vue
@@ -0,0 +1,415 @@
+
+
+
+
+
+
+
+
+ {{ task.id }}
+
+ {{ task.name }}
+
+
+ 全部设备
+ 指定设备
+ {{ task.deviceScope }}
+
+
+ 待执行
+ 执行中
+ 已完成
+ 已取消
+ {{ task.status }}
+
+
+ {{
+ task.createTime
+ ? formatDate(task.createTime, 'YYYY-MM-DD HH:mm:ss')
+ : '-'
+ }}
+
+
+ {{ task.description }}
+
+
+
+
+
+
+
+
+
+
+ {{
+ Object.values(taskStatistics).reduce(
+ (sum, count) => sum + (count || 0),
+ 0,
+ ) || 0
+ }}
+
+
升级设备总数
+
+
+
+
+
+ {{
+ taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0
+ }}
+
+
待推送
+
+
+
+
+
+ {{
+ taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
+ }}
+
+
已推送
+
+
+
+
+
+ {{
+ taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] ||
+ 0
+ }}
+
+
正在升级
+
+
+
+
+
+ {{
+ taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0
+ }}
+
+
升级成功
+
+
+
+
+
+ {{
+ taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0
+ }}
+
+
升级失败
+
+
+
+
+
+ {{
+ taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0
+ }}
+
+
升级取消
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 待推送
+ 已推送
+
+ 升级中
+
+ 成功
+ 失败
+ 已取消
+ {{ record.status }}
+
+
+
+
+ {{ record.progress }}%
+
+
+
+
+
+ 取消
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-form.vue b/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-form.vue
new file mode 100644
index 000000000..bb3818cc5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-form.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-list.vue b/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-list.vue
new file mode 100644
index 000000000..a8a92762b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/ota/modules/task/ota-task-list.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 全部设备
+ 指定设备
+ {{ record.deviceScope }}
+
+
+
+
+ {{ record.deviceSuccessCount }}/{{ record.deviceTotalCount }}
+
+
+
+
+ 待执行
+ 执行中
+ 已完成
+ 已取消
+ {{ record.status }}
+
+
+
+
+
+ 详情
+
+ 取消
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/category/data.ts b/apps/web-antdv-next/src/views/iot/product/category/data.ts
new file mode 100644
index 000000000..57dcae56f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/category/data.ts
@@ -0,0 +1,138 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '分类名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名字',
+ },
+ rules: z
+ .string()
+ .min(1, '分类名字不能为空')
+ .max(64, '分类名字长度不能超过 64 个字符'),
+ },
+ {
+ fieldName: 'sort',
+ label: '分类排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入分类排序',
+ class: 'w-full',
+ min: 0,
+ precision: 0,
+ },
+ defaultValue: 0,
+ rules: z.number().min(0, '分类排序不能小于 0'),
+ },
+ {
+ fieldName: 'status',
+ label: '分类状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'description',
+ label: '描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入分类描述',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分类名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名字',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'seq',
+ title: 'ID',
+ width: 80,
+ },
+ {
+ field: 'name',
+ title: '名字',
+ minWidth: 200,
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ width: 100,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'description',
+ title: '描述',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/product/category/index.vue b/apps/web-antdv-next/src/views/iot/product/category/index.vue
new file mode 100644
index 000000000..3dc7201b7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/category/index.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/category/modules/form.vue b/apps/web-antdv-next/src/views/iot/product/category/modules/form.vue
new file mode 100644
index 000000000..fc92904d0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/category/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/components/index.ts b/apps/web-antdv-next/src/views/iot/product/product/components/index.ts
new file mode 100644
index 000000000..5ddac0c31
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/components/index.ts
@@ -0,0 +1 @@
+export { default as ProductSelect } from './select.vue';
diff --git a/apps/web-antdv-next/src/views/iot/product/product/components/select.vue b/apps/web-antdv-next/src/views/iot/product/product/components/select.vue
new file mode 100644
index 000000000..9e5d00702
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/components/select.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/data.ts b/apps/web-antdv-next/src/views/iot/product/product/data.ts
new file mode 100644
index 000000000..f2c8fcb6c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/data.ts
@@ -0,0 +1,273 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { IotProductCategoryApi } from '#/api/iot/product/category';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { Button } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { getSimpleProductCategoryList } from '#/api/iot/product/category';
+
+/** 产品分类列表缓存 */
+let categoryList: IotProductCategoryApi.ProductCategory[] = [];
+getSimpleProductCategoryList().then((data) => (categoryList = data));
+
+/** 基础表单字段(不含图标、图片、描述) */
+export function useBasicFormSchema(
+ formApi?: any,
+ generateProductKey?: () => string,
+): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'productKey',
+ label: 'ProductKey',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 ProductKey',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ if(values) {
+ return !values.id;
+ },
+ },
+ rules: z
+ .string()
+ .min(1, 'ProductKey 不能为空')
+ .max(32, 'ProductKey 长度不能超过 32 个字符'),
+ suffix: () => {
+ return h(
+ Button,
+ {
+ type: 'default',
+ onClick: () => {
+ if (generateProductKey) {
+ formApi?.setFieldValue('productKey', generateProductKey());
+ }
+ },
+ },
+ { default: () => '重新生成' },
+ );
+ },
+ },
+ {
+ fieldName: 'productKey',
+ label: 'ProductKey',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 ProductKey',
+ disabled: true,
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ if(values) {
+ return !!values.id;
+ },
+ },
+ rules: z
+ .string()
+ .min(1, 'ProductKey 不能为空')
+ .max(32, 'ProductKey 长度不能超过 32 个字符'),
+ },
+ {
+ fieldName: 'name',
+ label: '产品名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入产品名称',
+ },
+ rules: z
+ .string()
+ .min(1, '产品名称不能为空')
+ .max(64, '产品名称长度不能超过 64 个字符'),
+ },
+ {
+ fieldName: 'categoryId',
+ label: '产品分类',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductCategoryList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品分类',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'deviceType',
+ label: '设备类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'netType',
+ label: '联网方式',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_NET_TYPE, 'number'),
+ placeholder: '请选择联网方式',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'protocolType',
+ label: '协议类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'string'),
+ placeholder: '请选择协议类型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'serializeType',
+ label: '序列化类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_SERIALIZE_TYPE, 'string'),
+ placeholder: '请选择序列化类型',
+ },
+ help: 'iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型',
+ rules: 'required',
+ },
+ // TODO @haohao:这个貌似不需要?!
+ {
+ fieldName: 'status',
+ label: '产品状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: 0,
+ rules: 'required',
+ },
+ ];
+}
+
+/** 高级设置表单字段(图标、图片、产品描述、动态注册) */
+export function useAdvancedFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'registerEnabled',
+ label: '动态注册',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '开',
+ unCheckedChildren: '关',
+ },
+ defaultValue: false,
+ help: '设备动态注册无需一一烧录设备证书(DeviceSecret),每台设备烧录相同的产品证书,即 ProductKey 和 ProductSecret ,云端鉴权通过后下发设备证书,您可以根据需要开启或关闭动态注册,保障安全性。',
+ },
+ {
+ fieldName: 'icon',
+ label: '产品图标',
+ component: 'IconPicker',
+ componentProps: {
+ placeholder: '请选择产品图标',
+ prefix: 'carbon',
+ },
+ },
+ {
+ fieldName: 'picUrl',
+ label: '产品图片',
+ component: 'ImageUpload',
+ },
+ {
+ fieldName: 'description',
+ label: '产品描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入产品描述',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: 'ID',
+ width: 80,
+ },
+ {
+ field: 'productKey',
+ title: 'ProductKey',
+ minWidth: 150,
+ },
+ {
+ field: 'categoryId',
+ title: '品类',
+ minWidth: 120,
+ formatter: ({ cellValue }) =>
+ categoryList.find((c) => c.id === cellValue)?.name || '未分类',
+ },
+ {
+ field: 'deviceType',
+ title: '设备类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE },
+ },
+ },
+ {
+ field: 'icon',
+ title: '产品图标',
+ width: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'picUrl',
+ title: '产品图片',
+ width: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'status',
+ title: '产品状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.IOT_PRODUCT_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/product/product/detail/index.vue b/apps/web-antdv-next/src/views/iot/product/product/detail/index.vue
new file mode 100644
index 000000000..5fc1fc7bd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/detail/index.vue
@@ -0,0 +1,92 @@
+
+
+
+
+ getProductData(id)"
+ />
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/detail/modules/header.vue b/apps/web-antdv-next/src/views/iot/product/product/detail/modules/header.vue
new file mode 100644
index 000000000..864946721
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/detail/modules/header.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
{{ product.name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ product.productKey }}
+
+
+
+
+ {{ product.deviceCount ?? '加载中...' }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/detail/modules/info.vue b/apps/web-antdv-next/src/views/iot/product/product/detail/modules/info.vue
new file mode 100644
index 000000000..74f1156ac
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/detail/modules/info.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+ {{ product.name }}
+
+
+ {{ product.categoryName || '-' }}
+
+
+
+
+
+ {{ formatDate(product.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ product.productSecret }}
+ ********
+
+
+
+
+ {{ product.registerEnabled ? '已开启' : '未开启' }}
+
+
+ {{ product.description || '-' }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/index.vue b/apps/web-antdv-next/src/views/iot/product/product/index.vue
new file mode 100644
index 000000000..0e94d1adf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/index.vue
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 产品名称
+
+
+
+
+ ProductKey
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/modules/card-view.vue b/apps/web-antdv-next/src/views/iot/product/product/modules/card-view.vue
new file mode 100644
index 000000000..cde81f578
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/modules/card-view.vue
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 产品分类
+
+ {{ getCategoryName(item.categoryId) }}
+
+
+
+ 产品类型
+
+
+
+ 产品标识
+
+
+ {{ item.productKey || item.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/product/product/modules/form.vue b/apps/web-antdv-next/src/views/iot/product/product/modules/form.vue
new file mode 100644
index 000000000..b3cdd6365
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/product/product/modules/form.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/data.ts b/apps/web-antdv-next/src/views/iot/rule/data/data.ts
new file mode 100644
index 000000000..efebb92d0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/data.ts
@@ -0,0 +1,103 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleProductList } from '#/api/iot/product/product';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '规则名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入规则名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'productId',
+ label: '产品',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleProductList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择产品',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '规则状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '规则编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '规则名称',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '规则描述',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '规则状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ // TODO @haohao:这里是【数据源】【数据目的】
+ {
+ field: 'sinkCount',
+ title: '数据流转数',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/index.vue b/apps/web-antdv-next/src/views/iot/rule/data/index.vue
new file mode 100644
index 000000000..f6eb38168
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/index.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/rule/components/source-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/rule/components/source-config-form.vue
new file mode 100644
index 000000000..62e050486
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/rule/components/source-config-form.vue
@@ -0,0 +1,309 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/rule/data-rule-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/rule/data-rule-form.vue
new file mode 100644
index 000000000..c0e939d53
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/rule/data-rule-form.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/rule/data.ts b/apps/web-antdv-next/src/views/iot/rule/data/rule/data.ts
new file mode 100644
index 000000000..d4e453b4b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/rule/data.ts
@@ -0,0 +1,150 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '规则名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入规则名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '规则状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 规则表单 Schema */
+export function useRuleFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ show: false,
+ triggerFields: ['id'],
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '规则名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入规则名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '规则描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入规则描述',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '规则状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ defaultValue: 0,
+ rules: 'required',
+ },
+ {
+ fieldName: 'sinkIds',
+ label: '数据目的',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择数据目的',
+ mode: 'multiple',
+ allowClear: true,
+ options: [],
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '规则编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '规则名称',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '规则描述',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '规则状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'sourceConfigs',
+ title: '数据源',
+ minWidth: 100,
+ formatter: ({ cellValue }: any) => `${cellValue?.length || 0} 个`,
+ },
+ {
+ field: 'sinkIds',
+ title: '数据目的',
+ minWidth: 100,
+ formatter: ({ cellValue }: any) => `${cellValue?.length || 0} 个`,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/rule/index.vue b/apps/web-antdv-next/src/views/iot/rule/data/rule/index.vue
new file mode 100644
index 000000000..6ec2d00d4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/rule/index.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/components/key-value-editor.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/components/key-value-editor.vue
new file mode 100644
index 000000000..2d33d8e1b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/components/key-value-editor.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/http-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/http-config-form.vue
new file mode 100644
index 000000000..bec6e969d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/http-config-form.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/index.ts b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/index.ts
new file mode 100644
index 000000000..6fb57388c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/index.ts
@@ -0,0 +1,6 @@
+export { default as HttpConfigForm } from './http-config-form.vue';
+export { default as KafkaMqConfigForm } from './kafka-mq-config-form.vue';
+export { default as MqttConfigForm } from './mqtt-config-form.vue';
+export { default as RabbitMqConfigForm } from './rabbit-mq-config-form.vue';
+export { default as RedisStreamConfigForm } from './redis-stream-config-form.vue';
+export { default as RocketMqConfigForm } from './rocket-mq-config-form.vue';
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/kafka-mq-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/kafka-mq-config-form.vue
new file mode 100644
index 000000000..8a3616513
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/kafka-mq-config-form.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/mqtt-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/mqtt-config-form.vue
new file mode 100644
index 000000000..e903bc8f5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/mqtt-config-form.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/rabbit-mq-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/rabbit-mq-config-form.vue
new file mode 100644
index 000000000..7a3a3d50c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/rabbit-mq-config-form.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/redis-stream-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/redis-stream-config-form.vue
new file mode 100644
index 000000000..75f4e4c5d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/redis-stream-config-form.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/config/rocket-mq-config-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/rocket-mq-config-form.vue
new file mode 100644
index 000000000..384e7e0b4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/config/rocket-mq-config-form.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/data-sink-form.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/data-sink-form.vue
new file mode 100644
index 000000000..98ce5faf7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/data-sink-form.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/data.ts b/apps/web-antdv-next/src/views/iot/rule/data/sink/data.ts
new file mode 100644
index 000000000..9af434569
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/data.ts
@@ -0,0 +1,155 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '目的名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入目的名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '目的状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '目的类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM, 'number'),
+ placeholder: '请选择目的类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 目的表单 Schema */
+export function useSinkFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ show: false,
+ triggerFields: ['id'],
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '目的名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入目的名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '目的描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入目的描述',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '目的类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM, 'number'),
+ placeholder: '请选择目的类型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '目的状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ defaultValue: 0,
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '目的编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '目的名称',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '目的描述',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '目的状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'type',
+ title: '目的类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/rule/data/sink/index.vue b/apps/web-antdv-next/src/views/iot/rule/data/sink/index.vue
new file mode 100644
index 000000000..8d81805c5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/data/sink/index.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/data.ts b/apps/web-antdv-next/src/views/iot/rule/scene/data.ts
new file mode 100644
index 000000000..fd948f9bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/data.ts
@@ -0,0 +1,139 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '规则名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入规则名称',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '规则状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'description',
+ label: '规则描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入规则描述',
+ rows: 3,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '规则名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入规则名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '规则状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '规则编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '规则名称',
+ minWidth: 150,
+ },
+ {
+ field: 'description',
+ title: '规则描述',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '规则状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'actionCount',
+ title: '执行动作数',
+ minWidth: 100,
+ },
+ {
+ field: 'executeCount',
+ title: '执行次数',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/alert-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/alert-config.vue
new file mode 100644
index 000000000..1edf04e92
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/alert-config.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/condition-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/condition-config.vue
new file mode 100644
index 000000000..3879d0fa6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/condition-config.vue
@@ -0,0 +1,338 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateConditionField('productId', value)
+ "
+ @change="handleProductChange"
+ />
+
+
+
+
+ updateConditionField('deviceId', value)
+ "
+ :product-id="condition.productId"
+ @change="handleDeviceChange"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateConditionField('identifier', value)
+ "
+ :trigger-type="triggerType"
+ :product-id="condition.productId"
+ :device-id="condition.deviceId"
+ @change="handlePropertyChange"
+ />
+
+
+
+
+
+
+ updateConditionField('operator', value)
+ "
+ :property-type="propertyType"
+ @change="handleOperatorChange"
+ />
+
+
+
+
+
+
+ updateConditionField('param', value)
+ "
+ :property-type="propertyType"
+ :operator="condition.operator"
+ :property-config="propertyConfig"
+ />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/current-time-condition-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/current-time-condition-config.vue
new file mode 100644
index 000000000..af63c77a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/current-time-condition-config.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 无需设置时间值
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/device-control-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/device-control-config.vue
new file mode 100644
index 000000000..28bef096c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/device-control-config.vue
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/device-trigger-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/device-trigger-config.vue
new file mode 100644
index 000000000..b3f9ad675
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/device-trigger-config.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
与"主条件"为且关系
+
+ {{ trigger.conditionGroups?.length || 0 }} 个子条件组
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ subGroupIndex + 1 }}
+
+
子条件组 {{ subGroupIndex + 1 }}
+
+
+ 组内条件为"且"关系
+
+
+ {{ (subGroup as any)?.length || 0 }}个条件
+
+
+
+
+
+
updateSubGroup(subGroupIndex, value)
+ "
+ :trigger-type="trigger.type as any"
+ :max-conditions="maxConditionsPerGroup"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
暂无子条件组
+
点击上方"添加子条件组"按钮开始配置
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/main-condition-inner-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/main-condition-inner-config.vue
new file mode 100644
index 000000000..59f72afe5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/main-condition-inner-config.vue
@@ -0,0 +1,374 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateConditionField('productId', value)
+ "
+ @change="handleProductChange"
+ />
+
+
+
+
+ updateConditionField('deviceId', value)
+ "
+ :product-id="condition.productId"
+ @change="handleDeviceChange"
+ />
+
+
+
+
+
+
+
+
+
+ updateConditionField('identifier', value)
+ "
+ :trigger-type="triggerType"
+ :product-id="condition.productId"
+ :device-id="condition.deviceId"
+ @change="handlePropertyChange"
+ />
+
+
+
+
+
+
+ updateConditionField('operator', value)
+ "
+ :property-type="propertyType"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ updateConditionField('value', value)
+ "
+ :property-type="propertyType"
+ :operator="condition.operator"
+ :property-config="propertyConfig"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ updateConditionField('productId', value)
+ "
+ @change="handleProductChange"
+ />
+
+
+
+
+ updateConditionField('deviceId', value)
+ "
+ :product-id="condition.productId"
+ @change="handleDeviceChange"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
+
+
此触发类型暂不需要配置额外条件
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue
new file mode 100644
index 000000000..9fcd86514
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ conditionIndex + 1 }}
+
+
+ 条件 {{ conditionIndex + 1 }}
+
+
+
+
+
+
+
+ updateCondition(conditionIndex, value)
+ "
+ :trigger-type="triggerType"
+ />
+
+
+
+
+
+
+
+
+ 最多可添加 {{ maxConditions }} 个条件
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/inputs/json-params-input.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/inputs/json-params-input.vue
new file mode 100644
index 000000000..6097c911c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/inputs/json-params-input.vue
@@ -0,0 +1,587 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+ {{ paramsLabel }}
+
+
+
+
+
+
+ {{ param.name }}
+
+ {{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
+
+
+
+ {{ param.identifier }}
+
+
+
+
+ {{ getParamTypeName(param.dataType) }}
+
+
+ {{ getExampleValue(param) }}
+
+
+
+
+
+
+
+ {{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
+
+
+ {{ generateExampleJson() }}
+
+
+
+
+
+
+
+
+ {{ emptyMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ jsonError || JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_CORRECT }}
+
+
+
+
+
+
+ {{ JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/inputs/value-input.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/inputs/value-input.vue
new file mode 100644
index 000000000..c17a8b3f6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/inputs/value-input.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 至
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 解析结果:
+
+ {{ item }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ propertyConfig.unit }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/rule-scene-form.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/rule-scene-form.vue
new file mode 100644
index 000000000..bd1d85135
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/rule-scene-form.vue
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/action-section.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/action-section.vue
new file mode 100644
index 000000000..12e2d5c07
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/action-section.vue
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+ 执行器配置
+ {{ actions.length }} 个执行器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
执行器 {{ index + 1 }}
+
+
+ {{ getActionTypeLabel(action.type as any) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
updateAction(index, value)"
+ />
+
+
+ updateActionAlertConfig(index, value)
+ "
+ />
+
+
+
+
+
+ 触发告警
+ 自动执行
+
+
+ 当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
+ 告警配置] 管理。
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/basic-info-section.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/basic-info-section.vue
new file mode 100644
index 000000000..034cb6bbe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/basic-info-section.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/trigger-section.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/trigger-section.vue
new file mode 100644
index 000000000..2087030ce
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/sections/trigger-section.vue
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+ 触发器配置
+ {{ triggers.length }} 个触发器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
触发器 {{ index + 1 }}
+
+
+ {{ getTriggerTypeLabel(triggerItem.type as any) }}
+
+
+
+
+
+
+
+
+
+
+
updateTriggerDeviceConfig(index, value)
+ "
+ @trigger-type-change="(type) => updateTriggerType(index, type)"
+ />
+
+
+
+
+
+
+ 定时触发配置
+
+
+
+
+
+
+ updateTriggerCronConfig(index, value)
+ "
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
暂无触发器配置
+
+ 请使用上方的"添加触发器"按钮来设置触发规则
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/device-selector.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/device-selector.vue
new file mode 100644
index 000000000..303eade75
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/device-selector.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/operator-selector.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/operator-selector.vue
new file mode 100644
index 000000000..1cab17955
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/operator-selector.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/product-selector.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/product-selector.vue
new file mode 100644
index 000000000..51588298e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/product-selector.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/property-selector.vue b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/property-selector.vue
new file mode 100644
index 000000000..31bf39012
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/form/selectors/property-selector.vue
@@ -0,0 +1,465 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedProperty.name }}
+
+
+ {{ getDataTypeName(selectedProperty.dataType) }}
+
+
+
+
+
+
+ 标识符:
+
+
+ {{ selectedProperty.identifier }}
+
+
+
+
+
+ 描述:
+
+
+ {{ selectedProperty.description }}
+
+
+
+
+
+ 单位:
+
+
+ {{ selectedProperty.unit }}
+
+
+
+
+
+ 取值范围:
+
+
+ {{ selectedProperty.range }}
+
+
+
+
+
+
+ 访问模式:
+
+
+ {{ getAccessModeLabel(selectedProperty.accessMode) }}
+
+
+
+
+
+ 事件类型:
+
+
+ {{ getEventTypeLabel(selectedProperty.eventType) }}
+
+
+
+
+
+ 调用类型:
+
+
+ {{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/index.vue b/apps/web-antdv-next/src/views/iot/rule/scene/index.vue
new file mode 100644
index 000000000..a81ba6d55
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/index.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/rule/scene/modules/form.vue b/apps/web-antdv-next/src/views/iot/rule/scene/modules/form.vue
new file mode 100644
index 000000000..7a2de11bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/rule/scene/modules/form.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/data.ts b/apps/web-antdv-next/src/views/iot/thingmodel/data.ts
new file mode 100644
index 000000000..ec6072cf9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/data.ts
@@ -0,0 +1,65 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'type',
+ label: '功能类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE, 'number'),
+ placeholder: '请选择功能类型',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'type',
+ title: '功能类型',
+ minWidth: 20,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.IOT_THING_MODEL_TYPE },
+ },
+ },
+ {
+ field: 'name',
+ title: '功能名称',
+ minWidth: 150,
+ },
+ {
+ field: 'identifier',
+ title: '标识符',
+ minWidth: 20,
+ },
+ {
+ field: 'dataType',
+ title: '数据类型',
+ minWidth: 50,
+ slots: { default: 'dataType' },
+ },
+ {
+ field: 'property',
+ title: '属性',
+ minWidth: 200,
+ slots: { default: 'dataDefinition' },
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/index.vue b/apps/web-antdv-next/src/views/iot/thingmodel/index.vue
new file mode 100644
index 000000000..2486a2590
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/index.vue
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+ {{ getDataTypeLabel(row) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/components/data-definition.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/components/data-definition.vue
new file mode 100644
index 000000000..192819d5a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/components/data-definition.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+ 取值范围:{{
+ `${data.property?.dataSpecs?.min}~${data.property?.dataSpecs?.max}`
+ }}
+
+
+
+ 数据长度:{{ data.property?.dataSpecs?.length }}
+
+
+
+ -
+
+
+
+
+
+ {{
+ IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
+ ? '布尔值'
+ : '枚举值'
+ }}:{{ shortText }}
+
+
+
+
+
+
+ 调用方式:
+ {{ getThingModelServiceCallTypeLabel(data.service?.callType as any) }}
+
+
+
+ 事件类型:{{ getEventTypeLabel(data.event?.type as any) }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/components/index.ts b/apps/web-antdv-next/src/views/iot/thingmodel/modules/components/index.ts
new file mode 100644
index 000000000..d6316d657
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/components/index.ts
@@ -0,0 +1 @@
+export { default as DataDefinition } from './data-definition.vue';
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/index.ts b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/index.ts
new file mode 100644
index 000000000..2aa250005
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/index.ts
@@ -0,0 +1,4 @@
+export { default as ThingModelArrayDataSpecs } from './thing-model-array-data-specs.vue';
+export { default as ThingModelEnumDataSpecs } from './thing-model-enum-data-specs.vue';
+export { default as ThingModelNumberDataSpecs } from './thing-model-number-data-specs.vue';
+export { default as ThingModelStructDataSpecs } from './thing-model-struct-data-specs.vue';
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-array-data-specs.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-array-data-specs.vue
new file mode 100644
index 000000000..114e1fde2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-array-data-specs.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+ {{ `${item.value}(${item.label})` }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-enum-data-specs.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-enum-data-specs.vue
new file mode 100644
index 000000000..9d2a6a03c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-enum-data-specs.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+ 参数值
+ 参数描述
+
+
+
+
+
+
~
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-number-data-specs.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-number-data-specs.vue
new file mode 100644
index 000000000..024d91c55
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-number-data-specs.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-struct-data-specs.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-struct-data-specs.vue
new file mode 100644
index 000000000..ca866d072
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/dataSpecs/thing-model-struct-data-specs.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-event.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-event.vue
new file mode 100644
index 000000000..b8b44fce2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-event.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+ {{ eventType.label }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-form.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-form.vue
new file mode 100644
index 000000000..5b048bcf9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-form.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-input-output-param.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-input-output-param.vue
new file mode 100644
index 000000000..10ddd7339
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-input-output-param.vue
@@ -0,0 +1,163 @@
+
+
+
+
+
+
参数名称:{{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-property.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-property.vue
new file mode 100644
index 000000000..579f62b88
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-property.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.value }}
+
-
+
+
+
+
+
+
+
+
+
+ 字节
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ accessMode.label }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-service.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-service.vue
new file mode 100644
index 000000000..2f2cffd92
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-service.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+ {{ callType.label }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-tsl.vue b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-tsl.vue
new file mode 100644
index 000000000..b8bb84005
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/thingmodel/modules/thing-model-tsl.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ 代码视图
+ 编辑器视图
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/iot/utils/constants.ts b/apps/web-antdv-next/src/views/iot/utils/constants.ts
new file mode 100644
index 000000000..e5c6b5db2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/iot/utils/constants.ts
@@ -0,0 +1,656 @@
+// TODO @AI:感觉这块,放到 biz-iot-enum 里好点。
+
+/** 检查值是否为空 */
+const isEmpty = (value: any): boolean => {
+ return value === null || value === undefined || value === '';
+};
+
+/** IoT 依赖注入 KEY */
+export const IOT_PROVIDE_KEY = {
+ PRODUCT: 'IOT_PRODUCT',
+};
+
+/** IoT 设备状态枚举 */
+export enum DeviceStateEnum {
+ INACTIVE = 0, // 未激活
+ ONLINE = 1, // 在线
+ OFFLINE = 2, // 离线
+}
+
+/** IoT 产品物模型类型枚举类 */
+export const IoTThingModelTypeEnum = {
+ PROPERTY: 1, // 属性
+ SERVICE: 2, // 服务
+ EVENT: 3, // 事件
+};
+
+// IoT 产品物模型服务调用方式枚举
+export const IoTThingModelServiceCallTypeEnum = {
+ ASYNC: {
+ label: '异步',
+ value: 'async',
+ },
+ SYNC: {
+ label: '同步',
+ value: 'sync',
+ },
+};
+export const getThingModelServiceCallTypeLabel = (
+ value: string,
+): string | undefined =>
+ Object.values(IoTThingModelServiceCallTypeEnum).find(
+ (type) => type.value === value,
+ )?.label;
+
+// IoT 产品物模型事件类型枚举
+export const IoTThingModelEventTypeEnum = {
+ INFO: {
+ label: '信息',
+ value: 'info',
+ },
+ ALERT: {
+ label: '告警',
+ value: 'alert',
+ },
+ ERROR: {
+ label: '故障',
+ value: 'error',
+ },
+};
+export const getEventTypeLabel = (value: string): string | undefined =>
+ Object.values(IoTThingModelEventTypeEnum).find((type) => type.value === value)
+ ?.label;
+
+// IoT 产品物模型参数是输入参数还是输出参数
+export const IoTThingModelParamDirectionEnum = {
+ INPUT: 'input', // 输入参数
+ OUTPUT: 'output', // 输出参数
+};
+
+// IoT 产品物模型访问模式枚举类
+export const IoTThingModelAccessModeEnum = {
+ READ_WRITE: {
+ label: '读写',
+ value: 'rw',
+ },
+ READ_ONLY: {
+ label: '只读',
+ value: 'r',
+ },
+ WRITE_ONLY: {
+ label: '只写',
+ value: 'w',
+ },
+};
+
+/** 获取访问模式标签 */
+export const getAccessModeLabel = (value: string): string => {
+ const mode = Object.values(IoTThingModelAccessModeEnum).find(
+ (mode) => mode.value === value,
+ );
+ return mode?.label || value;
+};
+
+/** 属性值的数据类型 */
+export const IoTDataSpecsDataTypeEnum = {
+ INT: 'int',
+ FLOAT: 'float',
+ DOUBLE: 'double',
+ ENUM: 'enum',
+ BOOL: 'bool',
+ TEXT: 'text',
+ DATE: 'date',
+ STRUCT: 'struct',
+ ARRAY: 'array',
+};
+
+export const getDataTypeOptions = () => {
+ return [
+ { value: IoTDataSpecsDataTypeEnum.INT, label: '整数型' },
+ { value: IoTDataSpecsDataTypeEnum.FLOAT, label: '单精度浮点型' },
+ { value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '双精度浮点型' },
+ { value: IoTDataSpecsDataTypeEnum.ENUM, label: '枚举型' },
+ { value: IoTDataSpecsDataTypeEnum.BOOL, label: '布尔型' },
+ { value: IoTDataSpecsDataTypeEnum.TEXT, label: '文本型' },
+ { value: IoTDataSpecsDataTypeEnum.DATE, label: '时间型' },
+ { value: IoTDataSpecsDataTypeEnum.STRUCT, label: '结构体' },
+ { value: IoTDataSpecsDataTypeEnum.ARRAY, label: '数组' },
+ ];
+};
+
+/** 获得物体模型数据类型配置项名称 */
+export const getDataTypeOptionsLabel = (value: string) => {
+ if (isEmpty(value)) {
+ return value;
+ }
+ const dataType = getDataTypeOptions().find(
+ (option) => option.value === value,
+ );
+ return dataType && `${dataType.value}(${dataType.label})`;
+};
+
+/** 获取数据类型显示名称(用于属性选择器) */
+export const getDataTypeName = (dataType: string): string => {
+ const typeMap: Record = {
+ [IoTDataSpecsDataTypeEnum.INT]: '整数',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
+ [IoTDataSpecsDataTypeEnum.TEXT]: '字符串',
+ [IoTDataSpecsDataTypeEnum.BOOL]: '布尔值',
+ [IoTDataSpecsDataTypeEnum.ENUM]: '枚举',
+ [IoTDataSpecsDataTypeEnum.DATE]: '日期',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: '结构体',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: '数组',
+ };
+ return typeMap[dataType] || dataType;
+};
+
+/** 获取数据类型标签类型(用于 tag 的 type 属性) */
+export const getDataTypeTagType = (
+ dataType: string,
+): 'danger' | 'info' | 'primary' | 'success' | 'warning' => {
+ const tagMap: Record<
+ string,
+ 'danger' | 'info' | 'primary' | 'success' | 'warning'
+ > = {
+ [IoTDataSpecsDataTypeEnum.INT]: 'primary',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
+ [IoTDataSpecsDataTypeEnum.TEXT]: 'info',
+ [IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
+ [IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
+ [IoTDataSpecsDataTypeEnum.DATE]: 'primary',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: 'warning',
+ };
+ return tagMap[dataType] || 'info';
+};
+
+/** 物模型组标签常量 */
+export const THING_MODEL_GROUP_LABELS = {
+ PROPERTY: '设备属性',
+ EVENT: '设备事件',
+ SERVICE: '设备服务',
+};
+
+// IoT OTA 任务设备范围枚举
+export const IoTOtaTaskDeviceScopeEnum = {
+ ALL: {
+ label: '全部设备',
+ value: 1,
+ },
+ SELECT: {
+ label: '指定设备',
+ value: 2,
+ },
+};
+
+// IoT OTA 任务状态枚举
+export const IoTOtaTaskStatusEnum = {
+ IN_PROGRESS: {
+ label: '进行中',
+ value: 10,
+ },
+ END: {
+ label: '已结束',
+ value: 20,
+ },
+ CANCELED: {
+ label: '已取消',
+ value: 30,
+ },
+};
+
+// IoT OTA 升级记录状态枚举
+export const IoTOtaTaskRecordStatusEnum = {
+ PENDING: {
+ label: '待推送',
+ value: 0,
+ },
+ PUSHED: {
+ label: '已推送',
+ value: 10,
+ },
+ UPGRADING: {
+ label: '升级中',
+ value: 20,
+ },
+ SUCCESS: {
+ label: '升级成功',
+ value: 30,
+ },
+ FAILURE: {
+ label: '升级失败',
+ value: 40,
+ },
+ CANCELED: {
+ label: '升级取消',
+ value: 50,
+ },
+};
+
+// ========== 场景联动规则相关常量 ==========
+
+/** IoT 场景联动触发器类型枚举 */
+export const IotRuleSceneTriggerTypeEnum = {
+ DEVICE_STATE_UPDATE: 1, // 设备上下线变更
+ DEVICE_PROPERTY_POST: 2, // 物模型属性上报
+ DEVICE_EVENT_POST: 3, // 设备事件上报
+ DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
+ TIMER: 100, // 定时触发
+};
+
+/** 触发器类型选项配置 */
+export const triggerTypeOptions = [
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+ label: '设备状态变更',
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+ label: '设备属性上报',
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+ label: '设备事件上报',
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+ label: '设备服务调用',
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.TIMER,
+ label: '定时触发',
+ },
+];
+
+/** 判断是否为设备触发器类型 */
+export function isDeviceTrigger(type: number): boolean {
+ const deviceTriggerTypes = [
+ IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+ IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+ IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+ IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+ ] as number[];
+ return deviceTriggerTypes.includes(type);
+}
+
+// ========== 场景联动规则执行器相关常量 ==========
+
+/** IoT 场景联动执行器类型枚举 */
+export const IotRuleSceneActionTypeEnum = {
+ DEVICE_PROPERTY_SET: 1, // 设备属性设置
+ DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
+ ALERT_TRIGGER: 100, // 告警触发
+ ALERT_RECOVER: 101, // 告警恢复
+};
+
+/** 执行器类型选项配置 */
+export const getActionTypeOptions = () => [
+ {
+ value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+ label: '设备属性设置',
+ },
+ {
+ value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
+ label: '设备服务调用',
+ },
+ {
+ value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+ label: '触发告警',
+ },
+ {
+ value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
+ label: '恢复告警',
+ },
+];
+
+/** 获取执行器类型标签 */
+export const getActionTypeLabel = (type: number): string => {
+ const option = getActionTypeOptions().find((opt) => opt.value === type);
+ return option?.label || '未知类型';
+};
+
+/** IoT 场景联动触发条件参数操作符枚举 */
+export const IotRuleSceneTriggerConditionParameterOperatorEnum = {
+ EQUALS: { name: '等于', value: '=' }, // 等于
+ NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
+ GREATER_THAN: { name: '大于', value: '>' }, // 大于
+ GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
+ LESS_THAN: { name: '小于', value: '<' }, // 小于
+ LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
+ IN: { name: '在...之中', value: 'in' }, // 在...之中
+ NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
+ BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
+ NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
+ LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
+ NOT_NULL: { name: '非空', value: 'not null' }, // 非空
+};
+
+/** IoT 场景联动触发条件类型枚举 */
+export const IotRuleSceneTriggerConditionTypeEnum = {
+ DEVICE_STATUS: 1, // 设备状态
+ DEVICE_PROPERTY: 2, // 设备属性
+ CURRENT_TIME: 3, // 当前时间
+};
+
+/** 获取条件类型选项 */
+export const getConditionTypeOptions = () => [
+ {
+ value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS,
+ label: '设备状态',
+ },
+ {
+ value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY,
+ label: '设备属性',
+ },
+ {
+ value: IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME,
+ label: '当前时间',
+ },
+];
+
+/** 设备状态枚举 - 统一的设备状态管理 */
+export const IoTDeviceStatusEnum = {
+ // 在线状态
+ ONLINE: {
+ label: '在线',
+ value: 'online',
+ tagType: 'success',
+ },
+ OFFLINE: {
+ label: '离线',
+ value: 'offline',
+ tagType: 'danger',
+ },
+ // 启用状态
+ ENABLED: {
+ label: '正常',
+ value: 0,
+ value2: 'enabled',
+ tagType: 'success',
+ },
+ DISABLED: {
+ label: '禁用',
+ value: 1,
+ value2: 'disabled',
+ tagType: 'danger',
+ },
+ // 激活状态
+ ACTIVATED: {
+ label: '已激活',
+ value2: 'activated',
+ tagType: 'success',
+ },
+ NOT_ACTIVATED: {
+ label: '未激活',
+ value2: 'not_activated',
+ tagType: 'info',
+ },
+};
+
+/** 设备选择器特殊选项 */
+export const DEVICE_SELECTOR_OPTIONS = {
+ ALL_DEVICES: {
+ id: 0,
+ deviceName: '全部设备',
+ },
+};
+
+/** IoT 场景联动触发时间操作符枚举 */
+export const IotRuleSceneTriggerTimeOperatorEnum = {
+ BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
+ AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
+ BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
+ AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
+ BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
+ AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
+ TODAY: { name: '在今日之间', value: 'today' }, // 在今日之间
+};
+
+/** 获取触发器类型标签 */
+export const getTriggerTypeLabel = (type: number): string => {
+ const option = triggerTypeOptions.find((item) => item.value === type);
+ return option?.label || '未知类型';
+};
+
+// ========== JSON 参数输入组件相关常量 ==========
+
+/** JSON 参数输入组件类型枚举 */
+export const JsonParamsInputTypeEnum = {
+ SERVICE: 'service',
+ EVENT: 'event',
+ PROPERTY: 'property',
+ CUSTOM: 'custom',
+};
+
+/** JSON 参数输入组件类型 */
+export type JsonParamsInputType =
+ (typeof JsonParamsInputTypeEnum)[keyof typeof JsonParamsInputTypeEnum];
+
+/** JSON 参数输入组件文本常量 */
+export const JSON_PARAMS_INPUT_CONSTANTS = {
+ // 基础文本
+ PLACEHOLDER: '请输入JSON格式的参数',
+ JSON_FORMAT_CORRECT: 'JSON 格式正确',
+ QUICK_FILL_LABEL: '快速填充:',
+ EXAMPLE_DATA_BUTTON: '示例数据',
+ CLEAR_BUTTON: '清空',
+ VIEW_EXAMPLE_TITLE: '查看参数示例',
+ COMPLETE_JSON_FORMAT: '完整 JSON 格式:',
+ REQUIRED_TAG: '必填',
+
+ // 错误信息
+ PARAMS_MUST_BE_OBJECT: '参数必须是一个有效的 JSON 对象',
+ PARAM_REQUIRED_ERROR: (paramName: string) => `参数 ${paramName} 为必填项`,
+ JSON_FORMAT_ERROR: (error: string) => `JSON格式错误: ${error}`,
+ UNKNOWN_ERROR: '未知错误',
+
+ // 类型相关标题
+ TITLES: {
+ SERVICE: (name?: string) => `${name || '服务'} - 输入参数示例`,
+ EVENT: (name?: string) => `${name || '事件'} - 输出参数示例`,
+ PROPERTY: '属性设置 - 参数示例',
+ CUSTOM: (name?: string) => `${name || '自定义'} - 参数示例`,
+ DEFAULT: '参数示例',
+ },
+
+ // 参数标签
+ PARAMS_LABELS: {
+ SERVICE: '输入参数',
+ EVENT: '输出参数',
+ PROPERTY: '属性参数',
+ CUSTOM: '参数列表',
+ DEFAULT: '参数',
+ },
+
+ // 空状态消息
+ EMPTY_MESSAGES: {
+ SERVICE: '此服务无需输入参数',
+ EVENT: '此事件无输出参数',
+ PROPERTY: '无可设置的属性',
+ CUSTOM: '无参数配置',
+ DEFAULT: '无参数',
+ },
+
+ // 无配置消息
+ NO_CONFIG_MESSAGES: {
+ SERVICE: '请先选择服务',
+ EVENT: '请先选择事件',
+ PROPERTY: '请先选择产品',
+ CUSTOM: '请先进行配置',
+ DEFAULT: '请先进行配置',
+ },
+};
+
+/** JSON 参数输入组件图标常量 */
+export const JSON_PARAMS_INPUT_ICONS = {
+ // 标题图标
+ TITLE_ICONS: {
+ SERVICE: 'ep:service',
+ EVENT: 'ep:bell',
+ PROPERTY: 'ep:edit',
+ CUSTOM: 'ep:document',
+ DEFAULT: 'ep:document',
+ },
+
+ // 参数图标
+ PARAMS_ICONS: {
+ SERVICE: 'ep:edit',
+ EVENT: 'ep:upload',
+ PROPERTY: 'ep:setting',
+ CUSTOM: 'ep:list',
+ DEFAULT: 'ep:edit',
+ },
+
+ // 状态图标
+ STATUS_ICONS: {
+ ERROR: 'ep:warning',
+ SUCCESS: 'ep:circle-check',
+ },
+};
+
+/** JSON 参数输入组件示例值常量 */
+export const JSON_PARAMS_EXAMPLE_VALUES: Record = {
+ [IoTDataSpecsDataTypeEnum.INT]: { display: '25', value: 25 },
+ [IoTDataSpecsDataTypeEnum.FLOAT]: { display: '25.5', value: 25.5 },
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: { display: '25.5', value: 25.5 },
+ [IoTDataSpecsDataTypeEnum.BOOL]: { display: 'false', value: false },
+ [IoTDataSpecsDataTypeEnum.TEXT]: { display: '"auto"', value: 'auto' },
+ [IoTDataSpecsDataTypeEnum.ENUM]: { display: '"option1"', value: 'option1' },
+ [IoTDataSpecsDataTypeEnum.STRUCT]: { display: '{}', value: {} },
+ [IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] },
+ DEFAULT: { display: '""', value: '' },
+};
+
+// ========== Modbus 通用常量 ==========
+
+/** Modbus 模式枚举 */
+export const ModbusModeEnum = {
+ POLLING: 1, // 云端轮询
+ ACTIVE_REPORT: 2, // 主动上报
+} as const;
+
+/** Modbus 帧格式枚举 */
+export const ModbusFrameFormatEnum = {
+ MODBUS_TCP: 1, // Modbus TCP
+ MODBUS_RTU: 2, // Modbus RTU
+} as const;
+
+/** Modbus 功能码枚举 */
+export const ModbusFunctionCodeEnum = {
+ READ_COILS: 1, // 读线圈
+ READ_DISCRETE_INPUTS: 2, // 读离散输入
+ READ_HOLDING_REGISTERS: 3, // 读保持寄存器
+ READ_INPUT_REGISTERS: 4, // 读输入寄存器
+} as const;
+
+/** Modbus 功能码选项 */
+export const ModbusFunctionCodeOptions = [
+ { value: 1, label: '01 - 读线圈 (Coils)', description: '可读写布尔值' },
+ {
+ value: 2,
+ label: '02 - 读离散输入 (Discrete Inputs)',
+ description: '只读布尔值',
+ },
+ {
+ value: 3,
+ label: '03 - 读保持寄存器 (Holding Registers)',
+ description: '可读写 16 位数据',
+ },
+ {
+ value: 4,
+ label: '04 - 读输入寄存器 (Input Registers)',
+ description: '只读 16 位数据',
+ },
+];
+
+/** Modbus 原始数据类型枚举 */
+export const ModbusRawDataTypeEnum = {
+ INT16: 'INT16',
+ UINT16: 'UINT16',
+ INT32: 'INT32',
+ UINT32: 'UINT32',
+ FLOAT: 'FLOAT',
+ DOUBLE: 'DOUBLE',
+ BOOLEAN: 'BOOLEAN',
+ STRING: 'STRING',
+} as const;
+
+/** Modbus 原始数据类型选项 */
+export const ModbusRawDataTypeOptions = [
+ {
+ value: 'INT16',
+ label: 'INT16',
+ description: '有符号16位整数',
+ registerCount: 1,
+ },
+ {
+ value: 'UINT16',
+ label: 'UINT16',
+ description: '无符号16位整数',
+ registerCount: 1,
+ },
+ {
+ value: 'INT32',
+ label: 'INT32',
+ description: '有符号32位整数',
+ registerCount: 2,
+ },
+ {
+ value: 'UINT32',
+ label: 'UINT32',
+ description: '无符号32位整数',
+ registerCount: 2,
+ },
+ {
+ value: 'FLOAT',
+ label: 'FLOAT',
+ description: '32位浮点数',
+ registerCount: 2,
+ },
+ {
+ value: 'DOUBLE',
+ label: 'DOUBLE',
+ description: '64位浮点数',
+ registerCount: 4,
+ },
+ {
+ value: 'BOOLEAN',
+ label: 'BOOLEAN',
+ description: '布尔值',
+ registerCount: 1,
+ },
+ {
+ value: 'STRING',
+ label: 'STRING',
+ description: '字符串',
+ registerCount: 0,
+ },
+];
+
+/** Modbus 字节序选项 - 16位 */
+export const ModbusByteOrder16Options = [
+ { value: 'AB', label: 'AB', description: '大端序' },
+ { value: 'BA', label: 'BA', description: '小端序' },
+];
+
+/** Modbus 字节序选项 - 32位 */
+export const ModbusByteOrder32Options = [
+ { value: 'ABCD', label: 'ABCD', description: '大端序' },
+ { value: 'CDAB', label: 'CDAB', description: '大端字交换' },
+ { value: 'DCBA', label: 'DCBA', description: '小端序' },
+ { value: 'BADC', label: 'BADC', description: '小端字交换' },
+];
+
+/** 根据数据类型获取字节序选项 */
+export const getByteOrderOptions = (rawDataType: string) => {
+ if (['FLOAT', 'INT32', 'UINT32'].includes(rawDataType)) {
+ return ModbusByteOrder32Options;
+ }
+ if (rawDataType === 'DOUBLE') {
+ // 64 位暂时复用 32 位字节序
+ return ModbusByteOrder32Options;
+ }
+ return ModbusByteOrder16Options;
+};
diff --git a/apps/web-antdv-next/src/views/mall/home/index.vue b/apps/web-antdv-next/src/views/mall/home/index.vue
new file mode 100644
index 000000000..ea1a55338
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/index.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/comparison-card.vue b/apps/web-antdv-next/src/views/mall/home/modules/comparison-card.vue
new file mode 100644
index 000000000..ebffe3064
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/comparison-card.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ {{ title }}
+ {{ tag }}
+
+
+
{{ prefix }}{{ formattedValue }}
+
+ {{ Math.abs(percent).toFixed(2) }}%
+
+
+
+
+
+ 昨日数据
+ {{ prefix }}{{ formattedReference }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/member-statistics-card.vue b/apps/web-antdv-next/src/views/mall/home/modules/member-statistics-card.vue
new file mode 100644
index 000000000..770249117
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/member-statistics-card.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+ 用户统计
+
+
+ {{ value.name }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/member-statistics-chart-options.ts b/apps/web-antdv-next/src/views/mall/home/modules/member-statistics-chart-options.ts
new file mode 100644
index 000000000..bd1e58474
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/member-statistics-chart-options.ts
@@ -0,0 +1,64 @@
+import dayjs from 'dayjs';
+
+/** 时间范围类型枚举 */
+export enum TimeRangeTypeEnum {
+ DAY30 = 1,
+ WEEK = 7,
+ MONTH = 30,
+ YEAR = 365,
+}
+
+/** 会员统计图表配置 */
+export function getMemberStatisticsChartOptions(list: any[]): any {
+ return {
+ dataset: {
+ dimensions: ['date', 'count'],
+ source: list,
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [{ name: '注册量', type: 'line', smooth: true, areaStyle: {} }],
+ toolbox: {
+ feature: {
+ // 数据区域缩放
+ dataZoom: {
+ yAxisIndex: false, // Y轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '会员统计' }, // 保存为图片
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ },
+ padding: [5, 10],
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ formatter: (date: string) => dayjs(date).format('MM-DD'),
+ },
+ },
+ yAxis: {
+ axisTick: {
+ show: false,
+ },
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/operation-data-card.vue b/apps/web-antdv-next/src/views/mall/home/modules/operation-data-card.vue
new file mode 100644
index 000000000..a0f46c2aa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/operation-data-card.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/shortcut-card.vue b/apps/web-antdv-next/src/views/mall/home/modules/shortcut-card.vue
new file mode 100644
index 000000000..6780fd759
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/shortcut-card.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
{{ menu.name }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/trade-trend-card.vue b/apps/web-antdv-next/src/views/mall/home/modules/trade-trend-card.vue
new file mode 100644
index 000000000..2c9d72583
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/trade-trend-card.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+ 交易量趋势
+
+
+ {{ value.name }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/home/modules/trade-trend-chart-options.ts b/apps/web-antdv-next/src/views/mall/home/modules/trade-trend-chart-options.ts
new file mode 100644
index 000000000..5fd951676
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/home/modules/trade-trend-chart-options.ts
@@ -0,0 +1,86 @@
+import dayjs from 'dayjs';
+
+/** 时间范围类型枚举 */
+export enum TimeRangeTypeEnum {
+ DAY30 = 1,
+ WEEK = 7,
+ MONTH = 30,
+ YEAR = 365,
+}
+
+/** 交易量趋势图表配置 */
+export function getTradeTrendChartOptions(
+ dates: string[],
+ series: any[],
+ timeRangeType: TimeRangeTypeEnum,
+): any {
+ return {
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ data: series.map((item) => item.name),
+ },
+ series,
+ toolbox: {
+ feature: {
+ // 数据区域缩放
+ dataZoom: {
+ yAxisIndex: false, // Y轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: { show: true, name: '订单量趋势' }, // 保存为图片
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ },
+ padding: [5, 10],
+ },
+ xAxis: {
+ type: 'category',
+ inverse: true,
+ boundaryGap: false,
+ axisTick: {
+ show: false,
+ },
+ data: dates,
+ axisLabel: {
+ formatter: (date: string) => {
+ switch (timeRangeType) {
+ case TimeRangeTypeEnum.DAY30: {
+ return dayjs(date).format('MM-DD');
+ }
+ case TimeRangeTypeEnum.MONTH: {
+ return dayjs(date).format('D');
+ }
+ case TimeRangeTypeEnum.WEEK: {
+ const weekDay = dayjs(date).day();
+ return weekDay === 0 ? '周日' : `周${weekDay}`;
+ }
+ case TimeRangeTypeEnum.YEAR: {
+ return `${dayjs(date).format('M')}月`;
+ }
+ default: {
+ return date;
+ }
+ }
+ },
+ },
+ },
+ yAxis: {
+ axisTick: {
+ show: false,
+ },
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/brand/data.ts b/apps/web-antdv-next/src/views/mall/product/brand/data.ts
new file mode 100644
index 000000000..f9cfa10c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/brand/data.ts
@@ -0,0 +1,148 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '品牌名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入品牌名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'picUrl',
+ label: '品牌图片',
+ component: 'ImageUpload',
+ componentProps: {
+ placeholder: '请上传品牌图片',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '品牌排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入品牌排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '品牌状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'description',
+ label: '品牌描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入品牌描述',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '品牌名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入品牌名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '品牌状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择品牌状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '品牌名称',
+ minWidth: 180,
+ },
+ {
+ field: 'picUrl',
+ title: '品牌图片',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'sort',
+ title: '品牌排序',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '品牌状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/brand/index.vue b/apps/web-antdv-next/src/views/mall/product/brand/index.vue
new file mode 100644
index 000000000..453bc44b1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/brand/index.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/brand/modules/form.vue b/apps/web-antdv-next/src/views/mall/product/brand/modules/form.vue
new file mode 100644
index 000000000..25fd13c97
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/brand/modules/form.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/category/components/index.ts b/apps/web-antdv-next/src/views/mall/product/category/components/index.ts
new file mode 100644
index 000000000..c3196cb27
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/category/components/index.ts
@@ -0,0 +1 @@
+export { default as ProductCategorySelect } from './select.vue';
diff --git a/apps/web-antdv-next/src/views/mall/product/category/components/select.vue b/apps/web-antdv-next/src/views/mall/product/category/components/select.vue
new file mode 100644
index 000000000..c214276fa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/category/components/select.vue
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/category/data.ts b/apps/web-antdv-next/src/views/mall/product/category/data.ts
new file mode 100644
index 000000000..6e1e1de3b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/category/data.ts
@@ -0,0 +1,148 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallCategoryApi } from '#/api/mall/product/category';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getCategoryList } from '#/api/mall/product/category';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '上级分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getCategoryList({ parentId: 0 });
+ data.unshift({
+ id: 0,
+ name: '顶级分类',
+ } as MallCategoryApi.Category);
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择上级分类',
+ treeDefaultExpandAll: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'picUrl',
+ label: '移动端分类图',
+ component: 'ImageUpload',
+ componentProps: {
+ placeholder: '请上传移动端分类图',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '分类排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入分类排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '分类名称',
+ minWidth: 200,
+ align: 'left',
+ fixed: 'left',
+ treeNode: true,
+ },
+ {
+ field: 'picUrl',
+ title: '移动端分类图',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'sort',
+ title: '分类排序',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '分类状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/category/index.vue b/apps/web-antdv-next/src/views/mall/product/category/index.vue
new file mode 100644
index 000000000..ec1d4c315
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/category/index.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/category/modules/form.vue b/apps/web-antdv-next/src/views/mall/product/category/modules/form.vue
new file mode 100644
index 000000000..1b0a9376d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/category/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/comment/data.ts b/apps/web-antdv-next/src/views/mall/product/comment/data.ts
new file mode 100644
index 000000000..b7e246d47
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/comment/data.ts
@@ -0,0 +1,253 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallCommentApi } from '#/api/mall/product/comment';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'spuId',
+ label: '商品',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请选择商品',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'skuId',
+ label: '商品规格',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请选择商品规格',
+ },
+ dependencies: {
+ triggerFields: ['spuId'],
+ show: (values) => !!values.spuId,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'userAvatar',
+ label: '用户头像',
+ component: 'ImageUpload',
+ componentProps: {
+ placeholder: '请上传用户头像',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'userNickname',
+ label: '用户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'content',
+ label: '评论内容',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入评论内容',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'descriptionScores',
+ label: '描述星级',
+ component: 'Rate',
+ rules: z.number().min(1).max(5).default(5),
+ },
+ {
+ fieldName: 'benefitScores',
+ label: '服务星级',
+ component: 'Rate',
+ rules: z.number().min(1).max(5).default(5),
+ },
+ {
+ fieldName: 'picUrls',
+ label: '评论图片',
+ component: 'ImageUpload',
+ componentProps: {
+ maxNumber: 9,
+ placeholder: '请上传评论图片',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'replyStatus',
+ label: '回复状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '已回复', value: true },
+ { label: '未回复', value: false },
+ ],
+ placeholder: '请选择回复状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'spuName',
+ label: '商品名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入商品名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userNickname',
+ label: '用户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'orderId',
+ label: '订单编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '评论时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: boolean,
+ row: MallCommentApi.Comment,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '评论编号',
+ fixed: 'left',
+ minWidth: 80,
+ },
+ {
+ field: 'skuPicUrl',
+ title: '商品图片',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'spuName',
+ title: '商品名称',
+ minWidth: 250,
+ },
+ {
+ field: 'skuProperties',
+ title: '商品属性',
+ minWidth: 200,
+ formatter: ({ cellValue }) => {
+ return cellValue && cellValue.length > 0
+ ? cellValue
+ .map((item: any) => `${item.propertyName} : ${item.valueName}`)
+ .join('\n')
+ : '-';
+ },
+ },
+ {
+ field: 'userNickname',
+ title: '用户名称',
+ minWidth: 100,
+ },
+ {
+ field: 'descriptionScores',
+ title: '商品评分',
+ minWidth: 150,
+ slots: {
+ default: 'descriptionScores',
+ },
+ },
+ {
+ field: 'benefitScores',
+ title: '服务评分',
+ minWidth: 150,
+ slots: {
+ default: 'benefitScores',
+ },
+ },
+ {
+ field: 'content',
+ title: '评论内容',
+ minWidth: 210,
+ },
+ {
+ field: 'picUrls',
+ title: '评论图片',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImages',
+ },
+ },
+ {
+ field: 'replyContent',
+ title: '回复内容',
+ minWidth: 250,
+ },
+ {
+ field: 'createTime',
+ title: '评论时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'visible',
+ title: '是否展示',
+ minWidth: 110,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: true,
+ unCheckedValue: false,
+ },
+ },
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/comment/index.vue b/apps/web-antdv-next/src/views/mall/product/comment/index.vue
new file mode 100644
index 000000000..8a5071615
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/comment/index.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/comment/modules/form.vue b/apps/web-antdv-next/src/views/mall/product/comment/modules/form.vue
new file mode 100644
index 000000000..5922eb42b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/comment/modules/form.vue
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/property/data.ts b/apps/web-antdv-next/src/views/mall/product/property/data.ts
new file mode 100644
index 000000000..e8c3b2080
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/property/data.ts
@@ -0,0 +1,207 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getPropertySimpleList } from '#/api/mall/product/property';
+import { getRangePickerDefaultProps } from '#/utils';
+
+// ============================== 属性 ==============================
+
+/** 属性新增/修改的表单 */
+export function usePropertyFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '属性名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入属性名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 属性列表的搜索表单 */
+export function usePropertyGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '属性名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入属性名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 属性列表的字段 */
+export function usePropertyGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '属性编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '属性名称',
+ minWidth: 200,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ============================== 属性值 ==============================
+
+/** 属性值新增/修改的表单 */
+export function useValueFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'propertyId',
+ label: '属性',
+ component: 'ApiSelect',
+ componentProps: (values) => {
+ return {
+ api: getPropertySimpleList,
+ placeholder: '请选择属性',
+ labelField: 'name',
+ valueField: 'id',
+ disabled: !!values.id,
+ };
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: [''],
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '属性值名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入属性值名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 属性值列表搜索表单 */
+export function useValueGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'propertyId',
+ label: '属性项',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getPropertySimpleList,
+ placeholder: '请选择属性项',
+ labelField: 'name',
+ valueField: 'id',
+ disabled: true,
+ allowClear: false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '属性值名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入属性值名称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 属性值表格列 */
+export function useValueGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '属性值编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '属性值名称',
+ minWidth: 180,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 180,
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/property/index.vue b/apps/web-antdv-next/src/views/mall/product/property/index.vue
new file mode 100644
index 000000000..302b0a320
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/property/index.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/property/modules/property-form.vue b/apps/web-antdv-next/src/views/mall/product/property/modules/property-form.vue
new file mode 100644
index 000000000..0af8accb9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/property/modules/property-form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/property/modules/property-grid.vue b/apps/web-antdv-next/src/views/mall/product/property/modules/property-grid.vue
new file mode 100644
index 000000000..73aa359b6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/property/modules/property-grid.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/property/modules/value-form.vue b/apps/web-antdv-next/src/views/mall/product/property/modules/value-form.vue
new file mode 100644
index 000000000..6c01434b3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/property/modules/value-form.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/property/modules/value-grid.vue b/apps/web-antdv-next/src/views/mall/product/property/modules/value-grid.vue
new file mode 100644
index 000000000..c95d29cfc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/property/modules/value-grid.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/index.ts b/apps/web-antdv-next/src/views/mall/product/spu/components/index.ts
new file mode 100644
index 000000000..bedd27a45
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/index.ts
@@ -0,0 +1,8 @@
+export * from './property-util';
+export { default as SkuList } from './sku-list.vue';
+export { default as SkuTableSelect } from './sku-table-select.vue';
+export { default as SpuAndSkuList } from './spu-and-sku-list.vue';
+export { default as SpuSkuSelect } from './spu-select.vue';
+export { default as SpuShowcase } from './spu-showcase.vue';
+export { default as SpuTableSelect } from './spu-table-select.vue';
+export * from './type';
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/property-util.ts b/apps/web-antdv-next/src/views/mall/product/spu/components/property-util.ts
new file mode 100644
index 000000000..7f6524969
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/property-util.ts
@@ -0,0 +1,36 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import type { MallSpuApi } from '#/api/mall/product/spu';
+import type { PropertyAndValues } from '#/views/mall/product/spu/components/type';
+
+/** 获得商品的规格列表 - 商品相关的公共函数(被其它模块如 promotion 使用) */
+const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
+ // 直接拿返回的 skus 属性逆向生成出 propertyList
+ const properties: PropertyAndValues[] = [];
+ // 只有是多规格才处理
+ if (spu.specType) {
+ spu.skus?.forEach((sku) => {
+ sku.properties?.forEach(
+ ({ propertyId, propertyName, valueId, valueName }) => {
+ // 添加属性
+ if (!properties?.some((item) => item.id === propertyId)) {
+ properties.push({
+ id: propertyId!,
+ name: propertyName!,
+ values: [],
+ });
+ }
+ // 添加属性值
+ const index = properties?.findIndex((item) => item.id === propertyId);
+ if (
+ !properties[index]?.values?.some((value) => value.id === valueId)
+ ) {
+ properties[index]?.values?.push({ id: valueId!, name: valueName! });
+ }
+ },
+ );
+ });
+ }
+ return properties;
+};
+
+export { getPropertyList };
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/sku-list.vue b/apps/web-antdv-next/src/views/mall/product/spu/components/sku-list.vue
new file mode 100644
index 000000000..f64a981f1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/sku-list.vue
@@ -0,0 +1,616 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.properties?.[index]?.valueName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.properties?.[index]?.valueName }}
+
+
+
+
+
+
+ {{ row.barCode }}
+
+
+
+
+ {{ row.price }}
+
+
+
+
+ {{ row.marketPrice }}
+
+
+
+
+ {{ row.costPrice }}
+
+
+
+
+ {{ row.stock }}
+
+
+
+
+ {{ row.weight }}
+
+
+
+
+ {{ row.volume }}
+
+
+
+
+
+ {{ row.firstBrokeragePrice }}
+
+
+
+
+ {{ row.secondBrokeragePrice }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.properties?.[index]?.valueName }}
+
+
+
+
+
+
+ {{ row.barCode }}
+
+
+
+
+ {{ formatToFraction(row.price) }}
+
+
+
+
+ {{ formatToFraction(row.marketPrice) }}
+
+
+
+
+ {{ formatToFraction(row.costPrice) }}
+
+
+
+
+ {{ row.stock }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/sku-table-select.vue b/apps/web-antdv-next/src/views/mall/product/spu/components/sku-table-select.vue
new file mode 100644
index 000000000..a93eed7a0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/sku-table-select.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/spu-and-sku-list.vue b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-and-sku-list.vue
new file mode 100644
index 000000000..319e538e2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-and-sku-list.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatToFraction(row.price) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/spu-select-data.ts b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-select-data.ts
new file mode 100644
index 000000000..d0a9dfc9f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-select-data.ts
@@ -0,0 +1,169 @@
+import type { Ref } from 'vue';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridProps, VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallCategoryApi } from '#/api/mall/product/category';
+import type { MallSpuApi } from '#/api/mall/product/spu';
+
+import { computed } from 'vue';
+
+import { fenToYuan } from '@vben/utils';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(
+ categoryTreeList: Ref,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '商品名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入商品名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'categoryId',
+ label: '商品分类',
+ component: 'TreeSelect',
+ componentProps: {
+ treeData: computed(() => categoryTreeList.value),
+ fieldNames: {
+ label: 'name',
+ value: 'id',
+ },
+ treeCheckStrictly: true,
+ placeholder: '请选择商品分类',
+ allowClear: true,
+ showSearch: true,
+ treeNodeFilterProp: 'name',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ isSelectSku: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'expand',
+ width: 30,
+ visible: isSelectSku,
+ slots: { content: 'expand_content' },
+ },
+ { type: 'checkbox', width: 55 },
+ {
+ field: 'id',
+ title: '商品编号',
+ minWidth: 100,
+ align: 'center',
+ },
+ {
+ field: 'picUrl',
+ title: '商品图',
+ width: 100,
+ align: 'center',
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'name',
+ title: '商品名称',
+ minWidth: 300,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'price',
+ title: '商品售价(元)',
+ minWidth: 90,
+ align: 'center',
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'salesCount',
+ title: '销量',
+ minWidth: 90,
+ align: 'center',
+ },
+ {
+ field: 'stock',
+ title: '库存',
+ minWidth: 90,
+ align: 'center',
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 70,
+ align: 'center',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ width: 180,
+ align: 'center',
+ formatter: 'formatDateTime',
+ },
+ ] as VxeTableGridOptions['columns'];
+}
+
+/** SKU 列表的字段 */
+export function useSkuGridColumns(): VxeGridProps['columns'] {
+ return [
+ {
+ type: 'radio',
+ width: 55,
+ },
+ {
+ field: 'id',
+ title: '商品编号',
+ minWidth: 100,
+ align: 'center',
+ },
+ {
+ field: 'picUrl',
+ title: '图片',
+ width: 100,
+ align: 'center',
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'properties',
+ title: '规格',
+ minWidth: 120,
+ align: 'center',
+ formatter: ({ cellValue }) => {
+ return (
+ cellValue?.map((p: MallSpuApi.Property) => p.valueName)?.join(' ') ||
+ '-'
+ );
+ },
+ },
+ {
+ field: 'price',
+ title: '销售价(元)',
+ width: 120,
+ align: 'center',
+ formatter: ({ cellValue }) => {
+ return fenToYuan(cellValue);
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/spu-select.vue b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-select.vue
new file mode 100644
index 000000000..4d3be741b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-select.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/spu-showcase.vue b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-showcase.vue
new file mode 100644
index 000000000..17e405bfb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-showcase.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/spu-table-select.vue b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-table-select.vue
new file mode 100644
index 000000000..c06027b24
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/spu-table-select.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/components/type.ts b/apps/web-antdv-next/src/views/mall/product/spu/components/type.ts
new file mode 100644
index 000000000..7e1155af1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/components/type.ts
@@ -0,0 +1,32 @@
+/** 商品属性及其值的树形结构(用于前端展示和操作) */
+export interface PropertyAndValues {
+ id: number;
+ name: string;
+ values?: PropertyAndValues[];
+}
+
+export interface RuleConfig {
+ // 需要校验的字段
+ // 例:name: 'name' 则表示校验 sku.name 的值
+ // 例:name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性
+ name: string;
+ // 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。
+ // 例:需要校验价格必须大于0.01
+ // {
+ // name:'price',
+ // rule:(arg: number) => arg > 0.01
+ // }
+ rule: (arg: any) => boolean;
+ // 校验不通过时的消息提示
+ message: string;
+}
+
+export interface SpuProperty {
+ propertyList: PropertyAndValues[];
+ spuDetail: T;
+ spuId: number;
+}
+
+// Re-export for use in generic constraint
+
+export { type MallSpuApi } from '#/api/mall/product/spu';
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/data.ts b/apps/web-antdv-next/src/views/mall/product/spu/data.ts
new file mode 100644
index 000000000..cc71ef707
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/data.ts
@@ -0,0 +1,158 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallSpuApi } from '#/api/mall/product/spu';
+
+import { handleTree, treeToString } from '@vben/utils';
+
+import { getCategoryList } from '#/api/mall/product/category';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let categoryList: any[] = [];
+getCategoryList({}).then((data) => {
+ categoryList = handleTree(data, 'id', 'parentId', 'children');
+});
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '商品名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入商品名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'categoryId',
+ label: '商品分类',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ placeholder: '请选择商品分类',
+ allowClear: true,
+ options: categoryList,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: MallSpuApi.Spu,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '商品编号',
+ fixed: 'left',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '商品名称',
+ fixed: 'left',
+ minWidth: 200,
+ },
+ {
+ field: 'picUrl',
+ title: '商品图片',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'categoryId',
+ title: '商品分类',
+ minWidth: 150,
+ formatter: ({ row }) => {
+ return treeToString(categoryList, row.categoryId);
+ },
+ },
+ {
+ field: 'status',
+ title: '销售状态',
+ minWidth: 100,
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: 1,
+ checkedChildren: '上架',
+ unCheckedValue: 0,
+ unCheckedChildren: '下架',
+ },
+ },
+ },
+ {
+ field: 'price',
+ title: '价格(元)',
+ minWidth: 100,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'marketPrice',
+ title: '市场价(元)',
+ minWidth: 100,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'costPrice',
+ title: '成本价(元)',
+ minWidth: 100,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'salesCount',
+ title: '销量',
+ minWidth: 80,
+ },
+ {
+ field: 'virtualSalesCount',
+ title: '虚拟销量',
+ minWidth: 100,
+ },
+ {
+ field: 'stock',
+ title: '库存',
+ minWidth: 80,
+ },
+ {
+ field: 'browseCount',
+ title: '浏览量',
+ minWidth: 100,
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 80,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 300,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/form/data.ts b/apps/web-antdv-next/src/views/mall/product/spu/form/data.ts
new file mode 100644
index 000000000..d2ff7eeb5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/form/data.ts
@@ -0,0 +1,307 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { getSimpleBrandList } from '#/api/mall/product/brand';
+import { getCategoryList } from '#/api/mall/product/category';
+import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate';
+
+/** 基础设置的表单 */
+export function useInfoFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '商品名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商品名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'categoryId',
+ label: '分类名称',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getCategoryList({});
+ return handleTree(data);
+ },
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择商品分类',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'brandId',
+ label: '商品品牌',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleBrandList,
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择商品品牌',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'keyword',
+ label: '商品关键字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入商品关键字',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'introduction',
+ label: '商品简介',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入商品简介',
+ autoSize: { minRows: 2, maxRows: 2 },
+ showCount: true,
+ maxlength: 128,
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'picUrl',
+ label: '商品封面图',
+ component: 'ImageUpload',
+ componentProps: {
+ maxSize: 30,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sliderPicUrls',
+ label: '商品轮播图',
+ component: 'ImageUpload',
+ componentProps: {
+ maxNumber: 10,
+ multiple: true,
+ maxSize: 30,
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 价格库存的表单 */
+export function useSkuFormSchema(
+ propertyList: any[] = [],
+ isDetail: boolean = false,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'subCommissionType',
+ label: '分销类型',
+ component: 'RadioGroup',
+ componentProps: {
+ allowClear: true,
+ options: [
+ {
+ label: '默认设置',
+ value: false,
+ },
+ {
+ label: '单独设置',
+ value: true,
+ },
+ ],
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'specType',
+ label: '商品规格',
+ component: 'RadioGroup',
+ componentProps: {
+ allowClear: true,
+ options: [
+ {
+ label: '单规格',
+ value: false,
+ },
+ {
+ label: '多规格',
+ value: true,
+ },
+ ],
+ },
+ rules: 'required',
+ },
+ // 单规格时显示的 SkuList
+ {
+ fieldName: 'singleSkuList',
+ label: '',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['specType'],
+ // 当 specType 为 false(单规格)时显示
+ show: (values) => values.specType === false,
+ },
+ },
+ // 多规格时显示的商品属性(占位,实际通过插槽渲染)
+ {
+ fieldName: 'productAttributes',
+ label: '商品属性',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['specType'],
+ // 当 specType 为 true(多规格)时显示
+ show: (values) => values.specType === true,
+ },
+ },
+ // 多规格 - 批量设置
+ {
+ fieldName: 'batchSkuList',
+ label: '批量设置',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['specType'],
+ // 当 specType 为 true(多规格)且 propertyList 有数据时显示,且非详情模式
+ show: (values) =>
+ values.specType === true && propertyList.length > 0 && !isDetail,
+ },
+ },
+ // 多规格 - 规格列表
+ {
+ fieldName: 'multiSkuList',
+ label: '规格列表',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['specType'],
+ // 当 specType 为 true(多规格)且 propertyList 有数据时显示
+ show: (values) => values.specType === true && propertyList.length > 0,
+ },
+ },
+ ];
+}
+
+/** 物流设置的表单 */
+export function useDeliveryFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'deliveryTypes',
+ label: '配送方式',
+ component: 'CheckboxGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE, 'number'),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'deliveryTemplateId',
+ label: '运费模板',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleTemplateList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ dependencies: {
+ triggerFields: ['deliveryTypes'],
+ show: (values) =>
+ !!values.deliveryTypes &&
+ values.deliveryTypes.includes(DeliveryTypeEnum.EXPRESS.type),
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 商品详情的表单 */
+export function useDescriptionFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'description',
+ label: '商品详情',
+ component: 'RichTextarea',
+ componentProps: {
+ placeholder: '请输入商品详情',
+ height: 1000,
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 其它设置的表单 */
+export function useOtherFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '商品排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'giveIntegral',
+ label: '赠送积分',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'virtualSalesCount',
+ label: '虚拟销量',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ },
+ rules: 'required',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/form/index.vue b/apps/web-antdv-next/src/views/mall/product/spu/form/index.vue
new file mode 100644
index 000000000..8e9e4bd52
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/form/index.vue
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/form/modules/product-attributes.vue b/apps/web-antdv-next/src/views/mall/product/spu/form/modules/product-attributes.vue
new file mode 100644
index 000000000..5bbb17baf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/form/modules/product-attributes.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+ 属性名:
+
+ {{ attribute.name }}
+
+
+
+
属性值:
+
+ {{ value?.name }}
+
+
+
+
+
+ 添加
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/form/modules/product-property-add-form.vue b/apps/web-antdv-next/src/views/mall/product/spu/form/modules/product-property-add-form.vue
new file mode 100644
index 000000000..f34a8d194
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/form/modules/product-property-add-form.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/product/spu/index.vue b/apps/web-antdv-next/src/views/mall/product/spu/index.vue
new file mode 100644
index 000000000..dbeefd6d7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/product/spu/index.vue
@@ -0,0 +1,301 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/article/category/data.ts b/apps/web-antdv-next/src/views/mall/promotion/article/category/data.ts
new file mode 100644
index 000000000..3929ecb8e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/article/category/data.ts
@@ -0,0 +1,146 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'picUrl',
+ label: '分类图片',
+ component: 'ImageUpload',
+ componentProps: {
+ placeholder: '请上传分类图片',
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分类名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '分类名称',
+ minWidth: 240,
+ align: 'left',
+ fixed: 'left',
+ },
+ {
+ field: 'picUrl',
+ title: '分类图片',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 150,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ minWidth: 150,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ minWidth: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/article/category/index.vue b/apps/web-antdv-next/src/views/mall/promotion/article/category/index.vue
new file mode 100644
index 000000000..408158e2e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/article/category/index.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/article/category/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/article/category/modules/form.vue
new file mode 100644
index 000000000..82e51be36
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/article/category/modules/form.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/article/data.ts b/apps/web-antdv-next/src/views/mall/promotion/article/data.ts
new file mode 100644
index 000000000..5af659775
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/article/data.ts
@@ -0,0 +1,251 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+import type { MallArticleCategoryApi } from '#/api/mall/promotion/article/category';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleArticleCategoryList } from '#/api/mall/promotion/article/category';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let categoryList: MallArticleCategoryApi.ArticleCategory[] = [];
+getSimpleArticleCategoryList().then((data) => (categoryList = data));
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '文章标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文章标题',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'categoryId',
+ label: '文章分类',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleArticleCategoryList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择文章分类',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'author',
+ label: '文章作者',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文章作者',
+ },
+ },
+ {
+ fieldName: 'introduction',
+ label: '文章简介',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文章简介',
+ },
+ },
+ {
+ fieldName: 'picUrl',
+ label: '文章封面',
+ component: 'ImageUpload',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '请上传文章封面',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'recommendHot',
+ label: '是否热门',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ defaultValue: true,
+ },
+ {
+ fieldName: 'recommendBanner',
+ label: '是否轮播图',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ defaultValue: true,
+ },
+ {
+ fieldName: 'spuId',
+ label: '商品关联',
+ component: 'Input',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'content',
+ label: '文章内容',
+ component: 'RichTextarea',
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'categoryId',
+ label: '文章分类',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleArticleCategoryList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择文章分类',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '文章标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入文章标题',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'title',
+ title: '标题',
+ minWidth: 200,
+ },
+ {
+ field: 'picUrl',
+ title: '封面',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'categoryId',
+ title: '分类',
+ minWidth: 100,
+ formatter: ({ cellValue }) =>
+ categoryList.find((item) => item.id === cellValue)?.name || '-',
+ },
+ {
+ field: 'browseCount',
+ title: '浏览量',
+ minWidth: 100,
+ },
+ {
+ field: 'author',
+ title: '作者',
+ minWidth: 120,
+ },
+ {
+ field: 'introduction',
+ title: '文章简介',
+ minWidth: 250,
+ },
+ {
+ field: 'sort',
+ title: '排序',
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/article/index.vue b/apps/web-antdv-next/src/views/mall/promotion/article/index.vue
new file mode 100644
index 000000000..82c1a89fe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/article/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/article/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/article/modules/form.vue
new file mode 100644
index 000000000..0e73e0eca
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/article/modules/form.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/banner/data.ts b/apps/web-antdv-next/src/views/mall/promotion/banner/data.ts
new file mode 100644
index 000000000..0490be5dd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/banner/data.ts
@@ -0,0 +1,191 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: 'Banner 标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 Banner 标题',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'picUrl',
+ label: '图片地址',
+ component: 'ImageUpload',
+ componentProps: {
+ placeholder: '请上传图片',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'position',
+ label: '定位',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'url',
+ label: '跳转地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入跳转地址',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'memo',
+ label: '描述',
+ component: 'Textarea',
+ componentProps: {
+ rows: 4,
+ placeholder: '请输入描述',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'title',
+ label: 'Banner 标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 Banner 标题',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择活动状态',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: 'Banner标题',
+ field: 'title',
+ minWidth: 100,
+ },
+ {
+ title: '图片',
+ field: 'picUrl',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ title: '状态',
+ field: 'status',
+ minWidth: 150,
+ cellRender: {
+ name: 'CellDict',
+ props: {
+ type: DICT_TYPE.COMMON_STATUS,
+ },
+ },
+ },
+ {
+ title: '定位',
+ field: 'position',
+ minWidth: 150,
+ cellRender: {
+ name: 'CellDict',
+ props: {
+ type: DICT_TYPE.PROMOTION_BANNER_POSITION,
+ },
+ },
+ },
+ {
+ title: '跳转地址',
+ field: 'url',
+ minWidth: 200,
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '排序',
+ field: 'sort',
+ minWidth: 100,
+ },
+ {
+ title: '描述',
+ field: 'memo',
+ minWidth: 150,
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/banner/index.vue b/apps/web-antdv-next/src/views/mall/promotion/banner/index.vue
new file mode 100644
index 000000000..a3d0bc36a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/banner/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/banner/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/banner/modules/form.vue
new file mode 100644
index 000000000..818380589
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/banner/modules/form.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/data.ts b/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/data.ts
new file mode 100644
index 000000000..d7474eeb5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/data.ts
@@ -0,0 +1,238 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDate } from '@vben/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ },
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'startTime',
+ label: '开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ placeholder: '请选择开始时间',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ placeholder: '请选择结束时间',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'helpMaxCount',
+ label: '助力人数',
+ component: 'InputNumber',
+ componentProps: {
+ min: 1,
+ placeholder: '达到该人数才能砍到低价',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'bargainCount',
+ label: '砍价次数',
+ component: 'InputNumber',
+ componentProps: {
+ min: 1,
+ placeholder: '最大帮砍次数',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'totalLimitCount',
+ label: '购买限制',
+ component: 'InputNumber',
+ componentProps: {
+ min: 1,
+ placeholder: '最大购买次数',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'randomMinPrice',
+ label: '最小砍价金额(元)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.01,
+ placeholder: '用户每次砍价的最小金额',
+ },
+ },
+ {
+ fieldName: 'randomMaxPrice',
+ label: '最大砍价金额(元)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.01,
+ placeholder: '用户每次砍价的最大金额',
+ },
+ },
+ {
+ fieldName: 'spuId',
+ label: '砍价商品',
+ component: 'Input',
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择活动状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '活动编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '活动名称',
+ minWidth: 140,
+ },
+ {
+ field: 'activityTime',
+ title: '活动时间',
+ minWidth: 210,
+ formatter: ({ row }) => {
+ if (!row.startTime || !row.endTime) return '';
+ return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
+ },
+ },
+ {
+ field: 'picUrl',
+ title: '商品图片',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ },
+ },
+ },
+ {
+ field: 'spuName',
+ title: '商品标题',
+ minWidth: 300,
+ },
+ {
+ field: 'bargainFirstPrice',
+ title: '起始价格',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'bargainMinPrice',
+ title: '砍价底价',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'recordUserCount',
+ title: '总砍价人数',
+ minWidth: 100,
+ },
+ {
+ field: 'recordSuccessUserCount',
+ title: '成功砍价人数',
+ minWidth: 110,
+ },
+ {
+ field: 'helpUserCount',
+ title: '助力人数',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '活动状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'stock',
+ title: '库存',
+ minWidth: 80,
+ },
+ {
+ field: 'totalStock',
+ title: '总库存',
+ minWidth: 80,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/index.vue b/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/index.vue
new file mode 100644
index 000000000..b8e058834
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/index.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/modules/form.vue
new file mode 100644
index 000000000..38d492084
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/bargain/activity/modules/form.vue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/bargain/record/data.ts b/apps/web-antdv-next/src/views/mall/promotion/bargain/record/data.ts
new file mode 100644
index 000000000..c19cd824e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/bargain/record/data.ts
@@ -0,0 +1,164 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'status',
+ label: '砍价状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择砍价状态',
+ allowClear: true,
+ options: getDictOptions(
+ DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS,
+ 'number',
+ ),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 50,
+ },
+ {
+ field: 'avatar',
+ title: '用户头像',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ shape: 'circle',
+ },
+ },
+ },
+ {
+ field: 'nickname',
+ title: '用户昵称',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '发起时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'activity.name',
+ title: '砍价活动',
+ minWidth: 150,
+ },
+ {
+ field: 'activity.bargainMinPrice',
+ title: '最低价',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'bargainPrice',
+ title: '当前价',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'activity.helpMaxCount',
+ title: '总砍价次数',
+ minWidth: 100,
+ },
+ {
+ field: 'helpCount',
+ title: '剩余砍价次数',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '砍价状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS },
+ },
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'orderId',
+ title: '订单编号',
+ minWidth: 100,
+ },
+ {
+ title: '操作',
+ width: 100,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 助力列表表格列配置 */
+export function useHelpGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 80,
+ },
+ {
+ field: 'avatar',
+ title: '用户头像',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ shape: 'circle',
+ },
+ },
+ },
+ {
+ field: 'nickname',
+ title: '用户昵称',
+ minWidth: 100,
+ },
+ {
+ field: 'reducePrice',
+ title: '砍价金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'createTime',
+ title: '助力时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/bargain/record/index.vue b/apps/web-antdv-next/src/views/mall/promotion/bargain/record/index.vue
new file mode 100644
index 000000000..134c2145f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/bargain/record/index.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/bargain/record/modules/list.vue b/apps/web-antdv-next/src/views/mall/promotion/bargain/record/modules/list.vue
new file mode 100644
index 000000000..49223d5d6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/bargain/record/modules/list.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/activity/data.ts b/apps/web-antdv-next/src/views/mall/promotion/combination/activity/data.ts
new file mode 100644
index 000000000..8891f892c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/activity/data.ts
@@ -0,0 +1,230 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDate } from '@vben/utils';
+
+/** 表单配置 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ },
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'startTime',
+ label: '开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择开始时间',
+ showTime: true,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择结束时间',
+ showTime: true,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'userSize',
+ label: '拼团人数',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '达到该人数即成团',
+ min: 2,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'limitDuration',
+ label: '限制时长',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '限制时长(小时)',
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'totalLimitCount',
+ label: '总限购数量',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入总限购数量',
+ min: 0,
+ },
+ },
+ {
+ fieldName: 'singleLimitCount',
+ label: '单次限购数量',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入单次限购数量',
+ min: 0,
+ },
+ },
+ {
+ fieldName: 'virtualGroup',
+ label: '虚拟成团',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ },
+ },
+ {
+ fieldName: 'spuId',
+ label: '拼团商品',
+ component: 'Input',
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择活动状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '活动编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '活动名称',
+ minWidth: 140,
+ },
+ {
+ field: 'activityTime',
+ title: '活动时间',
+ minWidth: 210,
+ formatter: ({ row }) => {
+ if (!row.startTime || !row.endTime) return '';
+ return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
+ },
+ },
+ {
+ field: 'picUrl',
+ title: '商品图片',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ },
+ },
+ },
+ {
+ field: 'spuName',
+ title: '商品标题',
+ minWidth: 300,
+ },
+ {
+ field: 'marketPrice',
+ title: '原价',
+ minWidth: 100,
+ formatter: ({ cellValue }) => {
+ return `¥${(cellValue / 100).toFixed(2)}`;
+ },
+ },
+ {
+ field: 'combinationPrice',
+ title: '拼团价',
+ minWidth: 100,
+ formatter: ({ row }) => {
+ if (!row.products || row.products.length === 0) return '';
+ const combinationPrice = Math.min(
+ ...row.products.map((item: any) => item.combinationPrice),
+ );
+ return `¥${(combinationPrice / 100).toFixed(2)}`;
+ },
+ },
+ {
+ field: 'groupCount',
+ title: '开团组数',
+ minWidth: 100,
+ },
+ {
+ field: 'groupSuccessCount',
+ title: '成团组数',
+ minWidth: 100,
+ },
+ {
+ field: 'recordCount',
+ title: '购买次数',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '活动状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/activity/index.vue b/apps/web-antdv-next/src/views/mall/promotion/combination/activity/index.vue
new file mode 100644
index 000000000..ba430fabb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/activity/index.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/activity/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/combination/activity/modules/form.vue
new file mode 100644
index 000000000..22c3cbb45
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/activity/modules/form.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/components/index.ts b/apps/web-antdv-next/src/views/mall/promotion/combination/components/index.ts
new file mode 100644
index 000000000..36d56af42
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/components/index.ts
@@ -0,0 +1 @@
+export { default as CombinationShowcase } from './showcase.vue';
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/components/showcase.vue b/apps/web-antdv-next/src/views/mall/promotion/combination/components/showcase.vue
new file mode 100644
index 000000000..167a979bf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/components/showcase.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/components/table-select.vue b/apps/web-antdv-next/src/views/mall/promotion/combination/components/table-select.vue
new file mode 100644
index 000000000..f36a2b588
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/components/table-select.vue
@@ -0,0 +1,285 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/record/data.ts b/apps/web-antdv-next/src/views/mall/promotion/combination/record/data.ts
new file mode 100644
index 000000000..567e2e02f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/record/data.ts
@@ -0,0 +1,178 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'status',
+ label: '拼团状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择拼团状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ placeholder: ['开始时间', '结束时间'],
+ allowClear: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '拼团编号',
+ minWidth: 80,
+ },
+ {
+ field: 'avatar',
+ title: '头像',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ shape: 'circle',
+ },
+ },
+ },
+ {
+ field: 'nickname',
+ title: '昵称',
+ minWidth: 100,
+ },
+ {
+ field: 'headId',
+ title: '开团团长',
+ minWidth: 100,
+ },
+ {
+ field: 'picUrl',
+ title: '拼团商品图',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'spuName',
+ title: '拼团商品',
+ minWidth: 120,
+ },
+ {
+ field: 'activityName',
+ title: '拼团活动',
+ minWidth: 140,
+ },
+ {
+ field: 'userSize',
+ title: '几人团',
+ minWidth: 80,
+ },
+ {
+ field: 'userCount',
+ title: '参与人数',
+ minWidth: 80,
+ },
+ {
+ field: 'createTime',
+ title: '参团时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '拼团状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 100,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 用户列表表格列配置 */
+export function useUserGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 80,
+ },
+ {
+ field: 'avatar',
+ title: '用户头像',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ shape: 'circle',
+ },
+ },
+ },
+ {
+ field: 'nickname',
+ title: '用户昵称',
+ minWidth: 100,
+ },
+ {
+ field: 'headId',
+ title: '开团团长',
+ minWidth: 100,
+ formatter: ({ cellValue }) => {
+ return cellValue === 0 ? '团长' : '团员';
+ },
+ },
+ {
+ field: 'createTime',
+ title: '参团时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '拼团状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/record/index.vue b/apps/web-antdv-next/src/views/mall/promotion/combination/record/index.vue
new file mode 100644
index 000000000..6057f019b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/record/index.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/combination/record/modules/list.vue b/apps/web-antdv-next/src/views/mall/promotion/combination/record/modules/list.vue
new file mode 100644
index 000000000..459cd34d3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/combination/record/modules/list.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/data.ts b/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/data.ts
new file mode 100644
index 000000000..3eb3db71f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/data.ts
@@ -0,0 +1,220 @@
+/** APP 链接分组 */
+export interface AppLinkGroup {
+ name: string; // 分组名称
+ links: AppLink[]; // 链接列表
+}
+
+/** APP 链接 */
+export interface AppLink {
+ name: string; // 链接名称
+ path: string; // 链接地址
+ type?: APP_LINK_TYPE_ENUM; // 链接的类型
+}
+
+/** APP 链接类型(需要特殊处理,例如商品详情) */
+export enum APP_LINK_TYPE_ENUM {
+ ACTIVITY_COMBINATION, // 拼团活动
+ ACTIVITY_POINT, // 积分商城活动
+ ACTIVITY_SECKILL, // 秒杀活动
+ ARTICLE_DETAIL, // 文章详情
+ COUPON_DETAIL, // 优惠券详情
+ DIY_PAGE_DETAIL, // 自定义页面详情
+ PRODUCT_CATEGORY_LIST, // 品类列表
+ PRODUCT_DETAIL_COMBINATION, // 拼团商品详情
+ PRODUCT_DETAIL_NORMAL, // 商品详情
+ PRODUCT_DETAIL_SECKILL, // 秒杀商品详情
+ PRODUCT_LIST, // 商品列表
+}
+
+/** APP 链接列表(做一下持久化?) */
+export const APP_LINK_GROUP_LIST = [
+ {
+ name: '商城',
+ links: [
+ {
+ name: '首页',
+ path: '/pages/index/index',
+ },
+ {
+ name: '商品分类',
+ path: '/pages/index/category',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST,
+ },
+ {
+ name: '购物车',
+ path: '/pages/index/cart',
+ },
+ {
+ name: '个人中心',
+ path: '/pages/index/user',
+ },
+ {
+ name: '商品搜索',
+ path: '/pages/index/search',
+ },
+ {
+ name: '自定义页面',
+ path: '/pages/index/page',
+ type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL,
+ },
+ {
+ name: '客服',
+ path: '/pages/chat/index',
+ },
+ {
+ name: '系统设置',
+ path: '/pages/public/setting',
+ },
+ {
+ name: '常见问题',
+ path: '/pages/public/faq',
+ },
+ ],
+ },
+ {
+ name: '商品',
+ links: [
+ {
+ name: '商品列表',
+ path: '/pages/goods/list',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_LIST,
+ },
+ {
+ name: '商品详情',
+ path: '/pages/goods/index',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL,
+ },
+ {
+ name: '拼团商品详情',
+ path: '/pages/goods/groupon',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION,
+ },
+ {
+ name: '秒杀商品详情',
+ path: '/pages/goods/seckill',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL,
+ },
+ ],
+ },
+ {
+ name: '营销活动',
+ links: [
+ {
+ name: '拼团订单',
+ path: '/pages/activity/groupon/order',
+ },
+ {
+ name: '营销商品',
+ path: '/pages/activity/index',
+ },
+ {
+ name: '拼团活动',
+ path: '/pages/activity/groupon/list',
+ type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION,
+ },
+ {
+ name: '秒杀活动',
+ path: '/pages/activity/seckill/list',
+ type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL,
+ },
+ {
+ name: '积分商城活动',
+ path: '/pages/activity/point/list',
+ type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT,
+ },
+ {
+ name: '签到中心',
+ path: '/pages/app/sign',
+ },
+ {
+ name: '优惠券中心',
+ path: '/pages/coupon/list',
+ },
+ {
+ name: '优惠券详情',
+ path: '/pages/coupon/detail',
+ type: APP_LINK_TYPE_ENUM.COUPON_DETAIL,
+ },
+ {
+ name: '文章详情',
+ path: '/pages/public/richtext',
+ type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL,
+ },
+ ],
+ },
+ {
+ name: '分销商城',
+ links: [
+ {
+ name: '分销中心',
+ path: '/pages/commission/index',
+ },
+ {
+ name: '推广商品',
+ path: '/pages/commission/goods',
+ },
+ {
+ name: '分销订单',
+ path: '/pages/commission/order',
+ },
+ {
+ name: '我的团队',
+ path: '/pages/commission/team',
+ },
+ ],
+ },
+ {
+ name: '支付',
+ links: [
+ {
+ name: '充值余额',
+ path: '/pages/pay/recharge',
+ },
+ {
+ name: '充值记录',
+ path: '/pages/pay/recharge-log',
+ },
+ ],
+ },
+ {
+ name: '用户中心',
+ links: [
+ {
+ name: '用户信息',
+ path: '/pages/user/info',
+ },
+ {
+ name: '用户订单',
+ path: '/pages/order/list',
+ },
+ {
+ name: '售后订单',
+ path: '/pages/order/aftersale/list',
+ },
+ {
+ name: '商品收藏',
+ path: '/pages/user/goods-collect',
+ },
+ {
+ name: '浏览记录',
+ path: '/pages/user/goods-log',
+ },
+ {
+ name: '地址管理',
+ path: '/pages/user/address/list',
+ },
+ {
+ name: '用户佣金',
+ path: '/pages/user/wallet/commission',
+ },
+ {
+ name: '用户余额',
+ path: '/pages/user/wallet/money',
+ },
+ {
+ name: '用户积分',
+ path: '/pages/user/wallet/score',
+ },
+ ],
+ },
+] as AppLinkGroup[];
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/index.vue
new file mode 100644
index 000000000..b4125104b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/index.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/select-dialog.vue b/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/select-dialog.vue
new file mode 100644
index 000000000..77b5a8f92
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/app-link-input/select-dialog.vue
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/color-input/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/color-input/index.vue
new file mode 100644
index 000000000..d4745a0ed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/color-input/index.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-container-property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-container-property.vue
new file mode 100644
index 000000000..ea688e0cb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-container-property.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+ 组件样式:
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-container.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-container.vue
new file mode 100644
index 000000000..a2a16d657
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-container.vue
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+ {{ component.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-library.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-library.vue
new file mode 100644
index 000000000..f447919a4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/component-library.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+ {{ element.name }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/config.ts
new file mode 100644
index 000000000..40e049d30
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/config.ts
@@ -0,0 +1,51 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 轮播图属性 */
+export interface CarouselProperty {
+ type: 'card' | 'default'; // 类型:默认 | 卡片
+ indicator: 'dot' | 'number'; // 指示器样式:点 | 数字
+ autoplay: boolean; // 是否自动播放
+ interval: number; // 播放间隔
+ height: number; // 轮播高度
+ items: CarouselItemProperty[]; // 轮播内容
+ style: ComponentStyle; // 组件样式
+}
+
+/** 轮播内容属性 */
+export interface CarouselItemProperty {
+ type: 'img' | 'video'; // 类型:图片 | 视频
+ imgUrl: string; // 图片链接
+ videoUrl: string; // 视频链接
+ url: string; // 跳转链接
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'Carousel',
+ name: '轮播图',
+ icon: 'system-uicons:carousel',
+ property: {
+ type: 'default',
+ indicator: 'dot',
+ autoplay: false,
+ interval: 3,
+ height: 174,
+ items: [
+ {
+ type: 'img',
+ imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg',
+ videoUrl: '',
+ },
+ {
+ type: 'img',
+ imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg',
+ videoUrl: '',
+ },
+ ] as CarouselItemProperty[],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/index.vue
new file mode 100644
index 000000000..6b89b4c41
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/index.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentIndex }} / {{ property.items.length }}
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/property.vue
new file mode 100644
index 000000000..bebf71f72
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/carousel/property.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/component.tsx b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/component.tsx
new file mode 100644
index 000000000..5782b9a9f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/component.tsx
@@ -0,0 +1,88 @@
+/* eslint-disable vue/one-component-per-file */
+import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
+
+import { defineComponent } from 'vue';
+
+import {
+ CouponTemplateValidityTypeEnum,
+ PromotionDiscountTypeEnum,
+} from '@vben/constants';
+import { floatToFixed2, formatDate } from '@vben/utils';
+
+/** 有效期 */
+export const CouponValidTerm = defineComponent({
+ name: 'CouponValidTerm',
+ props: {
+ coupon: {
+ type: Object as () => MallCouponTemplateApi.CouponTemplate,
+ required: true,
+ },
+ },
+ setup(props) {
+ const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate;
+ const text =
+ coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type
+ ? `有效期:${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 至 ${formatDate(
+ coupon.validEndTime,
+ 'YYYY-MM-DD',
+ )}`
+ : `领取后第 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 天内可用`;
+ return () => {text}
;
+ },
+});
+
+/** 优惠值 */
+export const CouponDiscount = defineComponent({
+ name: 'CouponDiscount',
+ props: {
+ coupon: {
+ type: Object as () => MallCouponTemplateApi.CouponTemplate,
+ required: true,
+ },
+ },
+ setup(props) {
+ const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate;
+ // 折扣
+ let value = `${(coupon.discountPercent ?? 0) / 10}`;
+ let suffix = ' 折';
+ // 满减
+ if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+ value = floatToFixed2(coupon.discountPrice);
+ suffix = ' 元';
+ }
+ return () => (
+
+ {value}
+ {suffix}
+
+ );
+ },
+});
+
+/** 优惠描述 */
+export const CouponDiscountDesc = defineComponent({
+ name: 'CouponDiscountDesc',
+ props: {
+ coupon: {
+ type: Object as () => MallCouponTemplateApi.CouponTemplate,
+ required: true,
+ },
+ },
+ setup(props) {
+ const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate;
+ // 使用条件
+ const useCondition =
+ coupon.usePrice > 0 ? `满${floatToFixed2(coupon.usePrice)}元,` : '';
+ // 优惠描述
+ const discountDesc =
+ coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
+ ? `减${floatToFixed2(coupon.discountPrice)}元`
+ : `打${(coupon.discountPercent ?? 0) / 10}折`;
+ return () => (
+
+ {useCondition}
+ {discountDesc}
+
+ );
+ },
+});
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/config.ts
new file mode 100644
index 000000000..723461b4a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/config.ts
@@ -0,0 +1,38 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 优惠劵卡片属性 */
+export interface CouponCardProperty {
+ columns: number; // 列数
+ bgImg: string; // 背景图
+ textColor: string; // 文字颜色
+ button: {
+ bgColor: string; // 背景颜色
+ color: string; // 文字颜色
+ }; // 按钮样式
+ space: number; // 间距
+ couponIds: number[]; // 优惠券编号列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'CouponCard',
+ name: '优惠券',
+ icon: 'ep:ticket',
+ property: {
+ columns: 1,
+ bgImg: '',
+ textColor: '#E9B461',
+ button: {
+ color: '#434343',
+ bgColor: '',
+ },
+ space: 0,
+ couponIds: [],
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/index.vue
new file mode 100644
index 000000000..2cb2f1348
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/index.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 仅剩:{{ coupon.totalCount - coupon.takeCount }}张
+
+
仅剩:不限制
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/property.vue
new file mode 100644
index 000000000..3d0412309
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/coupon-card/property.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/config.ts
new file mode 100644
index 000000000..3ab6eef11
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/config.ts
@@ -0,0 +1,24 @@
+import type { DiyComponent } from '../../../util';
+
+/** 分割线属性 */
+export interface DividerProperty {
+ height: number; // 高度
+ lineWidth: number; // 线宽
+ paddingType: 'horizontal' | 'none'; // 边距类型
+ lineColor: string; // 颜色
+ borderType: 'dashed' | 'dotted' | 'none' | 'solid'; // 类型
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'Divider',
+ name: '分割线',
+ icon: 'tdesign:component-divider-vertical',
+ property: {
+ height: 30,
+ lineWidth: 1,
+ paddingType: 'none',
+ lineColor: '#dcdfe6',
+ borderType: 'solid',
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/index.vue
new file mode 100644
index 000000000..e07eda87e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/property.vue
new file mode 100644
index 000000000..68243de31
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/divider/property.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/config.ts
new file mode 100644
index 000000000..d93b1f462
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/config.ts
@@ -0,0 +1,29 @@
+import type { DiyComponent } from '../../../util';
+
+/** 悬浮按钮属性 */
+export interface FloatingActionButtonProperty {
+ direction: 'horizontal' | 'vertical'; // 展开方向
+ showText: boolean; // 是否显示文字
+ list: FloatingActionButtonItemProperty[]; // 按钮列表
+}
+
+/** 悬浮按钮项属性 */
+export interface FloatingActionButtonItemProperty {
+ imgUrl: string; // 图片地址
+ url: string; // 跳转连接
+ text: string; // 文字
+ textColor: string; // 文字颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'FloatingActionButton',
+ name: '悬浮按钮',
+ icon: 'tabler:float-right',
+ position: 'fixed',
+ property: {
+ direction: 'vertical',
+ showText: true,
+ list: [{ textColor: '#fff' }],
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/index.vue
new file mode 100644
index 000000000..7451e8d87
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/index.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/property.vue
new file mode 100644
index 000000000..1b633ceeb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/floating-action-button/property.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/controller.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/controller.ts
new file mode 100644
index 000000000..b4000eccc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/controller.ts
@@ -0,0 +1,176 @@
+import type { StyleValue } from 'vue';
+
+import type { HotZoneItemProperty } from '../../config';
+
+export const HOT_ZONE_MIN_SIZE = 100; // 热区的最小宽高
+
+/** 控制的类型 */
+export enum CONTROL_TYPE_ENUM {
+ LEFT,
+ TOP,
+ WIDTH,
+ HEIGHT,
+}
+
+/** 定义热区的控制点 */
+export interface ControlDot {
+ position: string;
+ types: CONTROL_TYPE_ENUM[];
+ style: StyleValue;
+}
+
+/** 热区的 8 个控制点 */
+export const CONTROL_DOT_LIST = [
+ {
+ position: '左上角',
+ types: [
+ CONTROL_TYPE_ENUM.LEFT,
+ CONTROL_TYPE_ENUM.TOP,
+ CONTROL_TYPE_ENUM.WIDTH,
+ CONTROL_TYPE_ENUM.HEIGHT,
+ ],
+ style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' },
+ },
+ {
+ position: '上方中间',
+ types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT],
+ style: {
+ left: '50%',
+ top: '-5px',
+ cursor: 'n-resize',
+ transform: 'translateX(-50%)',
+ },
+ },
+ {
+ position: '右上角',
+ types: [
+ CONTROL_TYPE_ENUM.TOP,
+ CONTROL_TYPE_ENUM.WIDTH,
+ CONTROL_TYPE_ENUM.HEIGHT,
+ ],
+ style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' },
+ },
+ {
+ position: '右侧中间',
+ types: [CONTROL_TYPE_ENUM.WIDTH],
+ style: {
+ right: '-5px',
+ top: '50%',
+ cursor: 'e-resize',
+ transform: 'translateX(-50%)',
+ },
+ },
+ {
+ position: '右下角',
+ types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+ style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' },
+ },
+ {
+ position: '下方中间',
+ types: [CONTROL_TYPE_ENUM.HEIGHT],
+ style: {
+ left: '50%',
+ bottom: '-5px',
+ cursor: 's-resize',
+ transform: 'translateX(-50%)',
+ },
+ },
+ {
+ position: '左下角',
+ types: [
+ CONTROL_TYPE_ENUM.LEFT,
+ CONTROL_TYPE_ENUM.WIDTH,
+ CONTROL_TYPE_ENUM.HEIGHT,
+ ],
+ style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' },
+ },
+ {
+ position: '左侧中间',
+ types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH],
+ style: {
+ left: '-5px',
+ top: '50%',
+ cursor: 'w-resize',
+ transform: 'translateX(-50%)',
+ },
+ },
+] as ControlDot[];
+
+// region 热区的缩放
+export const HOT_ZONE_SCALE_RATE = 2; // 热区的缩放比例
+
+/** 缩小:缩回适合手机屏幕的大小 */
+export function zoomOut(list?: HotZoneItemProperty[]) {
+ return (
+ list?.map((hotZone) => ({
+ ...hotZone,
+ left: (hotZone.left /= HOT_ZONE_SCALE_RATE),
+ top: (hotZone.top /= HOT_ZONE_SCALE_RATE),
+ width: (hotZone.width /= HOT_ZONE_SCALE_RATE),
+ height: (hotZone.height /= HOT_ZONE_SCALE_RATE),
+ })) || []
+ );
+}
+
+/** 放大:作用是为了方便在电脑屏幕上编辑 */
+export function zoomIn(list?: HotZoneItemProperty[]) {
+ return (
+ list?.map((hotZone) => ({
+ ...hotZone,
+ left: (hotZone.left *= HOT_ZONE_SCALE_RATE),
+ top: (hotZone.top *= HOT_ZONE_SCALE_RATE),
+ width: (hotZone.width *= HOT_ZONE_SCALE_RATE),
+ height: (hotZone.height *= HOT_ZONE_SCALE_RATE),
+ })) || []
+ );
+}
+
+// endregion
+
+/**
+ * 封装热区拖拽
+ *
+ * 注:为什么不使用vueuse的useDraggable。在本场景下,其使用方式比较复杂
+ * @param hotZone 热区
+ * @param downEvent 鼠标按下事件
+ * @param callback 回调函数
+ */
+export const useDraggable = (
+ hotZone: HotZoneItemProperty,
+ downEvent: MouseEvent,
+ callback: (
+ left: number,
+ top: number,
+ width: number,
+ height: number,
+ moveWidth: number,
+ moveHeight: number,
+ ) => void,
+) => {
+ // 阻止事件冒泡
+ downEvent.stopPropagation();
+
+ // 移动前的鼠标坐标
+ const { clientX: startX, clientY: startY } = downEvent;
+ // 移动前的热区坐标、大小
+ const { left, top, width, height } = hotZone;
+
+ // 监听鼠标移动
+ const handleMouseMove = (e: MouseEvent) => {
+ // 移动宽度
+ const moveWidth = e.clientX - startX;
+ // 移动高度
+ const moveHeight = e.clientY - startY;
+ // 移动回调
+ callback(left, top, width, height, moveWidth, moveHeight);
+ };
+
+ // 松开鼠标后,结束拖拽
+ const handleMouseUp = () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+};
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/index.vue
new file mode 100644
index 000000000..2f7136ffd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/index.vue
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+ {{ item.name || '双击选择链接' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/config.ts
new file mode 100644
index 000000000..9178ae0b5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/config.ts
@@ -0,0 +1,34 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 热区属性 */
+export interface HotZoneProperty {
+ imgUrl: string; // 图片地址
+ list: HotZoneItemProperty[]; // 导航菜单列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 热区项目属性 */
+export interface HotZoneItemProperty {
+ name: string; // 链接的名称
+ url: string; // 链接
+ width: number; // 宽
+ height: number; // 高
+ top: number; // 上
+ left: number; // 左
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'HotZone',
+ name: '热区',
+ icon: 'tabler:hand-click',
+ property: {
+ imgUrl: '',
+ list: [] as HotZoneItemProperty[],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/index.vue
new file mode 100644
index 000000000..0d289f17f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/index.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/property.vue
new file mode 100644
index 000000000..cb669fe17
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/hot-zone/property.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/config.ts
new file mode 100644
index 000000000..3b402822a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/config.ts
@@ -0,0 +1,24 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 图片展示属性 */
+export interface ImageBarProperty {
+ imgUrl: string; // 图片链接
+ url: string; // 跳转链接
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'ImageBar',
+ name: '图片展示',
+ icon: 'lucide:image',
+ property: {
+ imgUrl: '',
+ url: '',
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/index.vue
new file mode 100644
index 000000000..cae1a68e3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/index.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/property.vue
new file mode 100644
index 000000000..179b0134c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/image-bar/property.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/index.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/index.ts
new file mode 100644
index 000000000..5b2c4ccf9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/index.ts
@@ -0,0 +1,62 @@
+/**
+ * 组件注册
+ *
+ * 组件规范:每个子目录就是一个独立的组件,每个目录包括以下三个文件:
+ * 1. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型
+ * 2. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可
+ * 3. property.vue:组件属性表单,用于配置组件,必选,
+ *
+ * 注:
+ * 组件 ID 以 config.ts 中配置的 id 为准,与组件目录的名称无关,但还是建议组件目录的名称与组件 ID 保持一致
+ */
+import { defineAsyncComponent } from 'vue';
+
+const viewModules: Record = import.meta.glob('./*/*.vue'); // 导入组件界面模块
+const configModules: Record = import.meta.glob('./*/config.ts', {
+ eager: true,
+}); // 导入配置模块
+
+const components: Record = {}; // 界面模块
+const componentConfigs: Record = {}; // 组件配置模块
+
+type ViewType = 'index' | 'property'; // 组件界面的类型
+
+/**
+ * 注册组件的界面模块
+ *
+ * @param componentId 组件ID
+ * @param configPath 配置模块的文件路径
+ * @param viewType 组件界面的类型
+ */
+const registerComponentViewModule = (
+ componentId: string,
+ configPath: string,
+ viewType: ViewType,
+) => {
+ const viewPath = configPath.replace('config.ts', `${viewType}.vue`);
+ const viewModule = viewModules[viewPath];
+ if (viewModule) {
+ // 定义异步组件
+ components[componentId] = defineAsyncComponent(viewModule);
+ }
+};
+
+// 注册
+Object.keys(configModules).forEach((modulePath: string) => {
+ const component = configModules[modulePath].component;
+ const componentId = component?.id;
+ if (componentId) {
+ // 注册组件
+ componentConfigs[componentId] = component;
+ // 注册预览界面
+ registerComponentViewModule(componentId, modulePath, 'index');
+ // 注册属性配置表单
+ registerComponentViewModule(
+ `${componentId}Property`,
+ modulePath,
+ 'property',
+ );
+ }
+});
+
+export { componentConfigs, components };
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/config.ts
new file mode 100644
index 000000000..02c7bc1c4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/config.ts
@@ -0,0 +1,38 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 广告魔方属性 */
+export interface MagicCubeProperty {
+ borderRadiusTop: number; // 上圆角
+ borderRadiusBottom: number; // 下圆角
+ space: number; // 间隔
+ list: MagicCubeItemProperty[]; // 导航菜单列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 广告魔方项目属性 */
+export interface MagicCubeItemProperty {
+ imgUrl: string; // 图标链接
+ url: string; // 链接
+ width: number; // 宽
+ height: number; // 高
+ top: number; // 上
+ left: number; // 左
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'MagicCube',
+ name: '广告魔方',
+ icon: 'bi:columns',
+ property: {
+ borderRadiusTop: 0,
+ borderRadiusBottom: 0,
+ space: 0,
+ list: [],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/index.vue
new file mode 100644
index 000000000..bd91a7f31
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/index.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/property.vue
new file mode 100644
index 000000000..a6df2228d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/magic-cube/property.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/config.ts
new file mode 100644
index 000000000..0bf47b15d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/config.ts
@@ -0,0 +1,67 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+import { cloneDeep } from '@vben/utils';
+
+/** 宫格导航属性 */
+export interface MenuGridProperty {
+ column: number; // 列数
+ list: MenuGridItemProperty[]; // 导航菜单列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 宫格导航项目属性 */
+export interface MenuGridItemProperty {
+ iconUrl: string; // 图标链接
+ title: string; // 标题
+ titleColor: string; // 标题颜色
+ subtitle: string; // 副标题
+ subtitleColor: string; // 副标题颜色
+ url: string; // 链接
+ badge: {
+ bgColor: string; // 角标背景颜色
+ show: boolean; // 是否显示
+ text: string; // 角标文字
+ textColor: string; // 角标文字颜色
+ };
+}
+
+/** 宫格导航项目默认属性 */
+export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
+ title: '标题',
+ titleColor: '#333',
+ subtitle: '副标题',
+ subtitleColor: '#bbb',
+ badge: {
+ show: false,
+ textColor: '#fff',
+ bgColor: '#FF6000',
+ },
+} as MenuGridItemProperty;
+
+/** 定义组件 */
+export const component = {
+ id: 'MenuGrid',
+ name: '宫格导航',
+ icon: 'bi:grid-3x3-gap',
+ property: {
+ column: 3,
+ list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ marginLeft: 8,
+ marginRight: 8,
+ padding: 8,
+ paddingTop: 8,
+ paddingRight: 8,
+ paddingBottom: 8,
+ paddingLeft: 8,
+ borderRadius: 8,
+ borderTopLeftRadius: 8,
+ borderTopRightRadius: 8,
+ borderBottomRightRadius: 8,
+ borderBottomLeftRadius: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/index.vue
new file mode 100644
index 000000000..38336f5a9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/index.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ {{ item.badge.text }}
+
+
+
+ {{ item.title }}
+
+
+ {{ item.subtitle }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/property.vue
new file mode 100644
index 000000000..eac0f2ce9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-grid/property.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/config.ts
new file mode 100644
index 000000000..375546ae3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/config.ts
@@ -0,0 +1,42 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+import { cloneDeep } from '@vben/utils';
+
+/** 列表导航属性 */
+export interface MenuListProperty {
+ list: MenuListItemProperty[]; // 导航菜单列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 列表导航项目属性 */
+export interface MenuListItemProperty {
+ iconUrl: string; // 图标链接
+ title: string; // 标题
+ titleColor: string; // 标题颜色
+ subtitle: string; // 副标题
+ subtitleColor: string; // 副标题颜色
+ url: string; // 链接
+}
+
+/** 空的列表导航项目属性 */
+export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
+ title: '标题',
+ titleColor: '#333',
+ subtitle: '副标题',
+ subtitleColor: '#bbb',
+};
+
+/** 定义组件 */
+export const component = {
+ id: 'MenuList',
+ name: '列表导航',
+ icon: 'fa-solid:list',
+ property: {
+ list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/index.vue
new file mode 100644
index 000000000..e0e3c8496
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/index.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+ {{ item.subtitle }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/property.vue
new file mode 100644
index 000000000..cc771fb2b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-list/property.vue
@@ -0,0 +1,71 @@
+
+
+
+
+ 菜单设置
+ 拖动左侧的小圆点可以调整顺序
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/config.ts
new file mode 100644
index 000000000..fa83498c8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/config.ts
@@ -0,0 +1,55 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+import { cloneDeep } from '@vben/utils';
+
+/** 菜单导航属性 */
+export interface MenuSwiperProperty {
+ layout: 'icon' | 'iconText'; // 布局:图标+文字 | 图标
+ row: number; // 行数
+ column: number; // 列数
+ list: MenuSwiperItemProperty[]; // 导航菜单列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 菜单导航项目属性 */
+export interface MenuSwiperItemProperty {
+ iconUrl: string; // 图标链接
+ title: string; // 标题
+ titleColor: string; // 标题颜色
+ url: string; // 链接
+ badge: {
+ bgColor: string; // 角标背景颜色
+ show: boolean; // 是否显示
+ text: string; // 角标文字
+ textColor: string; // 角标文字颜色
+ }; // 角标
+}
+
+/** 空菜单导航项目属性 */
+export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
+ title: '标题',
+ titleColor: '#333',
+ badge: {
+ show: false,
+ textColor: '#fff',
+ bgColor: '#FF6000',
+ },
+} as MenuSwiperItemProperty;
+
+/** 定义组件 */
+export const component = {
+ id: 'MenuSwiper',
+ name: '菜单导航',
+ icon: 'bi:grid-3x2-gap',
+ property: {
+ layout: 'iconText',
+ row: 1,
+ column: 3,
+ list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/index.vue
new file mode 100644
index 000000000..b18980ebe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/index.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+ {{ item.badge.text }}
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/property.vue
new file mode 100644
index 000000000..5f1e581de
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/menu-swiper/property.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/components/cell-property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/components/cell-property.vue
new file mode 100644
index 000000000..7545cdfe2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/components/cell-property.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
![]()
+
+
+
+
+
+ 文字
+ 图片
+ 搜索框
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 建议尺寸 56*56
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/config.ts
new file mode 100644
index 000000000..c2769cc51
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/config.ts
@@ -0,0 +1,64 @@
+import type { DiyComponent } from '../../../util';
+
+/** 顶部导航栏属性 */
+export interface NavigationBarProperty {
+ bgType: 'color' | 'img'; // 背景类型
+ bgColor: string; // 背景颜色
+ bgImg: string; // 图片链接
+ styleType: 'inner' | 'normal'; // 样式类型:默认 | 沉浸式
+ alwaysShow: boolean; // 常驻显示
+ mpCells: NavigationBarCellProperty[]; // 小程序单元格列表
+ otherCells: NavigationBarCellProperty[]; // 其它平台单元格列表
+ _local: {
+ previewMp: boolean; // 预览顶部导航(小程序)
+ previewOther: boolean; // 预览顶部导航(非小程序)
+ }; // 本地变量
+}
+
+/** 顶部导航栏 - 单元格 属性 */
+export interface NavigationBarCellProperty {
+ type: 'image' | 'search' | 'text'; // 类型:文字 | 图片 | 搜索框
+ width: number; // 宽度
+ height: number; // 高度
+ top: number; // 顶部位置
+ left: number; // 左侧位置
+ text: string; // 文字内容
+ textColor: string; // 文字颜色
+ imgUrl: string; // 图片地址
+ url: string; // 图片链接
+ backgroundColor: string; // 搜索框:框体颜色
+ placeholder: string; // 搜索框:提示文字
+ placeholderPosition: string; // 搜索框:提示文字位置
+ showScan: boolean; // 搜索框:是否显示扫一扫
+ borderRadius: number; // 搜索框:边框圆角半径
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'NavigationBar',
+ name: '顶部导航栏',
+ icon: 'tabler:layout-navbar',
+ property: {
+ bgType: 'color',
+ bgColor: '#fff',
+ bgImg: '',
+ styleType: 'normal',
+ alwaysShow: true,
+ mpCells: [
+ {
+ type: 'text',
+ textColor: '#111111',
+ },
+ ],
+ otherCells: [
+ {
+ type: 'text',
+ textColor: '#111111',
+ },
+ ],
+ _local: {
+ previewMp: true,
+ previewOther: false,
+ },
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/index.vue
new file mode 100644
index 000000000..923e65d5e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
{{ cell.text }}
+
![]()
+
+
+
+
![]()
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/property.vue
new file mode 100644
index 000000000..3603c46a0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/navigation-bar/property.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/config.ts
new file mode 100644
index 000000000..b8fc2e910
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/config.ts
@@ -0,0 +1,39 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 公告栏属性 */
+export interface NoticeBarProperty {
+ iconUrl: string; // 图标地址
+ contents: NoticeContentProperty[]; // 公告内容列表
+ backgroundColor: string; // 背景颜色
+ textColor: string; // 文字颜色
+ style: ComponentStyle; // 组件样式
+}
+
+/** 内容属性 */
+export interface NoticeContentProperty {
+ text: string; // 内容文字
+ url: string; // 链接地址
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'NoticeBar',
+ name: '公告栏',
+ icon: 'lucide:bell',
+ property: {
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
+ contents: [
+ {
+ text: '',
+ url: '',
+ },
+ ],
+ backgroundColor: '#fff',
+ textColor: '#333',
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/index.vue
new file mode 100644
index 000000000..bc5605562
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/index.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/property.vue
new file mode 100644
index 000000000..f32b63410
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/notice-bar/property.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/page-config/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/page-config/config.ts
new file mode 100644
index 000000000..79661b0f3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/page-config/config.ts
@@ -0,0 +1,20 @@
+import type { DiyComponent } from '../../../util';
+
+/** 页面设置属性 */
+export interface PageConfigProperty {
+ description: string; // 页面描述
+ backgroundColor: string; // 页面背景颜色
+ backgroundImage: string; // 页面背景图片
+}
+
+/** 定义页面组件 */
+export const component = {
+ id: 'PageConfig',
+ name: '页面设置',
+ icon: 'lucide:file-text',
+ property: {
+ description: '',
+ backgroundColor: '#f5f5f5',
+ backgroundImage: '',
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/page-config/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/page-config/property.vue
new file mode 100644
index 000000000..5c06402ef
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/page-config/property.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/config.ts
new file mode 100644
index 000000000..52bbeacdb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/config.ts
@@ -0,0 +1,24 @@
+import type { DiyComponent } from '../../../util';
+
+/** 弹窗广告属性 */
+export interface PopoverProperty {
+ list: PopoverItemProperty[]; // 弹窗列表
+}
+
+/** 弹窗广告项目属性 */
+export interface PopoverItemProperty {
+ imgUrl: string; // 图片地址
+ url: string; // 跳转连接
+ showType: 'always' | 'once'; // 显示类型:仅显示一次、每次启动都会显示
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'Popover',
+ name: '弹窗广告',
+ icon: 'carbon:popup',
+ position: 'fixed',
+ property: {
+ list: [{ showType: 'once' }],
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/index.vue
new file mode 100644
index 000000000..4d8cd9e40
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/index.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
{{ index + 1 }}
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/property.vue
new file mode 100644
index 000000000..1f29e08df
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/popover/property.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/config.ts
new file mode 100644
index 000000000..82009b6b7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/config.ts
@@ -0,0 +1,74 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 商品卡片属性 */
+export interface ProductCardProperty {
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列大图 | 单列小图 | 双列
+ fields: {
+ introduction: ProductCardFieldProperty; // 商品简介
+ marketPrice: ProductCardFieldProperty; // 商品市场价
+ name: ProductCardFieldProperty; // 商品名称
+ price: ProductCardFieldProperty; // 商品价格
+ salesCount: ProductCardFieldProperty; // 商品销量
+ stock: ProductCardFieldProperty; // 商品库存
+ }; // 商品字段
+ badge: {
+ imgUrl: string; // 角标图片
+ show: boolean; // 是否显示
+ }; // 角标
+ btnBuy: {
+ bgBeginColor: string; // 文字按钮:背景渐变起始颜色
+ bgEndColor: string; // 文字按钮:背景渐变结束颜色
+ imgUrl: string; // 图片按钮:图片地址
+ text: string; // 文字
+ type: 'img' | 'text'; // 类型:文字 | 图片
+ }; // 按钮
+ borderRadiusTop: number; // 上圆角
+ borderRadiusBottom: number; // 下圆角
+ space: number; // 间距
+ spuIds: number[]; // 商品编号列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 商品字段属性 */
+export interface ProductCardFieldProperty {
+ show: boolean; // 是否显示
+ color: string; // 颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'ProductCard',
+ name: '商品卡片',
+ icon: 'lucide:grid-3x3',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' },
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '立即购买',
+ // todo: @owen 根据主题色配置
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: '',
+ },
+ borderRadiusTop: 6,
+ borderRadiusBottom: 6,
+ space: 8,
+ spuIds: [],
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/index.vue
new file mode 100644
index 000000000..0eb3b26bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/index.vue
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ spu.name }}
+
+
+
+ {{ spu.introduction }}
+
+
+
+
+ ¥{{ fenToYuan(spu.price as any) }}
+
+
+ ¥{{ fenToYuan(spu.marketPrice) }}
+
+
+
+
+
+ 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
+
+
+
+ 库存{{ spu.stock || 0 }}
+
+
+
+
+
+
+
+ {{ property.btnBuy.text }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/property.vue
new file mode 100644
index 000000000..e15f975fe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-card/property.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/config.ts
new file mode 100644
index 000000000..f5d3e4e5f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/config.ts
@@ -0,0 +1,51 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 商品栏属性 */
+export interface ProductListProperty {
+ layoutType: 'horizSwiper' | 'threeCol' | 'twoCol'; // 布局类型:双列 | 三列 | 水平滑动
+ fields: {
+ name: ProductListFieldProperty; // 商品名称
+ price: ProductListFieldProperty; // 商品价格
+ }; // 商品字段
+ badge: {
+ imgUrl: string; // 角标图片
+ show: boolean; // 是否显示
+ }; // 角标
+ borderRadiusTop: number; // 上圆角
+ borderRadiusBottom: number; // 下圆角
+ space: number; // 间距
+ spuIds: number[]; // 商品编号列表
+ style: ComponentStyle; // 组件样式
+}
+
+/** 商品字段属性 */
+export interface ProductListFieldProperty {
+ show: boolean; // 是否显示
+ color: string; // 颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'ProductList',
+ name: '商品栏',
+ icon: 'fluent:text-column-two-24-filled',
+ property: {
+ layoutType: 'twoCol',
+ fields: {
+ name: { show: true, color: '#000' },
+ price: { show: true, color: '#ff3000' },
+ },
+ badge: { show: false, imgUrl: '' },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ spuIds: [],
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/index.vue
new file mode 100644
index 000000000..39a3887f0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/index.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ spu.name }}
+
+
+
+
+ ¥{{ fenToYuan(spu.price || 0) }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/property.vue
new file mode 100644
index 000000000..ed3985b97
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/product-list/property.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/config.ts
new file mode 100644
index 000000000..ac47bf4b5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/config.ts
@@ -0,0 +1,23 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 营销文章属性 */
+export interface PromotionArticleProperty {
+ id: number; // 文章编号
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'PromotionArticle',
+ name: '营销文章',
+ icon: 'ph:article-medium',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/index.vue
new file mode 100644
index 000000000..cb90e63f2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/property.vue
new file mode 100644
index 000000000..2024181b9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-article/property.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/config.ts
new file mode 100644
index 000000000..f9db86b73
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/config.ts
@@ -0,0 +1,72 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 拼团属性 */
+export interface PromotionCombinationProperty {
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列 | 三列
+ fields: {
+ introduction: PromotionCombinationFieldProperty; // 商品简介
+ marketPrice: PromotionCombinationFieldProperty; // 市场价
+ name: PromotionCombinationFieldProperty; // 商品名称
+ price: PromotionCombinationFieldProperty; // 商品价格
+ salesCount: PromotionCombinationFieldProperty; // 商品销量
+ stock: PromotionCombinationFieldProperty; // 商品库存
+ }; // 商品字段
+ badge: {
+ imgUrl: string; // 角标图片
+ show: boolean; // 是否显示
+ }; // 角标
+ btnBuy: {
+ bgBeginColor: string; // 文字按钮:背景渐变起始颜色
+ bgEndColor: string; // 文字按钮:背景渐变结束颜色
+ imgUrl: string; // 图片按钮:图片地址
+ text: string; // 文字
+ type: 'img' | 'text'; // 类型:文字 | 图片
+ }; // 按钮
+ borderRadiusTop: number; // 上圆角
+ borderRadiusBottom: number; // 下圆角
+ space: number; // 间距
+ activityIds: number[]; // 拼团活动编号
+ style: ComponentStyle; // 组件样式
+}
+
+/** 商品字段属性 */
+export interface PromotionCombinationFieldProperty {
+ show: boolean; // 是否显示
+ color: string; // 颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'PromotionCombination',
+ name: '拼团',
+ icon: 'mdi:account-group',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' },
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '去拼团',
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: '',
+ },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/index.vue
new file mode 100644
index 000000000..bda1d61fb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/index.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ spu.name }}
+
+
+
+ {{ spu.introduction }}
+
+
+
+
+ ¥{{ fenToYuan(spu.price || Infinity) }}
+
+
+
+ ¥{{ fenToYuan(spu.marketPrice) }}
+
+
+
+
+
+ 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
+
+
+
+ 库存{{ spu.stock || 0 }}
+
+
+
+
+
+
+
+ {{ property.btnBuy.text }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/property.vue
new file mode 100644
index 000000000..709d6063d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-combination/property.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/config.ts
new file mode 100644
index 000000000..8393f8ca2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/config.ts
@@ -0,0 +1,73 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 积分商城属性 */
+export interface PromotionPointProperty {
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列 | 三列
+ fields: {
+ introduction: PromotionPointFieldProperty; // 商品简介
+ marketPrice: PromotionPointFieldProperty; // 市场价
+ name: PromotionPointFieldProperty; // 商品名称
+ price: PromotionPointFieldProperty; // 商品价格
+ salesCount: PromotionPointFieldProperty; // 商品销量
+ stock: PromotionPointFieldProperty; // 商品库存
+ }; // 商品字段
+ badge: {
+ imgUrl: string; // 角标图片
+ show: boolean; // 是否显示
+ }; // 角标
+ // 按钮
+ btnBuy: {
+ bgBeginColor: string; // 文字按钮:背景渐变起始颜色
+ bgEndColor: string; // 文字按钮:背景渐变结束颜色
+ imgUrl: string; // 图片按钮:图片地址
+ text: string; // 文字
+ type: 'img' | 'text'; // 类型:文字 | 图片
+ }; // 按钮
+ borderRadiusTop: number; // 上圆角
+ borderRadiusBottom: number; // 下圆角
+ space: number; // 间距
+ activityIds: number[]; // 积分活动编号
+ style: ComponentStyle; // 组件样式
+}
+
+/** 商品字段属性 */
+export interface PromotionPointFieldProperty {
+ show: boolean; // 是否显示
+ color: string; // 颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'PromotionPoint',
+ name: '积分商城',
+ icon: 'ep:present',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' },
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '立即兑换',
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: '',
+ },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/index.vue
new file mode 100644
index 000000000..c8b01081a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/index.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ spu.name }}
+
+
+
+ {{ spu.introduction }}
+
+
+
+
+ {{ spu.point }}积分
+ {{
+ !spu.pointPrice || spu.pointPrice === 0
+ ? ''
+ : `+${fenToYuan(spu.pointPrice)}元`
+ }}
+
+
+
+ ¥{{ fenToYuan(spu.marketPrice) }}
+
+
+
+
+
+ 已兑{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}件
+
+
+
+ 库存{{ spu.pointTotalStock || 0 }}
+
+
+
+
+
+
+
+ {{ property.btnBuy.text }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/property.vue
new file mode 100644
index 000000000..4a729e956
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-point/property.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/config.ts
new file mode 100644
index 000000000..87feae63b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/config.ts
@@ -0,0 +1,72 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 秒杀属性 */
+export interface PromotionSeckillProperty {
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列 | 三列
+ fields: {
+ introduction: PromotionSeckillFieldProperty; // 商品简介
+ marketPrice: PromotionSeckillFieldProperty; // 市场价
+ name: PromotionSeckillFieldProperty; // 商品名称
+ price: PromotionSeckillFieldProperty; // 商品价格
+ salesCount: PromotionSeckillFieldProperty; // 商品销量
+ stock: PromotionSeckillFieldProperty; // 商品库存
+ }; // 商品字段
+ badge: {
+ imgUrl: string; // 角标图片
+ show: boolean; // 是否显示
+ }; // 角标
+ btnBuy: {
+ bgBeginColor: string; // 文字按钮:背景渐变起始颜色
+ bgEndColor: string; // 文字按钮:背景渐变结束颜色
+ imgUrl: string; // 图片按钮:图片地址
+ text: string; // 文字
+ type: 'img' | 'text'; // 类型:文字 | 图片
+ }; // 按钮
+ borderRadiusTop: number; // 上圆角
+ borderRadiusBottom: number; // 下圆角
+ space: number; // 间距
+ activityIds: number[]; // 秒杀活动编号
+ style: ComponentStyle; // 组件样式
+}
+
+/** 商品字段属性 */
+export interface PromotionSeckillFieldProperty {
+ show: boolean; // 是否显示
+ color: string; // 颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'PromotionSeckill',
+ name: '秒杀',
+ icon: 'mdi:calendar-time',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' },
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '立即秒杀',
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: '',
+ },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/index.vue
new file mode 100644
index 000000000..dc4e64b30
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/index.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ spu.name }}
+
+
+
+ {{ spu.introduction }}
+
+
+
+
+ ¥{{ fenToYuan(spu.price || Infinity) }}
+
+
+
+ ¥{{ fenToYuan(spu.marketPrice) }}
+
+
+
+
+
+ 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
+
+
+
+ 库存{{ spu.stock || 0 }}
+
+
+
+
+
+
+
+ {{ property.btnBuy.text }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/property.vue
new file mode 100644
index 000000000..0ab48b76d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/promotion-seckill/property.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/config.ts
new file mode 100644
index 000000000..a3c39aab7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/config.ts
@@ -0,0 +1,43 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 搜索框属性 */
+export interface SearchProperty {
+ height: number; // 搜索栏高度
+ showScan: boolean; // 显示扫一扫
+ borderRadius: number; // 框体样式
+ placeholder: string; // 占位文字
+ placeholderPosition: PlaceholderPosition; // 占位文字位置
+ backgroundColor: string; // 框体颜色
+ textColor: string; // 字体颜色
+ hotKeywords: string[]; // 热词
+ style: ComponentStyle;
+}
+
+/** 文字位置 */
+export type PlaceholderPosition = 'center' | 'left';
+
+/** 定义组件 */
+export const component = {
+ id: 'SearchBar',
+ name: '搜索框',
+ icon: 'lucide:search',
+ property: {
+ height: 28,
+ showScan: false,
+ borderRadius: 0,
+ placeholder: '搜索商品',
+ placeholderPosition: 'left',
+ backgroundColor: 'rgb(238, 238, 238)',
+ textColor: 'rgb(150, 151, 153)',
+ hotKeywords: [],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ paddingTop: 8,
+ paddingRight: 8,
+ paddingBottom: 8,
+ paddingLeft: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/index.vue
new file mode 100644
index 000000000..8d44dd79e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/index.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ {{ property.placeholder || '搜索商品' }}
+
+
+
+
+ {{ keyword }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/property.vue
new file mode 100644
index 000000000..82dc8e6a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/search-bar/property.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/config.ts
new file mode 100644
index 000000000..efdb4da21
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/config.ts
@@ -0,0 +1,160 @@
+import type { DiyComponent } from '../../../util';
+
+/** 底部导航菜单属性 */
+export interface TabBarProperty {
+ items: TabBarItemProperty[]; // 选项列表
+ theme: string; // 主题
+ style: TabBarStyle; // 样式
+}
+
+/** 选项属性 */
+export interface TabBarItemProperty {
+ text: string; // 标签文字
+ url: string; // 链接
+ iconUrl: string; // 默认图标链接
+ activeIconUrl: string; // 选中的图标链接
+}
+
+/** 样式 */
+export interface TabBarStyle {
+ bgType: 'color' | 'img'; // 背景类型
+ bgColor: string; // 背景颜色
+ bgImg: string; // 图片链接
+ color: string; // 默认颜色
+ activeColor: string; // 选中的颜色
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'TabBar',
+ name: '底部导航',
+ icon: 'fluent:table-bottom-row-16-filled',
+ property: {
+ theme: 'red',
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ color: '#282828',
+ activeColor: '#fc4141',
+ },
+ items: [
+ {
+ text: '首页',
+ url: '/pages/index/index',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png',
+ },
+ {
+ text: '分类',
+ url: '/pages/index/category?id=3',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png',
+ },
+ {
+ text: '购物车',
+ url: '/pages/index/cart',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png',
+ },
+ {
+ text: '我的',
+ url: '/pages/index/user',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png',
+ },
+ ],
+ },
+} as DiyComponent;
+
+export const THEME_LIST = [
+ {
+ id: 'red',
+ name: '中国红',
+ icon: 'icon-park-twotone:theme',
+ color: '#d10019',
+ },
+ {
+ id: 'orange',
+ name: '桔橙',
+ icon: 'icon-park-twotone:theme',
+ color: '#f37b1d',
+ },
+ {
+ id: 'gold',
+ name: '明黄',
+ icon: 'icon-park-twotone:theme',
+ color: '#fbbd08',
+ },
+ {
+ id: 'green',
+ name: '橄榄绿',
+ icon: 'icon-park-twotone:theme',
+ color: '#8dc63f',
+ },
+ {
+ id: 'cyan',
+ name: '天青',
+ icon: 'icon-park-twotone:theme',
+ color: '#1cbbb4',
+ },
+ {
+ id: 'blue',
+ name: '海蓝',
+ icon: 'icon-park-twotone:theme',
+ color: '#0081ff',
+ },
+ {
+ id: 'purple',
+ name: '姹紫',
+ icon: 'icon-park-twotone:theme',
+ color: '#6739b6',
+ },
+ {
+ id: 'brightRed',
+ name: '嫣红',
+ icon: 'icon-park-twotone:theme',
+ color: '#e54d42',
+ },
+ {
+ id: 'forestGreen',
+ name: '森绿',
+ icon: 'icon-park-twotone:theme',
+ color: '#39b54a',
+ },
+ {
+ id: 'mauve',
+ name: '木槿',
+ icon: 'icon-park-twotone:theme',
+ color: '#9c26b0',
+ },
+ {
+ id: 'pink',
+ name: '桃粉',
+ icon: 'icon-park-twotone:theme',
+ color: '#e03997',
+ },
+ {
+ id: 'brown',
+ name: '棕褐',
+ icon: 'icon-park-twotone:theme',
+ color: '#a5673f',
+ },
+ {
+ id: 'grey',
+ name: '玄灰',
+ icon: 'icon-park-twotone:theme',
+ color: '#8799a3',
+ },
+ {
+ id: 'gray',
+ name: '草灰',
+ icon: 'icon-park-twotone:theme',
+ color: '#aaaaaa',
+ },
+ {
+ id: 'black',
+ name: '墨黑',
+ icon: 'icon-park-twotone:theme',
+ color: '#333333',
+ },
+];
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/index.vue
new file mode 100644
index 000000000..0d2466917
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/index.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/property.vue
new file mode 100644
index 000000000..6248ddc51
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/tab-bar/property.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/config.ts
new file mode 100644
index 000000000..2ef0eb772
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/config.ts
@@ -0,0 +1,55 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 标题栏属性 */
+export interface TitleBarProperty {
+ bgImgUrl: string; // 背景图
+ marginLeft: number; // 偏移
+ textAlign: 'center' | 'left'; // 显示位置
+ title: string; // 主标题
+ description: string; // 副标题
+ titleSize: number; // 标题大小
+ descriptionSize: number; // 描述大小
+ titleWeight: number; // 标题粗细
+ descriptionWeight: number; // 描述粗细
+ titleColor: string; // 标题颜色
+ descriptionColor: string; // 描述颜色
+ height: number; // 高度
+ more: {
+ show: false; // 是否显示查看更多
+ text: string; // 自定义文字
+ type: 'all' | 'icon' | 'text'; // 样式选择
+ url: string; // 链接
+ }; // 查看更多
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'TitleBar',
+ name: '标题栏',
+ icon: 'material-symbols:line-start',
+ property: {
+ title: '主标题',
+ description: '副标题',
+ titleSize: 16,
+ descriptionSize: 12,
+ titleWeight: 400,
+ textAlign: 'left',
+ descriptionWeight: 200,
+ titleColor: 'rgba(50, 50, 51, 10)',
+ descriptionColor: 'rgba(150, 151, 153, 10)',
+ marginLeft: 0,
+ height: 40,
+ more: {
+ // 查看更多
+ show: false,
+ type: 'icon',
+ text: '查看更多',
+ url: '',
+ },
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/index.vue
new file mode 100644
index 000000000..cf0fee121
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/index.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+ {{ property.title }}
+
+
+
+ {{ property.description }}
+
+
+
+
+
+ {{ property.more.text }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/property.vue
new file mode 100644
index 000000000..4dc06495c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/title-bar/property.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/config.ts
new file mode 100644
index 000000000..568cfb94f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/config.ts
@@ -0,0 +1,20 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 用户卡片属性 */
+export interface UserCardProperty {
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'UserCard',
+ name: '用户卡片',
+ icon: 'mdi:user-card-details',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/index.vue
new file mode 100644
index 000000000..91b13cc77
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+ 点击绑定手机号
+
+ 去绑定
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/property.vue
new file mode 100644
index 000000000..46a0e16c5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-card/property.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/config.ts
new file mode 100644
index 000000000..4666980ea
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/config.ts
@@ -0,0 +1,22 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 用户卡券属性 */
+export interface UserCouponProperty {
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'UserCoupon',
+ name: '用户卡券',
+ icon: 'lucide:ticket',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/index.vue
new file mode 100644
index 000000000..a990140e9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/index.vue
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/property.vue
new file mode 100644
index 000000000..9a5633b36
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-coupon/property.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/config.ts
new file mode 100644
index 000000000..fcc4a6b53
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/config.ts
@@ -0,0 +1,22 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 用户订单属性 */
+export interface UserOrderProperty {
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'UserOrder',
+ name: '用户订单',
+ icon: 'lucide:clipboard-list',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/index.vue
new file mode 100644
index 000000000..af6f888c7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/index.vue
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/property.vue
new file mode 100644
index 000000000..e670a1e44
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-order/property.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/config.ts
new file mode 100644
index 000000000..ab48845d7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/config.ts
@@ -0,0 +1,22 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 用户资产属性 */
+export interface UserWalletProperty {
+ style: ComponentStyle; // 组件样式
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'UserWallet',
+ name: '用户资产',
+ icon: 'lucide:wallet',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8,
+ } as ComponentStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/index.vue
new file mode 100644
index 000000000..8104d8fe6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/index.vue
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/property.vue
new file mode 100644
index 000000000..fc7adf438
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/user-wallet/property.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/config.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/config.ts
new file mode 100644
index 000000000..db001525a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/config.ts
@@ -0,0 +1,32 @@
+import type { ComponentStyle, DiyComponent } from '../../../util';
+
+/** 视频播放属性 */
+export interface VideoPlayerProperty {
+ videoUrl: string; // 视频链接
+ posterUrl: string; // 封面链接
+ autoplay: boolean; // 是否自动播放
+ style: VideoPlayerStyle; // 组件样式
+}
+
+/** 视频播放样式 */
+export interface VideoPlayerStyle extends ComponentStyle {
+ height: number; // 视频高度
+}
+
+/** 定义组件 */
+export const component = {
+ id: 'VideoPlayer',
+ name: '视频播放',
+ icon: 'lucide:video',
+ property: {
+ videoUrl: '',
+ posterUrl: '',
+ autoplay: false,
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ height: 300,
+ } as VideoPlayerStyle,
+ },
+} as DiyComponent;
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/index.vue
new file mode 100644
index 000000000..08118a30b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/index.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/property.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/property.vue
new file mode 100644
index 000000000..ba4896181
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/components/mobile/video-player/property.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/index.vue
new file mode 100644
index 000000000..e420c7837
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/index.vue
@@ -0,0 +1,510 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleMoveComponent(index, direction)
+ "
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pageConfigComponent.name }}
+
+
+
+
+ {{ component.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedComponent?.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/util.ts b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/util.ts
new file mode 100644
index 000000000..8926ca581
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/diy-editor/util.ts
@@ -0,0 +1,115 @@
+import type { NavigationBarProperty } from './components/mobile/navigation-bar/config';
+import type { PageConfigProperty } from './components/mobile/page-config/config';
+import type { TabBarProperty } from './components/mobile/tab-bar/config';
+
+/** 页面装修组件 */
+export interface DiyComponent {
+ uid?: number; // 用于区分同一种组件的不同实例
+ id: string; // 组件唯一标识
+ name: string; // 组件名称
+ icon: string; // 组件图标
+ /*
+ 组件位置:
+ top: 固定于手机顶部,例如 顶部的导航栏
+ bottom: 固定于手机底部,例如 底部的菜单导航栏
+ center: 位于手机中心,每个组件占一行,顺序向下排列
+ 空:同 center
+ fixed: 由组件自己决定位置,如弹窗位于手机中心、浮动按钮一般位于手机右下角
+ */
+ position?: '' | 'bottom' | 'center' | 'fixed' | 'top';
+ property: T; // 组件属性
+}
+
+/** 页面装修组件库 */
+export interface DiyComponentLibrary {
+ name: string; // 组件库名称
+ extended: boolean; // 是否展开
+ components: string[]; // 组件列表
+}
+
+/** 组件样式 */
+export interface ComponentStyle {
+ bgType: 'color' | 'img'; // 背景类型
+ bgColor: string; // 背景颜色
+ bgImg: string; // 背景图片
+ // 外边距
+ margin: number;
+ marginTop: number;
+ marginRight: number;
+ marginBottom: number;
+ marginLeft: number;
+ // 内边距
+ padding: number;
+ paddingTop: number;
+ paddingRight: number;
+ paddingBottom: number;
+ paddingLeft: number;
+ // 边框圆角
+ borderRadius: number;
+ borderTopLeftRadius: number;
+ borderTopRightRadius: number;
+ borderBottomRightRadius: number;
+ borderBottomLeftRadius: number;
+}
+
+/** 页面配置 */
+export interface PageConfig {
+ page: PageConfigProperty; // 页面属性
+ navigationBar: NavigationBarProperty; // 顶部导航栏属性
+ tabBar?: TabBarProperty; // 底部导航菜单属性
+
+ components: PageComponent[]; // 页面组件列表
+}
+
+export type PageComponent = Pick, 'id' | 'property'>; // 页面组件,只保留组件 ID,组件属性
+
+/** 页面组件库 */
+export const PAGE_LIBS = [
+ {
+ name: '基础组件',
+ extended: true,
+ components: [
+ 'SearchBar',
+ 'NoticeBar',
+ 'MenuSwiper',
+ 'MenuGrid',
+ 'MenuList',
+ 'Popover',
+ 'FloatingActionButton',
+ ],
+ },
+ {
+ name: '图文组件',
+ extended: true,
+ components: [
+ 'ImageBar',
+ 'Carousel',
+ 'TitleBar',
+ 'VideoPlayer',
+ 'Divider',
+ 'MagicCube',
+ 'HotZone',
+ ],
+ },
+ {
+ name: '商品组件',
+ extended: true,
+ components: ['ProductCard', 'ProductList'],
+ },
+ {
+ name: '用户组件',
+ extended: true,
+ components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon'],
+ },
+ {
+ name: '营销组件',
+ extended: true,
+ components: [
+ 'PromotionCombination',
+ 'PromotionSeckill',
+ 'PromotionPoint',
+ 'CouponCard',
+ 'PromotionArticle',
+ ],
+ },
+] as DiyComponentLibrary[];
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/draggable/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/draggable/index.vue
new file mode 100644
index 000000000..3bd6a3d58
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/draggable/index.vue
@@ -0,0 +1,97 @@
+
+
+
+ 拖动左上角的小圆点可对其排序
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/index.ts b/apps/web-antdv-next/src/views/mall/promotion/components/index.ts
new file mode 100644
index 000000000..6dbe001fe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/index.ts
@@ -0,0 +1,10 @@
+export { default as AppLinkInput } from './app-link-input/index.vue';
+export { default as AppLinkSelectDialog } from './app-link-input/select-dialog.vue';
+export { default as ColorInput } from './color-input/index.vue';
+export { default as DiyEditor } from './diy-editor/index.vue';
+export { type DiyComponentLibrary, PAGE_LIBS } from './diy-editor/util';
+export { default as Draggable } from './draggable/index.vue';
+export { default as InputWithColor } from './input-with-color/index.vue';
+
+export { default as MagicCubeEditor } from './magic-cube-editor/index.vue';
+export { default as VerticalButtonGroup } from './vertical-button-group/index.vue';
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/input-with-color/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/input-with-color/index.vue
new file mode 100644
index 000000000..1671318ec
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/input-with-color/index.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/magic-cube-editor/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/magic-cube-editor/index.vue
new file mode 100644
index 000000000..96e7e053b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/magic-cube-editor/index.vue
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
+
+
+
+
+ {{ `${hotArea.width}×${hotArea.height}` }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/magic-cube-editor/util.ts b/apps/web-antdv-next/src/views/mall/promotion/components/magic-cube-editor/util.ts
new file mode 100644
index 000000000..d55cd4b65
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/magic-cube-editor/util.ts
@@ -0,0 +1,71 @@
+/** 坐标点 */
+export interface Point {
+ x: number;
+ y: number;
+}
+
+/** 矩形 */
+export interface Rect {
+ left: number; // 左上角 X 轴坐标
+ top: number; // 左上角 Y 轴坐标
+ right: number; // 右下角 X 轴坐标
+ bottom: number; // 右下角 Y 轴坐标
+ width: number; // 矩形宽度
+ height: number; // 矩形高度
+}
+
+/**
+ * 判断两个矩形是否重叠
+ *
+ * @param a 矩形 A
+ * @param b 矩形 B
+ */
+export function isOverlap(a: Rect, b: Rect): boolean {
+ return (
+ a.left < b.left + b.width &&
+ a.left + a.width > b.left &&
+ a.top < b.top + b.height &&
+ a.height + a.top > b.top
+ );
+}
+
+/**
+ * 检查坐标点是否在矩形内
+ * @param hotArea 矩形
+ * @param point 坐标
+ */
+export function isContains(hotArea: Rect, point: Point): boolean {
+ return (
+ point.x >= hotArea.left &&
+ point.x < hotArea.right &&
+ point.y >= hotArea.top &&
+ point.y < hotArea.bottom
+ );
+}
+
+/**
+ * 在两个坐标点中间,创建一个矩形
+ *
+ * 存在以下情况:
+ * 1. 两个坐标点是同一个位置,只占一个位置的正方形,宽高都为 1
+ * 2. X 轴坐标相同,只占一行的矩形,高度为 1
+ * 3. Y 轴坐标相同,只占一列的矩形,宽度为 1
+ * 4. 多行多列的矩形
+ *
+ * @param a 坐标点一
+ * @param b 坐标点二
+ */
+export function createRect(a: Point, b: Point): Rect {
+ // 计算矩形的范围
+ let [left, left2] = [a.x, b.x].toSorted();
+ left = left ?? 0;
+ left2 = left2 ?? 0;
+ let [top, top2] = [a.y, b.y].toSorted();
+ top = top ?? 0;
+ top2 = top2 ?? 0;
+ const right = left2 + 1;
+ const bottom = top2 + 1;
+ const height = bottom - top;
+ const width = right - left;
+ return { left, right, top, bottom, height, width };
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/components/vertical-button-group/index.vue b/apps/web-antdv-next/src/views/mall/promotion/components/vertical-button-group/index.vue
new file mode 100644
index 000000000..2f865b3da
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/components/vertical-button-group/index.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/components/index.ts b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/index.ts
new file mode 100644
index 000000000..24cb4e274
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/index.ts
@@ -0,0 +1,2 @@
+export { default as CouponSelect } from './select.vue';
+export { default as CouponSendForm } from './send-form.vue';
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/components/select-data.ts b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/select-data.ts
new file mode 100644
index 000000000..e8c0ad53e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/select-data.ts
@@ -0,0 +1,119 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import {
+ discountFormat,
+ remainedCountFormat,
+ takeLimitCountFormat,
+ validityTypeFormat,
+} from '../formatter';
+
+/** 优惠券选择的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '优惠券名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入优惠券名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'discountType',
+ label: '优惠类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'),
+ placeholder: '请选择优惠类型',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 优惠券选择的表格列 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 55 },
+ {
+ field: 'name',
+ title: '优惠券名称',
+ minWidth: 140,
+ },
+ {
+ field: 'productScope',
+ title: '类型',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
+ },
+ },
+ {
+ field: 'discountType',
+ title: '优惠类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
+ },
+ },
+ {
+ field: 'discountPrice',
+ title: '优惠力度',
+ minWidth: 100,
+ formatter: ({ row }) => discountFormat(row),
+ },
+ {
+ field: 'takeType',
+ title: '领取方式',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
+ },
+ },
+ {
+ field: 'validityType',
+ title: '使用时间',
+ minWidth: 185,
+ align: 'center',
+ formatter: ({ row }) => validityTypeFormat(row),
+ },
+ {
+ field: 'totalCount',
+ title: '发放数量',
+ minWidth: 100,
+ align: 'center',
+ },
+ {
+ field: 'remainedCount',
+ title: '剩余数量',
+ minWidth: 100,
+ align: 'center',
+ formatter: ({ row }) => remainedCountFormat(row),
+ },
+ {
+ field: 'takeLimitCount',
+ title: '领取上限',
+ minWidth: 100,
+ align: 'center',
+ formatter: ({ row }) => takeLimitCountFormat(row),
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 80,
+ align: 'center',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/components/select.vue b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/select.vue
new file mode 100644
index 000000000..94d79a0bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/select.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/components/send-form-data.ts b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/send-form-data.ts
new file mode 100644
index 000000000..35a823b79
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/send-form-data.ts
@@ -0,0 +1,64 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridProps } from '#/adapter/vxe-table';
+
+import {
+ discountFormat,
+ remainedCountFormat,
+ usePriceFormat,
+ validityTypeFormat,
+} from '../formatter';
+
+/** 搜索表单的 schema */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '优惠券名称',
+ componentProps: {
+ placeholder: '请输入优惠券名称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridProps['columns'] {
+ return [
+ {
+ title: '优惠券名称',
+ field: 'name',
+ minWidth: 120,
+ },
+ {
+ title: '优惠金额 / 折扣',
+ field: 'discount',
+ minWidth: 120,
+ formatter: ({ row }) => discountFormat(row),
+ },
+ {
+ title: '最低消费',
+ field: 'usePrice',
+ minWidth: 100,
+ formatter: ({ row }) => usePriceFormat(row),
+ },
+ {
+ title: '有效期限',
+ field: 'validityType',
+ minWidth: 140,
+ formatter: ({ row }) => validityTypeFormat(row),
+ },
+ {
+ title: '剩余数量',
+ minWidth: 100,
+ formatter: ({ row }) => remainedCountFormat(row),
+ },
+ {
+ title: '操作',
+ width: 100,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/components/send-form.vue b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/send-form.vue
new file mode 100644
index 000000000..f858cc75f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/components/send-form.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/data.ts b/apps/web-antdv-next/src/views/mall/promotion/coupon/data.ts
new file mode 100644
index 000000000..6f30c5597
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/data.ts
@@ -0,0 +1,111 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+import { discountFormat } from './formatter';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'nickname',
+ label: '会员昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入会员昵称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '领取时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'nickname',
+ title: '会员昵称',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '优惠券名称',
+ minWidth: 140,
+ },
+ {
+ field: 'productScope',
+ title: '类型',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
+ },
+ },
+ {
+ field: 'discountType',
+ title: '优惠',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
+ },
+ },
+ {
+ field: 'discountPrice',
+ title: '优惠力度',
+ minWidth: 110,
+ formatter: ({ row }) => {
+ return discountFormat(row);
+ },
+ },
+ {
+ field: 'takeType',
+ title: '领取方式',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_COUPON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '领取时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'useTime',
+ title: '使用时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ width: 100,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/formatter.ts b/apps/web-antdv-next/src/views/mall/promotion/coupon/formatter.ts
new file mode 100644
index 000000000..2f34863ff
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/formatter.ts
@@ -0,0 +1,64 @@
+import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
+
+import {
+ CouponTemplateValidityTypeEnum,
+ PromotionDiscountTypeEnum,
+} from '@vben/constants';
+import { floatToFixed2, formatDate } from '@vben/utils';
+
+/** 格式化【优惠金额/折扣】 */
+export function discountFormat(row: MallCouponTemplateApi.CouponTemplate) {
+ if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+ return `¥${floatToFixed2(row.discountPrice)}`;
+ }
+ if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
+ return `${row.discountPercent}%`;
+ }
+ return `未知【${row.discountType}】`;
+}
+
+/** 格式化【领取上限】 */
+export function takeLimitCountFormat(
+ row: MallCouponTemplateApi.CouponTemplate,
+) {
+ if (row.takeLimitCount) {
+ if (row.takeLimitCount === -1) {
+ return '无领取限制';
+ }
+ return `${row.takeLimitCount} 张/人`;
+ } else {
+ return ' ';
+ }
+}
+
+/** 格式化【有效期限】 */
+export function validityTypeFormat(row: MallCouponTemplateApi.CouponTemplate) {
+ if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
+ return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`;
+ }
+ if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
+ return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`;
+ }
+ return `未知【${row.validityType}】`;
+}
+
+/** 格式化【totalCount】 */
+export function totalCountFormat(row: MallCouponTemplateApi.CouponTemplate) {
+ if (row.totalCount === -1) {
+ return '不限制';
+ }
+ return row.totalCount;
+}
+
+/** 格式化【剩余数量】 */
+export function remainedCountFormat(row: MallCouponTemplateApi.CouponTemplate) {
+ if (row.totalCount === -1) {
+ return '不限制';
+ }
+ return row.totalCount - row.takeCount;
+}
+
+/** 格式化【最低消费】 */
+export function usePriceFormat(row: MallCouponTemplateApi.CouponTemplate) {
+ return `¥${floatToFixed2(row.usePrice)}`;
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/index.vue b/apps/web-antdv-next/src/views/mall/promotion/coupon/index.vue
new file mode 100644
index 000000000..5a442b1de
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/index.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/template/data.ts b/apps/web-antdv-next/src/views/mall/promotion/coupon/template/data.ts
new file mode 100644
index 000000000..11f86c422
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/template/data.ts
@@ -0,0 +1,463 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
+
+import {
+ CommonStatusEnum,
+ CouponTemplateTakeTypeEnum,
+ CouponTemplateValidityTypeEnum,
+ DICT_TYPE,
+ PromotionDiscountTypeEnum,
+ PromotionProductScopeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+import {
+ discountFormat,
+ remainedCountFormat,
+ takeLimitCountFormat,
+ totalCountFormat,
+ validityTypeFormat,
+} from '../formatter';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '优惠券名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入优惠券名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '优惠券描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入优惠券描述',
+ },
+ },
+ {
+ fieldName: 'productScope',
+ label: '优惠劵类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'),
+ },
+ rules: 'required',
+ defaultValue: PromotionProductScopeEnum.ALL.scope,
+ },
+ {
+ fieldName: 'productSpuIds',
+ label: '商品',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['productScope', 'productScopeValues'],
+ show: (model) =>
+ model.productScope === PromotionProductScopeEnum.SPU.scope,
+ trigger(values, form) {
+ // 当加载已有数据时,根据 productScopeValues 设置 productSpuIds
+ if (
+ values.productScope === PromotionProductScopeEnum.SPU.scope &&
+ values.productScopeValues
+ ) {
+ form.setFieldValue('productSpuIds', values.productScopeValues);
+ }
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productCategoryIds',
+ label: '商品分类',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['productScope', 'productScopeValues'],
+ show: (model) =>
+ model.productScope === PromotionProductScopeEnum.CATEGORY.scope,
+ trigger(values, form) {
+ // 当加载已有数据时,根据 productScopeValues 设置 productCategoryIds
+ if (
+ values.productScope === PromotionProductScopeEnum.CATEGORY.scope &&
+ values.productScopeValues
+ ) {
+ const categoryIds = values.productScopeValues;
+ // 单选时使用数组不能反显,取第一个元素
+ form.setFieldValue(
+ 'productCategoryIds',
+ Array.isArray(categoryIds) && categoryIds.length > 0
+ ? categoryIds[0]
+ : categoryIds,
+ );
+ }
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'discountType',
+ label: '优惠类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'),
+ },
+ rules: 'required',
+ defaultValue: PromotionDiscountTypeEnum.PRICE.type,
+ },
+ {
+ fieldName: 'discountPrice',
+ label: '优惠券面额',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入优惠金额,单位:元',
+ addonAfter: '元',
+ },
+ dependencies: {
+ triggerFields: ['discountType'],
+ show: (model) =>
+ model.discountType === PromotionDiscountTypeEnum.PRICE.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '优惠券折扣',
+ component: 'InputNumber',
+ componentProps: {
+ min: 1,
+ max: 9.9,
+ precision: 1,
+ placeholder: '优惠券折扣不能小于 1 折,且不可大于 9.9 折',
+ addonAfter: '折',
+ },
+ dependencies: {
+ triggerFields: ['discountType'],
+ show: (model) =>
+ model.discountType === PromotionDiscountTypeEnum.PERCENT.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'discountLimitPrice',
+ label: '最多优惠',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入最多优惠',
+ addonAfter: '元',
+ },
+ dependencies: {
+ triggerFields: ['discountType'],
+ show: (model) =>
+ model.discountType === PromotionDiscountTypeEnum.PERCENT.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'usePrice',
+ label: '满多少元可以使用',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '无门槛请设为 0',
+ addonAfter: '元',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'takeType',
+ label: '领取方式',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE, 'number'),
+ },
+ rules: 'required',
+ defaultValue: CouponTemplateTakeTypeEnum.USER.type,
+ },
+ {
+ fieldName: 'totalCount',
+ label: '发放数量',
+ component: 'InputNumber',
+ componentProps: {
+ min: -1,
+ placeholder: '发放数量,没有之后不能领取或发放,-1 为不限制',
+ addonAfter: '张',
+ },
+ dependencies: {
+ triggerFields: ['takeType'],
+ show: (model) =>
+ model.takeType === CouponTemplateTakeTypeEnum.USER.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'takeLimitCount',
+ label: '每人限领个数',
+ component: 'InputNumber',
+ componentProps: {
+ min: -1,
+ placeholder: '设置为 -1 时,可无限领取',
+ addonAfter: '张',
+ },
+ dependencies: {
+ triggerFields: ['takeType'],
+ show: (model) => model.takeType === 1,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'validityType',
+ label: '有效期类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE,
+ 'number',
+ ),
+ },
+ defaultValue: CouponTemplateValidityTypeEnum.DATE.type,
+ rules: 'required',
+ },
+ {
+ fieldName: 'validTimes',
+ label: '固定日期',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ valueFormat: 'x',
+ },
+ dependencies: {
+ triggerFields: ['validityType'],
+ show: (model) =>
+ model.validityType === CouponTemplateValidityTypeEnum.DATE.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'fixedStartTerm',
+ label: '领取日期',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '第 0 为今天生效',
+ addonBefore: '第',
+ addonAfter: '天',
+ },
+ dependencies: {
+ triggerFields: ['validityType'],
+ show: (model) =>
+ model.validityType === CouponTemplateValidityTypeEnum.TERM.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'fixedEndTerm',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入结束天数',
+ addonBefore: '至',
+ addonAfter: '天有效',
+ },
+ dependencies: {
+ triggerFields: ['validityType'],
+ show: (model) =>
+ model.validityType === CouponTemplateValidityTypeEnum.TERM.type,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productScopeValues',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['productScope', 'productSpuIds', 'productCategoryIds'],
+ show: () => false,
+ trigger(values, form) {
+ switch (values.productScope) {
+ case PromotionProductScopeEnum.CATEGORY.scope: {
+ const categoryIds = Array.isArray(values.productCategoryIds)
+ ? values.productCategoryIds
+ : [values.productCategoryIds];
+ form.setFieldValue('productScopeValues', categoryIds);
+ break;
+ }
+ case PromotionProductScopeEnum.SPU.scope: {
+ form.setFieldValue('productScopeValues', values.productSpuIds);
+ break;
+ }
+ }
+ },
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '优惠券名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入优惠劵名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'discountType',
+ label: '优惠类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择优惠类型',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '优惠券状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择优惠券状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: MallCouponTemplateApi.CouponTemplate,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '优惠券名称',
+ minWidth: 140,
+ },
+ {
+ field: 'productScope',
+ title: '类型',
+ minWidth: 130,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
+ },
+ },
+ {
+ field: 'discountType',
+ title: '优惠',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
+ },
+ },
+ {
+ field: 'discountPrice',
+ title: '优惠力度',
+ minWidth: 110,
+ formatter: ({ row }) => {
+ return discountFormat(row);
+ },
+ },
+ {
+ field: 'takeType',
+ title: '领取方式',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
+ },
+ },
+ {
+ field: 'validityType',
+ title: '使用时间',
+ minWidth: 180,
+ formatter: ({ row }) => {
+ return validityTypeFormat(row);
+ },
+ },
+ {
+ field: 'totalCount',
+ title: '发放数量',
+ minWidth: 100,
+ formatter: ({ row }) => {
+ return totalCountFormat(row);
+ },
+ },
+ {
+ field: 'remainedCount',
+ title: '剩余数量',
+ minWidth: 100,
+ formatter: ({ row }) => {
+ return remainedCountFormat(row);
+ },
+ },
+ {
+ field: 'takeLimitCount',
+ title: '领取上限',
+ minWidth: 100,
+ formatter: ({ row }) => {
+ return takeLimitCountFormat(row);
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: CommonStatusEnum.ENABLE,
+ unCheckedValue: CommonStatusEnum.DISABLE,
+ },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/template/index.vue b/apps/web-antdv-next/src/views/mall/promotion/coupon/template/index.vue
new file mode 100644
index 000000000..050a4fb6b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/template/index.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/coupon/template/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/coupon/template/modules/form.vue
new file mode 100644
index 000000000..23bc87900
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/coupon/template/modules/form.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/discountActivity/data.ts b/apps/web-antdv-next/src/views/mall/promotion/discountActivity/data.ts
new file mode 100644
index 000000000..db639dcb2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/discountActivity/data.ts
@@ -0,0 +1,165 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDate } from '@vben/utils';
+
+/** 表单配置 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择活动状态',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'startTime',
+ label: '开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择开始时间',
+ showTime: false,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择结束时间',
+ showTime: false,
+ valueFormat: 'x',
+ format: 'YYYY-MM-DD',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'spuIds',
+ label: '活动商品',
+ component: 'Input',
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择活动状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'activeTime',
+ label: '活动时间',
+ component: 'RangePicker',
+ componentProps: {
+ placeholder: ['开始时间', '结束时间'],
+ allowClear: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '活动编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '活动名称',
+ minWidth: 140,
+ },
+ {
+ field: 'activityTime',
+ title: '活动时间',
+ minWidth: 210,
+ formatter: ({ row }) => {
+ if (!row.startTime || !row.endTime) return '';
+ return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
+ },
+ },
+ {
+ field: 'status',
+ title: '活动状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/discountActivity/index.vue b/apps/web-antdv-next/src/views/mall/promotion/discountActivity/index.vue
new file mode 100644
index 000000000..48e45c8e9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/discountActivity/index.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/discountActivity/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/discountActivity/modules/form.vue
new file mode 100644
index 000000000..a0eff3e1d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/discountActivity/modules/form.vue
@@ -0,0 +1,364 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/page/data.ts b/apps/web-antdv-next/src/views/mall/promotion/diy/page/data.ts
new file mode 100644
index 000000000..cec26ffcf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/page/data.ts
@@ -0,0 +1,110 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单配置 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '页面名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入页面名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ },
+ {
+ fieldName: 'previewPicUrls',
+ component: 'ImageUpload',
+ label: '预览图',
+ componentProps: {
+ maxNumber: 10,
+ multiple: true,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '页面名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入页面名称',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 80,
+ },
+ {
+ field: 'previewPicUrls',
+ title: '预览图',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImages',
+ },
+ },
+ {
+ field: 'name',
+ title: '页面名称',
+ minWidth: 150,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/page/decorate/index.vue b/apps/web-antdv-next/src/views/mall/promotion/diy/page/decorate/index.vue
new file mode 100644
index 000000000..322ad56c8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/page/decorate/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/page/index.vue b/apps/web-antdv-next/src/views/mall/promotion/diy/page/index.vue
new file mode 100644
index 000000000..c1af949b1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/page/index.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/page/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/diy/page/modules/form.vue
new file mode 100644
index 000000000..b3e383e83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/page/modules/form.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/template/data.ts b/apps/web-antdv-next/src/views/mall/promotion/diy/template/data.ts
new file mode 100644
index 000000000..e3c8f1421
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/template/data.ts
@@ -0,0 +1,121 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 表单配置 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ },
+ {
+ fieldName: 'previewPicUrls',
+ component: 'ImageUpload',
+ label: '预览图',
+ componentProps: {
+ maxNumber: 10,
+ multiple: true,
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ clearable: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 80,
+ },
+ {
+ field: 'previewPicUrls',
+ title: '预览图',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImages',
+ },
+ },
+ {
+ field: 'name',
+ title: '模板名称',
+ minWidth: 150,
+ },
+ {
+ field: 'used',
+ title: '是否使用',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 250,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/template/decorate/index.vue b/apps/web-antdv-next/src/views/mall/promotion/diy/template/decorate/index.vue
new file mode 100644
index 000000000..fc188072a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/template/decorate/index.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/template/index.vue b/apps/web-antdv-next/src/views/mall/promotion/diy/template/index.vue
new file mode 100644
index 000000000..ec43bcea0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/template/index.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/diy/template/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/diy/template/modules/form.vue
new file mode 100644
index 000000000..339c949e9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/diy/template/modules/form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/a.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/a.png
new file mode 100644
index 000000000..32939004d
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/a.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/aini.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/aini.png
new file mode 100644
index 000000000..02cf5c498
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/aini.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/aixin.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/aixin.png
new file mode 100644
index 000000000..25e642234
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/aixin.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/baiyan.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/baiyan.png
new file mode 100644
index 000000000..d16260afd
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/baiyan.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/bizui.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/bizui.png
new file mode 100644
index 000000000..a3b18002e
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/bizui.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/buhaoyisi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/buhaoyisi.png
new file mode 100644
index 000000000..54c4b3f71
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/buhaoyisi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/bukesiyi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/bukesiyi.png
new file mode 100644
index 000000000..5f272e3e4
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/bukesiyi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/dajing.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/dajing.png
new file mode 100644
index 000000000..8649727ec
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/dajing.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/danao.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/danao.png
new file mode 100644
index 000000000..aa85a2947
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/danao.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/daxiao.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/daxiao.png
new file mode 100644
index 000000000..26206bc05
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/daxiao.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/dianzan.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/dianzan.png
new file mode 100644
index 000000000..2e7f00eba
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/dianzan.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/emo.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/emo.png
new file mode 100644
index 000000000..9c8455165
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/emo.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/esi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/esi.png
new file mode 100644
index 000000000..84e9726f1
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/esi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fadai.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fadai.png
new file mode 100644
index 000000000..0772de262
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fadai.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fankun.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fankun.png
new file mode 100644
index 000000000..6e18dac3e
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fankun.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/feiwen.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/feiwen.png
new file mode 100644
index 000000000..be9761658
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/feiwen.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fennu.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fennu.png
new file mode 100644
index 000000000..20c57338c
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/fennu.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ganga.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ganga.png
new file mode 100644
index 000000000..30ec329d2
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ganga.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ganmao.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ganmao.png
new file mode 100644
index 000000000..35bbb89f3
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ganmao.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/hanyan.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/hanyan.png
new file mode 100644
index 000000000..a0bc838b1
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/hanyan.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/haochi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/haochi.png
new file mode 100644
index 000000000..2e52b6bee
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/haochi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/hongxin.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/hongxin.png
new file mode 100644
index 000000000..65b5de8f0
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/hongxin.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/huaixiao.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/huaixiao.png
new file mode 100644
index 000000000..bc0e76c4c
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/huaixiao.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingkong.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingkong.png
new file mode 100644
index 000000000..7aa65845f
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingkong.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingshu.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingshu.png
new file mode 100644
index 000000000..0e984d68d
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingshu.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingya.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingya.png
new file mode 100644
index 000000000..9ba6bab32
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/jingya.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/kaixin.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/kaixin.png
new file mode 100644
index 000000000..29c9f5ddb
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/kaixin.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/keai.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/keai.png
new file mode 100644
index 000000000..d3b582c69
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/keai.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/keshui.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/keshui.png
new file mode 100644
index 000000000..cef489ea4
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/keshui.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/kun.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/kun.png
new file mode 100644
index 000000000..1ddc388a6
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/kun.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/lengku.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/lengku.png
new file mode 100644
index 000000000..c5c6feebb
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/lengku.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liuhan.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liuhan.png
new file mode 100644
index 000000000..e6ddc6f4d
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liuhan.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liukoushui.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liukoushui.png
new file mode 100644
index 000000000..3e2fba656
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liukoushui.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liulei.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liulei.png
new file mode 100644
index 000000000..dbf820404
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/liulei.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/mengbi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/mengbi.png
new file mode 100644
index 000000000..a4206eefb
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/mengbi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/mianwubiaoqing.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/mianwubiaoqing.png
new file mode 100644
index 000000000..6f315b98e
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/mianwubiaoqing.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/nanguo.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/nanguo.png
new file mode 100644
index 000000000..19b9fb94a
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/nanguo.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/outu.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/outu.png
new file mode 100644
index 000000000..2f9a06d63
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/outu.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/picture.svg b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/picture.svg
new file mode 100644
index 000000000..8811d4957
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/picture.svg
@@ -0,0 +1,10 @@
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/shengqi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/shengqi.png
new file mode 100644
index 000000000..7dce41dc9
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/shengqi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/shuizhuo.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/shuizhuo.png
new file mode 100644
index 000000000..97d0f0a67
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/shuizhuo.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/tianshi.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/tianshi.png
new file mode 100644
index 000000000..eb922dd7a
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/tianshi.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiaodiaoya.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiaodiaoya.png
new file mode 100644
index 000000000..29fbc0e19
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiaodiaoya.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiaoku.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiaoku.png
new file mode 100644
index 000000000..88a169d4f
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiaoku.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xinsui.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xinsui.png
new file mode 100644
index 000000000..a0f572a11
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xinsui.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiong.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiong.png
new file mode 100644
index 000000000..43dfd7090
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/xiong.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/yiwen.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/yiwen.png
new file mode 100644
index 000000000..4c0da7095
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/yiwen.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/yun.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/yun.png
new file mode 100644
index 000000000..56e5d0218
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/yun.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ziya.png b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ziya.png
new file mode 100644
index 000000000..593ef5e68
Binary files /dev/null and b/apps/web-antdv-next/src/views/mall/promotion/kefu/asserts/ziya.png differ
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/index.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/index.vue
new file mode 100644
index 000000000..55e833784
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/index.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/conversation-list.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/conversation-list.vue
new file mode 100644
index 000000000..174d845a3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/conversation-list.vue
@@ -0,0 +1,250 @@
+
+
+
+
+
+ 会话记录
+
+ {{ kefuStore.getConversationList.length }}
+
+
+
+
+
+
+
+
+
+
+ -
+
+ 置顶会话
+
+ -
+
+ 取消置顶
+
+ -
+
+ 删除会话
+
+ -
+
+ 取消
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/member-info.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/member-info.vue
new file mode 100644
index 000000000..742509d83
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/member-info.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+ 会员信息
+
+
+ 最近浏览
+
+
+ 交易订单
+
+
+
+
+
+
+
+
+ 基本信息
+
+
+
+
+
+ 账户信息
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/order-browsing-history.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/order-browsing-history.vue
new file mode 100644
index 000000000..a7a5fa65c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/order-browsing-history.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/product-browsing-history.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/product-browsing-history.vue
new file mode 100644
index 000000000..c35d87bd9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/member/product-browsing-history.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message-list.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message-list.vue
new file mode 100644
index 000000000..dcb34aaca
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message-list.vue
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+ {{ conversation.userNickname }}
+
+
+
+
+
+
+
+
+ {{ formatDate(item.createTime) }}
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 有新消息
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/message-item.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/message-item.vue
new file mode 100644
index 000000000..7c8e3153b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/message-item.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/order-item.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/order-item.vue
new file mode 100644
index 000000000..ff0ecd986
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/order-item.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
订单号:
+
+ {{ getMessageContent.no }}
+
+
+
+ {{ formatOrderStatus(getMessageContent) }}
+
+
+
+
+
+
+ 共 {{ getMessageContent?.productCount }} 件商品,总金额:
+
+
+ ¥{{ fenToYuan(getMessageContent?.payPrice) }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/product-item.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/product-item.vue
new file mode 100644
index 000000000..e16df5fb8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/message/product-item.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
{{ title }}
+
+ 库存: {{ stock || 0 }}
+ 销量: {{ salesCount || 0 }}
+
+
+ ¥{{ fenToYuan(price) }}
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/constants.ts b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/constants.ts
new file mode 100644
index 000000000..266a6cf02
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/constants.ts
@@ -0,0 +1,17 @@
+/** 客服消息类型枚举类 */
+export const KeFuMessageContentTypeEnum = {
+ TEXT: 1, // 文本消息
+ IMAGE: 2, // 图片消息
+ VOICE: 3, // 语音消息
+ VIDEO: 4, // 视频消息
+ SYSTEM: 5, // 系统消息
+ // ========== 商城特殊消息 ==========
+ PRODUCT: 10, // 商品消息
+ ORDER: 11, // 订单消息"
+};
+
+/** Promotion 的 WebSocket 消息类型枚举类 */
+export const WebSocketMessageTypeConstants = {
+ KEFU_MESSAGE_TYPE: 'kefu_message_type', // 客服消息类型
+ KEFU_MESSAGE_ADMIN_READ: 'kefu_message_read_status_change', // 客服消息管理员已读
+};
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/emoji-select-popover.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/emoji-select-popover.vue
new file mode 100644
index 000000000..c1dcf8a2c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/emoji-select-popover.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/emoji.ts b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/emoji.ts
new file mode 100644
index 000000000..f1bd19319
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/emoji.ts
@@ -0,0 +1,126 @@
+import { onMounted, ref } from 'vue';
+
+import { isEmpty } from '@vben/utils';
+
+const emojiList = [
+ { name: '[笑掉牙]', file: 'xiaodiaoya.png' },
+ { name: '[可爱]', file: 'keai.png' },
+ { name: '[冷酷]', file: 'lengku.png' },
+ { name: '[闭嘴]', file: 'bizui.png' },
+ { name: '[生气]', file: 'shengqi.png' },
+ { name: '[惊恐]', file: 'jingkong.png' },
+ { name: '[瞌睡]', file: 'keshui.png' },
+ { name: '[大笑]', file: 'daxiao.png' },
+ { name: '[爱心]', file: 'aixin.png' },
+ { name: '[坏笑]', file: 'huaixiao.png' },
+ { name: '[飞吻]', file: 'feiwen.png' },
+ { name: '[疑问]', file: 'yiwen.png' },
+ { name: '[开心]', file: 'kaixin.png' },
+ { name: '[发呆]', file: 'fadai.png' },
+ { name: '[流泪]', file: 'liulei.png' },
+ { name: '[汗颜]', file: 'hanyan.png' },
+ { name: '[惊悚]', file: 'jingshu.png' },
+ { name: '[困~]', file: 'kun.png' },
+ { name: '[心碎]', file: 'xinsui.png' },
+ { name: '[天使]', file: 'tianshi.png' },
+ { name: '[晕]', file: 'yun.png' },
+ { name: '[啊]', file: 'a.png' },
+ { name: '[愤怒]', file: 'fennu.png' },
+ { name: '[睡着]', file: 'shuizhuo.png' },
+ { name: '[面无表情]', file: 'mianwubiaoqing.png' },
+ { name: '[难过]', file: 'nanguo.png' },
+ { name: '[犯困]', file: 'fankun.png' },
+ { name: '[好吃]', file: 'haochi.png' },
+ { name: '[呕吐]', file: 'outu.png' },
+ { name: '[龇牙]', file: 'ziya.png' },
+ { name: '[懵比]', file: 'mengbi.png' },
+ { name: '[白眼]', file: 'baiyan.png' },
+ { name: '[饿死]', file: 'esi.png' },
+ { name: '[凶]', file: 'xiong.png' },
+ { name: '[感冒]', file: 'ganmao.png' },
+ { name: '[流汗]', file: 'liuhan.png' },
+ { name: '[笑哭]', file: 'xiaoku.png' },
+ { name: '[流口水]', file: 'liukoushui.png' },
+ { name: '[尴尬]', file: 'ganga.png' },
+ { name: '[惊讶]', file: 'jingya.png' },
+ { name: '[大惊]', file: 'dajing.png' },
+ { name: '[不好意思]', file: 'buhaoyisi.png' },
+ { name: '[大闹]', file: 'danao.png' },
+ { name: '[不可思议]', file: 'bukesiyi.png' },
+ { name: '[爱你]', file: 'aini.png' },
+ { name: '[红心]', file: 'hongxin.png' },
+ { name: '[点赞]', file: 'dianzan.png' },
+ { name: '[恶魔]', file: 'emo.png' },
+];
+
+export interface Emoji {
+ name: string;
+ url: string;
+}
+
+export function useEmoji() {
+ const emojiPathList = ref([]);
+
+ /** 加载本地图片 */
+ async function initStaticEmoji() {
+ const pathList = import.meta.glob('../../asserts/*.{png,jpg,jpeg,svg}');
+ for (const path in pathList) {
+ const imageModule: any = await pathList[path]?.();
+ emojiPathList.value.push({ path, src: imageModule.default });
+ }
+ }
+
+ /** 初始化 */
+ onMounted(async () => {
+ if (isEmpty(emojiPathList.value)) {
+ await initStaticEmoji();
+ }
+ });
+
+ /**
+ * 将文本中的表情替换成图片
+ *
+ * @return 替换后的文本
+ * @param content 消息内容
+ */
+ function replaceEmoji(content: string) {
+ let newData = content;
+ if (typeof newData !== 'object') {
+ const reg = /\[(.+?)\]/g; // [] 中括号
+ const zhEmojiName = newData.match(reg);
+ if (zhEmojiName) {
+ zhEmojiName.forEach((item) => {
+ const emojiFile = getEmojiFileByName(item);
+ newData = newData.replace(
+ item,
+ `
`,
+ );
+ });
+ }
+ }
+ return newData;
+ }
+
+ /** 获得所有表情 */
+ function getEmojiList(): Emoji[] {
+ return emojiList.map((item) => ({
+ url: getEmojiFileByName(item.name),
+ name: item.name,
+ })) as Emoji[];
+ }
+
+ function getEmojiFileByName(name: string) {
+ for (const emoji of emojiList) {
+ if (emoji.name === name) {
+ const emojiPath = emojiPathList.value.find(
+ (item: { path: string; src: string }) =>
+ item.path.includes(emoji.file),
+ );
+ return emojiPath ? emojiPath.src : undefined;
+ }
+ }
+ return false;
+ }
+
+ return { replaceEmoji, getEmojiList };
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/picture-select-upload.vue b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/picture-select-upload.vue
new file mode 100644
index 000000000..65f5a7516
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/kefu/modules/tools/picture-select-upload.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/point/activity/data.ts b/apps/web-antdv-next/src/views/mall/promotion/point/activity/data.ts
new file mode 100644
index 000000000..9b2abb065
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/point/activity/data.ts
@@ -0,0 +1,137 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { fenToYuan } from '@vben/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择活动状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的表格列 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '活动编号',
+ minWidth: 80,
+ },
+ {
+ field: 'picUrl',
+ title: '商品图片',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ },
+ },
+ },
+ {
+ field: 'spuName',
+ title: '商品标题',
+ minWidth: 300,
+ },
+ {
+ field: 'marketPrice',
+ title: '原价',
+ minWidth: 100,
+ formatter: ({ row }) => `¥${fenToYuan(row.marketPrice)}`,
+ },
+ {
+ field: 'status',
+ title: '活动状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'stock',
+ title: '库存',
+ minWidth: 80,
+ },
+ {
+ field: 'totalStock',
+ title: '总库存',
+ minWidth: 80,
+ },
+ {
+ field: 'redeemedQuantity',
+ title: '已兑换数量',
+ minWidth: 100,
+ formatter: ({ row }) => {
+ return (row.totalStock || 0) - (row.stock || 0);
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入排序',
+ class: '!w-full',
+ },
+ defaultValue: 0,
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'spuId',
+ label: '活动商品',
+ component: 'Input',
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/point/activity/index.vue b/apps/web-antdv-next/src/views/mall/promotion/point/activity/index.vue
new file mode 100644
index 000000000..d0ce962db
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/point/activity/index.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/point/activity/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/point/activity/modules/form.vue
new file mode 100644
index 000000000..9653a2b0e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/point/activity/modules/form.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/point/components/index.ts b/apps/web-antdv-next/src/views/mall/promotion/point/components/index.ts
new file mode 100644
index 000000000..4271f4e17
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/point/components/index.ts
@@ -0,0 +1,2 @@
+export { default as PointShowcase } from './showcase.vue';
+export { default as PointTableSelect } from './table-select.vue';
diff --git a/apps/web-antdv-next/src/views/mall/promotion/point/components/showcase.vue b/apps/web-antdv-next/src/views/mall/promotion/point/components/showcase.vue
new file mode 100644
index 000000000..9f16ac9d0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/point/components/showcase.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/point/components/table-select.vue b/apps/web-antdv-next/src/views/mall/promotion/point/components/table-select.vue
new file mode 100644
index 000000000..d4c1a2a27
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/point/components/table-select.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/data.ts b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/data.ts
new file mode 100644
index 000000000..c1232e43f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/data.ts
@@ -0,0 +1,255 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import {
+ DICT_TYPE,
+ PromotionConditionTypeEnum,
+ PromotionProductScopeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { $t } from '@vben/locales';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择活动状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '活动时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的表格列 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '活动名称',
+ minWidth: 200,
+ },
+ {
+ field: 'productScope',
+ title: '活动范围',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
+ },
+ },
+ {
+ field: 'startTime',
+ title: '活动开始时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'endTime',
+ title: '活动结束时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'startAndEndTime',
+ label: '活动时间',
+ component: 'RangePicker',
+ rules: 'required',
+ componentProps: {
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ placeholder: [
+ $t('utils.rangePicker.beginTime'),
+ $t('utils.rangePicker.endTime'),
+ ],
+ },
+ },
+ {
+ fieldName: 'conditionType',
+ label: '条件类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(PromotionConditionTypeEnum.PRICE.type),
+ },
+ {
+ fieldName: 'productScope',
+ label: '活动范围',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(PromotionProductScopeEnum.ALL.scope),
+ },
+ {
+ fieldName: 'productSpuIds',
+ label: '选择商品',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['productScope', 'productScopeValues'],
+ show: (values) => {
+ return values.productScope === PromotionProductScopeEnum.SPU.scope;
+ },
+ trigger(values, form) {
+ // 当加载已有数据时,根据 productScopeValues 设置 productSpuIds
+ if (
+ values.productScope === PromotionProductScopeEnum.SPU.scope &&
+ values.productScopeValues
+ ) {
+ form.setFieldValue('productSpuIds', values.productScopeValues);
+ }
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productCategoryIds',
+ label: '选择分类',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['productScope', 'productScopeValues'],
+ show: (values) => {
+ return (
+ values.productScope === PromotionProductScopeEnum.CATEGORY.scope
+ );
+ },
+ trigger(values, form) {
+ // 当加载已有数据时,根据 productScopeValues 设置 productCategoryIds
+ if (
+ values.productScope === PromotionProductScopeEnum.CATEGORY.scope &&
+ values.productScopeValues
+ ) {
+ const categoryIds = values.productScopeValues;
+ // 单选时使用数组不能反显,取第一个元素
+ form.setFieldValue(
+ 'productCategoryIds',
+ Array.isArray(categoryIds) && categoryIds.length > 0
+ ? categoryIds[0]
+ : categoryIds,
+ );
+ }
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'rules',
+ label: '优惠设置',
+ component: 'Input',
+ formItemClass: 'items-start',
+ rules: z
+ .array(z.any())
+ .min(1, { message: '请添加至少一条优惠规则' })
+ .default([]),
+ },
+ {
+ fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['productScope', 'productSpuIds', 'productCategoryIds'],
+ show: () => false,
+ trigger(values, form) {
+ switch (values.productScope) {
+ case PromotionProductScopeEnum.CATEGORY.scope: {
+ const categoryIds = Array.isArray(values.productCategoryIds)
+ ? values.productCategoryIds
+ : [values.productCategoryIds];
+ form.setFieldValue('productScopeValues', categoryIds);
+ break;
+ }
+ case PromotionProductScopeEnum.SPU.scope: {
+ form.setFieldValue('productScopeValues', values.productSpuIds);
+ break;
+ }
+ }
+ },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/index.vue b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/index.vue
new file mode 100644
index 000000000..20e892a12
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/index.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/form.vue
new file mode 100644
index 000000000..4a35c0316
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/form.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/reward-rule-coupon-select.vue b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/reward-rule-coupon-select.vue
new file mode 100644
index 000000000..fef6297ed
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/reward-rule-coupon-select.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+ {{ discountFormat(item) }}
+
+
+
+ 送
+
+ 张
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/reward-rule.vue b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/reward-rule.vue
new file mode 100644
index 000000000..a90d9ff97
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/rewardActivity/modules/reward-rule.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+ 活动层级 {{ index + 1 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提示:赠送积分为 0 时不赠送;未选择优惠券时不赠送。
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/data.ts b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/data.ts
new file mode 100644
index 000000000..2ebb63de5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/data.ts
@@ -0,0 +1,244 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleSeckillConfigList } from '#/api/mall/promotion/seckill/seckillConfig';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '活动状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择活动状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 新增/编辑的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '秒杀活动名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入活动名称',
+ },
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'startTime',
+ label: '活动开始时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择活动开始时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ class: 'w-full',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '活动结束时间',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '请选择活动结束时间',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'x',
+ class: 'w-full',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'configIds',
+ label: '秒杀时段',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '请选择秒杀时段',
+ mode: 'multiple',
+ api: getSimpleSeckillConfigList,
+ labelField: 'name',
+ valueField: 'id',
+ class: 'w-full',
+ },
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'totalLimitCount',
+ label: '总限购数量',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入总限购数量',
+ min: 0,
+ class: 'w-full',
+ },
+ rules: z.number().min(0).default(0),
+ },
+ {
+ fieldName: 'singleLimitCount',
+ label: '单次限购数量',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入单次限购数量',
+ min: 0,
+ class: 'w-full',
+ },
+ rules: z.number().min(0).default(0),
+ },
+ {
+ fieldName: 'sort',
+ label: '排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入排序',
+ min: 0,
+ class: 'w-full',
+ },
+ rules: z.number().min(0).default(0),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ rows: 4,
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'spuId',
+ label: '秒杀商品',
+ component: 'Input',
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '活动编号',
+ minWidth: 80,
+ },
+ {
+ field: 'name',
+ title: '活动名称',
+ minWidth: 140,
+ },
+ {
+ field: 'configIds',
+ title: '秒杀时段',
+ minWidth: 220,
+ slots: { default: 'configIds' },
+ },
+ {
+ field: 'startTime',
+ title: '活动时间',
+ minWidth: 210,
+ slots: { default: 'timeRange' },
+ },
+ {
+ field: 'picUrl',
+ title: '商品图片',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'spuName',
+ title: '商品标题',
+ minWidth: 300,
+ },
+ {
+ field: 'marketPrice',
+ title: '原价',
+ minWidth: 100,
+ formatter: ({ row }) => `¥${(row.marketPrice / 100).toFixed(2)}`,
+ },
+ {
+ field: 'seckillPrice',
+ title: '秒杀价',
+ minWidth: 100,
+ formatter: ({ row }) => {
+ if (!(row.products || row.products.length === 0)) {
+ return '¥0.00';
+ }
+ const seckillPrice = Math.min(
+ ...row.products.map((item: any) => item.seckillPrice),
+ );
+ return `¥${(seckillPrice / 100).toFixed(2)}`;
+ },
+ },
+ {
+ field: 'status',
+ title: '活动状态',
+ align: 'center',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'stock',
+ title: '库存',
+ align: 'center',
+ minWidth: 80,
+ },
+ {
+ field: 'totalStock',
+ title: '总库存',
+ align: 'center',
+ minWidth: 80,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ align: 'center',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ align: 'center',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/formatter.ts b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/formatter.ts
new file mode 100644
index 000000000..5ec39d387
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/formatter.ts
@@ -0,0 +1,37 @@
+import { formatDate } from '@vben/utils';
+
+// 全局变量,用于存储配置列表
+let configList: any[] = [];
+
+/** 设置配置列表 */
+export function setConfigList(list: any[]) {
+ configList = list;
+}
+
+/** 格式化配置名称 */
+export function formatConfigNames(configId: number | string): string {
+ const config = configList.find((item) => item.id === configId);
+ return config === null || config === undefined
+ ? ''
+ : `${config.name}[${config.startTime} ~ ${config.endTime}]`;
+}
+
+/** 格式化秒杀价格 */
+export function formatSeckillPrice(products: any[]): string {
+ if (!products || products.length === 0) {
+ return '¥0.00';
+ }
+ const seckillPrice = Math.min(...products.map((item) => item.seckillPrice));
+ return `¥${(seckillPrice / 100).toFixed(2)}`;
+}
+
+/** 格式化活动时间范围 */
+export function formatTimeRange(
+ startTime: Date | string | undefined,
+ endTime: Date | string | undefined,
+): string {
+ if (startTime && endTime) {
+ return `${formatDate(startTime, 'YYYY-MM-DD')} ~ ${formatDate(endTime, 'YYYY-MM-DD')}`;
+ }
+ return '';
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/index.vue b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/index.vue
new file mode 100644
index 000000000..dfed233c2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/index.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatConfigNames(configId) }}
+
+
+
+
+
+ {{ formatTimeRange(row.startTime, row.endTime) }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/modules/form.vue
new file mode 100644
index 000000000..0a9bc4839
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/activity/modules/form.vue
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/components/index.ts b/apps/web-antdv-next/src/views/mall/promotion/seckill/components/index.ts
new file mode 100644
index 000000000..dd452161c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/components/index.ts
@@ -0,0 +1 @@
+export { default as SeckillShowcase } from './showcase.vue';
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/components/showcase.vue b/apps/web-antdv-next/src/views/mall/promotion/seckill/components/showcase.vue
new file mode 100644
index 000000000..d5d414bdf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/components/showcase.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/components/table-select.vue b/apps/web-antdv-next/src/views/mall/promotion/seckill/components/table-select.vue
new file mode 100644
index 000000000..ec3315b1a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/components/table-select.vue
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/config/data.ts b/apps/web-antdv-next/src/views/mall/promotion/seckill/config/data.ts
new file mode 100644
index 000000000..87c25edeb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/config/data.ts
@@ -0,0 +1,155 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '秒杀时段名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入秒杀时段名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'startTime',
+ label: '开始时间点',
+ component: 'TimePicker',
+ componentProps: {
+ format: 'HH:mm',
+ valueFormat: 'HH:mm',
+ placeholder: '请选择开始时间点',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '结束时间点',
+ component: 'TimePicker',
+ componentProps: {
+ format: 'HH:mm',
+ valueFormat: 'HH:mm',
+ placeholder: '请选择结束时间点',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sliderPicUrls',
+ label: '秒杀轮播图',
+ component: 'ImageUpload',
+ componentProps: {
+ multiple: true,
+ maxNumber: 5,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '秒杀时段名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入秒杀时段名称',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: MallSeckillConfigApi.SeckillConfig,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '秒杀时段名称',
+ field: 'name',
+ minWidth: 200,
+ },
+ {
+ title: '开始时间点',
+ field: 'startTime',
+ minWidth: 120,
+ },
+ {
+ title: '结束时间点',
+ field: 'endTime',
+ minWidth: 120,
+ },
+ {
+ title: '秒杀轮播图',
+ field: 'sliderPicUrls',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImages',
+ },
+ },
+ {
+ title: '活动状态',
+ field: 'status',
+ minWidth: 100,
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: 1,
+ checkedChildren: '启用',
+ unCheckedValue: 0,
+ unCheckedChildren: '禁用',
+ },
+ },
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/config/index.vue b/apps/web-antdv-next/src/views/mall/promotion/seckill/config/index.vue
new file mode 100644
index 000000000..932e0235f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/config/index.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/promotion/seckill/config/modules/form.vue b/apps/web-antdv-next/src/views/mall/promotion/seckill/config/modules/form.vue
new file mode 100644
index 000000000..d7e7fb94a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/promotion/seckill/config/modules/form.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/index.vue b/apps/web-antdv-next/src/views/mall/statistics/member/index.vue
new file mode 100644
index 000000000..9524f9bb1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/index.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/area-card.vue b/apps/web-antdv-next/src/views/mall/statistics/member/modules/area-card.vue
new file mode 100644
index 000000000..e9284a436
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/area-card.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/area-chart-options.ts b/apps/web-antdv-next/src/views/mall/statistics/member/modules/area-chart-options.ts
new file mode 100644
index 000000000..b09f492f0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/area-chart-options.ts
@@ -0,0 +1,130 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallMemberStatisticsApi } from '#/api/mall/statistics/member';
+
+import { fenToYuan } from '@vben/utils';
+
+/** 会员地域分布图表配置 */
+export function getAreaChartOptions(
+ data: MallMemberStatisticsApi.AreaStatisticsRespVO[],
+): any {
+ if (!data || data.length === 0) {
+ return {
+ title: {
+ text: '暂无数据',
+ left: 'center',
+ top: 'center',
+ textStyle: {
+ color: '#999',
+ fontSize: 14,
+ },
+ },
+ };
+ }
+
+ // 计算 min 和 max 值
+ let min = Number.POSITIVE_INFINITY;
+ let max = Number.NEGATIVE_INFINITY;
+ const mapData = data.map((item) => {
+ const payUserCount = item.orderPayUserCount || 0;
+ min = Math.min(min, payUserCount);
+ max = Math.max(max, payUserCount);
+ return {
+ ...item,
+ name: item.areaName,
+ value: payUserCount,
+ };
+ });
+ // 如果所有值都为 0,设置合理的 min 和 max 值
+ if (min === max && min === 0) {
+ min = 0;
+ max = 10;
+ }
+
+ // 返回图表配置
+ return {
+ tooltip: {
+ trigger: 'item',
+ formatter: (params: any) => {
+ const itemData = params?.data;
+ if (!itemData) {
+ return `${params?.name || ''}
暂无数据`;
+ }
+ return `${itemData.areaName || params.name}
+会员数量:${itemData.userCount || 0}
+订单创建数量:${itemData.orderCreateUserCount || 0}
+订单支付数量:${itemData.orderPayUserCount || 0}
+订单支付金额:¥${Number(fenToYuan(itemData.orderPayPrice || 0)).toFixed(2)}`;
+ },
+ },
+ visualMap: {
+ text: ['高', '低'],
+ realtime: false,
+ calculable: true,
+ top: 'middle',
+ left: 10,
+ min,
+ max,
+ inRange: {
+ color: ['#e6f3ff', '#1890ff', '#0050b3'],
+ },
+ },
+ series: [
+ {
+ name: '会员地域分布',
+ type: 'map',
+ map: 'china',
+ roam: false,
+ selectedMode: false,
+ itemStyle: {
+ borderColor: '#389e0d',
+ borderWidth: 0.5,
+ },
+ emphasis: {
+ itemStyle: {
+ areaColor: '#ffec3d',
+ borderWidth: 1,
+ },
+ },
+ data: mapData,
+ },
+ ],
+ };
+}
+
+/** VXE Grid 表格列配置 */
+export function getAreaTableColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'areaName',
+ title: '省份',
+ minWidth: 80,
+ sortable: true,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'userCount',
+ title: '会员数量',
+ minWidth: 100,
+ sortable: true,
+ },
+ {
+ field: 'orderCreateUserCount',
+ title: '订单创建数量',
+ minWidth: 120,
+ sortable: true,
+ },
+ {
+ field: 'orderPayUserCount',
+ title: '订单支付数量',
+ minWidth: 120,
+ sortable: true,
+ },
+ {
+ field: 'orderPayPrice',
+ title: '订单支付金额',
+ minWidth: 120,
+ sortable: true,
+ formatter: 'formatFenToYuanAmount',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/funnel-card.vue b/apps/web-antdv-next/src/views/mall/statistics/member/modules/funnel-card.vue
new file mode 100644
index 000000000..fc1a7a4a0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/funnel-card.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+ 会员概览
+
+
+
+
+
+
+
+
+ 注册用户数量:
+ {{ analyseData?.comparison?.value?.registerUserCount || 0 }}
+
+
+ 环比增长率:
+ {{
+ calculateRelativeRate(
+ analyseData?.comparison?.value?.registerUserCount,
+ analyseData?.comparison?.reference?.registerUserCount,
+ ).toFixed(2)
+ }}%
+
+
+
+
+
+ {{ analyseData?.visitUserCount || 0 }}
+
+ 访客
+
+
+
+
+
+
+ 活跃用户数量:
+ {{ analyseData?.comparison?.value?.visitUserCount || 0 }}
+
+
+ 环比增长率:
+ {{
+ calculateRelativeRate(
+ analyseData?.comparison?.value?.visitUserCount,
+ analyseData?.comparison?.reference?.visitUserCount,
+ ).toFixed(2)
+ }}%
+
+
+
+
+
+ {{ analyseData?.orderUserCount || 0 }}
+
+ 下单
+
+
+
+
+
+
+
+ 充值用户数量:
+ {{ analyseData?.comparison?.value?.rechargeUserCount || 0 }}
+
+
+ 环比增长率:
+ {{
+ calculateRelativeRate(
+ analyseData?.comparison?.value?.rechargeUserCount,
+ analyseData?.comparison?.reference?.rechargeUserCount,
+ ).toFixed(2)
+ }}%
+
+
+
+
+ 客单价:{{ fenToYuan(analyseData?.atv || 0) }}
+
+
+
+
+
+
+ {{ analyseData?.payUserCount || 0 }}
+
+ 成交用户
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/sex-card.vue b/apps/web-antdv-next/src/views/mall/statistics/member/modules/sex-card.vue
new file mode 100644
index 000000000..b765475ab
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/sex-card.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/sex-chart-options.ts b/apps/web-antdv-next/src/views/mall/statistics/member/modules/sex-chart-options.ts
new file mode 100644
index 000000000..81a7faf87
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/sex-chart-options.ts
@@ -0,0 +1,28 @@
+/** 会员性别比例图表配置 */
+export function getSexChartOptions(data: any[]): any {
+ return {
+ tooltip: {
+ trigger: 'item',
+ confine: true,
+ formatter: '{a}
{b} : {c} ({d}%)',
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'right',
+ },
+ series: [
+ {
+ name: '会员性别',
+ type: 'pie',
+ roseType: 'area',
+ label: {
+ show: false,
+ },
+ labelLine: {
+ show: false,
+ },
+ data,
+ },
+ ],
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/terminal-card.vue b/apps/web-antdv-next/src/views/mall/statistics/member/modules/terminal-card.vue
new file mode 100644
index 000000000..bb3103704
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/terminal-card.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/member/modules/terminal-chart-options.ts b/apps/web-antdv-next/src/views/mall/statistics/member/modules/terminal-chart-options.ts
new file mode 100644
index 000000000..c3233bdd1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/member/modules/terminal-chart-options.ts
@@ -0,0 +1,27 @@
+/** 会员终端统计图配置 */
+export function getTerminalChartOptions(data: any[]): any {
+ return {
+ tooltip: {
+ trigger: 'item',
+ confine: true,
+ formatter: '{a}
{b} : {c} ({d}%)',
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'right',
+ },
+ series: [
+ {
+ name: '会员终端',
+ type: 'pie',
+ label: {
+ show: false,
+ },
+ labelLine: {
+ show: false,
+ },
+ data,
+ },
+ ],
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mall/statistics/product/index.vue b/apps/web-antdv-next/src/views/mall/statistics/product/index.vue
new file mode 100644
index 000000000..2e3834059
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/product/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/product/modules/rank-card.vue b/apps/web-antdv-next/src/views/mall/statistics/product/modules/rank-card.vue
new file mode 100644
index 000000000..ea58c3986
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/product/modules/rank-card.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/product/modules/summary-card.vue b/apps/web-antdv-next/src/views/mall/statistics/product/modules/summary-card.vue
new file mode 100644
index 000000000..b38b4517a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/product/modules/summary-card.vue
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/product/modules/summary-chart-options.ts b/apps/web-antdv-next/src/views/mall/statistics/product/modules/summary-chart-options.ts
new file mode 100644
index 000000000..ec6e474e1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/product/modules/summary-chart-options.ts
@@ -0,0 +1,129 @@
+/** 商品统计折线图配置 */
+export function getProductSummaryChartOptions(data: any[]): any {
+ // 处理数据:将金额从分转换为元
+ const processedData = data.map((item) => ({
+ ...item,
+ orderPayPrice: Number((item.orderPayPrice / 100).toFixed(2)),
+ afterSaleRefundPrice: Number((item.afterSaleRefundPrice / 100).toFixed(2)),
+ }));
+
+ return {
+ dataset: {
+ dimensions: [
+ 'time',
+ 'browseCount',
+ 'browseUserCount',
+ 'orderPayPrice',
+ 'afterSaleRefundPrice',
+ ],
+ source: processedData,
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [
+ {
+ name: '商品浏览量',
+ type: 'line',
+ smooth: true,
+ itemStyle: { color: '#B37FEB' },
+ },
+ {
+ name: '商品访客数',
+ type: 'line',
+ smooth: true,
+ itemStyle: { color: '#FFAB2B' },
+ },
+ {
+ name: '支付金额',
+ type: 'bar',
+ smooth: true,
+ yAxisIndex: 1,
+ itemStyle: { color: '#1890FF' },
+ },
+ {
+ name: '退款金额',
+ type: 'bar',
+ smooth: true,
+ yAxisIndex: 1,
+ itemStyle: { color: '#00C050' },
+ },
+ ],
+ toolbox: {
+ feature: {
+ // 数据区域缩放
+ dataZoom: {
+ yAxisIndex: false, // Y轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: {
+ show: true,
+ name: '商品状况',
+ }, // 保存为图片
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ },
+ padding: [5, 10],
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: true,
+ axisTick: {
+ show: false,
+ },
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '金额',
+ axisLine: {
+ show: false,
+ },
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ color: '#7F8B9C',
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#F5F7F9',
+ },
+ },
+ },
+ {
+ type: 'value',
+ name: '数量',
+ axisLine: {
+ show: false,
+ },
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ color: '#7F8B9C',
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#F5F7F9',
+ },
+ },
+ },
+ ],
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mall/statistics/trade/index.vue b/apps/web-antdv-next/src/views/mall/statistics/trade/index.vue
new file mode 100644
index 000000000..09fb18be0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/trade/index.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/trade/modules/trend-card.vue b/apps/web-antdv-next/src/views/mall/statistics/trade/modules/trend-card.vue
new file mode 100644
index 000000000..3d300dfca
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/trade/modules/trend-card.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/statistics/trade/modules/trend-chart-options.ts b/apps/web-antdv-next/src/views/mall/statistics/trade/modules/trend-chart-options.ts
new file mode 100644
index 000000000..899a6a32a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/statistics/trade/modules/trend-chart-options.ts
@@ -0,0 +1,124 @@
+import type { MallTradeStatisticsApi } from '#/api/mall/statistics/trade';
+
+import { fenToYuan } from '@vben/utils';
+
+/** 交易趋势折线图配置 */
+export function getTradeTrendChartOptions(
+ data: MallTradeStatisticsApi.TradeTrendSummaryRespVO[],
+): any {
+ // 处理数据:将分转换为元
+ const processedData = data.map((item) => ({
+ ...item,
+ turnoverPrice: Number(fenToYuan(item.turnoverPrice)),
+ orderPayPrice: Number(fenToYuan(item.orderPayPrice)),
+ rechargePrice: Number(fenToYuan(item.rechargePrice)),
+ expensePrice: Number(fenToYuan(item.expensePrice)),
+ }));
+
+ return {
+ dataset: {
+ dimensions: [
+ 'date',
+ 'turnoverPrice',
+ 'orderPayPrice',
+ 'rechargePrice',
+ 'expensePrice',
+ ],
+ source: processedData,
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true,
+ },
+ legend: {
+ top: 50,
+ },
+ series: [
+ {
+ name: '营业额',
+ type: 'line',
+ smooth: true,
+ itemStyle: { color: '#1890FF' },
+ },
+ {
+ name: '商品支付金额',
+ type: 'line',
+ smooth: true,
+ itemStyle: { color: '#722ED1' },
+ },
+ {
+ name: '充值金额',
+ type: 'line',
+ smooth: true,
+ itemStyle: { color: '#FAAD14' },
+ },
+ {
+ name: '支出金额',
+ type: 'line',
+ smooth: true,
+ itemStyle: { color: '#52C41A' },
+ },
+ ],
+ toolbox: {
+ feature: {
+ // 数据区域缩放
+ dataZoom: {
+ yAxisIndex: false, // Y轴不缩放
+ },
+ brush: {
+ type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮
+ },
+ saveAsImage: {
+ show: true,
+ name: '交易状况',
+ }, // 保存为图片
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ },
+ padding: [5, 10],
+ formatter(params: any) {
+ let result = `${params[0].data.time}
`;
+ params.forEach((item: any) => {
+ result += `
+
+ ${item.seriesName}: ¥${item.data[item.dimensionNames[item.encode.y[0]]]}
+
`;
+ });
+ return result;
+ },
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ axisTick: {
+ show: false,
+ },
+ },
+ yAxis: {
+ type: 'value',
+ axisLine: {
+ show: false,
+ },
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ formatter: '¥{value}',
+ color: '#7F8B9C',
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#F5F7F9',
+ },
+ },
+ },
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/afterSale/data.ts b/apps/web-antdv-next/src/views/mall/trade/afterSale/data.ts
new file mode 100644
index 000000000..2daee4757
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/afterSale/data.ts
@@ -0,0 +1,172 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 拒绝售后表单的 schema 配置 */
+export function useDisagreeFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Textarea',
+ fieldName: 'reason',
+ label: '拒绝原因',
+ componentProps: {
+ placeholder: '请输入拒绝原因',
+ rows: 4,
+ },
+ rules: z.string().min(2, { message: '拒绝原因不能少于 2 个字符' }),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'spuName',
+ label: '商品名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入商品名称',
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '退款编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入退款编号',
+ },
+ },
+ {
+ fieldName: 'orderNo',
+ label: '订单编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单编号',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '售后状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS, 'number'),
+ placeholder: '请选择售后状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'way',
+ label: '售后方式',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY, 'number'),
+ placeholder: '请选择售后方式',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '售后类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE, 'number'),
+ placeholder: '请选择售后类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ field: 'no',
+ title: '退款编号',
+ fixed: 'left',
+ minWidth: 200,
+ },
+ {
+ field: 'orderNo',
+ title: '订单编号',
+ fixed: 'left',
+ minWidth: 200,
+ slots: { default: 'orderNo' },
+ },
+ {
+ field: 'productInfo',
+ title: '商品信息',
+ minWidth: 600,
+ slots: { default: 'productInfo' },
+ },
+ {
+ field: 'refundPrice',
+ title: '订单金额',
+ width: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'user.nickname',
+ title: '买家',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '申请时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '售后状态',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: {
+ type: DICT_TYPE.TRADE_AFTER_SALE_STATUS,
+ },
+ },
+ },
+ {
+ field: 'way',
+ title: '售后方式',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: {
+ type: DICT_TYPE.TRADE_AFTER_SALE_WAY,
+ },
+ },
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ align: 'center',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/afterSale/detail/data.ts b/apps/web-antdv-next/src/views/mall/trade/afterSale/detail/data.ts
new file mode 100644
index 000000000..66fed1092
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/afterSale/detail/data.ts
@@ -0,0 +1,221 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { fenToYuan, formatDate } from '@vben/utils';
+
+import { Image } from 'ant-design-vue';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 订单信息 schema */
+export function useOrderInfoSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'orderNo',
+ label: '订单号',
+ },
+ {
+ field: 'order.deliveryType',
+ label: '配送方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_DELIVERY_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'order.type',
+ label: '订单类型',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_ORDER_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'order.receiverName',
+ label: '收货人',
+ },
+ {
+ field: 'order.userRemark',
+ label: '买家留言',
+ },
+ {
+ field: 'order.terminal',
+ label: '订单来源',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TERMINAL,
+ value: val,
+ }),
+ },
+ {
+ field: 'order.receiverMobile',
+ label: '联系电话',
+ },
+ {
+ field: 'order.remark',
+ label: '商家备注',
+ },
+ {
+ field: 'order.payOrderId',
+ label: '支付单号',
+ },
+ {
+ field: 'order.payChannelCode',
+ label: '付款方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_CHANNEL_CODE,
+ value: val,
+ }),
+ },
+ {
+ field: 'user.nickname',
+ label: '买家',
+ },
+ ];
+}
+
+/** 售后信息 schema */
+export function useAfterSaleInfoSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'no',
+ label: '退款编号',
+ },
+ {
+ field: 'auditTime',
+ label: '申请时间',
+ render: (val) => formatDate(val) as string,
+ },
+ {
+ field: 'type',
+ label: '售后类型',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_AFTER_SALE_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'way',
+ label: '售后方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_AFTER_SALE_WAY,
+ value: val,
+ }),
+ },
+ {
+ field: 'refundPrice',
+ label: '退款金额',
+ render: (val) => fenToYuan(val ?? 0),
+ },
+ {
+ field: 'applyReason',
+ label: '退款原因',
+ },
+ {
+ field: 'applyDescription',
+ label: '补充描述',
+ },
+ {
+ field: 'applyPicUrls',
+ label: '凭证图片',
+ render: (val) => {
+ const images = val || [];
+ return h(
+ 'div',
+ { class: 'flex gap-10px' },
+ images.map((url: string, index: number) =>
+ h(Image, {
+ key: index,
+ src: url,
+ width: 60,
+ height: 60,
+ }),
+ ),
+ );
+ },
+ },
+ ];
+}
+
+/** 退款状态 schema */
+export function useRefundStatusSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'status',
+ label: '退款状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_AFTER_SALE_STATUS,
+ value: val,
+ }),
+ },
+ {
+ field: 'reminder',
+ label: '提醒',
+ render: () =>
+ h('div', { class: 'text-red-500 mb-10px' }, [
+ h('div', '如果未发货,请点击同意退款给买家。'),
+ h('div', '如果实际已发货,请主动与买家联系。'),
+ h('div', '如果订单整体退款后,优惠券和余额会退还给买家.'),
+ ]),
+ },
+ ];
+}
+
+/** 商品信息 columns */
+export function useProductColumns() {
+ return [
+ {
+ field: 'spuName',
+ title: '商品信息',
+ minWidth: 300,
+ slots: { default: 'spuName' },
+ },
+ {
+ field: 'price',
+ title: '商品原价',
+ minWidth: 150,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'count',
+ title: '数量',
+ minWidth: 100,
+ },
+ {
+ field: 'payPrice',
+ title: '合计',
+ minWidth: 150,
+ formatter: 'formatFenToYuanAmount',
+ },
+ ];
+}
+
+/** 操作日志 columns */
+export function useOperateLogSchema() {
+ return [
+ {
+ field: 'createTime',
+ title: '操作时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'userType',
+ title: '操作人',
+ width: 100,
+ slots: { default: 'userType' },
+ },
+ {
+ field: 'content',
+ title: '操作内容',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/afterSale/detail/index.vue b/apps/web-antdv-next/src/views/mall/trade/afterSale/detail/index.vue
new file mode 100644
index 000000000..21893ea9b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/afterSale/detail/index.vue
@@ -0,0 +1,307 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ row.spuName }}
+
+
+ {{ property.propertyName }}: {{ property.valueName }}
+
+
+
+
+
+
+
+
+
+
+ 系统
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/afterSale/index.vue b/apps/web-antdv-next/src/views/mall/trade/afterSale/index.vue
new file mode 100644
index 000000000..c071fc038
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/afterSale/index.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ row.spuName }}
+
+
+ {{ property.propertyName }}: {{ property.valueName }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/afterSale/modules/disagree-form.vue b/apps/web-antdv-next/src/views/mall/trade/afterSale/modules/disagree-form.vue
new file mode 100644
index 000000000..6fe9282ad
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/afterSale/modules/disagree-form.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/record/data.ts b/apps/web-antdv-next/src/views/mall/trade/brokerage/record/data.ts
new file mode 100644
index 000000000..a4b82497b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/record/data.ts
@@ -0,0 +1,137 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { fenToYuan } from '@vben/utils';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bizType',
+ label: '业务类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择业务类型',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 60,
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 80,
+ },
+ {
+ field: 'userAvatar',
+ title: '头像',
+ minWidth: 70,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ height: 40,
+ width: 40,
+ shape: 'circle',
+ },
+ },
+ },
+ {
+ field: 'userNickname',
+ title: '昵称',
+ minWidth: 80,
+ },
+ {
+ field: 'bizType',
+ title: '业务类型',
+ minWidth: 85,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE },
+ },
+ },
+ {
+ field: 'bizId',
+ title: '业务编号',
+ minWidth: 80,
+ },
+ {
+ field: 'title',
+ title: '标题',
+ minWidth: 110,
+ },
+ {
+ field: 'price',
+ title: '金额',
+ minWidth: 60,
+ formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
+ },
+ {
+ field: 'description',
+ title: '说明',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 85,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BROKERAGE_RECORD_STATUS },
+ },
+ },
+ {
+ field: 'unfreezeTime',
+ title: '解冻时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/record/index.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/record/index.vue
new file mode 100644
index 000000000..a95964874
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/record/index.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/user/data.ts b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/data.ts
new file mode 100644
index 000000000..2200d2c71
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/data.ts
@@ -0,0 +1,367 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { fenToYuan } from '@vben/utils';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'bindUserId',
+ label: '推广员编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入推广员编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'brokerageEnabled',
+ label: '推广资格',
+ component: 'RadioGroup',
+ componentProps: {
+ placeholder: '请选择推广资格',
+ allowClear: true,
+ options: [
+ { label: '有', value: true },
+ { label: '无', value: false },
+ ],
+ },
+ defaultValue: true,
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onBrokerageEnabledChange?: (
+ newEnabled: boolean,
+ row: MallBrokerageUserApi.BrokerageUser,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '用户编号',
+ minWidth: 80,
+ },
+ {
+ field: 'avatar',
+ title: '头像',
+ minWidth: 70,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'nickname',
+ title: '昵称',
+ minWidth: 80,
+ },
+ {
+ field: 'brokerageUserCount',
+ title: '推广人数',
+ minWidth: 80,
+ },
+ {
+ field: 'brokerageOrderCount',
+ title: '推广订单数量',
+ minWidth: 110,
+ },
+ {
+ field: 'brokerageOrderPrice',
+ title: '推广订单金额',
+ minWidth: 110,
+ formatter: ({ row }) => `¥${fenToYuan(row.brokerageOrderPrice)}`,
+ },
+ {
+ field: 'withdrawPrice',
+ title: '已提现金额',
+ minWidth: 100,
+ formatter: ({ row }) => `¥${fenToYuan(row.withdrawPrice)}`,
+ },
+ {
+ field: 'withdrawCount',
+ title: '已提现次数',
+ minWidth: 100,
+ },
+ {
+ field: 'price',
+ title: '未提现金额',
+ minWidth: 100,
+ formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
+ },
+ {
+ field: 'frozenPrice',
+ title: '冻结中佣金',
+ minWidth: 100,
+ formatter: ({ row }) => `¥${fenToYuan(row.frozenPrice)}`,
+ },
+ {
+ field: 'brokerageEnabled',
+ title: '推广资格',
+ minWidth: 80,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onBrokerageEnabledChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: true,
+ uncheckedValue: false,
+ checkedChildren: '有',
+ unCheckedChildren: '无',
+ },
+ },
+ },
+ {
+ field: 'brokerageTime',
+ title: '成为推广员时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'bindUserId',
+ title: '上级推广员编号',
+ minWidth: 150,
+ },
+ {
+ field: 'bindUserTime',
+ title: '推广员绑定时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 创建分销员表单配置 */
+export function useCreateFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '分销员编号',
+ component: 'InputSearch',
+ componentProps: {
+ placeholder: '请输入分销员编号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'bindUserId',
+ label: '上级推广员编号',
+ component: 'InputSearch',
+ componentProps: {
+ placeholder: '请输入上级推广员编号',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 修改分销用户表单配置 */
+export function useUpdateFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'bindUserId',
+ label: '上级推广员编号',
+ component: 'InputSearch',
+ componentProps: {
+ placeholder: '请输入上级推广员编号',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 用户列表弹窗搜索表单配置 */
+export function useUserListFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'level',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '全部', value: undefined },
+ { label: '一级推广人', value: '1' },
+ { label: '二级推广人', value: '2' },
+ ],
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bindUserTime',
+ label: '绑定时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 用户列表弹窗表格列配置 */
+export function useUserListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '用户编号',
+ minWidth: 80,
+ },
+ {
+ field: 'avatar',
+ title: '头像',
+ minWidth: 70,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ width: 24,
+ height: 24,
+ shape: 'circle',
+ },
+ },
+ },
+ {
+ field: 'nickname',
+ title: '昵称',
+ minWidth: 80,
+ },
+ {
+ field: 'brokerageUserCount',
+ title: '推广人数',
+ minWidth: 80,
+ },
+ {
+ field: 'brokerageOrderCount',
+ title: '推广订单数量',
+ minWidth: 110,
+ },
+ {
+ field: 'brokerageEnabled',
+ title: '推广资格',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'bindUserTime',
+ title: '绑定时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
+
+/** 推广订单列表弹窗搜索表单配置 */
+export function useOrderListFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'sourceUserLevel',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '全部', value: 0 },
+ { label: '一级推广人', value: 1 },
+ { label: '二级推广人', value: 2 },
+ ],
+ },
+ defaultValue: 0,
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 推广订单列表弹窗表格列配置 */
+export function useOrderListColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'bizId',
+ title: '订单编号',
+ minWidth: 80,
+ },
+ {
+ field: 'sourceUserId',
+ title: '用户编号',
+ minWidth: 80,
+ },
+ {
+ field: 'sourceUserAvatar',
+ title: '头像',
+ minWidth: 70,
+ cellRender: {
+ name: 'CellImage',
+ props: {
+ width: 24,
+ height: 24,
+ },
+ },
+ },
+ {
+ field: 'sourceUserNickname',
+ title: '昵称',
+ minWidth: 80,
+ },
+ {
+ field: 'price',
+ title: '佣金',
+ minWidth: 100,
+ formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 85,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BROKERAGE_RECORD_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/user/index.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/index.vue
new file mode 100644
index 000000000..eb263d892
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/index.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/create-form.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/create-form.vue
new file mode 100644
index 000000000..501883f62
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/create-form.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ user?.nickname }}
+
+
+
+
+
+
+
+
+
+ {{ bindUser?.nickname }}
+
+
+
+
+
+ {{ formatDate(bindUser?.brokerageTime) }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue
new file mode 100644
index 000000000..7e9520fe5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/update-form.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/update-form.vue
new file mode 100644
index 000000000..cdb9695f2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/update-form.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ bindUser?.nickname }}
+
+
+
+
+
+ {{ formatDate(bindUser?.brokerageTime) }}
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue
new file mode 100644
index 000000000..40667488d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/withdraw/data.ts b/apps/web-antdv-next/src/views/mall/trade/brokerage/withdraw/data.ts
new file mode 100644
index 000000000..86452dd8e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/withdraw/data.ts
@@ -0,0 +1,148 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '提现类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择提现类型',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'userAccount',
+ label: '账号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入账号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userName',
+ label: '真实姓名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入真实姓名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bankName',
+ label: '提现银行',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择提现银行',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME, 'string'),
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '申请时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 80,
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 80,
+ },
+ {
+ field: 'userNickname',
+ title: '用户昵称',
+ minWidth: 80,
+ },
+ {
+ field: 'price',
+ title: '提现金额',
+ minWidth: 80,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'feePrice',
+ title: '提现手续费',
+ minWidth: 80,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'type',
+ title: '提现方式',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.BROKERAGE_WITHDRAW_TYPE },
+ },
+ },
+ {
+ title: '提现信息',
+ minWidth: 200,
+ slots: { default: 'withdraw-info' },
+ },
+ {
+ field: 'createTime',
+ title: '申请时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 120,
+ },
+ {
+ title: '状态',
+ minWidth: 200,
+ slots: { default: 'status-info' },
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/brokerage/withdraw/index.vue b/apps/web-antdv-next/src/views/mall/trade/brokerage/withdraw/index.vue
new file mode 100644
index 000000000..1c635a489
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/brokerage/withdraw/index.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+ -
+
+
+
+ 账号:{{ row.userAccount }}
+
+
+ 真实姓名:{{ row.userName }}
+
+
+
+ 银行名称:{{ row.bankName }}
+
+
+ 开户地址:{{ row.bankAddress }}
+
+
+
+
+
收款码:
+
![]()
+
+
+
+
+
+
+
+
+ 时间:{{ formatDateTime(row.auditTime) }}
+
+
+ 审核原因:{{ row.auditReason }}
+
+
+ 转账失败原因:{{ row.transferErrorMsg }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/config/data.ts b/apps/web-antdv-next/src/views/mall/trade/config/data.ts
new file mode 100644
index 000000000..564f9722c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/config/data.ts
@@ -0,0 +1,242 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+export const schema: VbenFormSchema[] = [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'type',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'afterSaleRefundReasons',
+ label: '退款理由',
+ component: 'Select',
+ componentProps: {
+ mode: 'tags',
+ placeholder: '请直接输入退款理由',
+ class: 'w-full',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'afterSale',
+ },
+ },
+ {
+ fieldName: 'afterSaleReturnReasons',
+ label: '退货理由',
+ component: 'Select',
+ componentProps: {
+ mode: 'tags',
+ placeholder: '请直接输入退货理由',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'afterSale',
+ },
+ },
+ {
+ fieldName: 'deliveryExpressFreeEnabled',
+ label: '启用包邮',
+ component: 'Switch',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'delivery',
+ },
+ help: '商城是否启用全场包邮',
+ },
+ {
+ fieldName: 'deliveryExpressFreePrice',
+ label: '满额包邮',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入满额包邮金额',
+ class: 'w-full',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'delivery',
+ },
+ help: '商城商品满多少金额即可包邮,单位:元',
+ },
+ {
+ fieldName: 'deliveryPickUpEnabled',
+ label: '启用门店自提',
+ component: 'Switch',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'delivery',
+ },
+ },
+ {
+ fieldName: 'brokerageEnabled',
+ label: '启用分佣',
+ component: 'Switch',
+ help: '商城是否开启分销模式',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ },
+ {
+ fieldName: 'brokerageEnabledCondition',
+ label: '分佣模式',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.BROKERAGE_ENABLED_CONDITION, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '人人分销:每个用户都可以成为推广员 \n 指定分销:仅可在后台手动设置推广员',
+ },
+ {
+ fieldName: 'brokerageBindMode',
+ label: '分销关系绑定',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.BROKERAGE_BIND_MODE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '首次绑定:只要用户没有推广人,随时都可以绑定推广关系 \n 注册绑定:只有新用户注册时或首次进入系统时才可以绑定推广关系',
+ },
+ {
+ fieldName: 'brokeragePosterUrls',
+ label: '分销海报图',
+ component: 'ImageUpload',
+ componentProps: {
+ maxNumber: 9,
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '个人中心分销海报图片,建议尺寸 600x1000',
+ },
+ {
+ fieldName: 'brokerageFirstPercent',
+ label: '一级返佣比例(%)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ max: 100,
+ placeholder: '请输入一级返佣比例',
+ class: 'w-full',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '订单交易成功后给推广人返佣的百分比',
+ },
+ {
+ fieldName: 'brokerageSecondPercent',
+ label: '二级返佣比例(%)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ max: 100,
+ placeholder: '请输入二级返佣比例',
+ class: 'w-full',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '订单交易成功后给推广人的推荐人返佣的百分比',
+ },
+ {
+ fieldName: 'brokerageFrozenDays',
+ label: '佣金冻结天数',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入佣金冻结天数',
+ class: 'w-full',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天',
+ },
+ {
+ fieldName: 'brokerageWithdrawMinPrice',
+ label: '提现最低金额(元)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ placeholder: '请输入提现最低金额',
+ class: 'w-full',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '用户提现最低金额限制,单位:元',
+ },
+ {
+ fieldName: 'brokerageWithdrawFeePercent',
+ label: '提现手续费(%)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ max: 100,
+ precision: 2,
+ placeholder: '请输入提现手续费百分比',
+ class: 'w-full',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '提现手续费百分比,范围 0-100,0 为无提现手续费。例:设置 10,即收取 10% 手续费,提现10 元,到账 9 元,1 元手续费',
+ },
+ {
+ fieldName: 'brokerageWithdrawTypes',
+ label: '提现方式',
+ component: 'CheckboxGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, 'number'),
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values.type === 'brokerage',
+ },
+ help: '商城开通提现的付款方式',
+ },
+];
diff --git a/apps/web-antdv-next/src/views/mall/trade/config/index.vue b/apps/web-antdv-next/src/views/mall/trade/config/index.vue
new file mode 100644
index 000000000..d1c5dedef
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/config/index.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/express/data.ts b/apps/web-antdv-next/src/views/mall/trade/delivery/express/data.ts
new file mode 100644
index 000000000..7b3fe0512
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/express/data.ts
@@ -0,0 +1,156 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'code',
+ label: '公司编码',
+ componentProps: {
+ placeholder: '请输入快递编码',
+ },
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '公司名称',
+ componentProps: {
+ placeholder: '请输入快递名称',
+ },
+ rules: 'required',
+ },
+ {
+ component: 'ImageUpload',
+ fieldName: 'logo',
+ label: '公司 logo',
+ rules: 'required',
+ help: '推荐 180x180 图片分辨率',
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '快递公司名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入快递公司名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '快递公司编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入快递公司编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'code',
+ title: '公司编码',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '公司名称',
+ minWidth: 150,
+ },
+ {
+ field: 'logo',
+ title: '公司 logo',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/express/index.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/express/index.vue
new file mode 100644
index 000000000..ea1df37e6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/express/index.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/express/modules/form.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/express/modules/form.vue
new file mode 100644
index 000000000..16325507f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/express/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/data.ts b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/data.ts
new file mode 100644
index 000000000..0a63d725c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/data.ts
@@ -0,0 +1,228 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 计费方式列标题映射 */
+export const CHARGE_MODE_TITLE_MAP: Record<
+ number,
+ {
+ extraCountTitle: string;
+ startCountTitle: string;
+ }
+> = {
+ 1: { startCountTitle: '首件', extraCountTitle: '续件' },
+ 2: { startCountTitle: '首件重量(kg)', extraCountTitle: '续件重量(kg)' },
+ 3: { startCountTitle: '首件体积(m³)', extraCountTitle: '续件体积(m³)' },
+};
+
+/** 包邮方式列标题映射 */
+export const FREE_MODE_TITLE_MAP: Record = {
+ 1: { freeCountTitle: '包邮件数' },
+ 2: { freeCountTitle: '包邮重量(kg)' },
+ 3: { freeCountTitle: '包邮体积(m³)' },
+};
+
+/** 运费设置表格列 */
+export function useChargesColumns(
+ chargeMode = 1,
+): VxeTableGridOptions['columns'] {
+ const chargeTitleMap = CHARGE_MODE_TITLE_MAP[chargeMode];
+ return [
+ {
+ field: 'areaIds',
+ title: '区域',
+ minWidth: 300,
+ slots: { default: 'areaIds' },
+ },
+ {
+ field: 'startCount',
+ title: chargeTitleMap?.startCountTitle,
+ width: 120,
+ slots: { default: 'startCount' },
+ },
+ {
+ field: 'startPrice',
+ title: '运费(元)',
+ width: 120,
+ slots: { default: 'startPrice' },
+ },
+ {
+ field: 'extraCount',
+ title: chargeTitleMap?.extraCountTitle,
+ width: 120,
+ slots: { default: 'extraCount' },
+ },
+ {
+ field: 'extraPrice',
+ title: '续费(元)',
+ width: 120,
+ slots: { default: 'extraPrice' },
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 包邮设置表格列 */
+export function useFreesColumns(
+ chargeMode = 1,
+): VxeTableGridOptions['columns'] {
+ const freeTitleMap = FREE_MODE_TITLE_MAP[chargeMode];
+ return [
+ {
+ field: 'areaIds',
+ title: '区域',
+ minWidth: 300,
+ slots: { default: 'areaIds' },
+ },
+ {
+ field: 'freeCount',
+ title: freeTitleMap?.freeCountTitle,
+ width: 120,
+ slots: { default: 'freeCount' },
+ },
+ {
+ field: 'freePrice',
+ title: '包邮金额(元)',
+ width: 120,
+ slots: { default: 'freePrice' },
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '模板名称',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'chargeMode',
+ label: '计费方式',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(1),
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入显示顺序',
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'charges',
+ label: '运费设置',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'frees',
+ label: '包邮设置',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'chargeMode',
+ label: '计费方式',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择计费方式',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE, 'number'),
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '模板名称',
+ minWidth: 200,
+ },
+ {
+ field: 'chargeMode',
+ title: '计费方式',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.EXPRESS_CHARGE_MODE },
+ },
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/index.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/index.vue
new file mode 100644
index 000000000..1a9400233
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/charge-item-form.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/charge-item-form.vue
new file mode 100644
index 000000000..77427ca89
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/charge-item-form.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/form.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/form.vue
new file mode 100644
index 000000000..13a9d2435
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/form.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/free-item-form.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/free-item-form.vue
new file mode 100644
index 000000000..1c8531add
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/expressTemplate/modules/free-item-form.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpOrder/data.ts b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpOrder/data.ts
new file mode 100644
index 000000000..f9c8823a1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpOrder/data.ts
@@ -0,0 +1,162 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
+
+import { ref } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { useUserStore } from '@vben/stores';
+
+import { getSimpleDeliveryPickUpStoreList } from '#/api/mall/trade/delivery/pickUpStore';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+const userStore = useUserStore();
+const pickUpStoreList = ref(
+ [],
+);
+getSimpleDeliveryPickUpStoreList().then((res) => {
+ pickUpStoreList.value = res;
+ // 移除自己无法核销的门店
+ const userId = userStore?.userInfo?.id;
+ pickUpStoreList.value = pickUpStoreList.value.filter((item) =>
+ item.verifyUserIds?.includes(userId),
+ );
+});
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'pickUpStoreIds',
+ label: '自提门店',
+ component: 'Select',
+ componentProps: {
+ options: pickUpStoreList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择自提门店',
+ },
+ defaultValue: pickUpStoreList.value[0]?.id,
+ },
+ {
+ fieldName: 'no',
+ label: '订单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '用户 UID',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户 UID',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userNickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userMobile',
+ label: '用户电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户电话',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ field: 'no',
+ title: '订单号',
+ fixed: 'left',
+ minWidth: 180,
+ },
+ {
+ field: 'user.nickname',
+ title: '用户信息',
+ minWidth: 100,
+ },
+ {
+ field: 'brokerageUser.nickname',
+ title: '推荐人信息',
+ minWidth: 100,
+ },
+ {
+ field: 'spuName',
+ title: '商品信息',
+ minWidth: 300,
+ slots: { default: 'spuName' },
+ },
+ {
+ field: 'payPrice',
+ title: '实付金额(元)',
+ formatter: 'formatAmount2',
+ minWidth: 180,
+ },
+ {
+ field: 'storeStaffName',
+ title: '核销员',
+ minWidth: 160,
+ },
+ {
+ field: 'pickUpStoreId',
+ title: '核销门店',
+ minWidth: 160,
+ formatter: ({ row }) => {
+ return (
+ pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)
+ ?.name || ''
+ );
+ },
+ },
+ {
+ field: 'payStatus',
+ title: '支付状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '订单状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.TRADE_ORDER_STATUS },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'createTime',
+ title: '下单时间',
+ formatter: 'formatDateTime',
+ minWidth: 160,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpOrder/index.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpOrder/index.vue
new file mode 100644
index 000000000..050065c10
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpOrder/index.vue
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.spuName }}
+
+
+ {{ property.propertyName }}: {{ property.valueName }}
+
+
+
+ {{ fenToYuan(item.price!) }} 元 x {{ item.count }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/data.ts b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/data.ts
new file mode 100644
index 000000000..666db4f8f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/data.ts
@@ -0,0 +1,264 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getAreaTree } from '#/api/system/area';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '门店名称',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入门店名称',
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'phone',
+ label: '门店手机',
+ rules: 'mobileRequired',
+ componentProps: {
+ placeholder: '请输入门店手机',
+ },
+ },
+ {
+ component: 'ImageUpload',
+ fieldName: 'logo',
+ label: '门店 logo',
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '请上传门店 logo',
+ },
+ help: '推荐 180x180 图片分辨率',
+ },
+ {
+ component: 'Textarea',
+ fieldName: 'introduction',
+ label: '门店简介',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '请输入门店简介',
+ rows: 4,
+ },
+ },
+ {
+ fieldName: 'areaId',
+ label: '门店所在地区',
+ component: 'ApiTreeSelect',
+ rules: 'required',
+ componentProps: {
+ api: getAreaTree,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择省市区',
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'detailAddress',
+ label: '门店详细地址',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入门店详细地址',
+ },
+ },
+ {
+ component: 'TimeRangePicker',
+ fieldName: 'rangeTime',
+ label: '营业时间',
+ rules: 'required',
+ componentProps: {
+ format: 'HH:mm',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '门店状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ component: 'Input',
+ fieldName: 'longitude',
+ label: '经度',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入门店经度',
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'latitude',
+ label: '纬度',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入门店纬度',
+ },
+ },
+ ];
+}
+
+/** 绑定店员的表单 */
+export function useBindFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '门店名称',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: true,
+ },
+ },
+ {
+ component: 'ApiSelect',
+ fieldName: 'verifyUserIds',
+ label: '门店店员',
+ rules: 'required',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ mode: 'tags',
+ allowClear: true,
+ placeholder: '请选择门店店员',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'phone',
+ label: '门店手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入门店手机',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '门店名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入门店名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '门店状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择门店状态',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 80,
+ },
+ {
+ field: 'logo',
+ title: '门店 logo',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'name',
+ title: '门店名称',
+ minWidth: 150,
+ },
+ {
+ field: 'phone',
+ title: '门店手机',
+ minWidth: 120,
+ },
+ {
+ field: 'detailAddress',
+ title: '地址',
+ minWidth: 200,
+ },
+ {
+ field: 'openingTime',
+ title: '营业时间',
+ minWidth: 160,
+ formatter: ({ row }) => {
+ return `${row.openingTime} ~ ${row.closingTime}`;
+ },
+ },
+ {
+ field: 'status',
+ title: '开启状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/index.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/index.vue
new file mode 100644
index 000000000..b2fa35a5f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/index.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue
new file mode 100644
index 000000000..7886e341d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/modules/form.vue b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/modules/form.vue
new file mode 100644
index 000000000..bd73fb5e3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/delivery/pickUpStore/modules/form.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/data.ts b/apps/web-antdv-next/src/views/mall/trade/order/data.ts
new file mode 100644
index 000000000..4eb98df87
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/data.ts
@@ -0,0 +1,450 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
+
+import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { convertToInteger, formatToFraction } from '@vben/utils';
+
+import { getSimpleDeliveryExpressList } from '#/api/mall/trade/delivery/express';
+import { getSimpleDeliveryPickUpStoreList } from '#/api/mall/trade/delivery/pickUpStore';
+import { getAreaTree } from '#/api/system/area';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let pickUpStoreList: MallDeliveryPickUpStoreApi.DeliveryPickUpStore[] = [];
+getSimpleDeliveryPickUpStoreList().then((data) => {
+ pickUpStoreList = data;
+});
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'status',
+ label: '订单状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TRADE_ORDER_STATUS, 'number'),
+ placeholder: '请选择订单状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'payChannelCode',
+ label: '支付方式',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'number'),
+ placeholder: '请选择支付方式',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'terminal',
+ label: '订单来源',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TERMINAL, 'number'),
+ placeholder: '请选择订单来源',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'deliveryType',
+ label: '配送方式',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE, 'number'),
+ placeholder: '请选择配送方式',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'logisticsId',
+ label: '快递公司',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeliveryExpressList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择快递公司',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['deliveryType'],
+ show: (values) => values.deliveryType === DeliveryTypeEnum.EXPRESS.type,
+ },
+ },
+ {
+ fieldName: 'pickUpStoreId',
+ label: '自提门店',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeliveryPickUpStoreList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择自提门店',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['deliveryType'],
+ show: (values) => values.deliveryType === DeliveryTypeEnum.PICK_UP.type,
+ },
+ },
+ {
+ fieldName: 'pickUpVerifyCode',
+ label: '核销码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入核销码',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['deliveryType'],
+ show: (values) => values.deliveryType === DeliveryTypeEnum.PICK_UP.type,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '订单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入订单号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '用户 UID',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户 UID',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userNickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userMobile',
+ label: '用户电话',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户电话',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ type: 'expand',
+ width: 80,
+ slots: { content: 'expand_content' },
+ fixed: 'left',
+ },
+ {
+ field: 'no',
+ title: '订单号',
+ fixed: 'left',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '下单时间',
+ formatter: 'formatDateTime',
+ minWidth: 160,
+ },
+ {
+ field: 'terminal',
+ title: '订单来源',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.TERMINAL },
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'payChannelCode',
+ title: '支付方式',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
+ },
+ minWidth: 120,
+ },
+ {
+ field: 'payTime',
+ title: '支付时间',
+ formatter: 'formatDateTime',
+ minWidth: 160,
+ },
+ {
+ field: 'type',
+ title: '订单类型',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.TRADE_ORDER_TYPE },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'payPrice',
+ title: '实际支付',
+ formatter: 'formatAmount2',
+ minWidth: 180,
+ },
+ {
+ field: 'user',
+ title: '买家/收货人',
+ formatter: ({ row }) => {
+ if (row.deliveryType === DeliveryTypeEnum.EXPRESS.type) {
+ return `买家:${row.user?.nickname} / 收货人: ${row.receiverName} ${row.receiverMobile}${row.receiverAreaName}${row.receiverDetailAddress}`;
+ }
+ if (row.deliveryType === DeliveryTypeEnum.PICK_UP.type) {
+ return `门店名称:${pickUpStoreList.find((item) => item.id === row.pickUpStoreId)?.name} /
+ 门店手机:${pickUpStoreList.find((item) => item.id === row.pickUpStoreId)?.phone} /
+ 自提门店:${pickUpStoreList.find((item) => item.id === row.pickUpStoreId)?.detailAddress}
+ `;
+ }
+ return '';
+ },
+ minWidth: 180,
+ },
+ {
+ field: 'deliveryType',
+ title: '配送方式',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.TRADE_DELIVERY_TYPE },
+ },
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '订单状态',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.TRADE_ORDER_STATUS },
+ },
+ minWidth: 80,
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 订单备注表单配置 */
+export function useRemarkFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Input',
+ componentProps: {
+ type: 'textarea',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 订单调价表单配置 */
+export function usePriceFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'payPrice',
+ label: '应付金额(总)',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入应付金额(总)',
+ disabled: true,
+ formatter: (value: string) => `${value}元`,
+ },
+ },
+ {
+ fieldName: 'adjustPrice',
+ label: '订单调价',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入订单调价',
+ step: 0.1,
+ precision: 2,
+ },
+ help: '订单调价。 正数,加价;负数,减价',
+ rules: 'required',
+ },
+ {
+ fieldName: 'newPayPrice',
+ label: '调价后',
+ component: 'Input',
+ componentProps: {
+ placeholder: '',
+ formatter: (value: string) => `${value}元`,
+ },
+ dependencies: {
+ triggerFields: ['payPrice', 'adjustPrice'],
+ disabled: true,
+ trigger(values, form) {
+ const originalPrice = convertToInteger(values.payPrice);
+ const adjustPrice = convertToInteger(values.adjustPrice);
+ const newPrice = originalPrice + adjustPrice;
+ form.setFieldValue('newPayPrice', formatToFraction(newPrice));
+ },
+ },
+ },
+ ];
+}
+
+/** 订单修改地址表单配置 */
+export function useAddressFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'receiverName',
+ label: '收件人',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入收件人名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'receiverMobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入收件人手机号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'receiverAreaId',
+ label: '所在地',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: getAreaTree,
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择收件人所在地',
+ treeDefaultExpandAll: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'receiverDetailAddress',
+ label: '详细地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入收件人详细地址',
+ type: 'textarea',
+ rows: 3,
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 订单发货表单配置 */
+export function useDeliveryFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'expressType',
+ label: '发货方式',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '快递', value: 'express' },
+ { label: '无需发货', value: 'none' },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: 'express',
+ },
+ {
+ fieldName: 'logisticsId',
+ label: '物流公司',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleDeliveryExpressList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择物流公司',
+ },
+ dependencies: {
+ triggerFields: ['expressType'],
+ show: (values) => values.expressType === 'express',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'logisticsNo',
+ label: '物流单号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入物流单号',
+ },
+ dependencies: {
+ triggerFields: ['expressType'],
+ show: (values) => values.expressType === 'express',
+ },
+ rules: 'required',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/detail/data.ts b/apps/web-antdv-next/src/views/mall/trade/order/detail/data.ts
new file mode 100644
index 000000000..febf4ed5e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/detail/data.ts
@@ -0,0 +1,255 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { fenToYuan, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 订单基础信息 schema */
+export function useOrderInfoSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'no',
+ label: '订单号',
+ },
+ {
+ field: 'user.nickname',
+ label: '买家',
+ },
+ {
+ field: 'type',
+ label: '订单类型',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_ORDER_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'terminal',
+ label: '订单来源',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TERMINAL,
+ value: val,
+ }),
+ },
+ {
+ field: 'userRemark',
+ label: '买家留言',
+ },
+ {
+ field: 'remark',
+ label: '商家备注',
+ },
+ {
+ field: 'payOrderId',
+ label: '支付单号',
+ },
+ {
+ field: 'payChannelCode',
+ label: '付款方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_CHANNEL_CODE,
+ value: val,
+ }),
+ },
+ {
+ field: 'brokerageUser.nickname',
+ label: '推广用户',
+ },
+ ];
+}
+
+/** 订单状态信息 schema */
+export function useOrderStatusSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'status',
+ label: '订单状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_ORDER_STATUS,
+ value: val,
+ }),
+ },
+ {
+ field: 'reminder',
+ label: '提醒',
+ render: () =>
+ h('div', { class: 'space-y-1' }, [
+ h('div', '买家付款成功后,货款将直接进入您的商户号(微信、支付宝)'),
+ h('div', '请及时关注你发出的包裹状态,确保可以配送至买家手中'),
+ h(
+ 'div',
+ '如果买家表示没收到货或货物有问题,请及时联系买家处理,友好协商',
+ ),
+ ]),
+ },
+ ];
+}
+
+/** 订单金额信息 schema */
+export function useOrderPriceSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'totalPrice',
+ label: '商品总额',
+ render: (val) => `${fenToYuan(val ?? 0)} 元`,
+ },
+ {
+ field: 'deliveryPrice',
+ label: '运费金额',
+ render: (val) => `${fenToYuan(val ?? 0)} 元`,
+ },
+ {
+ field: 'adjustPrice',
+ label: '订单调价',
+ render: (val) => `${fenToYuan(val ?? 0)} 元`,
+ },
+ {
+ field: 'couponPrice',
+ label: '优惠劵优惠',
+ render: (val) =>
+ h('span', { class: 'text-red-500' }, `${fenToYuan(val ?? 0)} 元`),
+ },
+ {
+ field: 'vipPrice',
+ label: 'VIP 优惠',
+ render: (val) =>
+ h('span', { class: 'text-red-500' }, `${fenToYuan(val ?? 0)} 元`),
+ },
+ {
+ field: 'discountPrice',
+ label: '活动优惠',
+ render: (val) =>
+ h('span', { class: 'text-red-500' }, `${fenToYuan(val ?? 0)} 元`),
+ },
+ {
+ field: 'pointPrice',
+ label: '积分抵扣',
+ render: (val) =>
+ h('span', { class: 'text-red-500' }, `${fenToYuan(val ?? 0)} 元`),
+ },
+ {
+ field: 'payPrice',
+ label: '应付金额',
+ render: (val) => `${fenToYuan(val ?? 0)} 元`,
+ },
+ ];
+}
+
+/** 收货信息 schema */
+export function useDeliveryInfoSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'deliveryType',
+ label: '配送方式',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.TRADE_DELIVERY_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'receiverName',
+ label: '收货人',
+ },
+ {
+ field: 'receiverMobile',
+ label: '联系电话',
+ },
+ {
+ field: 'receiverAddress',
+ label: '收货地址',
+ render: (val, data) => `${data?.receiverAreaName} ${val}`.trim(),
+ },
+ {
+ field: 'deliveryTime',
+ label: '发货时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
+
+/** 商品信息 columns */
+export function useProductColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'spuName',
+ title: '商品',
+ minWidth: 300,
+ slots: { default: 'spuName' },
+ },
+ {
+ field: 'price',
+ title: '商品原价',
+ width: 150,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'count',
+ title: '数量',
+ width: 100,
+ },
+ {
+ field: 'payPrice',
+ title: '合计',
+ width: 150,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'afterSaleStatus',
+ title: '售后状态',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS },
+ },
+ },
+ ];
+}
+
+/** 物流详情 columns */
+export function useExpressTrackColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'time',
+ title: '时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'content',
+ title: '物流状态',
+ minWidth: 300,
+ },
+ ];
+}
+
+/** 操作日志 columns */
+export function useOperateLogColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'createTime',
+ title: '操作时间',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'userType',
+ title: '操作人',
+ width: 100,
+ slots: { default: 'userType' },
+ },
+ {
+ field: 'content',
+ title: '操作内容',
+ minWidth: 200,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/detail/index.vue b/apps/web-antdv-next/src/views/mall/trade/order/detail/index.vue
new file mode 100644
index 000000000..4fdd3d2bb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/detail/index.vue
@@ -0,0 +1,349 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ row.spuName }}
+
+
+ {{ property.propertyName }}: {{ property.valueName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 系统
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/index.vue b/apps/web-antdv-next/src/views/mall/trade/order/index.vue
new file mode 100644
index 000000000..353bf3809
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/index.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.spuName }}
+
+ {{ property.propertyName }} : {{ property.valueName }}
+
+
+
+
+
+
+ {{
+ `原价:${fenToYuan(item.price)} 元 / 数量:${item.count} 个`
+ }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/modules/address-form.vue b/apps/web-antdv-next/src/views/mall/trade/order/modules/address-form.vue
new file mode 100644
index 000000000..478fc42ad
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/modules/address-form.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/modules/delivery-form.vue b/apps/web-antdv-next/src/views/mall/trade/order/modules/delivery-form.vue
new file mode 100644
index 000000000..a3bf33d96
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/modules/delivery-form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/modules/price-form.vue b/apps/web-antdv-next/src/views/mall/trade/order/modules/price-form.vue
new file mode 100644
index 000000000..9355a0fc7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/modules/price-form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mall/trade/order/modules/remark-form.vue b/apps/web-antdv-next/src/views/mall/trade/order/modules/remark-form.vue
new file mode 100644
index 000000000..61b5e6fb5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mall/trade/order/modules/remark-form.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/config/data.ts b/apps/web-antdv-next/src/views/member/config/data.ts
new file mode 100644
index 000000000..7f168418c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/config/data.ts
@@ -0,0 +1,52 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+export const schema: VbenFormSchema[] = [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Switch',
+ fieldName: 'pointTradeDeductEnable',
+ label: '积分抵扣',
+ help: '下单积分是否抵用订单金额',
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'pointTradeDeductUnitPrice',
+ label: '积分抵扣',
+ help: '积分抵用比例(1 积分抵多少金额),单位:元',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ class: 'w-full',
+ placeholder: '请输入积分抵扣单价',
+ },
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'pointTradeDeductMaxPrice',
+ label: '积分抵扣最大值',
+ help: '单次下单积分使用上限,0 不限制',
+ componentProps: {
+ min: 0,
+ class: 'w-full',
+ placeholder: '请输入积分抵扣最大值',
+ },
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'pointTradeGivePoint',
+ label: '1 元赠送多少分',
+ help: '下单支付金额按比例赠送积分(实际支付 1 元赠送多少积分)',
+ componentProps: {
+ min: 0,
+ class: 'w-full',
+ placeholder: '请输入赠送积分比例',
+ },
+ },
+];
diff --git a/apps/web-antdv-next/src/views/member/config/index.vue b/apps/web-antdv-next/src/views/member/config/index.vue
new file mode 100644
index 000000000..3c5e0407b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/config/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/group/data.ts b/apps/web-antdv-next/src/views/member/group/data.ts
new file mode 100644
index 000000000..973dd09c8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/group/data.ts
@@ -0,0 +1,126 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '分组名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分组名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '分组名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入分组名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ placeholder: ['开始日期', '结束日期'],
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '分组名称',
+ minWidth: 150,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/group/index.vue b/apps/web-antdv-next/src/views/member/group/index.vue
new file mode 100644
index 000000000..095a95d56
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/group/index.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/group/modules/form.vue b/apps/web-antdv-next/src/views/member/group/modules/form.vue
new file mode 100644
index 000000000..3d837f905
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/group/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/level/data.ts b/apps/web-antdv-next/src/views/member/level/data.ts
new file mode 100644
index 000000000..979ed9a1b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/level/data.ts
@@ -0,0 +1,188 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '等级名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入等级名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'level',
+ label: '等级',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ placeholder: '请输入等级',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'experience',
+ label: '升级经验',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ placeholder: '请输入升级经验',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'discountPercent',
+ label: '享受折扣(%)',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ max: 100,
+ precision: 0,
+ placeholder: '请输入享受折扣',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'icon',
+ label: '等级图标',
+ component: 'ImageUpload',
+ },
+ {
+ fieldName: 'backgroundUrl',
+ label: '等级背景图',
+ component: 'ImageUpload',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '等级名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入等级名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '等级编号',
+ minWidth: 80,
+ },
+ {
+ field: 'icon',
+ title: '等级图标',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'backgroundUrl',
+ title: '等级背景图',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'name',
+ title: '等级名称',
+ minWidth: 120,
+ },
+ {
+ field: 'level',
+ title: '等级',
+ minWidth: 80,
+ },
+ {
+ field: 'experience',
+ title: '升级经验',
+ minWidth: 100,
+ },
+ {
+ field: 'discountPercent',
+ title: '享受折扣(%)',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/level/index.vue b/apps/web-antdv-next/src/views/member/level/index.vue
new file mode 100644
index 000000000..f3032f835
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/level/index.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/level/modules/form.vue b/apps/web-antdv-next/src/views/member/level/modules/form.vue
new file mode 100644
index 000000000..6b7c094c1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/level/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/point/record/data.ts b/apps/web-antdv-next/src/views/member/point/record/data.ts
new file mode 100644
index 000000000..4e0f54945
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/point/record/data.ts
@@ -0,0 +1,121 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { Tag } from 'ant-design-vue';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'nickname',
+ label: '用户',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bizType',
+ label: '业务类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择业务类型',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '积分标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入积分标题',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createDate',
+ label: '获得时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '获得时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'nickname',
+ title: '用户',
+ minWidth: 150,
+ },
+ {
+ field: 'point',
+ title: '获得积分',
+ minWidth: 120,
+ slots: {
+ default: ({ row }) => {
+ return h(
+ Tag,
+ {
+ color: row.point > 0 ? 'blue' : 'red',
+ },
+ () => (row.point > 0 ? `+${row.point}` : row.point),
+ );
+ },
+ },
+ },
+ {
+ field: 'totalPoint',
+ title: '总积分',
+ minWidth: 100,
+ },
+ {
+ field: 'title',
+ title: '标题',
+ minWidth: 200,
+ },
+ {
+ field: 'description',
+ title: '描述',
+ minWidth: 200,
+ },
+ {
+ field: 'bizId',
+ title: '业务编码',
+ minWidth: 120,
+ },
+ {
+ field: 'bizType',
+ title: '业务类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MEMBER_POINT_BIZ_TYPE },
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/point/record/index.vue b/apps/web-antdv-next/src/views/member/point/record/index.vue
new file mode 100644
index 000000000..cbc15c614
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/point/record/index.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/signin/config/data.ts b/apps/web-antdv-next/src/views/member/signin/config/data.ts
new file mode 100644
index 000000000..2b28d2fc9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/signin/config/data.ts
@@ -0,0 +1,104 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'day',
+ label: '签到天数',
+ help: '只允许设置 1-7,默认签到 7 天为一个周期',
+ componentProps: {
+ min: 1,
+ max: 7,
+ precision: 0,
+ placeholder: '请输入签到天数',
+ },
+ rules: z.number().min(1).max(7, '签到天数必须在 1-7 之间'),
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'point',
+ label: '获得积分',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ placeholder: '请输入获得积分',
+ },
+ rules: z.number().min(0, '获得积分不能小于 0'),
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'experience',
+ label: '奖励经验',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ placeholder: '请输入奖励经验',
+ },
+ rules: z.number().min(0, '奖励经验不能小于 0'),
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'day',
+ title: '签到天数',
+ minWidth: 120,
+ formatter: ({ cellValue }) => ['第', cellValue, '天'].join(' '),
+ },
+ {
+ field: 'point',
+ title: '获得积分',
+ minWidth: 120,
+ },
+ {
+ field: 'experience',
+ title: '奖励经验',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/signin/config/index.vue b/apps/web-antdv-next/src/views/member/signin/config/index.vue
new file mode 100644
index 000000000..cc68b8a21
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/signin/config/index.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/signin/config/modules/form.vue b/apps/web-antdv-next/src/views/member/signin/config/modules/form.vue
new file mode 100644
index 000000000..0864461f6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/signin/config/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/signin/record/data.ts b/apps/web-antdv-next/src/views/member/signin/record/data.ts
new file mode 100644
index 000000000..5449b888c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/signin/record/data.ts
@@ -0,0 +1,86 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { h } from 'vue';
+
+import { Tag } from 'ant-design-vue';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'nickname',
+ label: '签到用户',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入签到用户',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'day',
+ label: '签到天数',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入签到天数',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '签到时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'nickname',
+ title: '签到用户',
+ minWidth: 150,
+ },
+ {
+ field: 'day',
+ title: '签到天数',
+ minWidth: 120,
+ formatter: ({ cellValue }) => ['第', cellValue, '天'].join(' '),
+ },
+ {
+ field: 'point',
+ title: '获得积分',
+ minWidth: 120,
+ slots: {
+ default: ({ row }) => {
+ return h(
+ Tag,
+ {
+ class: 'mr-5px',
+ color: row.point > 0 ? 'blue' : 'red',
+ },
+ () => (row.point > 0 ? `+${row.point}` : row.point),
+ );
+ },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '签到时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/signin/record/index.vue b/apps/web-antdv-next/src/views/member/signin/record/index.vue
new file mode 100644
index 000000000..d5a36b78b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/signin/record/index.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/tag/data.ts b/apps/web-antdv-next/src/views/member/tag/data.ts
new file mode 100644
index 000000000..36f3c20e8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/tag/data.ts
@@ -0,0 +1,79 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '标签名称',
+ componentProps: {
+ placeholder: '请输入标签名称',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '标签名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入标签名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ placeholder: ['开始日期', '结束日期'],
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '标签名称',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/tag/index.vue b/apps/web-antdv-next/src/views/member/tag/index.vue
new file mode 100644
index 000000000..084d3ca81
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/tag/index.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/tag/modules/form.vue b/apps/web-antdv-next/src/views/member/tag/modules/form.vue
new file mode 100644
index 000000000..6d1df3a22
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/tag/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/data.ts b/apps/web-antdv-next/src/views/member/user/data.ts
new file mode 100644
index 000000000..295b23e89
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/data.ts
@@ -0,0 +1,500 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { h } from 'vue';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { convertToInteger, formatToFraction } from '@vben/utils';
+
+import { Tag } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { getSimpleGroupList } from '#/api/member/group';
+import { getSimpleLevelList } from '#/api/member/level';
+import { getSimpleTagList } from '#/api/member/tag';
+import { getAreaTree } from '#/api/system/area';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE).optional(),
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ },
+ },
+ {
+ fieldName: 'avatar',
+ label: '头像',
+ component: 'ImageUpload',
+ },
+ {
+ fieldName: 'name',
+ label: '真实名字',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入真实名字',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '用户性别',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ fieldName: 'birthday',
+ label: '出生日期',
+ component: 'DatePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ placeholder: '请选择出生日期',
+ },
+ },
+ {
+ fieldName: 'areaId',
+ label: '所在地',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: getAreaTree,
+ fieldNames: { label: 'name', value: 'id', children: 'children' },
+ placeholder: '请选择所在地',
+ },
+ },
+ {
+ fieldName: 'tagIds',
+ label: '用户标签',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleTagList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择用户标签',
+ },
+ },
+ {
+ fieldName: 'groupId',
+ label: '用户分组',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleGroupList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择用户分组',
+ },
+ },
+ {
+ fieldName: 'mark',
+ label: '会员备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入会员备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'loginDate',
+ label: '登录时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '注册时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'tagIds',
+ label: '用户标签',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleTagList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择用户标签',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'levelId',
+ label: '用户等级',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleLevelList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择用户等级',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'groupId',
+ label: '用户分组',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleGroupList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择用户分组',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ type: 'checkbox',
+ width: 50,
+ },
+ {
+ field: 'id',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'avatar',
+ title: '头像',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'mobile',
+ title: '手机号',
+ minWidth: 120,
+ },
+ {
+ field: 'nickname',
+ title: '昵称',
+ minWidth: 120,
+ },
+ {
+ field: 'levelName',
+ title: '等级',
+ minWidth: 100,
+ },
+ {
+ field: 'groupName',
+ title: '分组',
+ minWidth: 100,
+ },
+ {
+ field: 'tagNames',
+ title: '用户标签',
+ minWidth: 150,
+ slots: {
+ default: ({ row }) => {
+ return row.tagNames?.map((tagName: string, index: number) => {
+ return h(
+ Tag,
+ {
+ key: index,
+ class: 'mr-1',
+ color: 'blue',
+ },
+ () => tagName,
+ );
+ });
+ },
+ },
+ },
+ {
+ field: 'point',
+ title: '积分',
+ minWidth: 80,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'loginDate',
+ title: '登录时间',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '注册时间',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 修改用户等级 */
+export function useLevelFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'levelId',
+ label: '用户等级',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleLevelList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择用户等级',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'reason',
+ label: '修改原因',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入修改原因',
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 修改用户余额 */
+export function useBalanceFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'balance',
+ label: '变动前余额(元)',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'changeType',
+ label: '变动类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '增加', value: 1 },
+ { label: '减少', value: -1 },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ },
+ {
+ fieldName: 'changeBalance',
+ label: '变动余额(元)',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.1,
+ placeholder: '请输入变动余额',
+ },
+ defaultValue: 0,
+ },
+ {
+ fieldName: 'balanceResult',
+ label: '变动后余额(元)',
+ component: 'Input',
+ dependencies: {
+ triggerFields: ['balance', 'changeBalance', 'changeType'],
+ disabled: true,
+ trigger(values, form) {
+ form.setFieldValue(
+ 'balanceResult',
+ formatToFraction(
+ convertToInteger(values.balance) +
+ convertToInteger(values.changeBalance) * values.changeType,
+ ),
+ );
+ },
+ },
+ },
+ ];
+}
+
+/** 修改用户积分 */
+export function usePointFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'point',
+ label: '变动前积分',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'changeType',
+ label: '变动类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '增加', value: 1 },
+ { label: '减少', value: -1 },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: 1,
+ },
+ {
+ fieldName: 'changePoint',
+ label: '变动积分',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ min: 0,
+ precision: 0,
+ placeholder: '请输入变动积分',
+ },
+ },
+ {
+ fieldName: 'pointResult',
+ label: '变动后积分',
+ component: 'Input',
+ componentProps: {
+ placeholder: '',
+ },
+ dependencies: {
+ triggerFields: ['point', 'changePoint', 'changeType'],
+ disabled: true,
+ trigger(values, form) {
+ form.setFieldValue(
+ 'pointResult',
+ values.point + values.changePoint * values.changeType ||
+ values.point,
+ );
+ },
+ },
+ rules: z.number().min(0),
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/member/user/detail/index.vue b/apps/web-antdv-next/src/views/member/user/detail/index.vue
new file mode 100644
index 000000000..f0def1afb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/index.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+ 基本信息
+
+
+
+
+
+ 账户信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/account-info.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/account-info.vue
new file mode 100644
index 000000000..1bc7a0c9c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/account-info.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/address-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/address-list.vue
new file mode 100644
index 000000000..488058e07
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/address-list.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/after-sale-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/after-sale-list.vue
new file mode 100644
index 000000000..db51216ac
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/after-sale-list.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ row.spuName }}
+
+
+ {{ property.propertyName }}: {{ property.valueName }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/balance-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/balance-list.vue
new file mode 100644
index 000000000..86b148ce7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/balance-list.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/basic-info.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/basic-info.vue
new file mode 100644
index 000000000..c3a8b4294
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/basic-info.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/brokerage-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/brokerage-list.vue
new file mode 100644
index 000000000..5adb98f20
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/brokerage-list.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/coupon-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/coupon-list.vue
new file mode 100644
index 000000000..5d64482fa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/coupon-list.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/experience-record-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/experience-record-list.vue
new file mode 100644
index 000000000..770bdef76
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/experience-record-list.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/favorite-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/favorite-list.vue
new file mode 100644
index 000000000..7fa7231c7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/favorite-list.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/order-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/order-list.vue
new file mode 100644
index 000000000..23b5e1b30
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/order-list.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+ {{ item.spuName }}
+
+ {{ property.propertyName }} : {{ property.valueName }}
+
+
+
+
+
+
+ {{
+ `原价:${fenToYuan(item.price)} 元 / 数量:${item.count} 个`
+ }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/point-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/point-list.vue
new file mode 100644
index 000000000..40ef48271
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/point-list.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/detail/modules/sign-list.vue b/apps/web-antdv-next/src/views/member/user/detail/modules/sign-list.vue
new file mode 100644
index 000000000..afede6537
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/detail/modules/sign-list.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/index.vue b/apps/web-antdv-next/src/views/member/user/index.vue
new file mode 100644
index 000000000..1c25480d5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/index.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/modules/balance-form.vue b/apps/web-antdv-next/src/views/member/user/modules/balance-form.vue
new file mode 100644
index 000000000..6f13bd0ab
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/modules/balance-form.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/modules/form.vue b/apps/web-antdv-next/src/views/member/user/modules/form.vue
new file mode 100644
index 000000000..69a48156a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/modules/level-form.vue b/apps/web-antdv-next/src/views/member/user/modules/level-form.vue
new file mode 100644
index 000000000..2a2c8b2ca
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/modules/level-form.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/member/user/modules/point-form.vue b/apps/web-antdv-next/src/views/member/user/modules/point-form.vue
new file mode 100644
index 000000000..da6c88e8d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/member/user/modules/point-form.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/account/data.ts b/apps/web-antdv-next/src/views/mp/account/data.ts
new file mode 100644
index 000000000..eb57a796a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/account/data.ts
@@ -0,0 +1,143 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入名称',
+ },
+ },
+ {
+ fieldName: 'account',
+ label: '微信号',
+ component: 'Input',
+ help: '在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 账号详情] 中能找到「微信号」',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入微信号',
+ },
+ },
+ {
+ fieldName: 'appId',
+ label: 'appId',
+ component: 'Input',
+ help: '在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者ID(AppID)」',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入公众号 appId',
+ },
+ },
+ {
+ fieldName: 'appSecret',
+ label: 'appSecret',
+ component: 'Input',
+ help: '在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者密码(AppSecret)」',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入公众号 appSecret',
+ },
+ },
+ {
+ fieldName: 'token',
+ label: 'token',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入公众号 token',
+ },
+ },
+ {
+ fieldName: 'aesKey',
+ label: '消息加解密密钥',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入消息加解密密钥',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 搜索表单配置 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入名称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ title: '名称',
+ field: 'name',
+ minWidth: 150,
+ },
+ {
+ title: '微信号',
+ field: 'account',
+ minWidth: 180,
+ },
+ {
+ title: 'appId',
+ field: 'appId',
+ minWidth: 180,
+ },
+ {
+ title: '服务器地址(URL)',
+ field: 'utl',
+ minWidth: 360,
+ slots: {
+ default: ({ row }) => {
+ return `http://服务端地址/admin-api/mp/open/${row.appId}`;
+ },
+ },
+ },
+ {
+ title: '二维码',
+ field: 'qrCodeUrl',
+ minWidth: 120,
+ cellRender: { name: 'CellImage' },
+ },
+ {
+ title: '备注',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/account/index.vue b/apps/web-antdv-next/src/views/mp/account/index.vue
new file mode 100644
index 000000000..27bd9b496
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/account/index.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/account/modules/form.vue b/apps/web-antdv-next/src/views/mp/account/modules/form.vue
new file mode 100644
index 000000000..cb2b79ed3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/account/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/autoReply/data.ts b/apps/web-antdv-next/src/views/mp/autoReply/data.ts
new file mode 100644
index 000000000..2f8c3e497
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/autoReply/data.ts
@@ -0,0 +1,147 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+
+import { markRaw } from 'vue';
+
+import {
+ AutoReplyMsgType,
+ DICT_TYPE,
+ RequestMessageTypes,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { WxReply } from '#/views/mp/components';
+
+/** 获取表格列配置 */
+export function useGridColumns(
+ msgType: AutoReplyMsgType,
+): VxeGridPropTypes.Columns {
+ const columns: VxeGridPropTypes.Columns = [];
+ // 请求消息类型列(仅消息回复显示)
+ if (msgType === AutoReplyMsgType.Message) {
+ columns.push({
+ field: 'requestMessageType',
+ title: '请求消息类型',
+ minWidth: 120,
+ });
+ }
+ // 关键词列(仅关键词回复显示)
+ if (msgType === AutoReplyMsgType.Keyword) {
+ columns.push({
+ field: 'requestKeyword',
+ title: '关键词',
+ minWidth: 150,
+ });
+ }
+ // 匹配类型列(仅关键词回复显示)
+ if (msgType === AutoReplyMsgType.Keyword) {
+ columns.push({
+ field: 'requestMatch',
+ title: '匹配类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH },
+ },
+ });
+ }
+ // 回复消息类型列
+ columns.push(
+ {
+ field: 'responseMessageType',
+ title: '回复消息类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MP_MESSAGE_TYPE },
+ },
+ },
+ {
+ field: 'responseContent',
+ title: '回复内容',
+ minWidth: 200,
+ slots: { default: 'replyContent' },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 140,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ );
+ return columns;
+}
+
+/** 新增/修改的表单 */
+export function useFormSchema(msgType: AutoReplyMsgType): VbenFormSchema[] {
+ const schema: VbenFormSchema[] = [];
+ // 消息类型(仅消息回复显示)
+ if (msgType === AutoReplyMsgType.Message) {
+ schema.push({
+ fieldName: 'requestMessageType',
+ label: '消息类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择',
+ options: getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE).filter((d) =>
+ RequestMessageTypes.has(d.value as string),
+ ),
+ },
+ });
+ }
+ // 匹配类型(仅关键词回复显示)
+ if (msgType === AutoReplyMsgType.Keyword) {
+ schema.push({
+ fieldName: 'requestMatch',
+ label: '匹配类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择匹配类型',
+ allowClear: true,
+ options: getDictOptions(
+ DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH,
+ 'number',
+ ),
+ },
+ rules: 'required',
+ });
+ }
+ // 关键词(仅关键词回复显示)
+ if (msgType === AutoReplyMsgType.Keyword) {
+ schema.push({
+ fieldName: 'requestKeyword',
+ label: '关键词',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入内容',
+ allowClear: true,
+ },
+ rules: 'required',
+ });
+ }
+ // 回复消息
+ schema.push({
+ fieldName: 'reply',
+ label: '回复消息',
+ component: markRaw(WxReply),
+ modelPropName: 'modelValue',
+ });
+ return schema;
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/autoReply/index.vue b/apps/web-antdv-next/src/views/mp/autoReply/index.vue
new file mode 100644
index 000000000..e4862a60e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/autoReply/index.vue
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onTabChange(activeKey as string)"
+ >
+
+
+
+ 关注时回复
+
+
+
+
+
+
+
+ 消息回复
+
+
+
+
+
+
+
+ 关键词回复
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/autoReply/modules/content.vue b/apps/web-antdv-next/src/views/mp/autoReply/modules/content.vue
new file mode 100644
index 000000000..caa022c6e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/autoReply/modules/content.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+ {{ props.row.responseContent }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/autoReply/modules/form.vue b/apps/web-antdv-next/src/views/mp/autoReply/modules/form.vue
new file mode 100644
index 000000000..09a64c66e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/autoReply/modules/form.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/index.ts b/apps/web-antdv-next/src/views/mp/components/index.ts
new file mode 100644
index 000000000..abab7ffb5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/index.ts
@@ -0,0 +1,9 @@
+export { default as WxAccountSelect } from './wx-account-select/wx-account-select.vue';
+export { default as WxLocation } from './wx-location/wx-location.vue';
+export { default as WxMaterialSelect } from './wx-material-select/wx-material-select.vue';
+export { default as WxMsg } from './wx-msg/wx-msg.vue';
+export { default as WxMusic } from './wx-music/wx-music.vue';
+export { default as WxNews } from './wx-news/wx-news.vue';
+export { default as WxReply } from './wx-reply/wx-reply.vue';
+export { default as WxVideoPlayer } from './wx-video-play/wx-video-play.vue';
+export { default as WxVoicePlayer } from './wx-voice-play/wx-voice-play.vue';
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-account-select/wx-account-select.vue b/apps/web-antdv-next/src/views/mp/components/wx-account-select/wx-account-select.vue
new file mode 100644
index 000000000..3d2b48f4c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-account-select/wx-account-select.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-location/types.ts b/apps/web-antdv-next/src/views/mp/components/wx-location/types.ts
new file mode 100644
index 000000000..9566c2a6c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-location/types.ts
@@ -0,0 +1,6 @@
+export interface WxLocationProps {
+ label: string;
+ locationX: number;
+ locationY: number;
+ qqMapKey?: string;
+}
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-location/wx-location.vue b/apps/web-antdv-next/src/views/mp/components/wx-location/wx-location.vue
new file mode 100644
index 000000000..f675df680
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-location/wx-location.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-material-select/wx-material-select.vue b/apps/web-antdv-next/src/views/mp/components/wx-material-select/wx-material-select.vue
new file mode 100644
index 000000000..aea34aa21
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-material-select/wx-material-select.vue
@@ -0,0 +1,575 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
![素材图片]()
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-msg/msg-event.vue b/apps/web-antdv-next/src/views/mp/components/wx-msg/msg-event.vue
new file mode 100644
index 000000000..24b039ebf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-msg/msg-event.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+ 关注
+
+
+ 取消关注
+
+
+ 点击菜单
+ 【{{ item.eventKey }}】
+
+
+ 点击菜单链接
+ 【{{ item.eventKey }}】
+
+
+ 扫码结果
+ 【{{ item.eventKey }}】
+
+
+ 扫码结果
+ 【{{ item.eventKey }}】
+
+
+ 系统拍照发图
+
+
+ 拍照或者相册
+
+
+ 微信相册
+
+
+ 选择地理位置
+
+
+ 未知事件类型
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-msg/msg-list.vue b/apps/web-antdv-next/src/views/mp/components/wx-msg/msg-list.vue
new file mode 100644
index 000000000..1b96d954a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-msg/msg-list.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
![]()
+
+ {{ getNickname(item.sendFrom) }}
+
+
+
+
+
+
+
+ {{ formatDateTime(item.createTime) }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-msg/msg.vue b/apps/web-antdv-next/src/views/mp/components/wx-msg/msg.vue
new file mode 100644
index 000000000..d2749d84b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-msg/msg.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
{{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-msg/wx-msg.vue b/apps/web-antdv-next/src/views/mp/components/wx-msg/wx-msg.vue
new file mode 100644
index 000000000..b87140582
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-msg/wx-msg.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+ 点击加载更多
+
+
+ 没有更多了
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-music/types.ts b/apps/web-antdv-next/src/views/mp/components/wx-music/types.ts
new file mode 100644
index 000000000..5164f5319
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-music/types.ts
@@ -0,0 +1,7 @@
+export interface WxMusicProps {
+ title?: string;
+ description?: string;
+ musicUrl?: string;
+ hqMusicUrl?: string;
+ thumbMediaUrl: string;
+}
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-music/wx-music.vue b/apps/web-antdv-next/src/views/mp/components/wx-music/wx-music.vue
new file mode 100644
index 000000000..8bd2a5094
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-music/wx-music.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
![音乐封面]()
+
+
+
+ {{ title }}
+
+
+ {{ description }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-news/wx-news.vue b/apps/web-antdv-next/src/views/mp/components/wx-news/wx-news.vue
new file mode 100644
index 000000000..6e52761c1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-news/wx-news.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-image.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-image.vue
new file mode 100644
index 000000000..9754b1fd3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-image.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
![图片素材]()
+
+ {{ reply.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-music.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-music.vue
new file mode 100644
index 000000000..baf411862
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-music.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
![音乐封面]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (reply.title = val || null)"
+ />
+ (reply.description = val || null)"
+ />
+
+
+
+
+ (reply.musicUrl = val || null)"
+ />
+ (reply.hqMusicUrl = val || null)"
+ />
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-news.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-news.vue
new file mode 100644
index 000000000..cd4bd6cfa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-news.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-text.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-text.vue
new file mode 100644
index 000000000..914d943a6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-text.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-video.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-video.vue
new file mode 100644
index 000000000..0aef852c3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-video.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+ (reply.title = val || null)"
+ />
+
+
+ (reply.description = val || null)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-voice.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-voice.vue
new file mode 100644
index 000000000..94db76d3d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/tab-voice.vue
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+ {{ reply.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/types.ts b/apps/web-antdv-next/src/views/mp/components/wx-reply/types.ts
new file mode 100644
index 000000000..502b4c574
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/types.ts
@@ -0,0 +1,42 @@
+import type { Ref } from 'vue';
+
+import type { ReplyType } from '@vben/constants';
+
+import { unref } from 'vue';
+
+export interface Reply {
+ accountId: number;
+ articles?: any[];
+ content?: null | string;
+ description?: null | string;
+ hqMusicUrl?: null | string;
+ introduction?: null | string;
+ mediaId?: null | string;
+ musicUrl?: null | string;
+ name?: null | string;
+ thumbMediaId?: null | string;
+ thumbMediaUrl?: null | string;
+ title?: null | string;
+ type: ReplyType;
+ url?: null | string;
+}
+
+/** 利用旧的 reply[accountId, type] 初始化新的 Reply */
+export function createEmptyReply(old: Ref | Reply): Reply {
+ return {
+ accountId: unref(old).accountId,
+ articles: [],
+ content: null,
+ description: null,
+ hqMusicUrl: null,
+ introduction: null,
+ mediaId: null,
+ musicUrl: null,
+ name: null,
+ thumbMediaId: null,
+ thumbMediaUrl: null,
+ title: null,
+ type: unref(old).type,
+ url: null,
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-reply/wx-reply.vue b/apps/web-antdv-next/src/views/mp/components/wx-reply/wx-reply.vue
new file mode 100644
index 000000000..f801f7649
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-reply/wx-reply.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+ 文本
+
+
+
+
+
+
+
+
+
+
+ 图片
+
+
+
+
+
+
+
+
+
+
+ 语音
+
+
+
+
+
+
+
+
+
+
+ 视频
+
+
+
+
+
+
+
+
+
+
+ 图文
+
+
+
+
+
+
+
+
+
+
+ 音乐
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-video-play/wx-video-play.vue b/apps/web-antdv-next/src/views/mp/components/wx-video-play/wx-video-play.vue
new file mode 100644
index 000000000..1c2330af3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-video-play/wx-video-play.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/components/wx-voice-play/wx-voice-play.vue b/apps/web-antdv-next/src/views/mp/components/wx-voice-play/wx-voice-play.vue
new file mode 100644
index 000000000..0ace543fe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/components/wx-voice-play/wx-voice-play.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+ {{ duration }} 秒
+
+
+ 语音识别
+ {{ content }}
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/draft/data.ts b/apps/web-antdv-next/src/views/mp/draft/data.ts
new file mode 100644
index 000000000..1ee1e10c1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/draft/data.ts
@@ -0,0 +1,45 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { formatDateTime } from '@vben/utils';
+
+/** 获取表格列配置 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'cover',
+ title: '图片',
+ width: 360,
+ slots: { default: 'cover' },
+ },
+ {
+ field: 'title',
+ title: '标题',
+ slots: { default: 'title' },
+ },
+ {
+ field: 'updateTime',
+ title: '修改时间',
+ formatter: ({ row }) => {
+ return formatDateTime(row.updateTime * 1000);
+ },
+ },
+ {
+ title: '操作',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/draft/index.vue b/apps/web-antdv-next/src/views/mp/draft/index.vue
new file mode 100644
index 000000000..ea3a87094
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/draft/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/draft/modules/cover-select.vue b/apps/web-antdv-next/src/views/mp/draft/modules/cover-select.vue
new file mode 100644
index 000000000..31f574729
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/draft/modules/cover-select.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
封面:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/draft/modules/form.vue b/apps/web-antdv-next/src/views/mp/draft/modules/form.vue
new file mode 100644
index 000000000..30481a35e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/draft/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/draft/modules/news-form.vue b/apps/web-antdv-next/src/views/mp/draft/modules/news-form.vue
new file mode 100644
index 000000000..93ae16a71
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/draft/modules/news-form.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+
+
![]()
+
+ {{ news.title }}
+
+
+
+
+
+
+
+
+
+
+ {{ news.title }}
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/freePublish/data.ts b/apps/web-antdv-next/src/views/mp/freePublish/data.ts
new file mode 100644
index 000000000..82cfb5f16
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/freePublish/data.ts
@@ -0,0 +1,59 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+import type { MpAccountApi } from '#/api/mp/account';
+
+import { formatDateTime } from '@vben/utils';
+
+import { getSimpleAccountList } from '#/api/mp/account';
+
+let accountList: MpAccountApi.Account[] = [];
+getSimpleAccountList().then((data) => (accountList = data));
+
+/** 搜索表单配置 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Select',
+ componentProps: {
+ options: accountList.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ placeholder: '请选择公众号',
+ clearable: true,
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ field: 'cover',
+ title: '图片',
+ width: 360,
+ slots: { default: 'cover' },
+ },
+ {
+ field: 'title',
+ title: '标题',
+ slots: { default: 'title' },
+ },
+ {
+ field: 'updateTime',
+ title: '修改时间',
+ formatter: ({ row }) => {
+ return formatDateTime(row.updateTime * 1000);
+ },
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/freePublish/index.vue b/apps/web-antdv-next/src/views/mp/freePublish/index.vue
new file mode 100644
index 000000000..56ca1e898
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/freePublish/index.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ {{ item.title }}
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/hooks/useUpload.ts b/apps/web-antdv-next/src/views/mp/hooks/useUpload.ts
new file mode 100644
index 000000000..14e2a4fa5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/hooks/useUpload.ts
@@ -0,0 +1,77 @@
+import { message } from 'ant-design-vue';
+// TODO @xingyu:这种,要想办法全局共享起来么?
+
+import { $t } from '#/locales';
+
+export enum UploadType {
+ Image = 'image',
+ Video = 'video',
+ Voice = 'voice',
+}
+
+interface UploadTypeConfig {
+ allowTypes: string[];
+ maxSizeMB: number;
+ i18nKey: string;
+}
+
+export interface UploadRawFile {
+ name: string;
+ size: number;
+ type: string;
+}
+
+const UPLOAD_CONFIGS: Record = {
+ [UploadType.Image]: {
+ allowTypes: [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'image/bmp',
+ 'image/jpg',
+ ],
+ maxSizeMB: 2,
+ i18nKey: 'mp.upload.image',
+ },
+ [UploadType.Video]: {
+ allowTypes: ['video/mp4'],
+ maxSizeMB: 10,
+ i18nKey: 'mp.upload.video',
+ },
+ [UploadType.Voice]: {
+ allowTypes: [
+ 'audio/mp3',
+ 'audio/mpeg',
+ 'audio/wma',
+ 'audio/wav',
+ 'audio/amr',
+ ],
+ maxSizeMB: 2,
+ i18nKey: 'mp.upload.voice',
+ },
+};
+
+export const useBeforeUpload = (type: UploadType, maxSizeMB?: number) => {
+ const fn = (rawFile: UploadRawFile): boolean => {
+ const config = UPLOAD_CONFIGS[type];
+ const finalMaxSize = maxSizeMB ?? config.maxSizeMB;
+
+ // 格式不正确
+ if (!config.allowTypes.includes(rawFile.type)) {
+ const typeName = $t(config.i18nKey);
+ message.error($t('mp.upload.invalidFormat', [typeName]));
+ return false;
+ }
+
+ // 大小不正确
+ if (rawFile.size / 1024 / 1024 > finalMaxSize) {
+ const typeName = $t(config.i18nKey);
+ message.error($t('mp.upload.maxSize', [typeName, finalMaxSize]));
+ return false;
+ }
+
+ return true;
+ };
+
+ return fn;
+};
diff --git a/apps/web-antdv-next/src/views/mp/material/index.vue b/apps/web-antdv-next/src/views/mp/material/index.vue
new file mode 100644
index 000000000..ade8c6e9b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/material/index.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 图片
+
+
+
+
+
+
+
+
+
+ 语音
+
+
+
+
+
+
+
+
+
+ 视频
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/material/modules/UploadFile.vue b/apps/web-antdv-next/src/views/mp/material/modules/UploadFile.vue
new file mode 100644
index 000000000..51d46894b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/material/modules/UploadFile.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/material/modules/UploadVideo.vue b/apps/web-antdv-next/src/views/mp/material/modules/UploadVideo.vue
new file mode 100644
index 000000000..a2f4a09ee
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/material/modules/UploadVideo.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+ 格式支持 MP4,文件大小不超过 10MB
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/material/modules/data.ts b/apps/web-antdv-next/src/views/mp/material/modules/data.ts
new file mode 100644
index 000000000..cd3e07ac1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/material/modules/data.ts
@@ -0,0 +1,145 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MpMaterialApi } from '#/api/mp/material';
+
+/** 视频表格列配置 */
+export function useVideoGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'mediaId',
+ title: '编号',
+ align: 'center',
+ width: 160,
+ },
+ {
+ field: 'name',
+ title: '文件名',
+ align: 'center',
+ minWidth: 100,
+ },
+ {
+ field: 'title',
+ title: '标题',
+ align: 'center',
+ minWidth: 200,
+ },
+ {
+ field: 'introduction',
+ title: '介绍',
+ align: 'center',
+ minWidth: 220,
+ },
+ {
+ field: 'video',
+ title: '视频',
+ align: 'center',
+ width: 220,
+ slots: { default: 'video' },
+ },
+ {
+ field: 'createTime',
+ title: '上传时间',
+ align: 'center',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ align: 'center',
+ fixed: 'right',
+ width: 180,
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 语音表格列配置 */
+export function useVoiceGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'mediaId',
+ title: '编号',
+ align: 'center',
+ width: 160,
+ },
+ {
+ field: 'name',
+ title: '文件名',
+ align: 'center',
+ minWidth: 100,
+ },
+ {
+ field: 'voice',
+ title: '语音',
+ align: 'center',
+ width: 220,
+ slots: { default: 'voice' },
+ },
+ {
+ field: 'createTime',
+ title: '上传时间',
+ align: 'center',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ align: 'center',
+ fixed: 'right',
+ width: 160,
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 图片表格列配置 */
+export function useImageGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'mediaId',
+ title: '编号',
+ align: 'center',
+ width: 400,
+ },
+ {
+ field: 'name',
+ title: '文件名',
+ align: 'center',
+ width: 200,
+ },
+ {
+ field: 'url',
+ title: '图片',
+ align: 'center',
+ width: 200,
+ slots: { default: 'image' },
+ },
+ {
+ field: 'createTime',
+ title: '上传时间',
+ align: 'center',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ align: 'center',
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/material/modules/upload.ts b/apps/web-antdv-next/src/views/mp/material/modules/upload.ts
new file mode 100644
index 000000000..5e6efdfca
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/material/modules/upload.ts
@@ -0,0 +1,46 @@
+import type { UploadProps } from 'ant-design-vue';
+
+import type { UploadRawFile } from '#/views/mp/hooks/useUpload';
+
+import { useAccessStore } from '@vben/stores';
+
+import { UploadType, useBeforeUpload } from '#/views/mp/hooks/useUpload';
+
+const accessStore = useAccessStore();
+const HEADERS = { Authorization: `Bearer ${accessStore.accessToken}` }; // 请求头
+const UPLOAD_URL = `${import.meta.env.VITE_BASE_URL}/admin-api/mp/material/upload-permanent`; // 上传地址
+
+interface UploadData {
+ accountId: number;
+ introduction: string;
+ title: string;
+ type: UploadType;
+}
+
+const beforeImageUpload: UploadProps['beforeUpload'] = function (
+ rawFile: UploadRawFile,
+) {
+ return useBeforeUpload(UploadType.Image, 2)(rawFile);
+};
+
+const beforeVoiceUpload: UploadProps['beforeUpload'] = function (
+ rawFile: UploadRawFile,
+) {
+ return useBeforeUpload(UploadType.Voice, 2)(rawFile);
+};
+
+const beforeVideoUpload: UploadProps['beforeUpload'] = function (
+ rawFile: UploadRawFile,
+) {
+ return useBeforeUpload(UploadType.Video, 10)(rawFile);
+};
+
+export {
+ beforeImageUpload,
+ beforeVideoUpload,
+ beforeVoiceUpload,
+ HEADERS,
+ UPLOAD_URL,
+ type UploadData,
+ UploadType,
+};
diff --git a/apps/web-antdv-next/src/views/mp/menu/assets/iphone_backImg.png b/apps/web-antdv-next/src/views/mp/menu/assets/iphone_backImg.png
new file mode 100644
index 000000000..bb09591a7
Binary files /dev/null and b/apps/web-antdv-next/src/views/mp/menu/assets/iphone_backImg.png differ
diff --git a/apps/web-antdv-next/src/views/mp/menu/assets/menu_foot.png b/apps/web-antdv-next/src/views/mp/menu/assets/menu_foot.png
new file mode 100644
index 000000000..4a89d4bd2
Binary files /dev/null and b/apps/web-antdv-next/src/views/mp/menu/assets/menu_foot.png differ
diff --git a/apps/web-antdv-next/src/views/mp/menu/assets/menu_head.png b/apps/web-antdv-next/src/views/mp/menu/assets/menu_head.png
new file mode 100644
index 000000000..248cfb761
Binary files /dev/null and b/apps/web-antdv-next/src/views/mp/menu/assets/menu_head.png differ
diff --git a/apps/web-antdv-next/src/views/mp/menu/data.ts b/apps/web-antdv-next/src/views/mp/menu/data.ts
new file mode 100644
index 000000000..ddb2bea42
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/menu/data.ts
@@ -0,0 +1,31 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { getSimpleAccountList } from '#/api/mp/account';
+
+/** 菜单未选中标识 */
+export const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__';
+
+/** 菜单级别枚举 */
+export enum Level {
+ Child = '2',
+ Parent = '1',
+ Undefined = '0',
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleAccountList,
+ labelField: 'name',
+ valueField: 'id',
+ autoSelect: 'first',
+ placeholder: '请选择公众号',
+ },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/menu/index.vue b/apps/web-antdv-next/src/views/mp/menu/index.vue
new file mode 100644
index 000000000..5669ae4b4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/menu/index.vue
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ accountName }}
+
+
+
+
menuClicked(parent, x)"
+ @submenu-clicked="(child, x, y) => subMenuClicked(child, x, y)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/menu/modules/editor.vue b/apps/web-antdv-next/src/views/mp/menu/modules/editor.vue
new file mode 100644
index 000000000..634ddc679
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/menu/modules/editor.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+ 菜单名称:
+
+
+
+
+ 菜单标识:
+
+
+
+ 菜单内容:
+
+
+
+ 跳转链接:
+
+
+
+
+
+
+
+ tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/menu/modules/previewer.vue b/apps/web-antdv-next/src/views/mp/menu/modules/previewer.vue
new file mode 100644
index 000000000..311e8a3b8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/menu/modules/previewer.vue
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+ {{ parent.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/menu/modules/types.ts b/apps/web-antdv-next/src/views/mp/menu/modules/types.ts
new file mode 100644
index 000000000..ae0edbd21
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/menu/modules/types.ts
@@ -0,0 +1,116 @@
+export interface Replay {
+ title: string;
+ description: string;
+ picUrl: string;
+ url: string;
+}
+
+export type MenuType =
+ | ''
+ | 'article_view_limited'
+ | 'click'
+ | 'location_select'
+ | 'pic_photo_or_album'
+ | 'pic_sysphoto'
+ | 'pic_weixin'
+ | 'scancode_push'
+ | 'scancode_waitmsg'
+ | 'view';
+
+interface _RawMenu {
+ // db
+ id: number;
+ parentId: number;
+ accountId: number;
+ appId: string;
+ createTime: number;
+
+ // mp-native
+ name: string;
+ menuKey: string;
+ type: MenuType;
+ url: string;
+ miniProgramAppId: string;
+ miniProgramPagePath: string;
+ articleId: string;
+ replyMessageType: string;
+ replyContent: string;
+ replyMediaId: string;
+ replyMediaUrl: string;
+ replyThumbMediaId: string;
+ replyThumbMediaUrl: string;
+ replyTitle: string;
+ replyDescription: string;
+ replyArticles: Replay;
+ replyMusicUrl: string;
+ replyHqMusicUrl: string;
+}
+
+export type RawMenu = Partial<_RawMenu>;
+
+interface _Reply {
+ type: string;
+ accountId: number;
+ content: string;
+ mediaId: string;
+ url: string;
+ thumbMediaId: string;
+ thumbMediaUrl: string;
+ title: string;
+ description: string;
+ articles: null | Replay[];
+ musicUrl: string;
+ hqMusicUrl: string;
+}
+
+export type Reply = Partial<_Reply>;
+
+interface _Menu extends RawMenu {
+ children: _Menu[];
+ reply: Reply;
+}
+
+export type Menu = Partial<_Menu>;
+
+export const menuOptions = [
+ {
+ value: 'view',
+ label: '跳转网页',
+ },
+ {
+ value: 'miniprogram',
+ label: '跳转小程序',
+ },
+ {
+ value: 'click',
+ label: '点击回复',
+ },
+ {
+ value: 'article_view_limited',
+ label: '跳转图文消息',
+ },
+ {
+ value: 'scancode_push',
+ label: '扫码直接返回结果',
+ },
+ {
+ value: 'scancode_waitmsg',
+ label: '扫码回复',
+ },
+ {
+ value: 'pic_sysphoto',
+ label: '系统拍照发图',
+ },
+ {
+ value: 'pic_photo_or_album',
+ label: '拍照或者相册',
+ },
+ {
+ value: 'pic_weixin',
+ label: '微信相册',
+ },
+ {
+ value: 'location_select',
+ label: '选择地理位置',
+ },
+];
diff --git a/apps/web-antdv-next/src/views/mp/message/data.ts b/apps/web-antdv-next/src/views/mp/message/data.ts
new file mode 100644
index 000000000..2aaf1303c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/message/data.ts
@@ -0,0 +1,94 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MpMessageApi } from '#/api/mp/message';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ {
+ fieldName: 'type',
+ label: '消息类型',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择消息类型',
+ options: getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'openid',
+ label: '用户标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户标识',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'createTime',
+ title: '发送时间',
+ width: 180,
+ align: 'center',
+ slots: { default: 'createTime' },
+ },
+ {
+ field: 'type',
+ title: '消息类型',
+ width: 80,
+ align: 'center',
+ },
+ {
+ field: 'sendFrom',
+ title: '发送方',
+ width: 80,
+ align: 'center',
+ slots: { default: 'sendFrom' },
+ },
+ {
+ field: 'openid',
+ title: '用户标识',
+ width: 300,
+ align: 'center',
+ },
+ {
+ field: 'content',
+ title: '内容',
+ align: 'left',
+ minWidth: 320,
+ slots: { default: 'content' },
+ },
+ {
+ field: 'actions',
+ title: '操作',
+ width: 120,
+ align: 'center',
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/message/index.vue b/apps/web-antdv-next/src/views/mp/message/index.vue
new file mode 100644
index 000000000..382c76287
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/message/index.vue
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+
+
+
+ {{ row.createTime ? formatDate2(row.createTime) : '' }}
+
+
+
+ 粉丝
+ 公众号
+
+
+
+
+ 关注
+
+
+ 取消关注
+
+
+ 点击菜单
+ 【{{ row.eventKey }}】
+
+
+ 点击菜单链接
+ 【{{ row.eventKey }}】
+
+
+ 扫码结果
+ 【{{ row.eventKey }}】
+
+
+ 扫码结果
+ 【{{ row.eventKey }}】
+
+
+ 系统拍照发图
+
+
+ 拍照或者相册
+
+
+ 微信相册
+
+
+ 选择地理位置
+
+
+ 未知事件类型
+
+
+ {{ row.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 未知消息类型
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/messageTemplate/data.ts b/apps/web-antdv-next/src/views/mp/messageTemplate/data.ts
new file mode 100644
index 000000000..aa0d59b6e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/messageTemplate/data.ts
@@ -0,0 +1,143 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+
+import { getUserPage } from '#/api/mp/user';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ ];
+}
+
+/** 发送消息模板表单 */
+export function useSendFormSchema(accountId?: number): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ label: '模板编号',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '模板标题',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '用户',
+ component: 'ApiSelect',
+ componentProps: {
+ api: async () => {
+ if (!accountId) {
+ return [];
+ }
+ const data = await getUserPage({
+ pageNo: 1,
+ pageSize: 100,
+ accountId,
+ });
+ return (data.list || []).map((user) => ({
+ label: user.nickname || user.openid,
+ value: user.id,
+ }));
+ },
+ showSearch: true,
+ placeholder: '请选择用户',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'data',
+ label: '模板数据',
+ component: 'Textarea',
+ componentProps: {
+ rows: 4,
+ placeholder:
+ '请输入模板数据(JSON 格式),例如:{"keyword1": {"value": "测试内容"}}',
+ },
+ },
+ {
+ fieldName: 'url',
+ label: '跳转链接',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入跳转链接',
+ },
+ },
+ {
+ fieldName: 'miniProgramAppId',
+ label: '小程序 appId',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入小程序 appId',
+ },
+ },
+ {
+ fieldName: 'miniProgramPagePath',
+ label: '小程序页面路径',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入小程序页面路径',
+ },
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ title: '公众号模板 ID',
+ field: 'templateId',
+ minWidth: 400,
+ },
+ {
+ title: '标题',
+ field: 'title',
+ minWidth: 150,
+ },
+ {
+ title: '模板内容',
+ field: 'content',
+ minWidth: 400,
+ },
+ {
+ title: '模板示例',
+ field: 'example',
+ minWidth: 200,
+ },
+ {
+ title: '一级行业',
+ field: 'primaryIndustry',
+ minWidth: 120,
+ },
+ {
+ title: '二级行业',
+ field: 'deputyIndustry',
+ minWidth: 120,
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 140,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/messageTemplate/index.vue b/apps/web-antdv-next/src/views/mp/messageTemplate/index.vue
new file mode 100644
index 000000000..fb6f2f2ec
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/messageTemplate/index.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/messageTemplate/modules/send-form.vue b/apps/web-antdv-next/src/views/mp/messageTemplate/modules/send-form.vue
new file mode 100644
index 000000000..bb25ae519
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/messageTemplate/modules/send-form.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/statistics/chart-options.ts b/apps/web-antdv-next/src/views/mp/statistics/chart-options.ts
new file mode 100644
index 000000000..700cb3471
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/statistics/chart-options.ts
@@ -0,0 +1,163 @@
+import type { MpStatisticsApi } from '#/api/mp/statistics';
+
+/** 用户增减数据图表配置项 */
+export function userSummaryOption(
+ data: MpStatisticsApi.StatisticsUserSummaryRespVO[],
+ dates: string[],
+): any {
+ return {
+ color: ['#67C23A', '#E5323E'],
+ legend: {
+ data: ['新增用户', '取消关注的用户'],
+ },
+ tooltip: {},
+ xAxis: {
+ data: dates,
+ },
+ yAxis: {
+ minInterval: 1,
+ },
+ series: [
+ {
+ name: '新增用户',
+ type: 'bar',
+ label: {
+ show: true,
+ },
+ barGap: 0,
+ data: data.map((item) => item.newUser), // 新增用户的数据
+ },
+ {
+ name: '取消关注的用户',
+ type: 'bar',
+ label: {
+ show: true,
+ },
+ data: data.map((item) => item.cancelUser), // 取消关注的用户的数据
+ },
+ ],
+ };
+}
+
+/** 累计用户数据图表配置项 */
+export function userCumulateOption(
+ data: MpStatisticsApi.StatisticsUserCumulateRespVO[],
+ dates: string[],
+): any {
+ return {
+ legend: {
+ data: ['累计用户量'],
+ },
+ xAxis: {
+ type: 'category',
+ data: dates,
+ },
+ yAxis: {
+ minInterval: 1,
+ },
+ series: [
+ {
+ name: '累计用户量',
+ data: data.map((item) => item.cumulateUser), // 累计用户量的数据
+ type: 'line',
+ smooth: true,
+ label: {
+ show: true,
+ },
+ },
+ ],
+ };
+}
+
+/** 消息发送概况数据图表配置项 */
+export function upstreamMessageOption(
+ data: MpStatisticsApi.StatisticsUpstreamMessageRespVO[],
+ dates: string[],
+): any {
+ return {
+ color: ['#67C23A', '#E5323E'],
+ legend: {
+ data: ['用户发送人数', '用户发送条数'],
+ },
+ tooltip: {},
+ xAxis: {
+ data: dates, // X 轴的日期范围
+ },
+ yAxis: {
+ minInterval: 1,
+ },
+ series: [
+ {
+ name: '用户发送人数',
+ type: 'line',
+ smooth: true,
+ label: {
+ show: true,
+ },
+ data: data.map((item) => item.msgUser), // 用户发送人数的数据
+ },
+ {
+ name: '用户发送条数',
+ type: 'line',
+ smooth: true,
+ label: {
+ show: true,
+ },
+ data: data.map((item) => item.msgCount), // 用户发送条数的数据
+ },
+ ],
+ };
+}
+
+/** 接口分析况数据图表配置项 */
+export function interfaceSummaryOption(
+ data: MpStatisticsApi.StatisticsInterfaceSummaryRespVO[],
+ dates: string[],
+): any {
+ return {
+ color: ['#67C23A', '#E5323E', '#E6A23C', '#409EFF'],
+ legend: {
+ data: ['被动回复用户消息的次数', '失败次数', '最大耗时', '总耗时'],
+ },
+ tooltip: {},
+ xAxis: {
+ data: dates, // X 轴的日期范围
+ },
+ yAxis: {},
+ series: [
+ {
+ name: '被动回复用户消息的次数',
+ type: 'bar',
+ label: {
+ show: true,
+ },
+ barGap: 0,
+ data: data.map((item) => item.callbackCount), // 被动回复用户消息的次数的数据
+ },
+ {
+ name: '失败次数',
+ type: 'bar',
+ label: {
+ show: true,
+ },
+ data: data.map((item) => item.failCount), // 失败次数的数据
+ },
+ {
+ name: '最大耗时',
+ type: 'bar',
+ label: {
+ show: true,
+ },
+ data: data.map((item) => item.maxTimeCost), // 最大耗时的数据
+ },
+ {
+ name: '总耗时',
+ type: 'bar',
+ label: {
+ show: true,
+ },
+ data: data.map((item) => item.totalTimeCost), // 总耗时的数据
+ },
+ ],
+ };
+}
diff --git a/apps/web-antdv-next/src/views/mp/statistics/data.ts b/apps/web-antdv-next/src/views/mp/statistics/data.ts
new file mode 100644
index 000000000..b1e34efbe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/statistics/data.ts
@@ -0,0 +1,27 @@
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { beginOfDay, endOfDay, formatDateTime } from '@vben/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ {
+ fieldName: 'dateRange',
+ label: '时间范围',
+ component: 'RangePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ defaultValue: [
+ formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
+ formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
+ ],
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/statistics/index.vue b/apps/web-antdv-next/src/views/mp/statistics/index.vue
new file mode 100644
index 000000000..9f90a3be1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/statistics/index.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/tag/data.ts b/apps/web-antdv-next/src/views/mp/tag/data.ts
new file mode 100644
index 000000000..ffce47fdd
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/tag/data.ts
@@ -0,0 +1,78 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeGridPropTypes } from '#/adapter/vxe-table';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '标签名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入名称',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ ];
+}
+
+/** 表格列配置 */
+export function useGridColumns(): VxeGridPropTypes.Columns {
+ return [
+ {
+ title: '编号',
+ field: 'id',
+ minWidth: 80,
+ },
+ {
+ title: '标签名称',
+ field: 'name',
+ minWidth: 150,
+ },
+ {
+ title: '粉丝数',
+ field: 'count',
+ minWidth: 100,
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ formatter: 'formatDateTime',
+ minWidth: 180,
+ },
+ {
+ title: '操作',
+ width: 140,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/tag/index.vue b/apps/web-antdv-next/src/views/mp/tag/index.vue
new file mode 100644
index 000000000..d39305cda
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/tag/index.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/tag/modules/form.vue b/apps/web-antdv-next/src/views/mp/tag/modules/form.vue
new file mode 100644
index 000000000..bd19e7d3d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/tag/modules/form.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/user/data.ts b/apps/web-antdv-next/src/views/mp/user/data.ts
new file mode 100644
index 000000000..fc1888268
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/user/data.ts
@@ -0,0 +1,124 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+/** 修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入昵称',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'accountId',
+ label: '公众号',
+ component: 'Input',
+ },
+ {
+ fieldName: 'openid',
+ label: '用户标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户标识',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入昵称',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'openid',
+ title: '用户标识',
+ minWidth: 260,
+ },
+ {
+ field: 'headImageUrl',
+ title: '用户头像',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'nickname',
+ title: '昵称',
+ minWidth: 120,
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 120,
+ },
+ {
+ field: 'tagIds',
+ title: '标签',
+ minWidth: 200,
+ cellRender: {
+ name: 'CellTags',
+ },
+ },
+ {
+ field: 'subscribeStatus',
+ title: '订阅状态',
+ minWidth: 100,
+ align: 'center',
+ formatter: ({ cellValue }) => {
+ return cellValue === 0 ? '已订阅' : '未订阅';
+ },
+ },
+ {
+ field: 'subscribeTime',
+ title: '订阅时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/mp/user/index.vue b/apps/web-antdv-next/src/views/mp/user/index.vue
new file mode 100644
index 000000000..3c6c423f6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/user/index.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/mp/user/modules/form.vue b/apps/web-antdv-next/src/views/mp/user/modules/form.vue
new file mode 100644
index 000000000..5677de9e9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/mp/user/modules/form.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/app/data.ts b/apps/web-antdv-next/src/views/pay/app/data.ts
new file mode 100644
index 000000000..d46607ca3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/app/data.ts
@@ -0,0 +1,673 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { PayAppApi } from '#/api/pay/app';
+
+import { h } from 'vue';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { InputUpload } from '#/components/upload';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入应用名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择开启状态',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ clearable: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: PayAppApi.App,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'appKey',
+ title: '应用标识',
+ minWidth: 40,
+ },
+ {
+ field: 'name',
+ title: '应用名',
+ minWidth: 40,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ align: 'center',
+ minWidth: 40,
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: CommonStatusEnum.ENABLE,
+ unCheckedValue: CommonStatusEnum.DISABLE,
+ },
+ },
+ },
+ {
+ title: '支付宝配置',
+ children: [
+ {
+ title: 'APP',
+ slots: {
+ default: 'alipayAppConfig',
+ },
+ },
+ {
+ title: 'PC 网站',
+ slots: {
+ default: 'alipayPCConfig',
+ },
+ },
+ {
+ title: 'WAP 网站',
+ slots: {
+ default: 'alipayWAPConfig',
+ },
+ minWidth: 10,
+ },
+ {
+ title: '扫码',
+ slots: {
+ default: 'alipayQrConfig',
+ },
+ },
+ {
+ title: '条码',
+ slots: {
+ default: 'alipayBarConfig',
+ },
+ },
+ ],
+ },
+ {
+ title: '微信配置',
+ children: [
+ {
+ title: '小程序',
+ slots: {
+ default: 'wxLiteConfig',
+ },
+ },
+ {
+ title: 'JSAPI',
+ slots: {
+ default: 'wxPubConfig',
+ },
+ },
+ {
+ title: 'APP',
+ slots: {
+ default: 'wxAppConfig',
+ },
+ },
+ {
+ title: 'Native',
+ slots: {
+ default: 'wxNativeConfig',
+ },
+ },
+ {
+ title: 'WAP 网站',
+ slots: {
+ default: 'wxWapConfig',
+ },
+ minWidth: 10,
+ },
+ {
+ title: '条码',
+ slots: {
+ default: 'wxBarConfig',
+ },
+ },
+ ],
+ },
+ {
+ title: '钱包支付配置',
+ field: 'walletConfig',
+ slots: {
+ default: 'walletConfig',
+ },
+ },
+ {
+ title: '模拟支付配置',
+ field: 'mockConfig',
+ slots: {
+ default: 'mockConfig',
+ },
+ },
+ {
+ title: '操作',
+ width: 140,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 应用新增/修改的表单 */
+export function useAppFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '应用名',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入应用名',
+ },
+ },
+ {
+ fieldName: 'appKey',
+ label: '应用标识',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入应用标识',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ fieldName: 'orderNotifyUrl',
+ label: '支付结果的回调地址',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入支付结果的回调地址',
+ },
+ },
+ {
+ fieldName: 'refundNotifyUrl',
+ label: '退款结果的回调地址',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入退款结果的回调地址',
+ },
+ },
+ {
+ fieldName: 'transferNotifyUrl',
+ label: '转账结果的回调地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入转账结果的回调地址',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ rows: 3,
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 渠道新增/修改的表单 */
+export function useChannelFormSchema(formType: string = ''): VbenFormSchema[] {
+ const schema: VbenFormSchema[] = [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ label: '应用编号',
+ fieldName: 'appId',
+ component: 'Input',
+ dependencies: {
+ show: () => false,
+ triggerFields: [''],
+ },
+ },
+ {
+ label: '渠道编码',
+ fieldName: 'code',
+ component: 'Input',
+ dependencies: {
+ show: () => false,
+ triggerFields: [''],
+ },
+ },
+ {
+ label: '渠道费率',
+ fieldName: 'feeRate',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入渠道费率',
+ addonAfter: '%',
+ },
+ defaultValue: 0,
+ },
+ {
+ label: '渠道状态',
+ fieldName: 'status',
+ component: 'RadioGroup',
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ ];
+ // 添加通用字段
+ // 根据类型添加特定字段
+ if (formType.includes('alipay_')) {
+ schema.push(
+ {
+ label: '开放平台 APPID',
+ fieldName: 'config.appId',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入开放平台 APPID',
+ },
+ },
+ {
+ label: '网关地址',
+ fieldName: 'config.serverUrl',
+ component: 'RadioGroup',
+ rules: 'required',
+ componentProps: {
+ options: [
+ {
+ value: 'https://openapi.alipay.com/gateway.do',
+ label: '线上环境',
+ },
+ {
+ value: 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
+ label: '沙箱环境',
+ },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ label: '算法类型',
+ fieldName: 'config.signType',
+ component: 'RadioGroup',
+ rules: 'required',
+ componentProps: {
+ options: [
+ {
+ value: 'RSA2',
+ label: 'RSA2',
+ },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: 'RSA2',
+ },
+ {
+ label: '公钥类型',
+ fieldName: 'config.mode',
+ component: 'RadioGroup',
+ rules: 'required',
+ componentProps: {
+ options: [
+ {
+ value: 1,
+ label: '公钥模式',
+ },
+ {
+ value: 2,
+ label: '证书模式',
+ },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ label: '应用私钥',
+ fieldName: 'config.privateKey',
+ component: 'Textarea',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入应用私钥',
+ rows: 3,
+ },
+ },
+ {
+ label: '支付宝公钥',
+ fieldName: 'config.alipayPublicKey',
+ component: 'Textarea',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入支付宝公钥',
+ rows: 3,
+ },
+ dependencies: {
+ show(values: any) {
+ return values?.config?.mode === 1;
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: '商户公钥应用证书',
+ fieldName: 'config.appCertContent',
+ component: h(InputUpload, {
+ inputType: 'textarea',
+ textareaProps: { rows: 3, placeholder: '请上传商户公钥应用证书' },
+ fileUploadProps: {
+ accept: ['crt'],
+ },
+ }),
+ rules: 'required',
+ dependencies: {
+ show(values: any) {
+ return values?.config?.mode === 2;
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: '支付宝公钥证书',
+ fieldName: 'config.alipayPublicCertContent',
+ component: h(InputUpload, {
+ inputType: 'textarea',
+ textareaProps: { rows: 3, placeholder: '请上传支付宝公钥证书' },
+ fileUploadProps: {
+ accept: ['crt'],
+ },
+ }),
+ rules: 'required',
+ dependencies: {
+ show(values: any) {
+ return values?.config?.mode === 2;
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: '根证书',
+ fieldName: 'config.rootCertContent',
+ component: h(InputUpload, {
+ inputType: 'textarea',
+ textareaProps: { rows: 3, placeholder: '请上传根证书' },
+ fileUploadProps: {
+ accept: ['crt'],
+ },
+ }),
+ rules: 'required',
+ dependencies: {
+ show(values: any) {
+ return values?.config?.mode === 2;
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: '接口内容加密方式',
+ fieldName: 'config.encryptType',
+ component: 'RadioGroup',
+ rules: 'required',
+ componentProps: {
+ options: [
+ {
+ value: 'NONE',
+ label: '无加密',
+ },
+ {
+ value: 'AES',
+ label: 'AES',
+ },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: 'NONE',
+ },
+ {
+ label: '接口内容加密密钥',
+ fieldName: 'config.encryptKey',
+ component: 'Input',
+ rules: 'required',
+ dependencies: {
+ show(values: any) {
+ return values?.config?.encryptType === 'AES';
+ },
+ triggerFields: ['config.encryptType', 'encryptType', 'config'],
+ },
+ },
+ );
+ } else if (formType.includes('wx_')) {
+ schema.push(
+ {
+ label: '微信 APPID',
+ fieldName: 'config.appId',
+ help: '前往微信商户平台[https://pay.weixin.qq.com/index.php/extend/merchant_appid/mapay_platform/account_manage]查看 APPID',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入微信 APPID',
+ },
+ },
+ {
+ label: '商户号',
+ fieldName: 'config.mchId',
+ help: '前往微信商户平台[https://pay.weixin.qq.com/index.php/extend/pay_setting]查看商户号',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入商户号',
+ },
+ },
+ {
+ label: 'API 版本',
+ fieldName: 'config.apiVersion',
+ component: 'RadioGroup',
+ rules: 'required',
+ componentProps: {
+ options: [
+ {
+ label: 'v2',
+ value: 'v2',
+ },
+ {
+ label: 'v3',
+ value: 'v3',
+ },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ },
+ {
+ label: '商户密钥',
+ fieldName: 'config.mchKey',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入商户密钥',
+ },
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v2';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: 'apiclient_cert.p12 证书',
+ fieldName: 'config.keyContent',
+ component: h(InputUpload, {
+ inputType: 'textarea',
+ textareaProps: {
+ rows: 3,
+ placeholder: '请上传 apiclient_cert.p12 证书',
+ },
+ fileUploadProps: {
+ accept: ['p12'],
+ },
+ }),
+ rules: 'required',
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v2';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: 'API V3 密钥',
+ fieldName: 'config.apiV3Key',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入 API V3 密钥',
+ },
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v3';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: 'apiclient_key.pem 证书',
+ fieldName: 'config.privateKeyContent',
+ component: h(InputUpload, {
+ inputType: 'textarea',
+ textareaProps: {
+ rows: 3,
+ placeholder: '请上传 apiclient_key.pem 证书',
+ },
+ fileUploadProps: {
+ accept: ['pem'],
+ },
+ }),
+ rules: 'required',
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v3';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: '证书序列号',
+ fieldName: 'config.certSerialNo',
+ component: 'Input',
+ help: '前往微信商户平台[https://pay.weixin.qq.com/index.php/core/cert/api_cert#/api-cert-manage]查看证书序列号',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入证书序列号',
+ },
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v3';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: 'public_key.pem 证书',
+ fieldName: 'config.publicKeyContent',
+ component: h(InputUpload, {
+ inputType: 'textarea',
+ textareaProps: {
+ rows: 3,
+ placeholder: '请上传 public_key.pem 证书',
+ },
+ fileUploadProps: {
+ accept: ['pem'],
+ },
+ }),
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v3';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ {
+ label: '公钥 ID',
+ fieldName: 'config.publicKeyId',
+ component: 'Input',
+ help: '微信支付公钥产品简介及使用说明[https://pay.weixin.qq.com/doc/v3/merchant/4012153196]',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入公钥 ID',
+ },
+ dependencies: {
+ show(values: any) {
+ return values?.config?.apiVersion === 'v3';
+ },
+ triggerFields: ['config.mode', 'mode', 'config'],
+ },
+ },
+ );
+ }
+ // 添加备注字段(所有类型都有)
+ schema.push({
+ label: '备注',
+ fieldName: 'remark',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ });
+ return schema;
+}
diff --git a/apps/web-antdv-next/src/views/pay/app/index.vue b/apps/web-antdv-next/src/views/pay/app/index.vue
new file mode 100644
index 000000000..671aa1aa1
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/app/index.vue
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/app/modules/app-form.vue b/apps/web-antdv-next/src/views/pay/app/modules/app-form.vue
new file mode 100644
index 000000000..dd3a1eeda
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/app/modules/app-form.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/app/modules/channel-form.vue b/apps/web-antdv-next/src/views/pay/app/modules/channel-form.vue
new file mode 100644
index 000000000..53ac4f505
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/app/modules/channel-form.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/cashier/data.ts b/apps/web-antdv-next/src/views/pay/cashier/data.ts
new file mode 100644
index 000000000..eb0cec957
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/cashier/data.ts
@@ -0,0 +1,83 @@
+import {
+ SvgAlipayAppIcon,
+ SvgAlipayBarIcon,
+ SvgAlipayPcIcon,
+ SvgAlipayQrIcon,
+ SvgAlipayWapIcon,
+ SvgMockIcon,
+ SvgWalletIcon,
+ SvgWxAppIcon,
+ SvgWxBarIcon,
+ SvgWxLiteIcon,
+ SvgWxNativeIcon,
+ SvgWxPubIcon,
+} from '@vben/icons';
+
+export const channelsAlipay = [
+ {
+ name: '支付宝 PC 网站支付',
+ icon: SvgAlipayPcIcon,
+ code: 'alipay_pc',
+ },
+ {
+ name: '支付宝 Wap 网站支付',
+ icon: SvgAlipayWapIcon,
+ code: 'alipay_wap',
+ },
+ {
+ name: '支付宝 App 网站支付',
+ icon: SvgAlipayAppIcon,
+ code: 'alipay_app',
+ },
+ {
+ name: '支付宝扫码支付',
+ icon: SvgAlipayQrIcon,
+ code: 'alipay_qr',
+ },
+ {
+ name: '支付宝条码支付',
+ icon: SvgAlipayBarIcon,
+ code: 'alipay_bar',
+ },
+];
+
+export const channelsWechat = [
+ {
+ name: '微信公众号支付',
+ icon: SvgWxPubIcon,
+ code: 'wx_pub',
+ },
+ {
+ name: '微信小程序支付',
+ icon: SvgWxLiteIcon,
+ code: 'wx_lite',
+ },
+ {
+ name: '微信 App 支付',
+ icon: SvgWxAppIcon,
+ code: 'wx_app',
+ },
+ {
+ name: '微信扫码支付',
+ icon: SvgWxNativeIcon,
+ code: 'wx_native',
+ },
+ {
+ name: '微信条码支付',
+ icon: SvgWxBarIcon,
+ code: 'wx_bar',
+ },
+];
+
+export const channelsMock = [
+ {
+ name: '钱包支付',
+ icon: SvgWalletIcon,
+ code: 'wallet',
+ },
+ {
+ name: '模拟支付',
+ icon: SvgMockIcon,
+ code: 'mock',
+ },
+];
diff --git a/apps/web-antdv-next/src/views/pay/cashier/index.vue b/apps/web-antdv-next/src/views/pay/cashier/index.vue
new file mode 100644
index 000000000..8f2087296
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/cashier/index.vue
@@ -0,0 +1,397 @@
+
+
+
+
+
+
+ {{ payOrder?.id }}
+
+
+ {{ payOrder?.subject }}
+
+
+ {{ payOrder?.body }}
+
+
+ {{ `¥${fenToYuan(payOrder?.price || 0)}` }}
+
+
+ {{ formatDate(payOrder?.createTime) }}
+
+
+ {{ formatDate(payOrder?.expireTime) }}
+
+
+
+
+
+
+
+
+
+
{{ channel.name }}
+
+
+
+
+
+
+
+
+
+
{{ channel.name }}
+
+
+
+
+
+
+
+
+
+
{{ channel.name }}
+
+
+
+
+
+
+
+ 或使用
+
+ 扫码
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/demo/order/data.ts b/apps/web-antdv-next/src/views/pay/demo/order/data.ts
new file mode 100644
index 000000000..429e5a2b0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/demo/order/data.ts
@@ -0,0 +1,115 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'spuId',
+ label: '商品',
+ component: 'Select',
+ componentProps: {
+ options: [
+ { label: '华为手机 --- 1.00元', value: 1 },
+ { label: '小米电视 --- 10.00元', value: 2 },
+ { label: '苹果手表 --- 100.00元', value: 3 },
+ { label: '华硕笔记本 --- 1000.00元', value: 4 },
+ { label: '蔚来汽车 --- 200000.00元', value: 5 },
+ ],
+ placeholder: '请选择下单商品',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '订单编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'spuName',
+ title: '商品名字',
+ minWidth: 150,
+ },
+ {
+ field: 'price',
+ title: '支付价格',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'refundPrice',
+ title: '退款金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'payOrderId',
+ title: '支付单号',
+ minWidth: 120,
+ },
+ {
+ field: 'payStatus',
+ title: '是否支付',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'payTime',
+ title: '支付时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'refundTime',
+ title: '退款时间',
+ minWidth: 180,
+ formatter: ({ cellValue, row }) => {
+ if (cellValue) {
+ return formatDateTime(cellValue) as string;
+ }
+ if (row.payRefundId) {
+ return '退款中,等待退款结果';
+ }
+ return '';
+ },
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/demo/order/index.vue b/apps/web-antdv-next/src/views/pay/demo/order/index.vue
new file mode 100644
index 000000000..35273adff
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/demo/order/index.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/demo/order/modules/form.vue b/apps/web-antdv-next/src/views/pay/demo/order/modules/form.vue
new file mode 100644
index 000000000..bdec3b4ab
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/demo/order/modules/form.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/demo/withdraw/data.ts b/apps/web-antdv-next/src/views/pay/demo/withdraw/data.ts
new file mode 100644
index 000000000..8b2c1b547
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/demo/withdraw/data.ts
@@ -0,0 +1,166 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'subject',
+ label: '提现标题',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入提现标题',
+ },
+ },
+ {
+ fieldName: 'price',
+ label: '提现金额',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ min: 1,
+ precision: 2,
+ step: 0.01,
+ placeholder: '请输入提现金额',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '提现类型',
+ component: 'Select',
+ rules: 'required',
+ componentProps: {
+ options: [
+ { label: '支付宝', value: 1 },
+ { label: '微信余额', value: 2 },
+ { label: '钱包余额', value: 3 },
+ ],
+ placeholder: '请选择提现类型',
+ },
+ },
+ {
+ fieldName: 'userAccount',
+ label: '收款人账号',
+ component: 'Input',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ componentProps: (values) => {
+ const type = values.type;
+ let placeholder = '请输入收款人账号';
+ switch (type) {
+ case 1: {
+ placeholder = '请输入支付宝账号';
+ break;
+ }
+ case 2: {
+ placeholder = '请输入微信 openid';
+ break;
+ }
+ case 3: {
+ placeholder = '请输入钱包编号';
+ break;
+ }
+ }
+ return {
+ placeholder,
+ };
+ },
+ },
+ },
+ {
+ fieldName: 'userName',
+ label: '收款人姓名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入收款人姓名',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '提现单编号',
+ minWidth: 100,
+ },
+ {
+ field: 'subject',
+ title: '提现标题',
+ minWidth: 150,
+ },
+ {
+ field: 'type',
+ title: '提现类型',
+ minWidth: 100,
+ slots: { default: 'type' },
+ },
+ {
+ field: 'price',
+ title: '提现金额',
+ minWidth: 100,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'userName',
+ title: '收款人姓名',
+ minWidth: 120,
+ },
+ {
+ field: 'userAccount',
+ title: '收款人账号',
+ minWidth: 150,
+ },
+ {
+ field: 'status',
+ title: '提现状态',
+ minWidth: 100,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'payTransferId',
+ title: '转账单号',
+ minWidth: 120,
+ },
+ {
+ field: 'transferChannelCode',
+ title: '转账渠道',
+ minWidth: 130,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'transferTime',
+ title: '转账时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'transferErrorMsg',
+ title: '转账失败原因',
+ minWidth: 150,
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/demo/withdraw/index.vue b/apps/web-antdv-next/src/views/pay/demo/withdraw/index.vue
new file mode 100644
index 000000000..0a64d3c2d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/demo/withdraw/index.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支付宝
+ 微信余额
+ 钱包余额
+
+
+ ¥{{ erpPriceInputFormatter(row.price) }}
+
+
+
+ 等待转账
+
+
+ 转账中
+
+ 转账成功
+ 转账失败
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/demo/withdraw/modules/form.vue b/apps/web-antdv-next/src/views/pay/demo/withdraw/modules/form.vue
new file mode 100644
index 000000000..78240de7c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/demo/withdraw/modules/form.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/notify/data.ts b/apps/web-antdv-next/src/views/pay/notify/data.ts
new file mode 100644
index 000000000..90b54dfeb
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/notify/data.ts
@@ -0,0 +1,271 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { getAppList } from '#/api/pay/app';
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'appId',
+ label: '应用编号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getAppList,
+ labelField: 'name',
+ valueField: 'id',
+ autoSelect: 'first',
+ placeholder: '请选择应用编号',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '通知类型',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PAY_NOTIFY_TYPE, 'number'),
+ placeholder: '请选择通知类型',
+ },
+ },
+ {
+ fieldName: 'dataId',
+ label: '关联编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入关联编号',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '通知状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PAY_NOTIFY_STATUS, 'number'),
+ placeholder: '请选择通知状态',
+ },
+ },
+ {
+ fieldName: 'merchantOrderId',
+ label: '商户订单编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商户订单编号',
+ },
+ },
+ {
+ fieldName: 'merchantRefundId',
+ label: '商户退款编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商户退款编号',
+ },
+ },
+ {
+ fieldName: 'merchantTransferId',
+ label: '商户转账编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商户转账编号',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '任务编号',
+ minWidth: 100,
+ },
+ {
+ field: 'appName',
+ title: '应用名称',
+ minWidth: 150,
+ },
+ {
+ field: 'merchantInfo',
+ title: '商户单信息',
+ minWidth: 240,
+ slots: {
+ default: 'merchantInfo',
+ },
+ },
+ {
+ field: 'type',
+ title: '通知类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_NOTIFY_TYPE },
+ },
+ },
+ {
+ field: 'dataId',
+ title: '关联编号',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '通知状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_NOTIFY_STATUS },
+ },
+ },
+ {
+ field: 'lastExecuteTime',
+ title: '最后通知时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'nextNotifyTime',
+ title: '下次通知时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'notifyTimes',
+ title: '通知次数',
+ minWidth: 120,
+ formatter: ({ row }) => `${row.notifyTimes} / ${row.maxNotifyTimes}`,
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'appId',
+ label: '应用编号',
+ },
+ {
+ field: 'appName',
+ label: '应用名称',
+ },
+ {
+ field: 'type',
+ label: '通知类型',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_NOTIFY_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'dataId',
+ label: '关联编号',
+ },
+ {
+ field: 'status',
+ label: '通知状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_NOTIFY_STATUS,
+ value: val,
+ }),
+ },
+ {
+ field: 'merchantOrderId',
+ label: '商户订单编号',
+ },
+ {
+ field: 'lastExecuteTime',
+ label: '最后通知时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'nextNotifyTime',
+ label: '下次通知时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'notifyTimes',
+ label: '通知次数',
+ },
+ {
+ field: 'maxNotifyTimes',
+ label: '最大通知次数',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '更新时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
+
+/** 详情的日志字段 */
+export function useDetailLogColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '日志编号',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '通知状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_NOTIFY_STATUS },
+ },
+ },
+ {
+ field: 'notifyTimes',
+ title: '通知次数',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '通知时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'response',
+ title: '响应结果',
+ minWidth: 200,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/notify/index.vue b/apps/web-antdv-next/src/views/pay/notify/index.vue
new file mode 100644
index 000000000..5140e38a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/notify/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 商户订单编号
+ {{ row.merchantOrderId }}
+
+
+ 商户退款编号
+ {{ row.merchantRefundId }}
+
+
+ 商户转账编号
+ {{ row.merchantTransferId }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/notify/modules/detail.vue b/apps/web-antdv-next/src/views/pay/notify/modules/detail.vue
new file mode 100644
index 000000000..90a6a67b0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/notify/modules/detail.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/order/data.ts b/apps/web-antdv-next/src/views/pay/order/data.ts
new file mode 100644
index 000000000..c088704a2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/order/data.ts
@@ -0,0 +1,271 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Tag } from 'ant-design-vue';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'appId',
+ label: '应用编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入应用编号',
+ },
+ },
+ {
+ fieldName: 'channelCode',
+ label: '支付渠道',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择支付渠道',
+ options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'string'),
+ },
+ },
+ {
+ fieldName: 'merchantOrderId',
+ label: '商户单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商户单号',
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '支付单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入支付单号',
+ },
+ },
+ {
+ fieldName: 'channelOrderNo',
+ label: '渠道单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入渠道单号',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '支付状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择支付状态',
+ options: getDictOptions(DICT_TYPE.PAY_ORDER_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'price',
+ title: '支付金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'refundPrice',
+ title: '退款金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'channelFeePrice',
+ title: '手续金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'no',
+ title: '订单号',
+ minWidth: 240,
+ slots: {
+ default: 'no',
+ },
+ },
+ {
+ field: 'status',
+ title: '支付状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_ORDER_STATUS },
+ },
+ },
+ {
+ field: 'channelCode',
+ title: '支付渠道',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'successTime',
+ title: '支付时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'appName',
+ title: '支付应用',
+ minWidth: 150,
+ },
+ {
+ field: 'subject',
+ title: '商品标题',
+ minWidth: 200,
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'merchantOrderId',
+ label: '商户单号',
+ },
+ {
+ field: 'no',
+ label: '支付单号',
+ },
+ {
+ field: 'appId',
+ label: '应用编号',
+ },
+ {
+ field: 'appName',
+ label: '应用名称',
+ },
+ {
+ field: 'status',
+ label: '支付状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_ORDER_STATUS,
+ value: val,
+ }),
+ },
+ {
+ field: 'price',
+ label: '支付金额',
+ render: (val) => `¥${erpPriceInputFormatter(val)}`,
+ },
+ {
+ field: 'channelFeePrice',
+ label: '手续费',
+ render: (val) => `¥${erpPriceInputFormatter(val)}`,
+ },
+ {
+ field: 'channelFeeRate',
+ label: '手续费比例',
+ render: (val) => `${erpPriceInputFormatter(val)}%`,
+ },
+ {
+ field: 'successTime',
+ label: '支付时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'expireTime',
+ label: '失效时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '更新时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'subject',
+ label: '商品标题',
+ },
+ {
+ field: 'body',
+ label: '商品描述',
+ },
+ {
+ field: 'channelCode',
+ label: '支付渠道',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_CHANNEL_CODE,
+ value: val,
+ }),
+ },
+ {
+ field: 'userIp',
+ label: '支付 IP',
+ },
+ {
+ field: 'channelOrderNo',
+ label: '渠道单号',
+ render: (val) => (val ? h(Tag, { color: 'green' }, () => val) : ''),
+ },
+ {
+ field: 'channelUserId',
+ label: '渠道用户',
+ },
+ {
+ field: 'refundPrice',
+ label: '退款金额',
+ render: (val) => `¥${erpPriceInputFormatter(val)}`,
+ },
+ {
+ field: 'notifyUrl',
+ label: '通知 URL',
+ },
+ {
+ field: 'channelNotifyData',
+ label: '支付通道异步回调内容',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/order/index.vue b/apps/web-antdv-next/src/views/pay/order/index.vue
new file mode 100644
index 000000000..c8c88bdc3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/order/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 商户 {{ row.merchantOrderId }}
+
+
+ 支付 {{ row.no }}
+
+
+ 渠道
+ {{ row.channelOrderNo }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/order/modules/detail.vue b/apps/web-antdv-next/src/views/pay/order/modules/detail.vue
new file mode 100644
index 000000000..2d4d84b10
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/order/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/refund/data.ts b/apps/web-antdv-next/src/views/pay/refund/data.ts
new file mode 100644
index 000000000..974acfe25
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/refund/data.ts
@@ -0,0 +1,280 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Tag } from 'ant-design-vue';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'appId',
+ label: '应用编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入应用编号',
+ },
+ },
+ {
+ fieldName: 'channelCode',
+ label: '退款渠道',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择退款渠道',
+ options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'string'),
+ },
+ },
+ {
+ fieldName: 'merchantOrderId',
+ label: '商户单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商户单号',
+ },
+ },
+ {
+ fieldName: 'merchantRefundId',
+ label: '退款单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入退款单号',
+ },
+ },
+ {
+ fieldName: 'channelOrderNo',
+ label: '渠道单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入渠道单号',
+ },
+ },
+ {
+ fieldName: 'channelRefundNo',
+ label: '渠道退款单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入渠道退款单号',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '退款状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请选择退款状态',
+ options: getDictOptions(DICT_TYPE.PAY_REFUND_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'payPrice',
+ title: '支付金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'refundPrice',
+ title: '退款金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'merchantRefundId',
+ title: '退款单号',
+ minWidth: 240,
+ slots: {
+ default: 'no',
+ },
+ },
+ {
+ field: 'status',
+ title: '退款状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_REFUND_STATUS },
+ },
+ },
+ {
+ field: 'channelCode',
+ title: '退款渠道',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'successTime',
+ title: '退款时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'reason',
+ title: '退款原因',
+ minWidth: 200,
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ // 基本信息部分
+ {
+ field: 'merchantRefundId',
+ label: '商户退款单号',
+ render: (val) => h(Tag, {}, () => val || '-'),
+ },
+ {
+ field: 'channelRefundNo',
+ label: '渠道退款单号',
+ render: (val) => h(Tag, { color: 'success' }, () => val || '-'),
+ },
+ {
+ field: 'merchantOrderId',
+ label: '商户支付单号',
+ render: (val) => h(Tag, {}, () => val || '-'),
+ },
+ {
+ field: 'channelOrderNo',
+ label: '渠道支付单号',
+ render: (val) => h(Tag, { color: 'success' }, () => val || '-'),
+ },
+ {
+ field: 'appId',
+ label: '应用编号',
+ },
+ {
+ field: 'appName',
+ label: '应用名称',
+ },
+ {
+ field: 'payPrice',
+ label: '支付金额',
+ render: (val) =>
+ h(
+ Tag,
+ { color: 'success' },
+ () => `¥${erpPriceInputFormatter(val || 0)}`,
+ ),
+ },
+ {
+ field: 'refundPrice',
+ label: '退款金额',
+ render: (val) =>
+ h(
+ Tag,
+ { color: 'danger' },
+ () => `¥${erpPriceInputFormatter(val || 0)}`,
+ ),
+ },
+ {
+ field: 'status',
+ label: '退款状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_REFUND_STATUS,
+ value: val,
+ }),
+ },
+ {
+ field: 'successTime',
+ label: '退款时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '更新时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ // 渠道信息部分
+ {
+ field: 'channelCode',
+ label: '退款渠道',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_CHANNEL_CODE,
+ value: val,
+ }),
+ },
+ {
+ field: 'reason',
+ label: '退款原因',
+ },
+ {
+ field: 'userIp',
+ label: '退款 IP',
+ },
+ {
+ field: 'notifyUrl',
+ label: '通知 URL',
+ },
+ // 错误信息部分
+ {
+ field: 'channelErrorCode',
+ label: '渠道错误码',
+ },
+ {
+ field: 'channelErrorMsg',
+ label: '渠道错误码描述',
+ },
+ {
+ field: 'channelNotifyData',
+ label: '支付通道异步回调内容',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/refund/index.vue b/apps/web-antdv-next/src/views/pay/refund/index.vue
new file mode 100644
index 000000000..d2d8cc575
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/refund/index.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 商户 {{ row.merchantOrderId }}
+
+
+ 退款
+ {{ row.merchantRefundId }}
+
+
+ 渠道
+ {{ row.channelRefundNo }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/refund/modules/detail.vue b/apps/web-antdv-next/src/views/pay/refund/modules/detail.vue
new file mode 100644
index 000000000..d78ea529d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/refund/modules/detail.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/transfer/data.ts b/apps/web-antdv-next/src/views/pay/transfer/data.ts
new file mode 100644
index 000000000..9f33dfe82
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/transfer/data.ts
@@ -0,0 +1,268 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Tag } from 'ant-design-vue';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '转账单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入转账单号',
+ },
+ },
+ {
+ fieldName: 'channelCode',
+ label: '转账渠道',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE),
+ allowClear: true,
+ placeholder: '请选择支付渠道',
+ },
+ },
+ {
+ fieldName: 'merchantTransferId',
+ label: '商户单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入商户单号',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PAY_TRANSFER_TYPE),
+ allowClear: true,
+ placeholder: '请选择类型',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '转账状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.PAY_TRANSFER_STATUS),
+ allowClear: true,
+ placeholder: '请选择转账状态',
+ },
+ },
+ {
+ fieldName: 'userName',
+ label: '收款人姓名',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入收款人姓名',
+ },
+ },
+ {
+ fieldName: 'userAccount',
+ label: '收款人账号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入收款人账号',
+ },
+ },
+ {
+ fieldName: 'channelTransferNo',
+ label: '渠道单号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入渠道单号',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'price',
+ title: '转账金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'merchantTransferId',
+ title: '转账单号',
+ minWidth: 350,
+ slots: {
+ default: 'no',
+ },
+ },
+ {
+ field: 'status',
+ title: '转账状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_TRANSFER_STATUS },
+ },
+ },
+ {
+ field: 'channelCode',
+ title: '转账渠道',
+ minWidth: 140,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'successTime',
+ title: '转账时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'subject',
+ title: '转账标题',
+ minWidth: 150,
+ },
+ {
+ field: 'appName',
+ title: '支付应用',
+ minWidth: 150,
+ },
+ {
+ field: 'userName',
+ title: '收款人姓名',
+ minWidth: 150,
+ },
+ {
+ field: 'userAccount',
+ title: '收款账号',
+ minWidth: 200,
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'merchantTransferId',
+ label: '商户单号',
+ render: (val) => h(Tag, {}, () => val),
+ },
+ {
+ field: 'no',
+ label: '转账单号',
+ render: (val) => h(Tag, { color: 'orange' }, () => val),
+ },
+ {
+ field: 'appId',
+ label: '应用编号',
+ },
+ {
+ field: 'status',
+ label: '转账状态',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_TRANSFER_STATUS,
+ value: val,
+ }),
+ },
+ {
+ field: 'price',
+ label: '转账金额',
+ render: (val) =>
+ h(
+ Tag,
+ { color: 'success' },
+ () => `¥${erpPriceInputFormatter(val || 0)}`,
+ ),
+ },
+ {
+ field: 'successTime',
+ label: '转账时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'userName',
+ label: '收款人姓名',
+ },
+ {
+ field: 'userAccount',
+ label: '收款人账号',
+ },
+ {
+ field: 'channelCode',
+ label: '支付渠道',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.PAY_CHANNEL_CODE,
+ value: val,
+ }),
+ },
+ {
+ field: 'userIp',
+ label: '支付 IP',
+ },
+ {
+ field: 'channelTransferNo',
+ label: '渠道单号',
+ render: (val) => (val ? h(Tag, { color: 'success' }, () => val) : ''),
+ },
+ {
+ field: 'notifyUrl',
+ label: '通知 URL',
+ },
+ {
+ field: 'channelNotifyData',
+ label: '转账渠道通知内容',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/transfer/index.vue b/apps/web-antdv-next/src/views/pay/transfer/index.vue
new file mode 100644
index 000000000..d688207ae
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/transfer/index.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 商户
+ {{ row.merchantTransferId }}
+
+
+ 转账 {{ row.no }}
+
+
+ 渠道
+ {{ row.channelTransferNo }}
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/transfer/modules/detail.vue b/apps/web-antdv-next/src/views/pay/transfer/modules/detail.vue
new file mode 100644
index 000000000..83150697c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/transfer/modules/detail.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/wallet/balance/data.ts b/apps/web-antdv-next/src/views/pay/wallet/balance/data.ts
new file mode 100644
index 000000000..51bf1c78d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/wallet/balance/data.ts
@@ -0,0 +1,137 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ placeholder: '请选择用户类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '编号',
+ field: 'id',
+ minWidth: 100,
+ },
+ {
+ title: '用户编号',
+ field: 'userId',
+ minWidth: 120,
+ },
+ {
+ title: '用户类型',
+ field: 'userType',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.USER_TYPE },
+ },
+ },
+ {
+ title: '余额',
+ field: 'balance',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '累计支出',
+ field: 'totalExpense',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '累计充值',
+ field: 'totalRecharge',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '冻结金额',
+ field: 'freezePrice',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ field: 'actions',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 钱包交易记录列表字段 */
+export function useTransactionGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'title',
+ title: '关联业务标题',
+ minWidth: 200,
+ },
+ {
+ field: 'price',
+ title: '交易金额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'balance',
+ title: '钱包余额',
+ minWidth: 120,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'createTime',
+ title: '交易时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/wallet/balance/index.vue b/apps/web-antdv-next/src/views/pay/wallet/balance/index.vue
new file mode 100644
index 000000000..e4b41f17b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/wallet/balance/index.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/wallet/balance/modules/detail.vue b/apps/web-antdv-next/src/views/pay/wallet/balance/modules/detail.vue
new file mode 100644
index 000000000..96749fd04
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/wallet/balance/modules/detail.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/data.ts b/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/data.ts
new file mode 100644
index 000000000..22010869e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/data.ts
@@ -0,0 +1,149 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '套餐名称',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '请输入套餐名称',
+ },
+ },
+ {
+ fieldName: 'payPrice',
+ label: '支付金额(元)',
+ component: 'InputNumber',
+ rules: z.number().min(0, '支付金额不能小于0'),
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.01,
+ placeholder: '请输入支付金额',
+ },
+ },
+ {
+ fieldName: 'bonusPrice',
+ label: '赠送金额(元)',
+ component: 'InputNumber',
+ rules: z.number().min(0, '赠送金额不能小于0'),
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.01,
+ placeholder: '请输入赠送金额',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '套餐名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入套餐名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '套餐编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '套餐名称',
+ minWidth: 150,
+ },
+ {
+ field: 'payPrice',
+ title: '支付金额',
+ minWidth: 120,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'bonusPrice',
+ title: '赠送金额',
+ minWidth: 120,
+ formatter: 'formatFenToYuanAmount',
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/index.vue b/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/index.vue
new file mode 100644
index 000000000..63bac0b41
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/index.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/modules/form.vue b/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/modules/form.vue
new file mode 100644
index 000000000..29173c689
--- /dev/null
+++ b/apps/web-antdv-next/src/views/pay/wallet/rechargePackage/modules/form.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/report/goview/index.vue b/apps/web-antdv-next/src/views/report/goview/index.vue
new file mode 100644
index 000000000..f228fa3e4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/report/goview/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/report/jmreport/bi.vue b/apps/web-antdv-next/src/views/report/jmreport/bi.vue
new file mode 100644
index 000000000..653a6ed4e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/report/jmreport/bi.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/report/jmreport/index.vue b/apps/web-antdv-next/src/views/report/jmreport/index.vue
new file mode 100644
index 000000000..73a47676c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/report/jmreport/index.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/area/data.ts b/apps/web-antdv-next/src/views/system/area/data.ts
new file mode 100644
index 000000000..e6cc06c99
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/area/data.ts
@@ -0,0 +1,48 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemAreaApi } from '#/api/system/area';
+
+import { z } from '#/adapter/form';
+
+/** 查询 IP 的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'ip',
+ label: 'IP 地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 IP 地址',
+ },
+ rules: z.string().ip({ message: '请输入正确的 IP 地址' }),
+ },
+ {
+ fieldName: 'result',
+ label: '地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '展示查询 IP 结果',
+ readonly: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '地区编码',
+ minWidth: 120,
+ align: 'left',
+ fixed: 'left',
+ treeNode: true,
+ },
+ {
+ field: 'name',
+ title: '地区名称',
+ minWidth: 200,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/area/index.vue b/apps/web-antdv-next/src/views/system/area/index.vue
new file mode 100644
index 000000000..a07141905
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/area/index.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/area/modules/form.vue b/apps/web-antdv-next/src/views/system/area/modules/form.vue
new file mode 100644
index 000000000..920f0c596
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/area/modules/form.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dept/components/index.ts b/apps/web-antdv-next/src/views/system/dept/components/index.ts
new file mode 100644
index 000000000..04d2b6145
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dept/components/index.ts
@@ -0,0 +1 @@
+export { default as DeptSelectModal } from './select-modal.vue';
diff --git a/apps/web-antdv-next/src/views/system/dept/components/select-modal.vue b/apps/web-antdv-next/src/views/system/dept/components/select-modal.vue
new file mode 100644
index 000000000..b53e32a49
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dept/components/select-modal.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dept/data.ts b/apps/web-antdv-next/src/views/system/dept/data.ts
new file mode 100644
index 000000000..f5421c429
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dept/data.ts
@@ -0,0 +1,162 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemDeptApi } from '#/api/system/dept';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 关联数据 */
+let userList: SystemUserApi.User[] = [];
+getSimpleUserList().then((data) => (userList = data));
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '上级部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getDeptList();
+ data.unshift({
+ id: 0,
+ name: '顶级部门',
+ });
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择上级部门',
+ treeDefaultExpandAll: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '部门名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入部门名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'leaderUserId',
+ label: '负责人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择负责人',
+ allowClear: true,
+ },
+ rules: z.number().optional(),
+ },
+ {
+ fieldName: 'phone',
+ label: '联系电话',
+ component: 'Input',
+ componentProps: {
+ maxLength: 11,
+ placeholder: '请输入联系电话',
+ },
+ rules: 'mobileRequired',
+ },
+ {
+ fieldName: 'email',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ },
+ rules: z.string().email('邮箱格式不正确').or(z.literal('')).optional(),
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'name',
+ title: '部门名称',
+ minWidth: 150,
+ align: 'left',
+ fixed: 'left',
+ treeNode: true,
+ },
+ {
+ field: 'leaderUserId',
+ title: '负责人',
+ minWidth: 150,
+ formatter: ({ cellValue }) =>
+ userList.find((user) => user.id === cellValue)?.nickname || '-',
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '部门状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/dept/index.vue b/apps/web-antdv-next/src/views/system/dept/index.vue
new file mode 100644
index 000000000..e85b290c2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dept/index.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dept/modules/form.vue b/apps/web-antdv-next/src/views/system/dept/modules/form.vue
new file mode 100644
index 000000000..07430494f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dept/modules/form.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dict/data.ts b/apps/web-antdv-next/src/views/system/dict/data.ts
new file mode 100644
index 000000000..a2c6ede66
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dict/data.ts
@@ -0,0 +1,353 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleDictTypeList } from '#/api/system/dict/type';
+
+// ============================== 字典类型 ==============================
+
+/** 类型新增/修改的表单 */
+export function useTypeFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '字典名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入字典名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '字典类型',
+ component: 'Input',
+ componentProps: (values) => {
+ return {
+ placeholder: '请输入字典类型',
+ disabled: !!values.id,
+ };
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: [''],
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 类型列表的搜索表单 */
+export function useTypeGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '字典名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入字典名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '字典类型',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入字典类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 类型列表的字段 */
+export function useTypeGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '字典编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '字典名称',
+ minWidth: 200,
+ },
+ {
+ field: 'type',
+ title: '字典类型',
+ minWidth: 220,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+// ============================== 字典数据 ==============================
+
+// TODO @芋艿:后续针对 antd,增加
+/**
+ * 颜色选项
+ */
+const colorOptions = [
+ { value: '', label: '无' },
+ { value: 'processing', label: '主要' },
+ { value: 'success', label: '成功' },
+ { value: 'default', label: '默认' },
+ { value: 'warning', label: '警告' },
+ { value: 'error', label: '危险' },
+ { value: 'pink', label: 'pink' },
+ { value: 'red', label: 'red' },
+ { value: 'orange', label: 'orange' },
+ { value: 'green', label: 'green' },
+ { value: 'cyan', label: 'cyan' },
+ { value: 'blue', label: 'blue' },
+ { value: 'purple', label: 'purple' },
+];
+
+/** 数据新增/修改的表单 */
+export function useDataFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'dictType',
+ label: '字典类型',
+ component: 'ApiSelect',
+ componentProps: (values) => {
+ return {
+ api: getSimpleDictTypeList,
+ placeholder: '请输入字典类型',
+ labelField: 'name',
+ valueField: 'type',
+ disabled: !!values.id,
+ };
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: [''],
+ },
+ },
+ {
+ fieldName: 'label',
+ label: '数据标签',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入数据标签',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'value',
+ label: '数据键值',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入数据键值',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '显示排序',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入显示排序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'colorType',
+ label: '颜色类型',
+ component: 'Select',
+ componentProps: {
+ options: colorOptions,
+ placeholder: '请选择颜色类型',
+ },
+ },
+ {
+ fieldName: 'cssClass',
+ label: 'CSS Class',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 CSS Class',
+ },
+ help: '输入 hex 模式的颜色, 例如 #108ee9',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 字典数据列表搜索表单 */
+export function useDataGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'label',
+ label: '字典标签',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入字典标签',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 字典数据表格列 */
+export function useDataGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '字典编码',
+ minWidth: 100,
+ },
+ {
+ field: 'label',
+ title: '字典标签',
+ minWidth: 180,
+ },
+ {
+ field: 'value',
+ title: '字典键值',
+ minWidth: 100,
+ },
+ {
+ field: 'sort',
+ title: '字典排序',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'colorType',
+ title: '颜色类型',
+ minWidth: 120,
+ slots: { default: 'colorType' },
+ },
+ {
+ field: 'cssClass',
+ title: 'CSS Class',
+ minWidth: 120,
+ slots: { default: 'cssClass' },
+ },
+ {
+ title: '创建时间',
+ field: 'createTime',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ minWidth: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/dict/index.vue b/apps/web-antdv-next/src/views/system/dict/index.vue
new file mode 100644
index 000000000..8d824256d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dict/index.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dict/modules/data-form.vue b/apps/web-antdv-next/src/views/system/dict/modules/data-form.vue
new file mode 100644
index 000000000..0511d90c4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dict/modules/data-form.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dict/modules/data-grid.vue b/apps/web-antdv-next/src/views/system/dict/modules/data-grid.vue
new file mode 100644
index 000000000..2e18c7027
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dict/modules/data-grid.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.colorType }}
+
+
+ {{ row.cssClass }}
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dict/modules/type-form.vue b/apps/web-antdv-next/src/views/system/dict/modules/type-form.vue
new file mode 100644
index 000000000..8de08d120
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dict/modules/type-form.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/dict/modules/type-grid.vue b/apps/web-antdv-next/src/views/system/dict/modules/type-grid.vue
new file mode 100644
index 000000000..5c333b306
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/dict/modules/type-grid.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/loginlog/data.ts b/apps/web-antdv-next/src/views/system/loginlog/data.ts
new file mode 100644
index 000000000..386d832de
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/loginlog/data.ts
@@ -0,0 +1,147 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'username',
+ label: '用户名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入用户名称',
+ },
+ },
+ {
+ fieldName: 'userIp',
+ label: '登录地址',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入登录地址',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '登录时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '日志编号',
+ minWidth: 100,
+ },
+ {
+ field: 'logType',
+ title: '登录类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_LOGIN_TYPE },
+ },
+ },
+ {
+ field: 'username',
+ title: '用户名称',
+ minWidth: 180,
+ },
+ {
+ field: 'userIp',
+ title: '登录地址',
+ minWidth: 180,
+ },
+ {
+ field: 'userAgent',
+ title: '浏览器',
+ minWidth: 200,
+ },
+ {
+ field: 'result',
+ title: '登录结果',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_LOGIN_RESULT },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '登录日期',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '日志编号',
+ },
+ {
+ field: 'logType',
+ label: '登录类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_LOGIN_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'username',
+ label: '用户名称',
+ },
+ {
+ field: 'userIp',
+ label: '登录地址',
+ },
+ {
+ field: 'userAgent',
+ label: '浏览器',
+ },
+ {
+ field: 'result',
+ label: '登录结果',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_LOGIN_RESULT,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'createTime',
+ label: '登录日期',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/loginlog/index.vue b/apps/web-antdv-next/src/views/system/loginlog/index.vue
new file mode 100644
index 000000000..e29ed41aa
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/loginlog/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/loginlog/modules/detail.vue b/apps/web-antdv-next/src/views/system/loginlog/modules/detail.vue
new file mode 100644
index 000000000..4982a32b3
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/loginlog/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/account/data.ts b/apps/web-antdv-next/src/views/system/mail/account/data.ts
new file mode 100644
index 000000000..2a00f39e4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/account/data.ts
@@ -0,0 +1,184 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'mail',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'username',
+ label: '用户名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'password',
+ label: '密码',
+ component: 'InputPassword',
+ componentProps: {
+ placeholder: '请输入密码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'host',
+ label: 'SMTP 服务器域名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 SMTP 服务器域名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'port',
+ label: 'SMTP 服务器端口',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入 SMTP 服务器端口',
+ min: 0,
+ max: 65_535,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sslEnable',
+ label: '是否开启 SSL',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.boolean().default(true),
+ },
+ {
+ fieldName: 'starttlsEnable',
+ label: '是否开启 STARTTLS',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.boolean().default(false),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'mail',
+ label: '邮箱',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入邮箱',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'username',
+ label: '用户名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'mail',
+ title: '邮箱',
+ minWidth: 160,
+ },
+ {
+ field: 'username',
+ title: '用户名',
+ minWidth: 160,
+ },
+ {
+ field: 'host',
+ title: 'SMTP 服务器域名',
+ minWidth: 150,
+ },
+ {
+ field: 'port',
+ title: 'SMTP 服务器端口',
+ minWidth: 130,
+ },
+ {
+ field: 'sslEnable',
+ title: '是否开启 SSL',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'starttlsEnable',
+ title: '是否开启 STARTTLS',
+ minWidth: 145,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/mail/account/index.vue b/apps/web-antdv-next/src/views/system/mail/account/index.vue
new file mode 100644
index 000000000..695683801
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/account/index.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/account/modules/form.vue b/apps/web-antdv-next/src/views/system/mail/account/modules/form.vue
new file mode 100644
index 000000000..b230aaa59
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/account/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/log/data.ts b/apps/web-antdv-next/src/views/system/mail/log/data.ts
new file mode 100644
index 000000000..3a12081d5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/log/data.ts
@@ -0,0 +1,259 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { getSimpleMailAccountList } from '#/api/system/mail/account';
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'sendTime',
+ label: '发送时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入用户编号',
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ allowClear: true,
+ placeholder: '请选择用户类型',
+ },
+ },
+ {
+ fieldName: 'sendStatus',
+ label: '发送状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择发送状态',
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '邮箱账号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleMailAccountList,
+ labelField: 'mail',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择邮箱账号',
+ },
+ },
+ {
+ fieldName: 'templateId',
+ label: '模板编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板编号',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'sendTime',
+ title: '发送时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'userType',
+ title: '接收用户',
+ minWidth: 150,
+ slots: { default: 'userInfo' },
+ },
+ {
+ field: 'toMails',
+ title: '接收信息',
+ minWidth: 300,
+ formatter: ({ row }) => {
+ const lines: string[] = [];
+ if (row.toMails && row.toMails.length > 0) {
+ lines.push(`收件:${row.toMails.join('、')}`);
+ }
+ if (row.ccMails && row.ccMails.length > 0) {
+ lines.push(`抄送:${row.ccMails.join('、')}`);
+ }
+ if (row.bccMails && row.bccMails.length > 0) {
+ lines.push(`密送:${row.bccMails.join('、')}`);
+ }
+ return lines.join('\n');
+ },
+ },
+ {
+ field: 'templateTitle',
+ title: '邮件标题',
+ minWidth: 120,
+ },
+ {
+ field: 'templateContent',
+ title: '邮件内容',
+ minWidth: 300,
+ },
+ {
+ field: 'fromMail',
+ title: '发送邮箱',
+ minWidth: 120,
+ },
+ {
+ field: 'sendStatus',
+ title: '发送状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS },
+ },
+ },
+ {
+ field: 'templateCode',
+ title: '模板编码',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '编号',
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => {
+ return formatDateTime(val) as string;
+ },
+ },
+ {
+ field: 'fromMail',
+ label: '发送邮箱',
+ },
+ {
+ field: 'userId',
+ label: '接收用户',
+ render: (val, data) => {
+ if (data?.userType && val) {
+ return h('div', [
+ h(DictTag, {
+ type: DICT_TYPE.USER_TYPE,
+ value: data.userType,
+ }),
+ ` (${val})`,
+ ]);
+ }
+ return '无';
+ },
+ },
+ {
+ field: 'toMails',
+ label: '接收信息',
+ render: (val, data) => {
+ const lines: string[] = [];
+ if (val && val.length > 0) {
+ lines.push(`收件:${val.join('、')}`);
+ }
+ if (data?.ccMails && data.ccMails.length > 0) {
+ lines.push(`抄送:${data.ccMails.join('、')}`);
+ }
+ if (data?.bccMails && data.bccMails.length > 0) {
+ lines.push(`密送:${data.bccMails.join('、')}`);
+ }
+ return h(
+ 'div',
+ {
+ style: { whiteSpace: 'pre-line' },
+ },
+ lines.join('\n'),
+ );
+ },
+ },
+ {
+ field: 'templateId',
+ label: '模板编号',
+ },
+ {
+ field: 'templateCode',
+ label: '模板编码',
+ },
+ {
+ field: 'templateTitle',
+ label: '邮件标题',
+ },
+ {
+ field: 'templateContent',
+ label: '邮件内容',
+ span: 2,
+ render: (val) => {
+ return h('div', {
+ innerHTML: val || '',
+ });
+ },
+ },
+ {
+ field: 'sendStatus',
+ label: '发送状态',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'sendTime',
+ label: '发送时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'sendMessageId',
+ label: '发送消息编号',
+ },
+ {
+ field: 'sendException',
+ label: '发送异常',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/mail/log/index.vue b/apps/web-antdv-next/src/views/system/mail/log/index.vue
new file mode 100644
index 000000000..9d0584e9a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/log/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ({{ row.userId }})
+
+ -
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/log/modules/detail.vue b/apps/web-antdv-next/src/views/system/mail/log/modules/detail.vue
new file mode 100644
index 000000000..3b04a9d04
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/log/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/template/data.ts b/apps/web-antdv-next/src/views/system/mail/template/data.ts
new file mode 100644
index 000000000..71f4ab397
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/template/data.ts
@@ -0,0 +1,258 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleMailAccountList } from '#/api/system/mail/account';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'code',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板编码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'accountId',
+ label: '邮箱账号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleMailAccountList,
+ labelField: 'mail',
+ valueField: 'id',
+ placeholder: '请选择邮箱账号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'nickname',
+ label: '发送人名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入发送人名称',
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '模板标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板标题',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'content',
+ label: '模板内容',
+ component: 'RichTextarea',
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 发送邮件表单 */
+export function useSendMailFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'templateParams',
+ label: '模板参数',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'content',
+ label: '模板内容',
+ component: 'RichTextarea',
+ componentProps: {
+ options: {
+ readonly: true,
+ },
+ },
+ },
+ {
+ fieldName: 'toMails',
+ label: '收件邮箱',
+ component: 'Select',
+ componentProps: {
+ mode: 'tags',
+ allowClear: true,
+ placeholder: '请输入收件邮箱,按 Enter 添加',
+ },
+ },
+ {
+ fieldName: 'ccMails',
+ label: '抄送邮箱',
+ component: 'Select',
+ componentProps: {
+ mode: 'tags',
+ allowClear: true,
+ placeholder: '请输入抄送邮箱,按 Enter 添加',
+ },
+ },
+ {
+ fieldName: 'bccMails',
+ label: '密送邮箱',
+ component: 'Select',
+ componentProps: {
+ mode: 'tags',
+ allowClear: true,
+ placeholder: '请输入密送邮箱,按 Enter 添加',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择开启状态',
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板编码',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板名称',
+ },
+ },
+ {
+ fieldName: 'accountId',
+ label: '邮箱账号',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleMailAccountList,
+ labelField: 'mail',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择邮箱账号',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ getAccountMail?: (accountId: number) => string | undefined,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'code',
+ title: '模板编码',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '模板名称',
+ minWidth: 120,
+ },
+ {
+ field: 'title',
+ title: '模板标题',
+ minWidth: 120,
+ },
+ {
+ field: 'accountId',
+ title: '邮箱账号',
+ minWidth: 120,
+ formatter: ({ cellValue }) => getAccountMail?.(cellValue) || '-',
+ },
+ {
+ field: 'nickname',
+ title: '发送人名称',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '开启状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/mail/template/index.vue b/apps/web-antdv-next/src/views/system/mail/template/index.vue
new file mode 100644
index 000000000..1ea2e4666
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/template/index.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/template/modules/form.vue b/apps/web-antdv-next/src/views/system/mail/template/modules/form.vue
new file mode 100644
index 000000000..a5eefe79b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/template/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/mail/template/modules/send-form.vue b/apps/web-antdv-next/src/views/system/mail/template/modules/send-form.vue
new file mode 100644
index 000000000..d24acd118
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/mail/template/modules/send-form.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/menu/data.ts b/apps/web-antdv-next/src/views/system/menu/data.ts
new file mode 100644
index 000000000..d7cfa104e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/menu/data.ts
@@ -0,0 +1,348 @@
+import type { Recordable } from '@vben/types';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemMenuApi } from '#/api/system/menu';
+
+import { h } from 'vue';
+
+import {
+ CommonStatusEnum,
+ DICT_TYPE,
+ SystemMenuTypeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { IconifyIcon } from '@vben/icons';
+import { handleTree, isHttpUrl } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getMenuList } from '#/api/system/menu';
+import { $t } from '#/locales';
+import { componentKeys } from '#/router/routes';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '上级菜单',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => {
+ const data = await getMenuList();
+ data.unshift({
+ id: 0,
+ name: '顶级部门',
+ } as SystemMenuApi.Menu);
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择上级菜单',
+ filterTreeNode(input: string, node: Recordable) {
+ if (!input || input.length === 0) {
+ return true;
+ }
+ const name: string = node.label ?? '';
+ if (!name) return false;
+ return name.includes(input) || $t(name).includes(input);
+ },
+ showSearch: true,
+ treeDefaultExpandedKeys: [0],
+ },
+ rules: 'selectRequired',
+ renderComponentContent() {
+ return {
+ title({ label, icon }: { icon: string; label: string }) {
+ const components = [];
+ if (!label) return '';
+ if (icon) {
+ components.push(h(IconifyIcon, { class: 'size-4', icon }));
+ }
+ components.push(h('span', { class: '' }, $t(label || '')));
+ return h('div', { class: 'flex items-center gap-1' }, components);
+ },
+ };
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '菜单名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入菜单名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '菜单类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(SystemMenuTypeEnum.DIR),
+ },
+ {
+ fieldName: 'icon',
+ label: '菜单图标',
+ component: 'IconPicker',
+ componentProps: {
+ placeholder: '请选择菜单图标',
+ prefix: 'carbon',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
+ values.type,
+ );
+ },
+ },
+ },
+ {
+ fieldName: 'path',
+ label: '路由地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入路由地址',
+ },
+ rules: z.string(),
+ help: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头',
+ dependencies: {
+ triggerFields: ['type', 'parentId'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
+ values.type,
+ );
+ },
+ rules: (values) => {
+ const schema = z.string().min(1, '路由地址不能为空');
+ if (isHttpUrl(values.path)) {
+ return schema;
+ }
+ if (values.parentId === 0) {
+ return schema.refine(
+ (path) => path.charAt(0) === '/',
+ '路径必须以 / 开头',
+ );
+ }
+ return schema.refine(
+ (path) => path.charAt(0) !== '/',
+ '路径不能以 / 开头',
+ );
+ },
+ },
+ },
+ {
+ fieldName: 'component',
+ label: '组件地址',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入组件地址',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.MENU].includes(values.type);
+ },
+ },
+ },
+ {
+ fieldName: 'componentName',
+ label: '组件名称',
+ component: 'AutoComplete',
+ componentProps: {
+ allowClear: true,
+ filterOption(input: string, option: { value: string }) {
+ return option.value.toLowerCase().includes(input.toLowerCase());
+ },
+ placeholder: '请选择组件名称',
+ options: componentKeys.map((v) => ({ value: v })),
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.MENU].includes(values.type);
+ },
+ },
+ },
+ {
+ fieldName: 'permission',
+ label: '权限标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入菜单描述',
+ },
+ dependencies: {
+ show: (values) => {
+ return [SystemMenuTypeEnum.BUTTON, SystemMenuTypeEnum.MENU].includes(
+ values.type,
+ );
+ },
+ triggerFields: ['type'],
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '菜单状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'visible',
+ label: '显示状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '显示', value: true },
+ { label: '隐藏', value: false },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ defaultValue: true,
+ help: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
+ values.type,
+ );
+ },
+ },
+ },
+ {
+ fieldName: 'alwaysShow',
+ label: '总是显示',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '总是', value: true },
+ { label: '不是', value: false },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ defaultValue: true,
+ help: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.MENU].includes(values.type);
+ },
+ },
+ },
+ {
+ fieldName: 'keepAlive',
+ label: '缓存状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '缓存', value: true },
+ { label: '不缓存', value: false },
+ ],
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ defaultValue: true,
+ help: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段',
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => {
+ return [SystemMenuTypeEnum.MENU].includes(values.type);
+ },
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '菜单名称',
+ minWidth: 250,
+ align: 'left',
+ fixed: 'left',
+ slots: { default: 'name' },
+ treeNode: true,
+ },
+ {
+ field: 'type',
+ title: '菜单类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_MENU_TYPE },
+ },
+ },
+ {
+ field: 'sort',
+ title: '显示排序',
+ minWidth: 100,
+ },
+ {
+ field: 'permission',
+ title: '权限标识',
+ minWidth: 200,
+ },
+ {
+ field: 'path',
+ title: '组件路径',
+ minWidth: 200,
+ },
+ {
+ field: 'componentName',
+ title: '组件名称',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/menu/index.vue b/apps/web-antdv-next/src/views/system/menu/index.vue
new file mode 100644
index 000000000..6bf0af42d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/menu/index.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t(row.name) }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/menu/modules/form.vue b/apps/web-antdv-next/src/views/system/menu/modules/form.vue
new file mode 100644
index 000000000..ae9662205
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/menu/modules/form.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notice/data.ts b/apps/web-antdv-next/src/views/system/notice/data.ts
new file mode 100644
index 000000000..86109dbf5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notice/data.ts
@@ -0,0 +1,138 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'title',
+ label: '公告标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入公告标题',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '公告类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'content',
+ label: '公告内容',
+ component: 'RichTextarea',
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '公告状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'title',
+ label: '公告标题',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入公告标题',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '公告状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择公告状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '公告编号',
+ minWidth: 100,
+ },
+ {
+ field: 'title',
+ title: '公告标题',
+ minWidth: 200,
+ },
+ {
+ field: 'type',
+ title: '公告类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_NOTICE_TYPE },
+ },
+ },
+ {
+ field: 'status',
+ title: '公告状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/notice/index.vue b/apps/web-antdv-next/src/views/system/notice/index.vue
new file mode 100644
index 000000000..97fbd767e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notice/index.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notice/modules/form.vue b/apps/web-antdv-next/src/views/system/notice/modules/form.vue
new file mode 100644
index 000000000..eac023885
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notice/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/message/data.ts b/apps/web-antdv-next/src/views/system/notify/message/data.ts
new file mode 100644
index 000000000..c6a315229
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/message/data.ts
@@ -0,0 +1,237 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入用户编号',
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ placeholder: '请选择用户类型',
+ },
+ },
+ {
+ fieldName: 'templateCode',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板编码',
+ },
+ },
+ {
+ fieldName: 'templateType',
+ label: '模版类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
+ 'number',
+ ),
+ allowClear: true,
+ placeholder: '请选择模版类型',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userType',
+ title: '用户类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.USER_TYPE },
+ },
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'templateCode',
+ title: '模板编码',
+ minWidth: 120,
+ },
+ {
+ field: 'templateNickname',
+ title: '发送人名称',
+ minWidth: 180,
+ },
+ {
+ field: 'templateContent',
+ title: '模版内容',
+ minWidth: 200,
+ },
+ {
+ field: 'templateParams',
+ title: '模版参数',
+ minWidth: 180,
+ formatter: ({ cellValue }) => {
+ try {
+ return JSON.stringify(cellValue);
+ } catch {
+ return '';
+ }
+ },
+ },
+ {
+ field: 'templateType',
+ title: '模版类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
+ },
+ },
+ {
+ field: 'readStatus',
+ title: '是否已读',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'readTime',
+ title: '阅读时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '编号',
+ },
+ {
+ field: 'userType',
+ label: '用户类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.USER_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'userId',
+ label: '用户编号',
+ },
+ {
+ field: 'templateId',
+ label: '模版编号',
+ },
+ {
+ field: 'templateCode',
+ label: '模板编码',
+ },
+ {
+ field: 'templateNickname',
+ label: '发送人名称',
+ },
+ {
+ field: 'templateContent',
+ label: '模版内容',
+ },
+ {
+ field: 'templateParams',
+ label: '模版参数',
+ render: (val) => {
+ try {
+ return JSON.stringify(val);
+ } catch {
+ return '';
+ }
+ },
+ },
+ {
+ field: 'templateType',
+ label: '模版类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'readStatus',
+ label: '是否已读',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.INFRA_BOOLEAN_STRING,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'readTime',
+ label: '阅读时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/notify/message/index.vue b/apps/web-antdv-next/src/views/system/notify/message/index.vue
new file mode 100644
index 000000000..5d7953246
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/message/index.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/message/modules/detail.vue b/apps/web-antdv-next/src/views/system/notify/message/modules/detail.vue
new file mode 100644
index 000000000..a0e100dd4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/message/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/my/data.ts b/apps/web-antdv-next/src/views/system/notify/my/data.ts
new file mode 100644
index 000000000..b31d437c2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/my/data.ts
@@ -0,0 +1,137 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'readStatus',
+ label: '是否已读',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
+ allowClear: true,
+ placeholder: '请选择是否已读',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '发送时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '',
+ width: 40,
+ type: 'checkbox',
+ },
+ {
+ field: 'templateNickname',
+ title: '发送人',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '发送时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'templateType',
+ title: '类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
+ },
+ },
+ {
+ field: 'templateContent',
+ title: '消息内容',
+ minWidth: 300,
+ },
+ {
+ field: 'readStatus',
+ title: '是否已读',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'readTime',
+ title: '阅读时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'templateNickname',
+ label: '发送人',
+ },
+ {
+ field: 'createTime',
+ label: '发送时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'templateType',
+ label: '消息类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'readStatus',
+ label: '是否已读',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.INFRA_BOOLEAN_STRING,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'readTime',
+ label: '阅读时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'templateContent',
+ label: '消息内容',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/notify/my/index.vue b/apps/web-antdv-next/src/views/system/notify/my/index.vue
new file mode 100644
index 000000000..5df46381e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/my/index.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/my/modules/detail.vue b/apps/web-antdv-next/src/views/system/notify/my/modules/detail.vue
new file mode 100644
index 000000000..a0e100dd4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/my/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/template/data.ts b/apps/web-antdv-next/src/views/system/notify/template/data.ts
new file mode 100644
index 000000000..d22b16ce9
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/template/data.ts
@@ -0,0 +1,288 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE, UserTypeEnum } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'code',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板编码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'nickname',
+ label: '发送人名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入发送人名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'content',
+ label: '模板内容',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入模板内容',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '模板类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
+ 'number',
+ ),
+ placeholder: '请选择模板类型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板名称',
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板编码',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择状态',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '模板类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
+ 'number',
+ ),
+ allowClear: true,
+ placeholder: '请选择模板类型',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 发送站内信表单 */
+export function useSendNotifyFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'content',
+ label: '模板内容',
+ component: 'Textarea',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'templateCode',
+ label: '模板编码',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ },
+ rules: z.number().default(UserTypeEnum.MEMBER),
+ },
+ {
+ fieldName: 'userId',
+ label: '接收人 ID',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户编号',
+ },
+ dependencies: {
+ show(values) {
+ return values.userType === UserTypeEnum.MEMBER;
+ },
+ triggerFields: ['userType'],
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'userId',
+ label: '接收人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '请选择接收人',
+ },
+ dependencies: {
+ show(values) {
+ return values.userType === UserTypeEnum.ADMIN;
+ },
+ triggerFields: ['userType'],
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'templateParams',
+ label: '模板参数',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '模板名称',
+ minWidth: 120,
+ },
+ {
+ field: 'code',
+ title: '模板编码',
+ minWidth: 120,
+ },
+ {
+ field: 'nickname',
+ title: '发送人名称',
+ minWidth: 120,
+ },
+ {
+ field: 'content',
+ title: '模板内容',
+ minWidth: 200,
+ },
+ {
+ field: 'type',
+ title: '模板类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 120,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/notify/template/index.vue b/apps/web-antdv-next/src/views/system/notify/template/index.vue
new file mode 100644
index 000000000..6b7cc439f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/template/index.vue
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/template/modules/form.vue b/apps/web-antdv-next/src/views/system/notify/template/modules/form.vue
new file mode 100644
index 000000000..20bb704a8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/template/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/notify/template/modules/send-form.vue b/apps/web-antdv-next/src/views/system/notify/template/modules/send-form.vue
new file mode 100644
index 000000000..94b4ad7b4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/notify/template/modules/send-form.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/oauth2/client/data.ts b/apps/web-antdv-next/src/views/system/oauth2/client/data.ts
new file mode 100644
index 000000000..b5bc683c0
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/oauth2/client/data.ts
@@ -0,0 +1,263 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'clientId',
+ label: '客户端编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户端编号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'secret',
+ label: '客户端密钥',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户端密钥',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'name',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入应用名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'logo',
+ label: '应用图标',
+ component: 'ImageUpload',
+ rules: 'required',
+ },
+ {
+ fieldName: 'description',
+ label: '应用描述',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入应用描述',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'accessTokenValiditySeconds',
+ label: '访问令牌的有效期',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入访问令牌的有效期,单位:秒',
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'refreshTokenValiditySeconds',
+ label: '刷新令牌的有效期',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入刷新令牌的有效期,单位:秒',
+ min: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'authorizedGrantTypes',
+ label: '授权类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE),
+ mode: 'multiple',
+ placeholder: '请输入授权类型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'scopes',
+ label: '授权范围',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请输入授权范围',
+ mode: 'tags',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'autoApproveScopes',
+ label: '自动授权范围',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请输入自动授权范围',
+ mode: 'multiple',
+ },
+ dependencies: {
+ triggerFields: ['scopes'],
+ componentProps: (values) => ({
+ options: values.scopes
+ ? values.scopes.map((scope: string) => ({
+ label: scope,
+ value: scope,
+ }))
+ : [],
+ }),
+ },
+ },
+ {
+ fieldName: 'redirectUris',
+ label: '可重定向的 URI 地址',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请输入可重定向的 URI 地址',
+ mode: 'tags',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'authorities',
+ label: '权限',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请输入权限',
+ mode: 'tags',
+ },
+ },
+ {
+ fieldName: 'resourceIds',
+ label: '资源',
+ component: 'Select',
+ componentProps: {
+ mode: 'tags',
+ placeholder: '请输入资源',
+ },
+ },
+ {
+ fieldName: 'additionalInformation',
+ label: '附加信息',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入附加信息,JSON 格式数据',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入应用名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请输入状态',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'clientId',
+ title: '客户端编号',
+ minWidth: 120,
+ },
+ {
+ field: 'secret',
+ title: '客户端密钥',
+ minWidth: 120,
+ },
+ {
+ field: 'name',
+ title: '应用名',
+ minWidth: 120,
+ },
+ {
+ field: 'logo',
+ title: '应用图标',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'accessTokenValiditySeconds',
+ title: '访问令牌的有效期',
+ minWidth: 150,
+ formatter: ({ cellValue }) => `${cellValue} 秒`,
+ },
+ {
+ field: 'refreshTokenValiditySeconds',
+ title: '刷新令牌的有效期',
+ minWidth: 150,
+ formatter: ({ cellValue }) => `${cellValue} 秒`,
+ },
+ {
+ field: 'authorizedGrantTypes',
+ title: '授权类型',
+ minWidth: 100,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/oauth2/client/index.vue b/apps/web-antdv-next/src/views/system/oauth2/client/index.vue
new file mode 100644
index 000000000..383951b66
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/oauth2/client/index.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/oauth2/client/modules/form.vue b/apps/web-antdv-next/src/views/system/oauth2/client/modules/form.vue
new file mode 100644
index 000000000..ea3f47d4b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/oauth2/client/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/oauth2/token/data.ts b/apps/web-antdv-next/src/views/system/oauth2/token/data.ts
new file mode 100644
index 000000000..ab5cf8a66
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/oauth2/token/data.ts
@@ -0,0 +1,93 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '用户编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ placeholder: '请选择用户类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'clientId',
+ label: '客户端编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户端编号',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'accessToken',
+ title: '访问令牌',
+ minWidth: 300,
+ },
+ {
+ field: 'refreshToken',
+ title: '刷新令牌',
+ minWidth: 300,
+ },
+ {
+ field: 'userId',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userType',
+ title: '用户类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.USER_TYPE },
+ },
+ },
+ {
+ field: 'clientId',
+ title: '客户端编号',
+ minWidth: 120,
+ },
+ {
+ field: 'expiresTime',
+ title: '过期时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/oauth2/token/index.vue b/apps/web-antdv-next/src/views/system/oauth2/token/index.vue
new file mode 100644
index 000000000..84a0886e8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/oauth2/token/index.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/operatelog/data.ts b/apps/web-antdv-next/src/views/system/operatelog/data.ts
new file mode 100644
index 000000000..c75d4c26f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/operatelog/data.ts
@@ -0,0 +1,200 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'userId',
+ label: '操作人',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择操作人员',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '操作模块',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入操作模块',
+ },
+ },
+ {
+ fieldName: 'subType',
+ label: '操作名',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入操作名',
+ },
+ },
+ {
+ fieldName: 'action',
+ label: '操作内容',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入操作内容',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '操作时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'bizId',
+ label: '业务编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入业务编号',
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '日志编号',
+ minWidth: 100,
+ },
+ {
+ field: 'userName',
+ title: '操作人',
+ minWidth: 120,
+ },
+ {
+ field: 'type',
+ title: '操作模块',
+ minWidth: 120,
+ },
+ {
+ field: 'subType',
+ title: '操作名',
+ minWidth: 160,
+ },
+ {
+ field: 'action',
+ title: '操作内容',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '操作时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'bizId',
+ title: '业务编号',
+ minWidth: 120,
+ },
+ {
+ field: 'userIp',
+ title: '操作 IP',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'id',
+ label: '日志编号',
+ },
+ {
+ field: 'traceId',
+ label: '链路追踪',
+ show: (data) => !data?.traceId,
+ },
+ {
+ field: 'userId',
+ label: '操作人编号',
+ },
+ {
+ field: 'userType',
+ label: '操作人类型',
+ render: (val) => h(DictTag, { type: DICT_TYPE.USER_TYPE, value: val }),
+ },
+ {
+ field: 'userName',
+ label: '操作人名字',
+ },
+ {
+ field: 'userIp',
+ label: '操作人 IP',
+ },
+ {
+ field: 'userAgent',
+ label: '操作人 UA',
+ },
+ {
+ field: 'type',
+ label: '操作模块',
+ },
+ {
+ field: 'subType',
+ label: '操作名',
+ },
+ {
+ field: 'action',
+ label: '操作内容',
+ },
+ {
+ field: 'extra',
+ label: '操作拓展参数',
+ show: (val) => !val,
+ },
+ {
+ field: 'requestUrl',
+ label: '请求 URL',
+ render: (val, data) => {
+ if (data?.requestMethod && val) {
+ return `${data.requestMethod} ${val}`;
+ }
+ return '';
+ },
+ },
+ {
+ field: 'createTime',
+ label: '操作时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'bizId',
+ label: '业务编号',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/operatelog/index.vue b/apps/web-antdv-next/src/views/system/operatelog/index.vue
new file mode 100644
index 000000000..fe5dcc220
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/operatelog/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/operatelog/modules/detail.vue b/apps/web-antdv-next/src/views/system/operatelog/modules/detail.vue
new file mode 100644
index 000000000..5ada4818a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/operatelog/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/post/data.ts b/apps/web-antdv-next/src/views/system/post/data.ts
new file mode 100644
index 000000000..2d1715389
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/post/data.ts
@@ -0,0 +1,155 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'name',
+ label: '岗位名称',
+ componentProps: {
+ placeholder: '请输入岗位名称',
+ },
+ rules: 'required',
+ },
+ {
+ component: 'Input',
+ fieldName: 'code',
+ label: '岗位编码',
+ componentProps: {
+ placeholder: '请输入岗位编码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '岗位状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '岗位备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入岗位备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '岗位名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入岗位名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '岗位编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入岗位编码',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '岗位状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择岗位状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '岗位编号',
+ minWidth: 200,
+ },
+ {
+ field: 'name',
+ title: '岗位名称',
+ minWidth: 200,
+ },
+ {
+ field: 'code',
+ title: '岗位编码',
+ minWidth: 200,
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ minWidth: 100,
+ },
+ {
+ field: 'remark',
+ title: '岗位备注',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '岗位状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/post/index.vue b/apps/web-antdv-next/src/views/system/post/index.vue
new file mode 100644
index 000000000..c59418e6c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/post/index.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/post/modules/form.vue b/apps/web-antdv-next/src/views/system/post/modules/form.vue
new file mode 100644
index 000000000..ebd8fdab4
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/post/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/role/data.ts b/apps/web-antdv-next/src/views/system/role/data.ts
new file mode 100644
index 000000000..48630c86e
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/role/data.ts
@@ -0,0 +1,264 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import {
+ CommonStatusEnum,
+ DICT_TYPE,
+ SystemDataScopeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '角色名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入角色名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'code',
+ label: '角色标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入角色标识',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'sort',
+ label: '显示顺序',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ placeholder: '请输入显示顺序',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '角色状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '角色备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入角色备注',
+ },
+ },
+ ];
+}
+
+/** 分配数据权限的表单 */
+export function useAssignDataPermissionFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '角色名称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ component: 'Input',
+ fieldName: 'code',
+ label: '角色标识',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ component: 'Select',
+ fieldName: 'dataScope',
+ label: '权限范围',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE, 'number'),
+ },
+ },
+ {
+ fieldName: 'dataScopeDeptIds',
+ label: '部门范围',
+ component: 'Input',
+ formItemClass: 'items-start',
+ dependencies: {
+ triggerFields: ['dataScope'],
+ show: (values) => {
+ return values.dataScope === SystemDataScopeEnum.DEPT_CUSTOM;
+ },
+ },
+ },
+ ];
+}
+
+/** 分配菜单的表单 */
+export function useAssignMenuFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '角色名称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '角色标识',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'menuIds',
+ label: '菜单权限',
+ component: 'Input',
+ formItemClass: 'items-start',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '角色名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入角色名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '角色标识',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入角色标识',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '角色状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择角色状态',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '角色编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '角色名称',
+ minWidth: 200,
+ },
+ {
+ field: 'type',
+ title: '角色类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_ROLE_TYPE },
+ },
+ },
+ {
+ field: 'code',
+ title: '角色标识',
+ minWidth: 200,
+ },
+ {
+ field: 'sort',
+ title: '显示顺序',
+ minWidth: 100,
+ },
+ {
+ field: 'remark',
+ title: '角色备注',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '角色状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 240,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/role/index.vue b/apps/web-antdv-next/src/views/system/role/index.vue
new file mode 100644
index 000000000..983ee472f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/role/index.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/role/modules/assign-data-permission-form.vue b/apps/web-antdv-next/src/views/system/role/modules/assign-data-permission-form.vue
new file mode 100644
index 000000000..c453246b2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/role/modules/assign-data-permission-form.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+ 全选
+
+
+ 全部展开
+
+
+ 父子联动
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/role/modules/assign-menu-form.vue b/apps/web-antdv-next/src/views/system/role/modules/assign-menu-form.vue
new file mode 100644
index 000000000..de7b51cc7
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/role/modules/assign-menu-form.vue
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+ 全选
+
+
+ 全部展开
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/role/modules/form.vue b/apps/web-antdv-next/src/views/system/role/modules/form.vue
new file mode 100644
index 000000000..2d0b27071
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/role/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/channel/data.ts b/apps/web-antdv-next/src/views/system/sms/channel/data.ts
new file mode 100644
index 000000000..3320291c2
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/channel/data.ts
@@ -0,0 +1,195 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'signature',
+ label: '短信签名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入短信签名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'code',
+ label: '渠道编码',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
+ placeholder: '请选择短信渠道',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '启用状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ {
+ fieldName: 'apiKey',
+ label: '短信 API 的账号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入短信 API 的账号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'apiSecret',
+ label: '短信 API 的密钥',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入短信 API 的密钥',
+ },
+ },
+ {
+ fieldName: 'callbackUrl',
+ label: '短信发送回调 URL',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入短信发送回调 URL',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'signature',
+ label: '短信签名',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入短信签名',
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '渠道编码',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
+ placeholder: '请选择短信渠道',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'signature',
+ title: '短信签名',
+ minWidth: 120,
+ },
+ {
+ field: 'code',
+ title: '渠道编码',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'status',
+ title: '启用状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'apiKey',
+ title: '短信 API 的账号',
+ minWidth: 180,
+ },
+ {
+ field: 'apiSecret',
+ title: '短信 API 的密钥',
+ minWidth: 180,
+ },
+ {
+ field: 'callbackUrl',
+ title: '短信发送回调 URL',
+ minWidth: 180,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/sms/channel/index.vue b/apps/web-antdv-next/src/views/system/sms/channel/index.vue
new file mode 100644
index 000000000..e62bd3e2a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/channel/index.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/channel/modules/form.vue b/apps/web-antdv-next/src/views/system/sms/channel/modules/form.vue
new file mode 100644
index 000000000..a88979a56
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/channel/modules/form.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/log/data.ts b/apps/web-antdv-next/src/views/system/sms/log/data.ts
new file mode 100644
index 000000000..3babce416
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/log/data.ts
@@ -0,0 +1,265 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { formatDateTime } from '@vben/utils';
+
+import { getSimpleSmsChannelList } from '#/api/system/sms/channel';
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'mobile',
+ label: '手机号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入手机号',
+ },
+ },
+ {
+ fieldName: 'channelId',
+ label: '短信渠道',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleSmsChannelList,
+ labelField: 'signature',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择短信渠道',
+ },
+ },
+ {
+ fieldName: 'templateId',
+ label: '模板编号',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板编号',
+ },
+ },
+ {
+ fieldName: 'sendStatus',
+ label: '发送状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择发送状态',
+ },
+ },
+ {
+ fieldName: 'sendTime',
+ label: '发送时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'receiveStatus',
+ label: '接收状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择接收状态',
+ },
+ },
+ {
+ fieldName: 'receiveTime',
+ label: '接收时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'mobile',
+ title: '手机号',
+ minWidth: 120,
+ },
+ {
+ field: 'templateContent',
+ title: '短信内容',
+ minWidth: 300,
+ },
+ {
+ field: 'sendStatus',
+ title: '发送状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_SEND_STATUS },
+ },
+ },
+ {
+ field: 'sendTime',
+ title: '发送时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'receiveStatus',
+ title: '接收状态',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS },
+ },
+ },
+ {
+ field: 'receiveTime',
+ title: '接收时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'channelCode',
+ title: '短信渠道',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'templateId',
+ title: '模板编号',
+ minWidth: 100,
+ },
+ {
+ field: 'templateType',
+ title: '短信类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'createTime',
+ label: '创建时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'mobile',
+ label: '手机号',
+ },
+ {
+ field: 'channelCode',
+ label: '短信渠道',
+ },
+ {
+ field: 'templateId',
+ label: '模板编号',
+ },
+ {
+ field: 'templateType',
+ label: '模板类型',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'templateContent',
+ label: '短信内容',
+ },
+ {
+ field: 'sendStatus',
+ label: '发送状态',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_SMS_SEND_STATUS,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'sendTime',
+ label: '发送时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'apiSendCode',
+ label: 'API 发送编码',
+ },
+ {
+ field: 'apiSendMsg',
+ label: 'API 发送消息',
+ },
+ {
+ field: 'receiveStatus',
+ label: '接收状态',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'receiveTime',
+ label: '接收时间',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'apiReceiveCode',
+ label: 'API 接收编码',
+ },
+ {
+ field: 'apiReceiveMsg',
+ label: 'API 接收消息',
+ span: 2,
+ },
+ {
+ field: 'apiRequestId',
+ label: 'API 请求 ID',
+ },
+ {
+ field: 'apiSerialNo',
+ label: 'API 序列号',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/sms/log/index.vue b/apps/web-antdv-next/src/views/system/sms/log/index.vue
new file mode 100644
index 000000000..ad2de3035
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/log/index.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/log/modules/detail.vue b/apps/web-antdv-next/src/views/system/sms/log/modules/detail.vue
new file mode 100644
index 000000000..2831f6ff5
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/log/modules/detail.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/template/data.ts b/apps/web-antdv-next/src/views/system/sms/template/data.ts
new file mode 100644
index 000000000..13786c56a
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/template/data.ts
@@ -0,0 +1,274 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getSimpleSmsChannelList } from '#/api/system/sms/channel';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '短信类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, 'number'),
+ placeholder: '请选择短信类型',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'code',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入模板编码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'channelId',
+ label: '短信渠道',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleSmsChannelList,
+ labelField: 'signature',
+ valueField: 'id',
+ placeholder: '请选择短信渠道',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'content',
+ label: '模板内容',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入模板内容',
+ rows: 4,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'apiTemplateId',
+ label: '短信 API 的模板编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入短信 API 的模板编号',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'type',
+ label: '短信类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, 'number'),
+ allowClear: true,
+ placeholder: '请选择短信类型',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '开启状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择开启状态',
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '模板编码',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板编码',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '模板名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入模板名称',
+ },
+ },
+ {
+ fieldName: 'channelId',
+ label: '短信渠道',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleSmsChannelList,
+ labelField: 'signature',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '请选择短信渠道',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 发送短信表单 */
+export function useSendSmsFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'content',
+ label: '模板内容',
+ component: 'Textarea',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'templateParams',
+ label: '模板参数',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'type',
+ title: '短信类型',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE },
+ },
+ },
+ {
+ field: 'name',
+ title: '模板名称',
+ minWidth: 120,
+ },
+ {
+ field: 'code',
+ title: '模板编码',
+ minWidth: 120,
+ },
+ {
+ field: 'content',
+ title: '模板内容',
+ minWidth: 200,
+ },
+ {
+ field: 'status',
+ title: '开启状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'apiTemplateId',
+ title: '短信 API 的模板编号',
+ minWidth: 180,
+ },
+ {
+ field: 'channelCode',
+ title: '短信渠道',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 120,
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/sms/template/index.vue b/apps/web-antdv-next/src/views/system/sms/template/index.vue
new file mode 100644
index 000000000..3563a0e07
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/template/index.vue
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/template/modules/form.vue b/apps/web-antdv-next/src/views/system/sms/template/modules/form.vue
new file mode 100644
index 000000000..8edbc735c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/template/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/sms/template/modules/send-form.vue b/apps/web-antdv-next/src/views/system/sms/template/modules/send-form.vue
new file mode 100644
index 000000000..be72a0831
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/sms/template/modules/send-form.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/social/client/data.ts b/apps/web-antdv-next/src/views/system/social/client/data.ts
new file mode 100644
index 000000000..c9638ff37
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/social/client/data.ts
@@ -0,0 +1,224 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import {
+ CommonStatusEnum,
+ DICT_TYPE,
+ SystemUserSocialTypeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入应用名',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'socialType',
+ label: '社交平台',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
+ placeholder: '请选择社交平台',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'clientId',
+ label: '客户端编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户端编号,对应各平台的 appKey',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'clientSecret',
+ label: '客户端密钥',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户端密钥,对应各平台的 appSecret',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'agentId',
+ label: 'agentId',
+ component: 'Input',
+ componentProps: {
+ placeholder: '授权方的网页应用 ID,有则填',
+ },
+ dependencies: {
+ triggerFields: ['socialType'],
+ show: (values) =>
+ values.socialType === SystemUserSocialTypeEnum.WECHAT_ENTERPRISE.type,
+ },
+ },
+ {
+ fieldName: 'publicKey',
+ label: 'publicKey',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入 publicKey 公钥',
+ },
+ dependencies: {
+ triggerFields: ['socialType'],
+ show: (values) => values.socialType === 40,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '应用名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入应用名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'socialType',
+ label: '社交平台',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
+ placeholder: '请选择社交平台',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'userType',
+ label: '用户类型',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
+ placeholder: '请选择用户类型',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'clientId',
+ label: '客户端编号',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入客户端编号',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '请选择状态',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '应用名',
+ minWidth: 120,
+ },
+ {
+ field: 'socialType',
+ title: '社交平台',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SOCIAL_TYPE },
+ },
+ },
+ {
+ field: 'userType',
+ title: '用户类型',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.USER_TYPE },
+ },
+ },
+ {
+ field: 'clientId',
+ title: '客户端编号',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/social/client/index.vue b/apps/web-antdv-next/src/views/system/social/client/index.vue
new file mode 100644
index 000000000..3f75d3867
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/social/client/index.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/social/client/modules/form.vue b/apps/web-antdv-next/src/views/system/social/client/modules/form.vue
new file mode 100644
index 000000000..34dc46520
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/social/client/modules/form.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/social/user/data.ts b/apps/web-antdv-next/src/views/system/social/user/data.ts
new file mode 100644
index 000000000..2c1604f95
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/social/user/data.ts
@@ -0,0 +1,152 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { Image } from 'ant-design-vue';
+
+import { DictTag } from '#/components/dict-tag';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'type',
+ label: '社交平台',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
+ placeholder: '请选择社交平台',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'openid',
+ label: '社交 openid',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入社交 openid',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'type',
+ title: '社交平台',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.SYSTEM_SOCIAL_TYPE },
+ },
+ },
+ {
+ field: 'openid',
+ title: '社交 openid',
+ minWidth: 180,
+ },
+ {
+ field: 'nickname',
+ title: '用户昵称',
+ minWidth: 120,
+ },
+ {
+ field: 'avatar',
+ title: '用户头像',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellImage',
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'updateTime',
+ title: '更新时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 详情页的字段 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'type',
+ label: '社交平台',
+ render: (val) => {
+ return h(DictTag, {
+ type: DICT_TYPE.SYSTEM_SOCIAL_TYPE,
+ value: val,
+ });
+ },
+ },
+ {
+ field: 'nickname',
+ label: '用户昵称',
+ },
+ {
+ field: 'avatar',
+ label: '用户头像',
+ render: (val) => (val ? h(Image, { src: val }) : '无'),
+ },
+ {
+ field: 'token',
+ label: '社交 token',
+ },
+ {
+ field: 'rawTokenInfo',
+ label: '原始 Token 数据',
+ },
+ {
+ field: 'rawUserInfo',
+ label: '原始 User 数据',
+ },
+ {
+ field: 'code',
+ label: '最后一次的认证 code',
+ },
+ {
+ field: 'state',
+ label: '最后一次的认证 state',
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/social/user/index.vue b/apps/web-antdv-next/src/views/system/social/user/index.vue
new file mode 100644
index 000000000..08a413543
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/social/user/index.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/social/user/modules/detail.vue b/apps/web-antdv-next/src/views/system/social/user/modules/detail.vue
new file mode 100644
index 000000000..de2b35cbf
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/social/user/modules/detail.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/tenant/data.ts b/apps/web-antdv-next/src/views/system/tenant/data.ts
new file mode 100644
index 000000000..4fca673bc
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/tenant/data.ts
@@ -0,0 +1,257 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getTenantPackageList } from '#/api/system/tenant-package';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 关联数据 */
+let tenantPackageList: SystemTenantPackageApi.TenantPackage[] = [];
+getTenantPackageList().then((data) => (tenantPackageList = data));
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '租户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入租户名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'packageId',
+ label: '租户套餐',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getTenantPackageList,
+ labelField: 'name',
+ valueField: 'id',
+ placeholder: '请选择租户套餐',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'contactName',
+ label: '联系人',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系人',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'contactMobile',
+ label: '联系手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系手机',
+ },
+ rules: 'mobile',
+ },
+ {
+ label: '用户名称',
+ fieldName: 'username',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名称',
+ },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ show: (values) => !values.id,
+ },
+ },
+ {
+ label: '用户密码',
+ fieldName: 'password',
+ component: 'InputPassword',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ show: (values) => !values.id,
+ },
+ },
+ {
+ label: '账号额度',
+ fieldName: 'accountCount',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '请输入账号额度',
+ },
+ rules: 'required',
+ },
+ {
+ label: '过期时间',
+ fieldName: 'expireTime',
+ component: 'DatePicker',
+ componentProps: {
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ placeholder: '请选择过期时间',
+ },
+ rules: 'required',
+ },
+ {
+ label: '绑定域名',
+ fieldName: 'websites',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请输入绑定域名',
+ mode: 'tags',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '租户状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '租户名',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入租户名',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'contactName',
+ label: '联系人',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系人',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'contactMobile',
+ label: '联系手机',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入联系手机',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ placeholder: '请选择状态',
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '租户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '租户名',
+ minWidth: 180,
+ },
+ {
+ field: 'packageId',
+ title: '租户套餐',
+ minWidth: 180,
+ formatter: ({ cellValue }) => {
+ return cellValue === 0
+ ? '系统租户'
+ : tenantPackageList.find((pkg) => pkg.id === cellValue)?.name || '-';
+ },
+ },
+ {
+ field: 'contactName',
+ title: '联系人',
+ minWidth: 100,
+ },
+ {
+ field: 'contactMobile',
+ title: '联系手机',
+ minWidth: 180,
+ },
+ {
+ field: 'accountCount',
+ title: '账号额度',
+ minWidth: 100,
+ },
+ {
+ field: 'expireTime',
+ title: '过期时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'websites',
+ title: '绑定域名',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '租户状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/tenant/index.vue b/apps/web-antdv-next/src/views/system/tenant/index.vue
new file mode 100644
index 000000000..ba35ef620
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/tenant/index.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/tenant/modules/form.vue b/apps/web-antdv-next/src/views/system/tenant/modules/form.vue
new file mode 100644
index 000000000..f7a60b4fe
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/tenant/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/tenantPackage/data.ts b/apps/web-antdv-next/src/views/system/tenantPackage/data.ts
new file mode 100644
index 000000000..a89b6a14d
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/tenantPackage/data.ts
@@ -0,0 +1,133 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '套餐名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入套餐名称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'menuIds',
+ label: '菜单权限',
+ component: 'Input',
+ formItemClass: 'items-start',
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '套餐名称',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '请输入套餐名称',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '状态',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ allowClear: true,
+ placeholder: '请选择状态',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '套餐编号',
+ minWidth: 100,
+ },
+ {
+ field: 'name',
+ title: '套餐名称',
+ minWidth: 180,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '备注',
+ minWidth: 200,
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/tenantPackage/index.vue b/apps/web-antdv-next/src/views/system/tenantPackage/index.vue
new file mode 100644
index 000000000..c7069db3c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/tenantPackage/index.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/tenantPackage/modules/form.vue b/apps/web-antdv-next/src/views/system/tenantPackage/modules/form.vue
new file mode 100644
index 000000000..19702165b
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/tenantPackage/modules/form.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+ 全选
+
+
+ 全部展开
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/components/index.ts b/apps/web-antdv-next/src/views/system/user/components/index.ts
new file mode 100644
index 000000000..9f00ad117
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/components/index.ts
@@ -0,0 +1 @@
+export { default as UserSelectModal } from './select-modal.vue';
diff --git a/apps/web-antdv-next/src/views/system/user/components/select-modal.vue b/apps/web-antdv-next/src/views/system/user/components/select-modal.vue
new file mode 100644
index 000000000..755399269
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/components/select-modal.vue
@@ -0,0 +1,539 @@
+
+
+
+
+
+
+
+
+ handleDeptSearch(e.target?.value ?? '')"
+ />
+
+
+
+
+
+
+
+ {{ item?.nickname }} ({{ item?.username }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/data.ts b/apps/web-antdv-next/src/views/system/user/data.ts
new file mode 100644
index 000000000..fb9ea8fe8
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/data.ts
@@ -0,0 +1,353 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemUserApi } from '#/api/system/user';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { $t } from '@vben/locales';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getDeptList } from '#/api/system/dept';
+import { getSimplePostList } from '#/api/system/post';
+import { getSimpleRoleList } from '#/api/system/role';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 新增/修改的表单 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'username',
+ label: '用户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名称',
+ },
+ rules: 'required',
+ },
+ {
+ label: '用户密码',
+ fieldName: 'password',
+ component: 'InputPassword',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ show: (values) => !values.id,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户昵称',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'deptId',
+ label: '归属部门',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '请选择归属部门',
+ treeDefaultExpandAll: true,
+ },
+ },
+ {
+ fieldName: 'postIds',
+ label: '岗位',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimplePostList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择岗位',
+ },
+ },
+ {
+ fieldName: 'email',
+ label: '邮箱',
+ component: 'Input',
+ rules: z.string().email('邮箱格式不正确').or(z.literal('')).optional(),
+ componentProps: {
+ placeholder: '请输入邮箱',
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ },
+ },
+ {
+ fieldName: 'sex',
+ label: '用户性别',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(1),
+ },
+ {
+ fieldName: 'status',
+ label: '用户状态',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '备注',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '请输入备注',
+ },
+ },
+ ];
+}
+
+/** 重置密码的表单 */
+export function useResetPasswordFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ component: 'VbenInputPassword',
+ componentProps: {
+ passwordStrength: true,
+ placeholder: '请输入新密码',
+ },
+ dependencies: {
+ rules(values) {
+ return z
+ .string({ message: '请输入新密码' })
+ .min(5, '密码长度不能少于 5 个字符')
+ .max(20, '密码长度不能超过 20 个字符')
+ .refine(
+ (value) => value !== values.oldPassword,
+ '新旧密码不能相同',
+ );
+ },
+ triggerFields: ['newPassword', 'oldPassword'],
+ },
+ fieldName: 'newPassword',
+ label: '新密码',
+ rules: 'required',
+ },
+ {
+ component: 'VbenInputPassword',
+ componentProps: {
+ passwordStrength: true,
+ placeholder: $t('authentication.confirmPassword'),
+ },
+ dependencies: {
+ rules(values) {
+ return z
+ .string({ message: '请输入确认密码' })
+ .min(5, '密码长度不能少于 5 个字符')
+ .max(20, '密码长度不能超过 20 个字符')
+ .refine(
+ (value) => value === values.newPassword,
+ '新密码和确认密码不一致',
+ );
+ },
+ triggerFields: ['newPassword', 'confirmPassword'],
+ },
+ fieldName: 'confirmPassword',
+ label: '确认密码',
+ rules: 'required',
+ },
+ ];
+}
+
+/** 分配角色的表单 */
+export function useAssignRoleFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'username',
+ label: '用户名称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'nickname',
+ label: '用户昵称',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'roleIds',
+ label: '角色',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleRoleList,
+ labelField: 'name',
+ valueField: 'id',
+ mode: 'multiple',
+ placeholder: '请选择角色',
+ },
+ },
+ ];
+}
+
+/** 用户导入的表单 */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '用户数据',
+ component: 'Upload',
+ rules: 'required',
+ help: '仅允许导入 xls、xlsx 格式文件',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '是否覆盖',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '是',
+ unCheckedChildren: '否',
+ },
+ rules: z.boolean().default(false),
+ help: '是否更新已经存在的用户数据',
+ },
+ ];
+}
+
+/** 列表的搜索表单 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'username',
+ label: '用户名称',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入用户名称',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'mobile',
+ label: '手机号码',
+ component: 'Input',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '创建时间',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 列表的字段 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: SystemUserApi.User,
+ ) => PromiseLike,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'id',
+ title: '用户编号',
+ minWidth: 100,
+ },
+ {
+ field: 'username',
+ title: '用户名称',
+ minWidth: 120,
+ },
+ {
+ field: 'nickname',
+ title: '用户昵称',
+ minWidth: 120,
+ },
+ {
+ field: 'deptName',
+ title: '部门',
+ minWidth: 120,
+ },
+ {
+ field: 'mobile',
+ title: '手机号码',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '状态',
+ minWidth: 100,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: CommonStatusEnum.ENABLE,
+ unCheckedValue: CommonStatusEnum.DISABLE,
+ },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '创建时间',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '操作',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/apps/web-antdv-next/src/views/system/user/index.vue b/apps/web-antdv-next/src/views/system/user/index.vue
new file mode 100644
index 000000000..5d1267a8f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/index.vue
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/modules/assign-role-form.vue b/apps/web-antdv-next/src/views/system/user/modules/assign-role-form.vue
new file mode 100644
index 000000000..61ed064f6
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/modules/assign-role-form.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/modules/dept-tree.vue b/apps/web-antdv-next/src/views/system/user/modules/dept-tree.vue
new file mode 100644
index 000000000..d1b7fb0ec
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/modules/dept-tree.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无数据
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/modules/form.vue b/apps/web-antdv-next/src/views/system/user/modules/form.vue
new file mode 100644
index 000000000..d4f8e7e8f
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/modules/form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/modules/import-form.vue b/apps/web-antdv-next/src/views/system/user/modules/import-form.vue
new file mode 100644
index 000000000..f3fad2d02
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/modules/import-form.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/src/views/system/user/modules/reset-password-form.vue b/apps/web-antdv-next/src/views/system/user/modules/reset-password-form.vue
new file mode 100644
index 000000000..0642f493c
--- /dev/null
+++ b/apps/web-antdv-next/src/views/system/user/modules/reset-password-form.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/apps/web-antdv-next/tsconfig.json b/apps/web-antdv-next/tsconfig.json
index 858a0ec08..3bf28ff0d 100644
--- a/apps/web-antdv-next/tsconfig.json
+++ b/apps/web-antdv-next/tsconfig.json
@@ -4,7 +4,8 @@
"compilerOptions": {
"paths": {
"#/*": ["./src/*"]
- }
+ },
+ "allowJs": true
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
diff --git a/apps/web-antdv-next/vite.config.ts b/apps/web-antdv-next/vite.config.ts
index b6360f1d4..a75425fb4 100644
--- a/apps/web-antdv-next/vite.config.ts
+++ b/apps/web-antdv-next/vite.config.ts
@@ -5,12 +5,13 @@ export default defineConfig(async () => {
application: {},
vite: {
server: {
+ allowedHosts: true,
proxy: {
- '/api': {
+ '/admin-api': {
changeOrigin: true,
- rewrite: (path) => path.replace(/^\/api/, ''),
+ rewrite: (path) => path.replace(/^\/admin-api/, ''),
// mock代理目标地址
- target: 'http://localhost:5320/api',
+ target: 'http://localhost:48080/admin-api',
ws: true,
},
},
diff --git a/apps/web-ele/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js b/apps/web-ele/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
index 15429d952..f343a787d 100644
--- a/apps/web-ele/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
+++ b/apps/web-ele/src/views/bpm/components/bpmn-process-designer/src/modules/rules/CustomRules.js
@@ -1,5 +1,4 @@
import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
-// eslint-disable-next-line n/no-extraneous-import
import inherits from 'inherits';
function CustomRules(eventBus) {
diff --git a/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue b/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue
index acaff4d0d..22a925ca4 100644
--- a/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue
+++ b/apps/web-ele/src/views/bpm/processInstance/detail/modules/operation-button.vue
@@ -284,9 +284,7 @@ async function openPopover(type: string) {
// 没有节点表单时,approveFormFApi 永远不会被赋值,跳过等待
if (runningTask.value?.formId > 0) {
// 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行
- await until(
- () => typeof approveFormFApi.value?.validate === 'function',
- )
+ await until(() => typeof approveFormFApi.value?.validate === 'function')
.toBeTruthy({ timeout: 1000 })
.catch(() => {});
}
diff --git a/package.json b/package.json
index 6eeddf37f..344bad3fe 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze",
"build:antd": "pnpm run build --filter=@vben/web-antd",
+ "build:antdv-next": "pnpm run build --filter=@vben/web-antdv-next",
"build:docker": "./scripts/deploy/build-local-docker-image.sh",
"build:docs": "pnpm run build --filter=@vben/docs",
"build:ele": "pnpm run build --filter=@vben/web-ele",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 48a54b6c8..72cd5c8a0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -204,9 +204,6 @@ catalogs:
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6
- antdv-next:
- specifier: ^1.1.9
- version: 1.1.9
archiver:
specifier: ^7.0.1
version: 7.0.1
@@ -852,6 +849,15 @@ importers:
apps/web-antdv-next:
dependencies:
+ '@form-create/ant-design-vue':
+ specifier: 'catalog:'
+ version: 3.2.38(vue@3.5.32(typescript@6.0.2))
+ '@form-create/antd-designer':
+ specifier: 'catalog:'
+ version: 3.4.0(vue@3.5.32(typescript@6.0.2))
+ '@tinymce/tinymce-vue':
+ specifier: 'catalog:'
+ version: 6.3.0(tinymce@7.9.2)(vue@3.5.32(typescript@6.0.2))
'@vben/access':
specifier: workspace:*
version: link:../../packages/effects/access
@@ -894,24 +900,78 @@ importers:
'@vben/utils':
specifier: workspace:*
version: link:../../packages/utils
+ '@videojs-player/vue':
+ specifier: 'catalog:'
+ version: 1.0.0(@types/video.js@7.3.58)(video.js@7.21.7)(vue@3.5.32(typescript@6.0.2))
'@vueuse/core':
specifier: 'catalog:'
version: 14.2.1(vue@3.5.32(typescript@6.0.2))
- antdv-next:
+ '@vueuse/integrations':
specifier: 'catalog:'
- version: 1.1.9(date-fns@4.1.0)(luxon@3.7.2)(vue@3.5.32(typescript@6.0.2))
+ version: 14.2.1(async-validator@4.2.5)(axios@1.15.0)(change-case@5.4.4)(focus-trap@8.0.1)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.7)(vue@3.5.32(typescript@6.0.2))
+ ant-design-vue:
+ specifier: 'catalog:'
+ version: 4.2.6(vue@3.5.32(typescript@6.0.2))
+ benz-amr-recorder:
+ specifier: 'catalog:'
+ version: 1.1.5
+ bpmn-js:
+ specifier: 'catalog:'
+ version: 17.11.1
+ bpmn-js-properties-panel:
+ specifier: 'catalog:'
+ version: 5.23.0(@bpmn-io/properties-panel@3.40.6)(bpmn-js@17.11.1)(camunda-bpmn-js-behaviors@1.14.1(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.12.0))(diagram-js@12.8.1)
+ bpmn-js-token-simulation:
+ specifier: 'catalog:'
+ version: 0.36.3
+ camunda-bpmn-moddle:
+ specifier: 'catalog:'
+ version: 7.0.1
+ cropperjs:
+ specifier: 'catalog:'
+ version: 1.6.2
dayjs:
specifier: 'catalog:'
version: 1.11.20
+ diagram-js:
+ specifier: 'catalog:'
+ version: 12.8.1
+ fast-xml-parser:
+ specifier: 'catalog:'
+ version: 4.5.6
+ highlight.js:
+ specifier: 'catalog:'
+ version: 11.11.1
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@6.0.2)(vue@3.5.32(typescript@6.0.2))
+ steady-xml:
+ specifier: 'catalog:'
+ version: 0.1.0
+ tinymce:
+ specifier: 'catalog:'
+ version: 7.9.2
+ video.js:
+ specifier: 'catalog:'
+ version: 7.21.7
vue:
specifier: ^3.5.32
version: 3.5.32(typescript@6.0.2)
+ vue-dompurify-html:
+ specifier: 'catalog:'
+ version: 5.3.0(vue@3.5.32(typescript@6.0.2))
vue-router:
specifier: 'catalog:'
version: 5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.2)(vue@3.5.32(typescript@6.0.2)))(vue@3.5.32(typescript@6.0.2))
+ vue3-print-nb:
+ specifier: 'catalog:'
+ version: 0.1.4(typescript@6.0.2)
+ vue3-signature:
+ specifier: 'catalog:'
+ version: 0.2.4(vue@3.5.32(typescript@6.0.2))
+ vuedraggable:
+ specifier: 'catalog:'
+ version: 4.1.0(vue@3.5.32(typescript@6.0.2))
apps/web-ele:
dependencies:
@@ -2355,20 +2415,6 @@ packages:
'@ant-design/colors@6.0.0':
resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==}
- '@ant-design/colors@7.2.1':
- resolution: {integrity: sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==}
-
- '@ant-design/colors@8.0.1':
- resolution: {integrity: sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==}
-
- '@ant-design/fast-color@2.0.6':
- resolution: {integrity: sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==}
- engines: {node: '>=8.x'}
-
- '@ant-design/fast-color@3.0.1':
- resolution: {integrity: sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==}
- engines: {node: '>=8.x'}
-
'@ant-design/icons-svg@4.4.2':
resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==}
@@ -2377,16 +2423,6 @@ packages:
peerDependencies:
vue: ^3.5.32
- '@antdv-next/cssinjs@1.0.6':
- resolution: {integrity: sha512-8NL+AzjFZVHfG9A2l+r4rNWdRfbS5FFFozGU9jVl7WNgd7y+wEYSzl+qvRjJuqd3IryacrDfsTxIgcSQkVlr5Q==}
- peerDependencies:
- vue: ^3.5.32
-
- '@antdv-next/icons@1.0.6':
- resolution: {integrity: sha512-SCPe/otLTmOEVoAdrZ/fn5pr1wlA1Tbzhk908gWPDnjSLEPIcv4n0Feh416RKj1oe8PKbPIC95BH+HaBXybojg==}
- peerDependencies:
- vue: ^3.5.32
-
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
@@ -3573,9 +3609,6 @@ packages:
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
- '@emotion/unitless@0.7.5':
- resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
-
'@emotion/unitless@0.8.1':
resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
@@ -6069,220 +6102,6 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
- '@v-c/async-validator@1.0.1':
- resolution: {integrity: sha512-2WXdbTso13119ZLiwUv1JkmdL0K4ll69id4oE3ft3LQX+YAHOoJtfx080u26lLtypgZS32PguoohgB0BVo39rg==}
-
- '@v-c/cascader@1.0.3':
- resolution: {integrity: sha512-TaXkWxP3N6LW1yezeN+91k7BFYDSsWj1Y1ujWv51I0lJY4l0iwtjKDDmLLMYoqtUjnW1Onz1g1h6PtiDFALcXQ==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/checkbox@1.0.1':
- resolution: {integrity: sha512-+QsE/0VfU6oeglwuHWYxRNTn3+eV08iG0uN/upDmqSGznezInzfUClh+t4acd/OxyJVtuob0WKsg/vPlT6WRWw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/collapse@1.0.0':
- resolution: {integrity: sha512-y4NAl3j4mka193ZMDLHdISA8to61qoROG6/kTQ0myM2ZuEsonnEK1QWlqoEw3gveMsa6a4RdyoXLxdGdcJyp0Q==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/color-picker@1.0.6':
- resolution: {integrity: sha512-XiTlEMG5p5jkCdKP7nbeo9EJrpdXnoW4uBOA2us8pVgA6m0GxrwMoCg//X+1XsuOaebG/1Swo65mjRuMBM6rZg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/dialog@1.0.3':
- resolution: {integrity: sha512-NfeuaHC1PPaRyekoXN6G9AjRL5L9eQiV9tnkRp62GyCfZsHMFgdwlREW6buHifHe7dk4z/ilvGms5tnbw63R4A==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/drawer@1.0.3':
- resolution: {integrity: sha512-k6oIWFZXtt9lR2A3zUzIVQZCl90adBy7bh1bs6uHPsEa5Ma73+7PbQlaxt0X+WQAVW9M5n9pqDAf6Qp7qexi+A==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/dropdown@1.0.2':
- resolution: {integrity: sha512-D6TACf3jUiRWx4xW5h2+wVT9SMYxUasFlAHESYJr4ZMjLTLLM1Q8iBjkjhGF+vA0eYR5zqRTwlaacN0DNDZBPw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/image@1.0.10':
- resolution: {integrity: sha512-rcFtfsmoZJrQoax+Y2rTxp60HcClNfPFrtwQCF8MAlCtkt+Z4wnavLikixW4H1QwJ1V8GOb4tPRjSUZslaU2xg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/input-number@1.0.5':
- resolution: {integrity: sha512-YQBpV1KnuYf0o2XrbC+OEyP6lkyqv1XhzDVh3QhHO2bs/Jr3uSD8b3cxdEGU+gjoNJUcoZx7XiIzcYhLlvMctw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/input@1.0.3':
- resolution: {integrity: sha512-vQic9OWfREBlNfJeRcejMcOPhp4xnbHVbqeqo/TfCOf73Ym+zjWXRGXiqM/pIqV9v95zuhYen3KzMeJYM4pViA==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/mentions@1.0.0':
- resolution: {integrity: sha512-trkG1lvfiaIY7UnHn0gx6B01o3rFLEMin3KGp1q4oU6zOCRWde4ejZ+EHSvmXzOz2N+FlRMTE4EMJFi4w0oOlQ==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/menu@1.0.13':
- resolution: {integrity: sha512-RbOuk+R0V2bm04daK5LvlJ95N/XS2k+YSnvrGUDCKVkCIAPEe6NZMU6clcj5F4yKtKwWDggq/n5NlJPfJKFSPA==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/mini-decimal@1.0.1':
- resolution: {integrity: sha512-76wZLdlkI017iDlaZMNOWZyDCv29YVabUJn5urQgIKtW4dnI5AkNXWtmLyhl/mu/OS7ZGisRi5ai/558QhLQxQ==}
-
- '@v-c/mutate-observer@1.0.1':
- resolution: {integrity: sha512-84+9KGORX8LY9u+K0DEGyRwRCJaky0sjRkXxBC7X/jahHJl8NQGQ0Gxve5IVwaxRTfZ9eftlRmHs90JD6Utfqg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/notification@1.0.0':
- resolution: {integrity: sha512-aU5g+ZiYxp0KVdKuho067wJRF38Mv7MrQS95dwSJLsbDmVFBpjO3Lo3ptakfPkwn+7uwRytHKIf39t9QVGk+sg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/overflow@1.0.5':
- resolution: {integrity: sha512-Ae5aSZItOQHYzscs5JcmLPBLLEkpCS6c8IcHv9jABH0lZZGlZiUF4WBkAvSSnU8Q9f14BJ6dEb5XLwbwpyTZEQ==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/pagination@1.0.0':
- resolution: {integrity: sha512-uYIMkvHKMtY+nwHTu5rXxiq6KPf0zGpZbtQTn1nDPng0tOyA1vLQ+R6OfE+1LOwuQqvFTEDnAq4vb90By+eBfw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/picker@1.0.4':
- resolution: {integrity: sha512-B98FSgE+Kh6lNwa5msySFL8NNiF3fFkmFuuL14WEdb4f6q47XYH7YvcQa2jySXnbYPKHdykZMFo37f/0BzFaPw==}
- peerDependencies:
- date-fns: '>= 2.x'
- dayjs: '>= 1.x'
- luxon: '>= 3.x'
- moment: '>= 2.x'
- vue: ^3.5.32
- peerDependenciesMeta:
- date-fns:
- optional: true
- dayjs:
- optional: true
- luxon:
- optional: true
- moment:
- optional: true
-
- '@v-c/portal@1.0.8':
- resolution: {integrity: sha512-93elruWfHKrdtRkpFBNpi47YhQjA75tCuG8C/WvcQ9x/dp+H3i+y7h6t0iyyDjZu1w3d2U5+d43vtslmoQXBYg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/progress@1.0.0':
- resolution: {integrity: sha512-kWDTU1uXnPDMmoezwyAECxuSH+WKn92OjSdk/GgDbQgZ0qNy9woOiRe5fOsrcy61agHdJxzf0MvsUy1b6bZVlA==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/qrcode@1.0.0':
- resolution: {integrity: sha512-OSMrYDhP/NQiUcO6J0X2X8BskHPRqX/E/F9npH3oayZgjCo5Aom+63Ja3J0u6SOmKP1JgLSgjrm5karc0671jw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/rate@1.0.1':
- resolution: {integrity: sha512-ZWWY01LeKu9S/JncdvSr2gz2Kwwum3bB/AxzzCsuhCyg+9P4BLwX7S2WZDMiJ92uYEpiYladVTCa9XdSCPaCaQ==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/resize-observer@1.0.8':
- resolution: {integrity: sha512-VH8WBsNfZA5KQ+CXVaQ1PK5B6FIHnuTdqOLrjRWiZTrIYDZi/MyREi9b21YDj55fbFWMRx4yapnO9tiZX1RNxA==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/segmented@1.0.2':
- resolution: {integrity: sha512-g/aWgURMJkytu05AACN3Pd9QKgka4ZQM8tl/y0AcabANldy2hmPIHek0ZH5dDdn5VmK1PhAfUTqtgDS19sLh5w==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/select@1.0.20':
- resolution: {integrity: sha512-gmG5U7r8YgXIjGQZ5qjHM3felSlYAep7pBjJRzA8ALgHhx0CKRk6RTadVDXzWh7LwOFvlgTn7OLUMTU6RACoaQ==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/slick@1.0.2':
- resolution: {integrity: sha512-8BbPxJgYST+tio9jyeFQgJb/3XSFhfWZN8Am7YVYeqsYsvn3g8wu61UZNSjlkp9ArOEe+ujOgX4izfDHeXh4RA==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/slider@1.0.10':
- resolution: {integrity: sha512-KMIVytBm8K8RQ+aPPraS28GmBptGHESF/gDRbGjOLD7xyivuQDJeEqVaUFY3EcCWsERjh4VP/L96gUbMTF0uag==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/steps@1.0.0':
- resolution: {integrity: sha512-DPL0OOb8pDLlTPZB93b8+Saxiz6V5zEpGXKaCnsbXUuOhimkc7089AuEKfpMw+8x1SrVe+gapWf5RRHWXUm2pg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/switch@1.0.0':
- resolution: {integrity: sha512-VIem244KJkYfqDgofpgHjK00sGL9rJ/9OtmK4Gbs4hnPsrTtzHDBRltYxR4IT7HQleathZfj6NhcZ1bjdWKYUw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/table@1.0.4':
- resolution: {integrity: sha512-Fvf5WzGidDdkRHa8pRiIKS38E8v+goa2Y8fktwB586+gEM1IwcWFGSnioa6tOH/hA0N2mCWfdAp+qFWb9LXRdw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/tabs@1.0.2':
- resolution: {integrity: sha512-FA/lG5TaYOVFhB3WjJ2x6O8egREm9FWdfEbnY1Tmg3D1avkxYDhISHlW4ot3NvWb8Ds4MxBe0T9xg206o0LHEw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/textarea@1.0.4':
- resolution: {integrity: sha512-VrQrLjKsiFh3bngXDULK+mUI4RmTT0hqDOUChsGmo5dZGgcmzsDbVLFluYCvpV+InUsnn2jGSQEuqMQy1Vga+g==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/tooltip@1.0.3':
- resolution: {integrity: sha512-72EkTfhb67RPJvMXIW6HUYiZ+Jdrb7tBQmS3wDtFDNU7uIrS5DQLyXJDCu9qWlrPv7cQ/RHA4JfCINw88vchzw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/tour@1.0.3':
- resolution: {integrity: sha512-y4DVJPP7jvL+MWUMAKQWxLAMXSWJEfZXaKASPn3DKbSQ8drBhsjMXwcep3glAfrCjCKfj/QD3OrUMxqydi4qFw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/tree-select@1.0.3':
- resolution: {integrity: sha512-N4mK8JXrCU+GFfhLG/zat3TAUt0Ju+P4S3hN6PlmuHPikQ4OWEA91CA8Br83i4zpW9TCH7xP0EfMUvhLtjqbsA==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/tree@1.0.5':
- resolution: {integrity: sha512-u6tja/kV9mupXWhFT+RtLUVqhCvNtb7LHuKkh4pka8sy5goQ7MjIODciuXn8mizIlEw3rGopq7C7auC9Sg1K8w==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/trigger@1.0.14':
- resolution: {integrity: sha512-3flCLvHvW2fJ8Rg/m4kCk7UGtL9GsrPgeSbdQJ1FU5+sZmfT2bcPwQZM824e7VFLmgaYPLiaQOL3l46uEPFWLw==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/upload@1.0.0':
- resolution: {integrity: sha512-W92PNCD61aM/B5w8oUzHQSDHur1T8484726Ls0IoNMO5nPiF/15eEE3RuuI/t7xXQVP/fA06hNSwzXwGWdDg1w==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/util@1.0.19':
- resolution: {integrity: sha512-apJGS4BVzhXbrNR6jxXF18jAiOWIn/UNmGjgSvB5r4ba9Wr/ireKCfJvhuuNsZi+scLaM0W3ghB81PbQ5vwoJg==}
- peerDependencies:
- vue: ^3.5.32
-
- '@v-c/virtual-list@1.0.6':
- resolution: {integrity: sha512-eXGbU1zME4pXAfQSBfhp1BkqBP3XYSX03CRJg7tsHgwXoaw8DE7pLuxzFSa+COJ66oOdYn99XS/sHTiz34aQCA==}
- peerDependencies:
- vue: ^3.5.32
-
'@valibot/to-json-schema@1.6.0':
resolution: {integrity: sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A==}
peerDependencies:
@@ -6721,9 +6540,6 @@ packages:
peerDependencies:
vue: ^3.5.32
- antdv-next@1.1.9:
- resolution: {integrity: sha512-lC4pEn7d+SPwVvSnfDC4G4b6WKniq3VDKU/tUSHa8W6emgkVH0pcsvD0bHQ5c14MSEVlgB4xUVs6X9qihTfh8w==}
-
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -7291,9 +7107,6 @@ packages:
compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
- compute-scroll-into-view@3.1.1:
- resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
-
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -9257,6 +9070,11 @@ packages:
cpu: [arm64]
os: [linux]
+ lefthook-linux-x64@2.1.5:
+ resolution: {integrity: sha512-bqK3LrAB5l5YaCaoHk6qRWlITrGWzP4FbwRxA31elbxjd0wgNWZ2Sn3zEfSEcxz442g7/PPkEwqqsTx0kSFzpg==}
+ cpu: [x64]
+ os: [linux]
+
lefthook-openbsd-arm64@2.1.5:
resolution: {integrity: sha512-5aSwK7vV3A6t0w9PnxCMiVjQlcvopBP50BtmnnLnNJyAYHnFbZ0Baq5M0WkE9IsUkWSux0fe6fd0jDkuG711MA==}
cpu: [arm64]
@@ -10809,9 +10627,6 @@ packages:
scroll-into-view-if-needed@2.2.31:
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
- scroll-into-view-if-needed@3.1.0:
- resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
-
scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
@@ -12511,20 +12326,6 @@ snapshots:
dependencies:
'@ctrl/tinycolor': 4.2.0
- '@ant-design/colors@7.2.1':
- dependencies:
- '@ant-design/fast-color': 2.0.6
-
- '@ant-design/colors@8.0.1':
- dependencies:
- '@ant-design/fast-color': 3.0.1
-
- '@ant-design/fast-color@2.0.6':
- dependencies:
- '@babel/runtime': 7.29.2
-
- '@ant-design/fast-color@3.0.1': {}
-
'@ant-design/icons-svg@4.4.2': {}
'@ant-design/icons-vue@7.0.1(vue@3.5.32(typescript@6.0.2))':
@@ -12533,22 +12334,6 @@ snapshots:
'@ant-design/icons-svg': 4.4.2
vue: 3.5.32(typescript@6.0.2)
- '@antdv-next/cssinjs@1.0.6(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@emotion/hash': 0.8.0
- '@emotion/unitless': 0.7.5
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- csstype: 3.2.3
- stylis: 4.3.6
- vue: 3.5.32(typescript@6.0.2)
-
- '@antdv-next/icons@1.0.6(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@ant-design/colors': 7.2.1
- '@ant-design/icons-svg': 4.4.2
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 1.6.0
@@ -14062,8 +13847,6 @@ snapshots:
'@emotion/hash@0.9.2': {}
- '@emotion/unitless@0.7.5': {}
-
'@emotion/unitless@0.8.1': {}
'@epic-web/invariant@1.0.0': {}
@@ -16208,250 +15991,6 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
- '@v-c/async-validator@1.0.1': {}
-
- '@v-c/cascader@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/select': 1.0.20(vue@3.5.32(typescript@6.0.2))
- '@v-c/tree': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/checkbox@1.0.1(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/collapse@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/color-picker@1.0.6(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@ant-design/fast-color': 3.0.1
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/dialog@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/portal': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/drawer@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/portal': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/dropdown@1.0.2(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/image@1.0.10(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/portal': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/input-number@1.0.5(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/input': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/mini-decimal': 1.0.1
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/input@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/mentions@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/input': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/menu': 1.0.13(vue@3.5.32(typescript@6.0.2))
- '@v-c/textarea': 1.0.4(vue@3.5.32(typescript@6.0.2))
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/menu@1.0.13(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/overflow': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/mini-decimal@1.0.1': {}
-
- '@v-c/mutate-observer@1.0.1(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/notification@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/overflow@1.0.5(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/pagination@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/picker@1.0.4(date-fns@4.1.0)(dayjs@1.11.20)(luxon@3.7.2)(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/overflow': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
- optionalDependencies:
- date-fns: 4.1.0
- dayjs: 1.11.20
- luxon: 3.7.2
-
- '@v-c/portal@1.0.8(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/progress@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/qrcode@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/rate@1.0.1(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/resize-observer@1.0.8(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- resize-observer-polyfill: 1.5.1
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/segmented@1.0.2(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/select@1.0.20(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/overflow': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- '@v-c/virtual-list': 1.0.6(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/slick@1.0.2(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- es-toolkit: 1.45.1
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/slider@1.0.10(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/steps@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/switch@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/table@1.0.4(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- '@v-c/virtual-list': 1.0.6(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/tabs@1.0.2(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/dropdown': 1.0.2(vue@3.5.32(typescript@6.0.2))
- '@v-c/menu': 1.0.13(vue@3.5.32(typescript@6.0.2))
- '@v-c/overflow': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/textarea@1.0.4(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/input': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/tooltip@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/tour@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/portal': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/tree-select@1.0.3(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/select': 1.0.20(vue@3.5.32(typescript@6.0.2))
- '@v-c/tree': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/tree@1.0.5(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- '@v-c/virtual-list': 1.0.6(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/trigger@1.0.14(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/portal': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/upload@1.0.0(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/util@1.0.19(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- vue: 3.5.32(typescript@6.0.2)
-
- '@v-c/virtual-list@1.0.6(vue@3.5.32(typescript@6.0.2))':
- dependencies:
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- vue: 3.5.32(typescript@6.0.2)
-
'@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@6.0.2))':
dependencies:
valibot: 1.3.1(typescript@6.0.2)
@@ -17006,61 +16545,6 @@ snapshots:
vue-types: 3.0.2(vue@3.5.32(typescript@6.0.2))
warning: 4.0.3
- antdv-next@1.1.9(date-fns@4.1.0)(luxon@3.7.2)(vue@3.5.32(typescript@6.0.2)):
- dependencies:
- '@ant-design/colors': 8.0.1
- '@ant-design/fast-color': 3.0.1
- '@antdv-next/cssinjs': 1.0.6(vue@3.5.32(typescript@6.0.2))
- '@antdv-next/icons': 1.0.6(vue@3.5.32(typescript@6.0.2))
- '@v-c/async-validator': 1.0.1
- '@v-c/cascader': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/checkbox': 1.0.1(vue@3.5.32(typescript@6.0.2))
- '@v-c/collapse': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/color-picker': 1.0.6(vue@3.5.32(typescript@6.0.2))
- '@v-c/dialog': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/drawer': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/dropdown': 1.0.2(vue@3.5.32(typescript@6.0.2))
- '@v-c/image': 1.0.10(vue@3.5.32(typescript@6.0.2))
- '@v-c/input': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/input-number': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/mentions': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/menu': 1.0.13(vue@3.5.32(typescript@6.0.2))
- '@v-c/mutate-observer': 1.0.1(vue@3.5.32(typescript@6.0.2))
- '@v-c/notification': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/pagination': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/picker': 1.0.4(date-fns@4.1.0)(dayjs@1.11.20)(luxon@3.7.2)(vue@3.5.32(typescript@6.0.2))
- '@v-c/progress': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/qrcode': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/rate': 1.0.1(vue@3.5.32(typescript@6.0.2))
- '@v-c/resize-observer': 1.0.8(vue@3.5.32(typescript@6.0.2))
- '@v-c/segmented': 1.0.2(vue@3.5.32(typescript@6.0.2))
- '@v-c/select': 1.0.20(vue@3.5.32(typescript@6.0.2))
- '@v-c/slick': 1.0.2(vue@3.5.32(typescript@6.0.2))
- '@v-c/slider': 1.0.10(vue@3.5.32(typescript@6.0.2))
- '@v-c/steps': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/switch': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/table': 1.0.4(vue@3.5.32(typescript@6.0.2))
- '@v-c/tabs': 1.0.2(vue@3.5.32(typescript@6.0.2))
- '@v-c/textarea': 1.0.4(vue@3.5.32(typescript@6.0.2))
- '@v-c/tooltip': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/tour': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/tree': 1.0.5(vue@3.5.32(typescript@6.0.2))
- '@v-c/tree-select': 1.0.3(vue@3.5.32(typescript@6.0.2))
- '@v-c/trigger': 1.0.14(vue@3.5.32(typescript@6.0.2))
- '@v-c/upload': 1.0.0(vue@3.5.32(typescript@6.0.2))
- '@v-c/util': 1.0.19(vue@3.5.32(typescript@6.0.2))
- '@v-c/virtual-list': 1.0.6(vue@3.5.32(typescript@6.0.2))
- '@vueuse/core': 14.2.1(vue@3.5.32(typescript@6.0.2))
- dayjs: 1.11.20
- es-toolkit: 1.45.1
- scroll-into-view-if-needed: 3.1.0
- throttle-debounce: 5.0.2
- transitivePeerDependencies:
- - date-fns
- - luxon
- - moment
- - vue
-
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -17669,8 +17153,6 @@ snapshots:
compute-scroll-into-view@1.0.20: {}
- compute-scroll-into-view@3.1.1: {}
-
concat-map@0.0.1: {}
confbox@0.1.8: {}
@@ -19840,6 +19322,9 @@ snapshots:
lefthook-linux-arm64@2.1.5:
optional: true
+ lefthook-linux-x64@2.1.5:
+ optional: true
+
lefthook-openbsd-arm64@2.1.5:
optional: true
@@ -19859,6 +19344,7 @@ snapshots:
lefthook-freebsd-arm64: 2.1.5
lefthook-freebsd-x64: 2.1.5
lefthook-linux-arm64: 2.1.5
+ lefthook-linux-x64: 2.1.5
lefthook-openbsd-arm64: 2.1.5
lefthook-openbsd-x64: 2.1.5
lefthook-windows-arm64: 2.1.5
@@ -21572,10 +21058,6 @@ snapshots:
dependencies:
compute-scroll-into-view: 1.0.20
- scroll-into-view-if-needed@3.1.0:
- dependencies:
- compute-scroll-into-view: 3.1.1
-
scule@1.3.0: {}
search-insights@2.17.3: {}