mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2026-05-15 11:47:47 +00:00
Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c164904a14 | ||
|
|
a0ceb45df9 | ||
|
|
c641542c71 | ||
|
|
a3d8e4bfc1 | ||
|
|
e385823d46 | ||
|
|
897220e19a | ||
|
|
b293e112c6 | ||
|
|
627e31f1b0 | ||
|
|
8020b4b743 | ||
|
|
228c5463da | ||
|
|
50ee691191 | ||
|
|
eda6ffaf1e | ||
|
|
f542db27f9 | ||
|
|
b2cf1646a4 | ||
|
|
adecddae67 | ||
|
|
a653e428f3 | ||
|
|
eb62e63a04 | ||
|
|
ccabbf0e97 | ||
|
|
5b84ac5b13 | ||
|
|
f610bd690b | ||
|
|
76f9d3d9fc | ||
|
|
1e6c39a4c6 | ||
|
|
7a1f8da68f | ||
|
|
51cae9b00c | ||
|
|
7cbeaa8390 | ||
|
|
6be3a0e204 | ||
|
|
a9b76ba2ed | ||
|
|
53ccec1d80 | ||
|
|
4af5d6152b | ||
|
|
307781f437 | ||
|
|
1326994d8e | ||
|
|
fd70a3f3e0 | ||
|
|
298930b0d7 | ||
|
|
54d95b8761 | ||
|
|
4a16040d3e | ||
|
|
ee95548340 | ||
|
|
320e687bad | ||
|
|
ad43c6817e | ||
|
|
c8747c079d | ||
|
|
224bfe7fcb | ||
|
|
f443bfbc7b | ||
|
|
195b2ea0d2 | ||
|
|
4150479549 | ||
|
|
5ebf513498 | ||
|
|
4e4ffc439c | ||
|
|
ad7ed50b52 | ||
|
|
92f8916225 | ||
|
|
7e4edd270d | ||
|
|
332ff44219 | ||
|
|
834ce3efc0 | ||
|
|
5211f5065d | ||
|
|
96d6f89732 | ||
|
|
6ab06584eb | ||
|
|
a6433c2b50 | ||
|
|
128a131797 | ||
|
|
c775d7ed80 | ||
|
|
b8b4308e1c | ||
|
|
80d6e2255f | ||
|
|
4e0968d4b7 | ||
|
|
44a5809a46 | ||
|
|
2428fb1407 | ||
|
|
bb78882f72 | ||
|
|
df88a23102 | ||
|
|
ca5f360231 | ||
|
|
147b50ec45 | ||
|
|
34439dce4e | ||
|
|
9a22027b35 | ||
|
|
282a102826 | ||
|
|
417e6c2ade | ||
|
|
9d69d7f46c | ||
|
|
87d1593a1f | ||
|
|
7fbdf3d914 | ||
|
|
65287cf4b7 | ||
|
|
6da3017dcf | ||
|
|
5c02057198 | ||
|
|
a7ca7cdb9f | ||
|
|
79408d406d | ||
|
|
e555f71bf8 | ||
|
|
4c320346c3 | ||
|
|
e5ec88169a | ||
|
|
914711ae04 | ||
|
|
4c1e3b9548 | ||
|
|
9cd3987475 | ||
|
|
47a853330d | ||
|
|
2aced2f659 | ||
|
|
cd955df02f | ||
|
|
0a819df2bf | ||
|
|
67afcadcf0 | ||
|
|
1128ef5acd | ||
|
|
ca39b8d0c9 | ||
|
|
fece74f744 | ||
|
|
6b3506f128 | ||
|
|
5613dcef99 | ||
|
|
3528517fe0 | ||
|
|
2a5b520ec9 | ||
|
|
e2fb3602f1 | ||
|
|
da3580cbd7 | ||
|
|
0c300d040c | ||
|
|
d43a3729c3 | ||
|
|
bed97a84d8 | ||
|
|
b908076846 | ||
|
|
885a0a9a00 | ||
|
|
340baf4f0b | ||
|
|
82cda0edaa | ||
|
|
9fe875355a | ||
|
|
5f21bd2036 | ||
|
|
5b5ea6d2d8 | ||
|
|
3dcfd23036 | ||
|
|
186914bcac | ||
|
|
4b3205fee8 | ||
|
|
e4453841db | ||
|
|
32db4cbd11 | ||
|
|
5558249cd3 | ||
|
|
86b636ae54 | ||
|
|
c9f7154524 | ||
|
|
d72f872369 | ||
|
|
b300011d07 | ||
|
|
3946253d6e | ||
|
|
11fc367845 | ||
|
|
bdc65cc250 | ||
|
|
70dad0f600 | ||
|
|
26e9aa244b | ||
|
|
913f77fd2f | ||
|
|
dba774e1c7 | ||
|
|
af09d652a3 | ||
|
|
0babdfbc44 | ||
|
|
f154d53be9 | ||
|
|
ed3cd2fe3b | ||
|
|
59912a00bc | ||
|
|
675d8b0179 | ||
|
|
a1ca296fc0 | ||
|
|
c1b1fe90fd | ||
|
|
30b5610a73 | ||
|
|
db9b9df8f7 | ||
|
|
ae6a75e913 | ||
|
|
37d72c1628 | ||
|
|
ab3e6bb37c | ||
|
|
9ddb899a1a | ||
|
|
1f0cda8aee | ||
|
|
90ae85317c | ||
|
|
a8ae891aff | ||
|
|
1f2df3e944 | ||
|
|
e39a432210 | ||
|
|
6b3bcee582 | ||
|
|
6c274b75b8 | ||
|
|
278032c94b | ||
|
|
23a8982f5c | ||
|
|
5df6c32d04 | ||
|
|
7cae330c3c | ||
|
|
100aaa4cee | ||
|
|
ead0b73e7b | ||
|
|
2ace846e38 | ||
|
|
1d98393f0c | ||
|
|
c48ee2a364 | ||
|
|
95d1e8432f | ||
|
|
4d59ac78bd | ||
|
|
f1143e134e | ||
|
|
e3e869faee | ||
|
|
8350e72393 | ||
|
|
15f74b9d97 | ||
|
|
55b54e24fe | ||
|
|
46b4ce81e4 | ||
|
|
7a723d03d0 | ||
|
|
9d6fbfd0d6 | ||
|
|
8fd6bf47b1 | ||
|
|
f25f3a34d0 | ||
|
|
2823848fae | ||
|
|
b9467b2bc3 | ||
|
|
fa190e0975 | ||
|
|
90dc8cf997 | ||
|
|
53c5ccc00a | ||
|
|
06c9e8d7c1 | ||
|
|
f32818c6aa | ||
|
|
fb03afb6b7 | ||
|
|
577efa56a9 | ||
|
|
cb98b3a47e | ||
|
|
8daf9a3ce5 | ||
|
|
a83d8248d7 | ||
|
|
4cdc92f759 | ||
|
|
54c668c3f0 | ||
|
|
ac3fc6b7d3 | ||
|
|
a6a6efdf59 | ||
|
|
8043faf6c7 | ||
|
|
ebed9e64ed | ||
|
|
913636ae44 | ||
|
|
7b064e9f33 | ||
|
|
16da0eaca3 | ||
|
|
6acfee2737 | ||
|
|
92abf7edaa | ||
|
|
395babc1f5 | ||
|
|
68cde54bad | ||
|
|
c7d7529c00 | ||
|
|
748f60c7bb | ||
|
|
ffee62e940 | ||
|
|
a0ea221131 | ||
|
|
2846bcb84e | ||
|
|
542ed6c08f | ||
|
|
6dabb848a5 | ||
|
|
de0181e0d9 | ||
|
|
a850d426ef | ||
|
|
771277d5d9 | ||
|
|
20b4f5c99f | ||
|
|
e7fa87b301 | ||
|
|
40c66958bc | ||
|
|
600fc71aed | ||
|
|
443e4b04cd | ||
|
|
556a3c0fab | ||
|
|
1eca52f962 | ||
|
|
e21adb395b | ||
|
|
0e4bf80bf4 | ||
|
|
107750971b | ||
|
|
24e1be47ca | ||
|
|
7e0978c764 | ||
|
|
83a0c9662d | ||
|
|
a4736a49f8 | ||
|
|
1cbdf442ee | ||
|
|
f91a2702c9 | ||
|
|
c885c0c71a | ||
|
|
a8f67ab717 | ||
|
|
aa7d8630b5 | ||
|
|
36313f378e | ||
|
|
45054d3238 | ||
|
|
173e6b08c9 | ||
|
|
75e4d07395 | ||
|
|
2a86404ba5 | ||
|
|
b8a0199cde | ||
|
|
a46ed55a86 | ||
|
|
1a9fbddef4 | ||
|
|
1209aaafb4 | ||
|
|
8e71261d49 | ||
|
|
586978f1b0 | ||
|
|
49e45eab54 | ||
|
|
bd22793ceb | ||
|
|
b2013436c5 | ||
|
|
cc808cb8c5 | ||
|
|
afffc4b3f0 | ||
|
|
99710ef9dc | ||
|
|
3d4ae04d9b | ||
|
|
707b391449 | ||
|
|
45b843f344 | ||
|
|
191fd90f06 | ||
|
|
05920cd66d | ||
|
|
01508d5e42 | ||
|
|
57cf6cbc9e | ||
|
|
dd69d7c1a5 | ||
|
|
63743b6929 | ||
|
|
38597dd19d | ||
|
|
03ebbea46a | ||
|
|
8e7a5d1ec3 | ||
|
|
e7365a4a00 | ||
|
|
aa74a2535b | ||
|
|
32379ba4b7 | ||
|
|
bf4fed78f2 | ||
|
|
722afc85df | ||
|
|
3036596d16 | ||
|
|
aee539f37e | ||
|
|
05b41692ba | ||
|
|
7fe8d7b4be | ||
|
|
aace726a91 | ||
|
|
e6f6e5464a | ||
|
|
7d04b600fb | ||
|
|
8a622889ff | ||
|
|
463bfde2ac | ||
|
|
893f74dc3e | ||
|
|
e136679934 | ||
|
|
8a215fbcc7 | ||
|
|
ac5e4c4722 | ||
|
|
04d01b0bab | ||
|
|
cb1d7565a3 | ||
|
|
1d9b6407a4 | ||
|
|
22ed522711 | ||
|
|
a3598ef859 | ||
|
|
6fe09ec2dd | ||
|
|
57911d9e09 | ||
|
|
3aee283495 | ||
|
|
54b24c2677 | ||
|
|
8cadad0a1e | ||
|
|
633c5f3cda | ||
|
|
a8431e2040 | ||
|
|
7a2b916387 | ||
|
|
f3deefae56 | ||
|
|
d0a7065991 | ||
|
|
5b7e7c4d56 | ||
|
|
f4dfb68b7b | ||
|
|
8f4f27d860 | ||
|
|
e9eab29953 | ||
|
|
4f1eeb7da5 | ||
|
|
6fd426d719 | ||
|
|
331da3c8c7 | ||
|
|
c48943bc67 | ||
|
|
7680b33b99 | ||
|
|
bb5d75bc7e | ||
|
|
528395e2c3 | ||
|
|
6a9012e5e4 | ||
|
|
6e8315ab40 |
BIN
.gitee/image/common/iot-feature.png
Normal file
BIN
.gitee/image/common/iot-feature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
.gitee/image/common/iot-preview.png
Normal file
BIN
.gitee/image/common/iot-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
.gitee/image/common/mes-feature.png
Normal file
BIN
.gitee/image/common/mes-feature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
.gitee/image/common/mes-preview.png
Normal file
BIN
.gitee/image/common/mes-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 46 KiB |
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
@@ -9,7 +9,7 @@ runs:
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: 'pnpm'
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/changeset-version.yml
vendored
2
.github/workflows/changeset-version.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -28,12 +28,12 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/draft.yml
vendored
2
.github/workflows/draft.yml
vendored
@@ -20,6 +20,6 @@ jobs:
|
||||
if: github.repository == 'vbenjs/vue-vben-admin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
- uses: release-drafter/release-drafter@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
8
.github/workflows/release-tag.yml
vendored
8
.github/workflows/release-tag.yml
vendored
@@ -19,15 +19,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
node-version: [22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v6
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "version=${version}" >> $GITHUB_OUTPUT
|
||||
echo "major=${major}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
- uses: release-drafter/release-drafter@v7
|
||||
with:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
publish: true
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -22,7 +22,7 @@ yarn.lock
|
||||
package-lock.json
|
||||
.VSCodeCounter
|
||||
**/backend-mock/data
|
||||
|
||||
.omx
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -50,3 +50,10 @@ vite.config.ts.*
|
||||
*.sw?
|
||||
.history
|
||||
.cursor
|
||||
|
||||
# AI
|
||||
.agent
|
||||
.agents
|
||||
.claude
|
||||
.codex
|
||||
skills-lock.json
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.1.0
|
||||
22.22.0
|
||||
|
||||
4
.npmrc
4
.npmrc
@@ -1,8 +1,8 @@
|
||||
registry=https://registry.npmmirror.com
|
||||
public-hoist-pattern[]=lefthook
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||
public-hoist-pattern[]=oxfmt
|
||||
public-hoist-pattern[]=oxlint
|
||||
public-hoist-pattern[]=stylelint
|
||||
public-hoist-pattern[]=*postcss*
|
||||
public-hoist-pattern[]=@commitlint/*
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
dist
|
||||
dev-dist
|
||||
.local
|
||||
.output.js
|
||||
node_modules
|
||||
.nvmrc
|
||||
coverage
|
||||
CODEOWNERS
|
||||
.nitro
|
||||
.output
|
||||
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
public
|
||||
.npmrc
|
||||
*-lock.yaml
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from '@vben/prettier-config';
|
||||
@@ -2,3 +2,7 @@ dist
|
||||
public
|
||||
__tests__
|
||||
coverage
|
||||
.codex
|
||||
.claude
|
||||
.agent
|
||||
.agents
|
||||
|
||||
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@@ -2,14 +2,18 @@
|
||||
"recommendations": [
|
||||
// Vue 3 的语言支持
|
||||
"Vue.volar",
|
||||
// 将 ESLint JavaScript 集成到 VS Code 中。
|
||||
// 将 eslint 集成到 VS Code 中。
|
||||
"dbaeumer.vscode-eslint",
|
||||
// 将 oxlint 集成到 VS Code 中。
|
||||
"oxc.oxc-vscode",
|
||||
// Visual Studio Code 的官方 Stylelint 扩展
|
||||
"stylelint.vscode-stylelint",
|
||||
// 使用 Prettier 的代码格式化程序
|
||||
"esbenp.prettier-vscode",
|
||||
// 使用 oxfmt 的代码格式化程序
|
||||
"oxc.oxc-vscode",
|
||||
// 支持 dotenv 文件语法
|
||||
"mikestead.dotenv",
|
||||
// YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
|
||||
"redhat.vscode-yaml",
|
||||
// 源代码的拼写检查器
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Tailwind CSS 的官方 VS Code 插件
|
||||
|
||||
56
.vscode/settings.json
vendored
56
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
|
||||
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
|
||||
"tailwindCSS.lint.suggestCanonicalClasses": "ignore",
|
||||
// workbench
|
||||
"workbench.list.smoothScrolling": true,
|
||||
"workbench.startupEditor": "newUntitledFile",
|
||||
@@ -31,39 +32,51 @@
|
||||
"editor.autoClosingOvertype": "always",
|
||||
"editor.autoClosingQuotes": "beforeWhitespace",
|
||||
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
|
||||
// lint && format
|
||||
"oxc.enable": true,
|
||||
"oxc.typeAware": true,
|
||||
"oxc.configPath": "oxlint.config.ts",
|
||||
"oxc.fmt.configPath": "oxfmt.config.ts",
|
||||
"eslint.useFlatConfig": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.fixAll.stylelint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
|
||||
// extensions
|
||||
"extensions.ignoreRecommendations": true,
|
||||
|
||||
@@ -79,6 +92,7 @@
|
||||
"files.insertFinalNewline": true,
|
||||
"files.simpleDialog.enable": true,
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss",
|
||||
"*.ejs": "html",
|
||||
"*.art": "html",
|
||||
"**/tsconfig.json": "jsonc",
|
||||
@@ -118,7 +132,7 @@
|
||||
// search
|
||||
"search.searchEditor.singleClickBehaviour": "peekDefinition",
|
||||
"search.followSymlinks": false,
|
||||
// 在使用搜索功能时,将这些文件夹/文件排除在外
|
||||
// 使用搜索功能时,将这些文件和文件夹排除在外
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/*.log": true,
|
||||
@@ -159,7 +173,7 @@
|
||||
"emmet.triggerExpansionOnTab": false,
|
||||
|
||||
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
|
||||
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
|
||||
"errorLens.excludeBySource": ["cSpell", "Grammarly"],
|
||||
|
||||
"stylelint.enable": true,
|
||||
"stylelint.packageManager": "pnpm",
|
||||
@@ -167,9 +181,10 @@
|
||||
"stylelint.customSyntax": "postcss-html",
|
||||
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
|
||||
|
||||
"typescript.inlayHints.enumMemberValues.enabled": true,
|
||||
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||
"js/ts.inlayHints.enumMemberValues.enabled": true,
|
||||
"js/ts.preferences.preferTypeOnlyAutoImports": true,
|
||||
"js/ts.preferences.includePackageJsonAutoImports": "on",
|
||||
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
@@ -193,7 +208,7 @@
|
||||
"*": false
|
||||
},
|
||||
|
||||
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
|
||||
"cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
|
||||
|
||||
"i18n-ally.localesPaths": [
|
||||
"packages/locales/src/langs",
|
||||
@@ -218,12 +233,9 @@
|
||||
"*.env": "$(capture).env.*",
|
||||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
|
||||
"tailwind.config.mjs": "postcss.*"
|
||||
"oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
|
||||
},
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"commentTranslate.multiLineMerge": true,
|
||||
"vue.server.hybridMode": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"oxc.enable": false
|
||||
"vue.server.hybridMode": true
|
||||
}
|
||||
|
||||
62
README.md
62
README.md
@@ -9,7 +9,7 @@
|
||||
|
||||
## 🐶 新手必读
|
||||
|
||||
- nodejs > v20.19.0 | v22 | v24 && pnpm > 10.20.0 (强制使用pnpm)
|
||||
- nodejs >= v20.19.0(推荐 v22 / v24) && pnpm >= 10.32.1(强制使用 pnpm)
|
||||
- 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
|
||||
- 演示地址【Vue3 + vben5(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
|
||||
- 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
|
||||
@@ -20,12 +20,12 @@
|
||||
|
||||
**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
|
||||
|
||||
- 采用最新 [vue-vben-admin](https://github.com/vbenjs/vue-vben-admin) v5 实现
|
||||
- 采用最新 [vue-vben-admin](https://github.com/vbenjs/vue-vben-admin) v5.7.0 实现
|
||||
- 支持 [Ant Design Vue](https://www.antdv.com/) | [Element Plus](https://element-plus.org/zh-CN/) | [Naive UI](https://www.naiveui.com/) | [TDesign](https://tdesign.tencent.com/) 多种免费开源的中后台模版,具备如下特性:
|
||||
|
||||

|
||||
|
||||
- **最新技术栈**:使用 Vue3、Vite7 等前端前沿技术开发
|
||||
- **最新技术栈**:使用 Vue3、Vite8 等前端前沿技术开发
|
||||
- **TypeScript**: 应用程序级 JavaScript 的语言
|
||||
- **主题**: 提供多套主题色彩,可配置自定义主题
|
||||
- **国际化**:内置完善的国际化方案
|
||||
@@ -41,24 +41,24 @@
|
||||
|
||||
| 框架 | 说明 | 版本 |
|
||||
| --- | --- | --- |
|
||||
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.27 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.3.1 |
|
||||
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.30 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 8.0.0 |
|
||||
| [Ant Design Vue](https://www.antdv.com/) | Ant Design Vue | 4.2.6 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.13.1 |
|
||||
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.43.2 |
|
||||
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.18.0 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.13.5 |
|
||||
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.44.1 |
|
||||
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.18.5 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.9.3 |
|
||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 3.0.4 |
|
||||
| [vueuse](https://vueuse.org/) | 常用工具集 | 14.1.0 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.2.8 |
|
||||
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.6.4 |
|
||||
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.19 |
|
||||
| [vueuse](https://vueuse.org/) | 常用工具集 | 14.2.1 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.3.0 |
|
||||
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 5.0.3 |
|
||||
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 4.2.1 |
|
||||
| [Iconify](https://iconify.design/) | 图标组件 | 5.0.0 |
|
||||
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.431 |
|
||||
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.449 |
|
||||
| [TinyMCE](https://www.tiny.cloud/) | 富文本编辑器 | 7.3.0 |
|
||||
| [Echarts](https://echarts.apache.org/) | 图表库 | 6.0.0 |
|
||||
| [axios](https://axios-http.com/) | http客户端 | 1.13.2 |
|
||||
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.19 |
|
||||
| [axios](https://axios-http.com/) | http客户端 | 1.13.6 |
|
||||
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.20 |
|
||||
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.1 |
|
||||
| [zod](https://zod.dev/) | 数据验证 | 3.25.76 |
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
|
||||

|
||||
|
||||
- 通用模块(必选):系统功能、基础设施
|
||||
- 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||
- 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
|
||||
* 通用模块(必选):系统功能、基础设施
|
||||
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||
* 业务系统(按需):ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
|
||||
|
||||
### 系统功能
|
||||
|
||||
@@ -219,6 +219,16 @@
|
||||
|
||||

|
||||
|
||||
### 会员中心
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|-----|------|----------------------------------|
|
||||
| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 |
|
||||
| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 |
|
||||
| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 |
|
||||
| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 |
|
||||
| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
|
||||
|
||||
### ERP 系统
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/erp-preview/>
|
||||
@@ -231,6 +241,14 @@
|
||||
|
||||

|
||||
|
||||
### MES 系统
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/mes-preview/>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### AI 大模型
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/ai-preview/>
|
||||
@@ -238,3 +256,11 @@
|
||||

|
||||
|
||||

|
||||
|
||||
### IoT 物联网
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/iot/build>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
/>
|
||||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||
<title><%= VITE_APP_TITLE %></title>
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script>
|
||||
var HM_ID = '<%= VITE_APP_BAIDU_CODE %>';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.5.9",
|
||||
"version": "5.7.0",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
8
apps/web-antd/public/tinymce/tinymce.d.ts
vendored
8
apps/web-antd/public/tinymce/tinymce.d.ts
vendored
@@ -2537,12 +2537,12 @@ interface EditorSelection {
|
||||
normalize: () => Range;
|
||||
selectorChanged: (selector: string, callback: (active: boolean, args: {
|
||||
node: Node;
|
||||
selector: String;
|
||||
selector: string;
|
||||
parents: Node[];
|
||||
}) => void) => EditorSelection;
|
||||
selectorChangedWithUnbind: (selector: string, callback: (active: boolean, args: {
|
||||
node: Node;
|
||||
selector: String;
|
||||
selector: string;
|
||||
parents: Node[];
|
||||
}) => void) => {
|
||||
unbind: () => void;
|
||||
@@ -3217,9 +3217,9 @@ interface Tools {
|
||||
<T, R>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, R>): R[];
|
||||
<T, R>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, R>): R[];
|
||||
};
|
||||
extend: (obj: Object, ext: Object, ...objs: Object[]) => any;
|
||||
extend: (obj: object, ext: object, ...objs: object[]) => any;
|
||||
walk: <T extends Record<string, any>>(obj: T, f: WalkCallback<T>, n?: keyof T, scope?: any) => void;
|
||||
resolve: (path: string, o?: Object) => any;
|
||||
resolve: (path: string, o?: object) => any;
|
||||
explode: (s: string | string[], d?: string | RegExp) => string[];
|
||||
_addCacheSuffix: (url: string) => string;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,39 @@
|
||||
/* eslint-disable vue/one-component-per-file */
|
||||
|
||||
import type {
|
||||
AutoCompleteProps,
|
||||
ButtonProps,
|
||||
CascaderProps,
|
||||
CheckboxGroupProps,
|
||||
CheckboxProps,
|
||||
DatePickerProps,
|
||||
DividerProps,
|
||||
InputNumberProps,
|
||||
InputProps,
|
||||
MentionsProps,
|
||||
RadioGroupProps,
|
||||
RadioProps,
|
||||
RateProps,
|
||||
SelectProps,
|
||||
SpaceProps,
|
||||
SwitchProps,
|
||||
TextAreaProps,
|
||||
TimePickerProps,
|
||||
TreeSelectProps,
|
||||
UploadChangeParam,
|
||||
UploadFile,
|
||||
UploadProps,
|
||||
} from 'ant-design-vue';
|
||||
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
|
||||
|
||||
import type { Component, Ref } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type {
|
||||
ApiComponentSharedProps,
|
||||
BaseFormComponentType,
|
||||
IconPickerProps,
|
||||
} from '@vben/common-ui';
|
||||
import type { Sortable } from '@vben/hooks';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
@@ -21,6 +46,9 @@ import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
h,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
render,
|
||||
unref,
|
||||
@@ -33,6 +61,7 @@ import {
|
||||
IconPicker,
|
||||
VCropper,
|
||||
} from '@vben/common-ui';
|
||||
import { useSortable } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
@@ -41,6 +70,15 @@ import { message, Modal, notification } from 'ant-design-vue';
|
||||
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
type AdapterUploadProps = UploadProps & {
|
||||
aspectRatio?: string;
|
||||
crop?: boolean;
|
||||
draggable?: boolean;
|
||||
handleChange?: (event: UploadChangeParam) => void;
|
||||
maxSize?: number;
|
||||
onDragSort?: (oldIndex: number, newIndex: number) => void;
|
||||
onHandleChange?: (event: UploadChangeParam) => void;
|
||||
};
|
||||
|
||||
const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
@@ -132,260 +170,261 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
});
|
||||
};
|
||||
|
||||
const withPreviewUpload = () => {
|
||||
// 检查是否为图片文件的辅助函数
|
||||
const isImageFile = (file: UploadFile): boolean => {
|
||||
const imageExtensions = new Set([
|
||||
'bmp',
|
||||
'gif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'svg',
|
||||
'webp',
|
||||
]);
|
||||
if (file.url) {
|
||||
try {
|
||||
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
} catch {
|
||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
const IMAGE_EXTENSIONS = new Set([
|
||||
'bmp',
|
||||
'gif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'svg',
|
||||
'webp',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 检查是否为图片文件
|
||||
*/
|
||||
function isImageFile(file: UploadFile): boolean {
|
||||
if (file.url) {
|
||||
try {
|
||||
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
|
||||
} catch {
|
||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
|
||||
}
|
||||
if (!file.type) {
|
||||
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
return file.type.startsWith('image/');
|
||||
}
|
||||
if (!file.type) {
|
||||
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
|
||||
}
|
||||
return file.type.startsWith('image/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的上传按钮插槽
|
||||
*/
|
||||
function createDefaultUploadSlots(listType: string, placeholder: string) {
|
||||
if (listType === 'picture-card') {
|
||||
return { default: () => placeholder };
|
||||
}
|
||||
return {
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
icon: h(IconifyIcon, {
|
||||
icon: 'ant-design:upload-outlined',
|
||||
class: 'mb-1 size-4',
|
||||
}),
|
||||
},
|
||||
() => placeholder,
|
||||
),
|
||||
};
|
||||
// 创建默认的上传按钮插槽
|
||||
const createDefaultSlotsWithUpload = (
|
||||
listType: string,
|
||||
placeholder: string,
|
||||
) => {
|
||||
switch (listType) {
|
||||
case 'picture-card': {
|
||||
return {
|
||||
default: () => placeholder,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
icon: h(IconifyIcon, {
|
||||
icon: 'ant-design:upload-outlined',
|
||||
class: 'mb-1 size-4',
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Base64
|
||||
*/
|
||||
function getBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result as string));
|
||||
reader.addEventListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
*/
|
||||
async function previewImage(
|
||||
file: UploadFile,
|
||||
visible: Ref<boolean>,
|
||||
fileList: Ref<UploadProps['fileList']>,
|
||||
) {
|
||||
// 非图片文件直接打开链接
|
||||
if (!isImageFile(file)) {
|
||||
const url = file.url || file.preview;
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
message.error($t('ui.formRules.previewWarning'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||
Image,
|
||||
PreviewGroup,
|
||||
]);
|
||||
|
||||
// 过滤图片文件并生成预览
|
||||
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
|
||||
|
||||
for (const imgFile of imageFiles) {
|
||||
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||
imgFile.preview = await getBase64(imgFile.originFileObj);
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
let isUnmounted = false;
|
||||
|
||||
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
|
||||
|
||||
const PreviewWrapper = {
|
||||
setup() {
|
||||
return () => {
|
||||
if (isUnmounted) return null;
|
||||
return h(
|
||||
PreviewGroupComponent,
|
||||
{
|
||||
class: 'hidden',
|
||||
preview: {
|
||||
visible: visible.value,
|
||||
current: currentIndex,
|
||||
onVisibleChange: (value: boolean) => {
|
||||
visible.value = value;
|
||||
if (!value) {
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
() => placeholder,
|
||||
},
|
||||
},
|
||||
() =>
|
||||
imageFiles.map((imgFile) =>
|
||||
h(ImageComponent, {
|
||||
key: imgFile.uid,
|
||||
src: imgFile.url || imgFile.preview,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
// 构建预览图片组
|
||||
const previewImage = async (
|
||||
file: UploadFile,
|
||||
visible: Ref<boolean>,
|
||||
fileList: Ref<UploadProps['fileList']>,
|
||||
) => {
|
||||
// 如果当前文件不是图片,直接打开
|
||||
if (!isImageFile(file)) {
|
||||
if (file.url) {
|
||||
window.open(file.url, '_blank');
|
||||
} else if (file.preview) {
|
||||
window.open(file.preview, '_blank');
|
||||
} else {
|
||||
message.error($t('ui.formRules.previewWarning'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于图片文件,继续使用预览组
|
||||
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||
Image,
|
||||
PreviewGroup,
|
||||
]);
|
||||
render(h(PreviewWrapper), container);
|
||||
}
|
||||
|
||||
const getBase64 = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
};
|
||||
// 从fileList中过滤出所有图片文件
|
||||
const imageFiles = (unref(fileList) || []).filter((element) =>
|
||||
isImageFile(element),
|
||||
);
|
||||
|
||||
// 为所有没有预览地址的图片生成预览
|
||||
for (const imgFile of imageFiles) {
|
||||
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
||||
}
|
||||
}
|
||||
const container: HTMLElement | null = document.createElement('div');
|
||||
/**
|
||||
* 图片裁剪操作
|
||||
*/
|
||||
function cropImage(file: File, aspectRatio: string | undefined) {
|
||||
return new Promise<Blob | string | undefined>((resolve, reject) => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
// 用于追踪组件是否已卸载
|
||||
let isUnmounted = false;
|
||||
let objectUrl: null | string = null;
|
||||
|
||||
const PreviewWrapper = {
|
||||
const open = ref<boolean>(true);
|
||||
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||
|
||||
const closeModal = () => {
|
||||
open.value = false;
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const CropperWrapper = {
|
||||
setup() {
|
||||
return () => {
|
||||
if (isUnmounted) return null;
|
||||
if (!objectUrl) {
|
||||
objectUrl = URL.createObjectURL(file);
|
||||
}
|
||||
return h(
|
||||
PreviewGroupComponent,
|
||||
Modal,
|
||||
{
|
||||
class: 'hidden',
|
||||
preview: {
|
||||
visible: visible.value,
|
||||
// 设置初始显示的图片索引
|
||||
current: imageFiles.findIndex((f) => f.uid === file.uid),
|
||||
onVisibleChange: (value: boolean) => {
|
||||
visible.value = value;
|
||||
if (!value) {
|
||||
// 延迟清理,确保动画完成
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
open: open.value,
|
||||
title: h('div', {}, [
|
||||
$t('ui.crop.title'),
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
|
||||
},
|
||||
$t('ui.crop.titleTip', [aspectRatio]),
|
||||
),
|
||||
]),
|
||||
centered: true,
|
||||
width: 548,
|
||||
keyboard: false,
|
||||
maskClosable: false,
|
||||
closable: false,
|
||||
cancelText: $t('common.cancel'),
|
||||
okText: $t('ui.crop.confirm'),
|
||||
destroyOnClose: true,
|
||||
onOk: async () => {
|
||||
const cropper = cropperRef.value;
|
||||
if (!cropper) {
|
||||
reject(new Error('Cropper not found'));
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dataUrl = await cropper.getCropImage();
|
||||
if (dataUrl) {
|
||||
resolve(dataUrl);
|
||||
} else {
|
||||
reject(new Error($t('ui.crop.errorTip')));
|
||||
}
|
||||
},
|
||||
} catch {
|
||||
reject(new Error($t('ui.crop.errorTip')));
|
||||
} finally {
|
||||
closeModal();
|
||||
}
|
||||
},
|
||||
onCancel() {
|
||||
resolve('');
|
||||
closeModal();
|
||||
},
|
||||
},
|
||||
() =>
|
||||
// 渲染所有图片文件
|
||||
imageFiles.map((imgFile) =>
|
||||
h(ImageComponent, {
|
||||
key: imgFile.uid,
|
||||
src: imgFile.url || imgFile.preview,
|
||||
}),
|
||||
),
|
||||
h(VCropper, {
|
||||
ref: (ref: any) => (cropperRef.value = ref),
|
||||
img: objectUrl as string,
|
||||
aspectRatio,
|
||||
}),
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
render(h(PreviewWrapper), container);
|
||||
};
|
||||
|
||||
// 图片裁剪操作
|
||||
const cropImage = (file: File, aspectRatio: string | undefined) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const container: HTMLElement | null = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
// 用于追踪组件是否已卸载
|
||||
let isUnmounted = false;
|
||||
let objectUrl: null | string = null;
|
||||
|
||||
const open = ref<boolean>(true);
|
||||
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||
|
||||
const closeModal = () => {
|
||||
open.value = false;
|
||||
// 延迟清理,确保动画完成
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const CropperWrapper = {
|
||||
setup() {
|
||||
return () => {
|
||||
if (isUnmounted) return null;
|
||||
if (!objectUrl) {
|
||||
objectUrl = URL.createObjectURL(file);
|
||||
}
|
||||
return h(
|
||||
Modal,
|
||||
{
|
||||
open: open.value,
|
||||
title: h('div', {}, [
|
||||
$t('ui.crop.title'),
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
|
||||
},
|
||||
$t('ui.crop.titleTip', [aspectRatio]),
|
||||
),
|
||||
]),
|
||||
centered: true,
|
||||
width: 548,
|
||||
keyboard: false,
|
||||
maskClosable: false,
|
||||
closable: false,
|
||||
cancelText: $t('common.cancel'),
|
||||
okText: $t('ui.crop.confirm'),
|
||||
destroyOnClose: true,
|
||||
onOk: async () => {
|
||||
const cropper = cropperRef.value;
|
||||
if (!cropper) {
|
||||
reject(new Error('Cropper not found'));
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dataUrl = await cropper.getCropImage();
|
||||
resolve(dataUrl);
|
||||
} catch {
|
||||
reject(new Error($t('ui.crop.errorTip')));
|
||||
} finally {
|
||||
closeModal();
|
||||
}
|
||||
},
|
||||
onCancel() {
|
||||
resolve('');
|
||||
closeModal();
|
||||
},
|
||||
},
|
||||
() =>
|
||||
h(VCropper, {
|
||||
ref: (ref: any) => (cropperRef.value = ref),
|
||||
img: objectUrl as string,
|
||||
aspectRatio,
|
||||
}),
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
render(h(CropperWrapper), container);
|
||||
});
|
||||
};
|
||||
render(h(CropperWrapper), container);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带预览功能的上传组件
|
||||
*/
|
||||
const withPreviewUpload = () => {
|
||||
return defineComponent({
|
||||
name: Upload.name,
|
||||
emits: ['update:modelValue'],
|
||||
setup: (
|
||||
setup(
|
||||
props: any,
|
||||
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
||||
) => {
|
||||
) {
|
||||
const previewVisible = ref<boolean>(false);
|
||||
|
||||
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
|
||||
|
||||
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
|
||||
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>(
|
||||
attrs?.fileList || attrs?.['file-list'] || [],
|
||||
);
|
||||
@@ -399,12 +438,14 @@ const withPreviewUpload = () => {
|
||||
file: UploadFile,
|
||||
originFileList: Array<File>,
|
||||
) => {
|
||||
// 文件大小限制
|
||||
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
|
||||
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
|
||||
file.status = 'removed';
|
||||
return false;
|
||||
}
|
||||
// 多选或者非图片不唤起裁剪框
|
||||
|
||||
// 图片裁剪处理
|
||||
if (
|
||||
attrs.crop &&
|
||||
!attrs.multiple &&
|
||||
@@ -412,14 +453,11 @@ const withPreviewUpload = () => {
|
||||
isImageFile(file)
|
||||
) {
|
||||
file.status = 'removed';
|
||||
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
|
||||
const blob = await cropImage(originFileList[0], aspectRatio.value);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!blob) {
|
||||
return reject(new Error($t('ui.crop.errorTip')));
|
||||
}
|
||||
resolve(blob);
|
||||
});
|
||||
if (!blob) {
|
||||
throw new Error($t('ui.crop.errorTip'));
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
return attrs.beforeUpload?.(file) ?? true;
|
||||
@@ -427,12 +465,9 @@ const withPreviewUpload = () => {
|
||||
|
||||
const handleChange = (event: UploadChangeParam) => {
|
||||
try {
|
||||
// 行内写法 handleChange: (event) => {}
|
||||
attrs.handleChange?.(event);
|
||||
// template写法 @handle-change="(event) => {}"
|
||||
attrs.onHandleChange?.(event);
|
||||
} catch (error) {
|
||||
// Avoid breaking internal v-model sync on user handler errors
|
||||
console.error(error);
|
||||
}
|
||||
fileList.value = event.fileList.filter(
|
||||
@@ -449,21 +484,88 @@ const withPreviewUpload = () => {
|
||||
await previewImage(file, previewVisible, fileList);
|
||||
};
|
||||
|
||||
const renderUploadButton = (): any => {
|
||||
const isDisabled = attrs.disabled;
|
||||
|
||||
// 如果禁用,不渲染上传按钮
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 否则渲染默认上传按钮
|
||||
const renderUploadButton = () => {
|
||||
if (attrs.disabled) return null;
|
||||
return isEmpty(slots)
|
||||
? createDefaultSlotsWithUpload(listType, placeholder)
|
||||
? createDefaultUploadSlots(listType, placeholder)
|
||||
: slots;
|
||||
};
|
||||
|
||||
// 可以监听到表单API设置的值
|
||||
// 拖拽排序
|
||||
const draggable = computed(
|
||||
() => (attrs.draggable ?? false) && !attrs.disabled,
|
||||
);
|
||||
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const sortableInstance = ref<null | Sortable>(null);
|
||||
|
||||
const styleId = `upload-drag-style-${uploadId}`;
|
||||
|
||||
function injectDragStyle() {
|
||||
if (!document.querySelector(`[id="${styleId}"]`)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
|
||||
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
|
||||
`;
|
||||
document.head.append(style);
|
||||
}
|
||||
}
|
||||
|
||||
function removeDragStyle() {
|
||||
document.querySelector(`[id="${styleId}"]`)?.remove();
|
||||
}
|
||||
|
||||
async function initSortable(retryCount = 0) {
|
||||
if (!draggable.value) return;
|
||||
|
||||
injectDragStyle();
|
||||
await nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const container = document.querySelector(
|
||||
`[data-upload-id="${uploadId}"] .ant-upload-list`,
|
||||
) as HTMLElement;
|
||||
|
||||
if (!container) {
|
||||
if (retryCount < 5) {
|
||||
setTimeout(() => initSortable(retryCount + 1), 200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { initializeSortable } = useSortable(container, {
|
||||
animation: 300,
|
||||
delay: 400,
|
||||
delayOnTouchOnly: true,
|
||||
filter:
|
||||
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (
|
||||
oldIndex === undefined ||
|
||||
newIndex === undefined ||
|
||||
oldIndex === newIndex
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = [...(fileList.value || [])];
|
||||
const [movedItem] = list.splice(oldIndex, 1);
|
||||
if (movedItem) {
|
||||
list.splice(newIndex, 0, movedItem);
|
||||
fileList.value = list;
|
||||
}
|
||||
|
||||
attrs.onDragSort?.(oldIndex, newIndex);
|
||||
emit('update:modelValue', fileList.value);
|
||||
},
|
||||
});
|
||||
|
||||
sortableInstance.value = await initializeSortable();
|
||||
}
|
||||
|
||||
// 监听表单值变化
|
||||
watch(
|
||||
() => attrs.modelValue,
|
||||
(res) => {
|
||||
@@ -471,18 +573,28 @@ const withPreviewUpload = () => {
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(initSortable);
|
||||
onUnmounted(() => {
|
||||
sortableInstance.value?.destroy();
|
||||
removeDragStyle();
|
||||
});
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
beforeUpload: handleBeforeUpload,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton(),
|
||||
'div',
|
||||
{ 'data-upload-id': uploadId, class: 'w-full' },
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
beforeUpload: handleBeforeUpload,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton() as any,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -523,6 +635,39 @@ export type ComponentType =
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
/**
|
||||
* 与 {@link ComponentType} 中注册的组件名一一对应,便于 Schema 上 `component` + `componentProps` 联动提示
|
||||
*/
|
||||
export interface ComponentPropsMap {
|
||||
ApiCascader: ApiComponentSharedProps & CascaderProps;
|
||||
ApiSelect: ApiComponentSharedProps & SelectProps;
|
||||
ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
|
||||
AutoComplete: AutoCompleteProps;
|
||||
Cascader: CascaderProps;
|
||||
Checkbox: CheckboxProps;
|
||||
CheckboxGroup: CheckboxGroupProps;
|
||||
DatePicker: DatePickerProps;
|
||||
DefaultButton: ButtonProps;
|
||||
Divider: DividerProps;
|
||||
IconPicker: IconPickerProps;
|
||||
Input: InputProps;
|
||||
InputNumber: InputNumberProps;
|
||||
InputPassword: InputProps;
|
||||
Mentions: MentionsProps;
|
||||
PrimaryButton: ButtonProps;
|
||||
Radio: RadioProps;
|
||||
RadioGroup: RadioGroupProps;
|
||||
RangePicker: RangePickerProps;
|
||||
Rate: RateProps;
|
||||
Select: SelectProps;
|
||||
Space: SpaceProps;
|
||||
Switch: SwitchProps;
|
||||
Textarea: TextAreaProps;
|
||||
TimePicker: TimePickerProps;
|
||||
TreeSelect: TreeSelectProps;
|
||||
Upload: AdapterUploadProps;
|
||||
}
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type {
|
||||
VbenFormProps as FormProps,
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
import type { ComponentPropsMap, ComponentType } from './component';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -61,9 +61,9 @@ async function initSetupVbenForm() {
|
||||
});
|
||||
}
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
const useVbenForm = useForm<ComponentType, ComponentPropsMap>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
export type VbenFormSchema = FormSchema<ComponentType, ComponentPropsMap>;
|
||||
export type VbenFormProps = FormProps<ComponentType, ComponentPropsMap>;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { ComponentPropsMap, ComponentType } from './component';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
@@ -10,7 +12,7 @@ import {
|
||||
AsyncVxeTable,
|
||||
createRequiredValidation,
|
||||
setupVbenVxeTable,
|
||||
useVbenVxeGrid,
|
||||
useVbenVxeGrid as useGrid,
|
||||
} from '@vben/plugins/vxe-table';
|
||||
import {
|
||||
erpCountInputFormatter,
|
||||
@@ -199,7 +201,7 @@ setupVbenVxeTable({
|
||||
vxeUI.renderer.add('CellOperation', {
|
||||
renderTableDefault({ attrs, options, props }, { column, row }) {
|
||||
const defaultProps = { size: 'small', type: 'link', ...props };
|
||||
let align = 'end';
|
||||
let align: string;
|
||||
switch (column.align) {
|
||||
case 'center': {
|
||||
align = 'center';
|
||||
@@ -363,10 +365,13 @@ setupVbenVxeTable({
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { createRequiredValidation, useVbenVxeGrid };
|
||||
export { createRequiredValidation };
|
||||
|
||||
export const [VxeTable, VxeColumn] = [AsyncVxeTable, AsyncVxeColumn];
|
||||
|
||||
export * from '#/components/table-action';
|
||||
export const useVbenVxeGrid = <T extends Record<string, any>>(
|
||||
...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>>
|
||||
) => useGrid<T, ComponentType, ComponentPropsMap>(...rest);
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
|
||||
@@ -16,10 +16,10 @@ export namespace CrmCustomerLimitConfigApi {
|
||||
|
||||
/** 客户限制配置类型 */
|
||||
export enum LimitConfType {
|
||||
/** 锁定客户数限制 */
|
||||
CUSTOMER_LOCK_LIMIT = 2,
|
||||
/** 拥有客户数限制 */
|
||||
CUSTOMER_QUANTITY_LIMIT = 1,
|
||||
/** 锁定客户数限制 */
|
||||
CUSTOMER_LOCK_LIMIT = 2,
|
||||
}
|
||||
|
||||
/** 查询客户限制配置列表 */
|
||||
|
||||
@@ -35,11 +35,11 @@ export namespace CrmPermissionApi {
|
||||
* CRM 业务类型枚举
|
||||
*/
|
||||
export enum BizTypeEnum {
|
||||
CRM_BUSINESS = 4, // 商机
|
||||
CRM_CLUE = 1, // 线索
|
||||
CRM_CONTACT = 3, // 联系人
|
||||
CRM_CONTRACT = 5, // 合同
|
||||
CRM_CUSTOMER = 2, // 客户
|
||||
CRM_CONTACT = 3, // 联系人
|
||||
CRM_BUSINESS = 4, // 商机
|
||||
CRM_CONTRACT = 5, // 合同
|
||||
CRM_PRODUCT = 6, // 产品
|
||||
CRM_RECEIVABLE = 7, // 回款
|
||||
CRM_RECEIVABLE_PLAN = 8, // 回款计划
|
||||
|
||||
30
apps/web-antd/src/api/iot/device/modbus/config/index.ts
Normal file
30
apps/web-antd/src/api/iot/device/modbus/config/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace IotDeviceModbusConfigApi {
|
||||
/** Modbus 连接配置 VO */
|
||||
export interface ModbusConfig {
|
||||
id?: number; // 主键
|
||||
deviceId: number; // 设备编号
|
||||
ip: string; // Modbus 服务器 IP 地址
|
||||
port: number; // Modbus 服务器端口
|
||||
slaveId: number; // 从站地址
|
||||
timeout: number; // 连接超时时间,单位:毫秒
|
||||
retryInterval: number; // 重试间隔,单位:毫秒
|
||||
mode: number; // 模式
|
||||
frameFormat: number; // 帧格式
|
||||
status: number; // 状态
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取设备的 Modbus 连接配置 */
|
||||
export function getModbusConfig(deviceId: number) {
|
||||
return requestClient.get<IotDeviceModbusConfigApi.ModbusConfig>(
|
||||
'/iot/device-modbus-config/get',
|
||||
{ params: { deviceId } },
|
||||
);
|
||||
}
|
||||
|
||||
/** 保存 Modbus 连接配置 */
|
||||
export function saveModbusConfig(data: IotDeviceModbusConfigApi.ModbusConfig) {
|
||||
return requestClient.post('/iot/device-modbus-config/save', data);
|
||||
}
|
||||
52
apps/web-antd/src/api/iot/device/modbus/point/index.ts
Normal file
52
apps/web-antd/src/api/iot/device/modbus/point/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace IotDeviceModbusPointApi {
|
||||
/** Modbus 点位配置 VO */
|
||||
export interface ModbusPoint {
|
||||
id?: number; // 主键
|
||||
deviceId: number; // 设备编号
|
||||
thingModelId?: number; // 物模型属性编号
|
||||
identifier: string; // 属性标识符
|
||||
name: string; // 属性名称
|
||||
functionCode?: number; // Modbus 功能码
|
||||
registerAddress?: number; // 寄存器起始地址
|
||||
registerCount?: number; // 寄存器数量
|
||||
byteOrder?: string; // 字节序
|
||||
rawDataType?: string; // 原始数据类型
|
||||
scale: number; // 缩放因子
|
||||
pollInterval: number; // 轮询间隔,单位:毫秒
|
||||
status: number; // 状态
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取设备的 Modbus 点位分页 */
|
||||
export function getModbusPointPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotDeviceModbusPointApi.ModbusPoint>>(
|
||||
'/iot/device-modbus-point/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取 Modbus 点位详情 */
|
||||
export function getModbusPoint(id: number) {
|
||||
return requestClient.get<IotDeviceModbusPointApi.ModbusPoint>(
|
||||
`/iot/device-modbus-point/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建 Modbus 点位配置 */
|
||||
export function createModbusPoint(data: IotDeviceModbusPointApi.ModbusPoint) {
|
||||
return requestClient.post('/iot/device-modbus-point/create', data);
|
||||
}
|
||||
|
||||
/** 更新 Modbus 点位配置 */
|
||||
export function updateModbusPoint(data: IotDeviceModbusPointApi.ModbusPoint) {
|
||||
return requestClient.put('/iot/device-modbus-point/update', data);
|
||||
}
|
||||
|
||||
/** 删除 Modbus 点位配置 */
|
||||
export function deleteModbusPoint(id: number) {
|
||||
return requestClient.delete(`/iot/device-modbus-point/delete?id=${id}`);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export namespace IotProductApi {
|
||||
productKey?: string; // 产品标识
|
||||
productSecret?: string; // 产品密钥
|
||||
protocolId?: number; // 协议编号
|
||||
protocolType?: number; // 接入协议类型
|
||||
protocolType?: string; // 协议类型
|
||||
categoryId?: number; // 产品所属品类标识符
|
||||
categoryName?: string; // 产品所属品类名称
|
||||
icon?: string; // 产品图标
|
||||
@@ -19,7 +19,7 @@ export namespace IotProductApi {
|
||||
status?: number; // 产品状态
|
||||
deviceType?: number; // 设备类型
|
||||
netType?: number; // 联网方式
|
||||
codecType?: string; // 数据格式(编解码器类型)
|
||||
serializeType?: string; // 序列化类型
|
||||
dataFormat?: number; // 数据格式
|
||||
validateType?: number; // 认证方式
|
||||
registerEnabled?: boolean; // 是否开启动态注册
|
||||
@@ -28,6 +28,25 @@ export namespace IotProductApi {
|
||||
}
|
||||
}
|
||||
|
||||
// IoT 协议类型枚举
|
||||
export enum ProtocolTypeEnum {
|
||||
COAP = 'coap',
|
||||
EMQX = 'emqx',
|
||||
HTTP = 'http',
|
||||
MODBUS_TCP_CLIENT = 'modbus_tcp_client',
|
||||
MODBUS_TCP_SERVER = 'modbus_tcp_server',
|
||||
MQTT = 'mqtt',
|
||||
TCP = 'tcp',
|
||||
UDP = 'udp',
|
||||
WEBSOCKET = 'websocket',
|
||||
}
|
||||
|
||||
// IoT 序列化类型枚举
|
||||
export enum SerializeTypeEnum {
|
||||
BINARY = 'binary',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
/** 查询产品分页 */
|
||||
export function getProductPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotProductApi.Product>>(
|
||||
|
||||
@@ -119,13 +119,58 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
response.data = apiEncrypt.decryptResponse(response.data);
|
||||
} catch (error) {
|
||||
console.error('响应数据解密失败:', error);
|
||||
throw new Error(`响应数据解密失败: ${(error as Error).message}`);
|
||||
throw new Error(`响应数据解密失败: ${(error as Error).message}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// add by 芋艿:对应 https://t.zsxq.com/SHqWw 反馈
|
||||
// 处理 Blob 响应中的业务错误(如 401):后端把「账号未登录」包成 HTTP 200 + body {code: 401, msg: ...},
|
||||
// download 强制 responseType: 'blob' 后被 axios 包成 application/json 的 Blob,defaultResponseInterceptor 走
|
||||
// responseReturn === 'body' 分支直接返回,绕过了 authenticateResponseInterceptor 的 401 token 刷新;
|
||||
// 这里把这种 Blob 解析回 JSON,再以 axios 风格抛出,让后续拦截器接管
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: async (response) => {
|
||||
const blob = response.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
return response;
|
||||
}
|
||||
// Blob.type 在部分环境可能为空或大小写不一,叠加 response header 一起判断更稳
|
||||
const blobType = (blob.type || '').toLowerCase();
|
||||
const headerType = String(
|
||||
response.headers?.['content-type'] ??
|
||||
response.headers?.['Content-Type'] ??
|
||||
'',
|
||||
).toLowerCase();
|
||||
if (
|
||||
!blobType.includes('application/json') &&
|
||||
!headerType.includes('application/json')
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(await blob.text());
|
||||
} catch {
|
||||
return response;
|
||||
}
|
||||
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
||||
response.data = parsed;
|
||||
throw Object.assign(new Error(parsed.msg ?? 'Request failed'), {
|
||||
config: response.config,
|
||||
response,
|
||||
data: parsed,
|
||||
isAxiosError: true,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
|
||||
@@ -41,8 +41,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm: handleOk,
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading(通过 handleReady)
|
||||
modalLoading(true);
|
||||
// 只有存在可加载图片时才显示 loading,避免空图或异常链接导致一直 loading
|
||||
modalLoading(!!src.value);
|
||||
} else {
|
||||
// 关闭时,清空右侧预览
|
||||
previewSource.value = '';
|
||||
@@ -65,10 +65,14 @@ function handleBeforeUpload(file: File) {
|
||||
reader.readAsDataURL(file);
|
||||
src.value = '';
|
||||
previewSource.value = '';
|
||||
modalLoading(true);
|
||||
reader.addEventListener('load', (e) => {
|
||||
src.value = (e.target?.result as string) ?? '';
|
||||
filename = file.name;
|
||||
});
|
||||
reader.addEventListener('error', () => {
|
||||
modalLoading(false);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,6 +86,10 @@ function handleReady(cropperInstance: CropperType) {
|
||||
modalLoading(false);
|
||||
}
|
||||
|
||||
function handleCropperError() {
|
||||
modalLoading(false);
|
||||
}
|
||||
|
||||
function handlerToolbar(event: string, arg?: number) {
|
||||
if (event === 'scaleX') {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||
@@ -133,6 +141,7 @@ async function handleOk() {
|
||||
:src="src"
|
||||
height="300px"
|
||||
@cropend="handleCropend"
|
||||
@cropend-error="handleCropperError"
|
||||
@ready="handleReady"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -143,6 +143,10 @@ function getRoundedCanvas() {
|
||||
context.fill();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function handleImageError() {
|
||||
emit('cropendError');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -154,6 +158,7 @@ function getRoundedCanvas() {
|
||||
:crossorigin="crossorigin"
|
||||
:src="src"
|
||||
:style="getImageStyle"
|
||||
@error="handleImageError"
|
||||
class="h-auto max-w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function useDescription(options?: Partial<DescriptionProps>) {
|
||||
inheritAttrs: false,
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => {
|
||||
// @ts-ignore - 避免类型实例化过深
|
||||
// @ts-expect-error - 避免类型实例化过深
|
||||
return h(Description, { ...propsState, ...attrs }, slots);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<!-- 省市区选择器 (Ant Design Vue 版本) -->
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { AreaLevelEnum } from '@vben/constants';
|
||||
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
|
||||
defineOptions({ name: 'AreaSelect' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: undefined,
|
||||
value: undefined,
|
||||
level: AreaLevelEnum.DISTRICT,
|
||||
disabled: false,
|
||||
placeholder: '请选择省市区',
|
||||
clearable: true,
|
||||
showAllLevels: true,
|
||||
separator: '/',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number[] | string[] | undefined): void;
|
||||
(e: 'update:value', value: number[] | string[] | undefined): void;
|
||||
}>();
|
||||
|
||||
// 地区数据接口
|
||||
interface AreaVO {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
parentId?: number;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
children?: AreaVO[];
|
||||
}
|
||||
|
||||
// 接受父组件参数
|
||||
interface Props {
|
||||
modelValue?: number[] | string[];
|
||||
value?: number[] | string[];
|
||||
level?: (typeof AreaLevelEnum)[keyof typeof AreaLevelEnum];
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
clearable?: boolean;
|
||||
showAllLevels?: boolean;
|
||||
separator?: string;
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
formCreateInject?: any;
|
||||
}
|
||||
|
||||
// Ant Design Vue Cascader 的 fieldNames 配置
|
||||
const fieldNames = {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
// 地区树形数据
|
||||
const areaTree = ref<AreaVO[]>([]);
|
||||
// 当前选中值
|
||||
const selectedValue = ref<number[] | undefined>();
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 加载地区树形数据
|
||||
async function loadAreaTree(): Promise<void> {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = await getAreaTree();
|
||||
|
||||
// 根据 level 限制层级
|
||||
areaTree.value = filterTreeByLevel((data || []) as AreaVO[], props.level);
|
||||
} catch (error) {
|
||||
console.warn('[AreaSelect] 加载地区数据失败:', error);
|
||||
areaTree.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据层级过滤树形数据
|
||||
function filterTreeByLevel(tree: AreaVO[], maxLevel: number): AreaVO[] {
|
||||
if (maxLevel <= 0) return [];
|
||||
|
||||
return tree.map((node) => {
|
||||
const newNode = { ...node };
|
||||
|
||||
// 如果当前是最后一层,移除 children
|
||||
if (maxLevel === 1) {
|
||||
delete newNode.children;
|
||||
} else if (node.children && node.children.length > 0) {
|
||||
// 递归处理子节点
|
||||
newNode.children = filterTreeByLevel(node.children, maxLevel - 1);
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
|
||||
// 处理选中值变化
|
||||
function handleChange(value: any): void {
|
||||
if (value === undefined || value === null) {
|
||||
emit('update:modelValue', undefined);
|
||||
emit('update:value', undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update:modelValue', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
// 同步 modelValue 或 value 到内部选中值
|
||||
function syncSelectedValue(): void {
|
||||
const newValue = props.modelValue || props.value;
|
||||
|
||||
if (newValue === undefined || newValue === null) {
|
||||
selectedValue.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保是数组格式
|
||||
selectedValue.value = Array.isArray(newValue)
|
||||
? (newValue as number[])
|
||||
: [newValue as number];
|
||||
}
|
||||
|
||||
// 监听 modelValue 和 value 变化
|
||||
watch(() => props.modelValue || props.value, syncSelectedValue, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(async () => {
|
||||
await loadAreaTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Cascader
|
||||
v-model:value="selectedValue"
|
||||
class="w-full"
|
||||
:options="areaTree"
|
||||
:field-names="fieldNames"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
:allow-clear="clearable"
|
||||
:show-search="true"
|
||||
:change-on-select="true"
|
||||
:loading="loading"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
@@ -39,15 +39,16 @@ interface DeptVO {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// TODO @puhui999:linter 报错;
|
||||
/** 接受父组件参数 */
|
||||
interface Props {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
modelValue?: number | number[] | string | string[];
|
||||
multiple?: boolean;
|
||||
returnType?: 'id' | 'name';
|
||||
defaultCurrentDept?: boolean;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
formCreateInject?: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<!-- 网页 iframe 组件 (Ant Design Vue 版本) -->
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { isUrl } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'IframeComponent' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
value: '',
|
||||
url: '',
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
frameborder: '0',
|
||||
allowfullscreen: true,
|
||||
loading: 'lazy',
|
||||
sandbox: '',
|
||||
});
|
||||
|
||||
// 接受父组件参数
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
frameborder?: string;
|
||||
allowfullscreen?: boolean;
|
||||
loading?: 'eager' | 'lazy';
|
||||
sandbox?: string;
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
formCreateInject?: any;
|
||||
}
|
||||
|
||||
// 显示的 URL(优先使用 url prop,其次使用 value 或 modelValue)
|
||||
const displayUrl = computed(
|
||||
() => props.url || props.value || props.modelValue || '',
|
||||
);
|
||||
|
||||
// 是否显示预览
|
||||
const showPreview = computed(() => {
|
||||
return displayUrl.value && isUrl(displayUrl.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="iframe-component">
|
||||
<!-- iframe 预览 -->
|
||||
<div v-if="showPreview" class="iframe-preview">
|
||||
<iframe
|
||||
:src="displayUrl"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:frameborder="frameborder"
|
||||
:allowfullscreen="allowfullscreen"
|
||||
:loading="loading"
|
||||
:sandbox="sandbox || undefined"
|
||||
class="iframe-content"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 无 URL 或无效 URL 提示 -->
|
||||
<div v-else class="iframe-placeholder">
|
||||
<a-empty description="请在右侧属性面板配置 URL 地址" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.iframe-component {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.iframe-preview {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.iframe-content {
|
||||
display: block;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.iframe-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -193,7 +193,8 @@ export function useApiSelect(option: ApiSelectProps) {
|
||||
let parse: any = null;
|
||||
if (props.parseFunc) {
|
||||
// 解析字符串函数
|
||||
// eslint-disable-next-line no-new-func
|
||||
// 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;
|
||||
|
||||
@@ -6,18 +6,36 @@ export function useImagesUpload() {
|
||||
return defineComponent({
|
||||
name: 'ImagesUpload',
|
||||
props: {
|
||||
multiple: {
|
||||
accept: {
|
||||
type: Array,
|
||||
default: () => ['image/jpeg', 'image/png', 'image/gif'],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
|
||||
<ImageUpload
|
||||
accept={props.accept as string[]}
|
||||
disabled={props.disabled}
|
||||
maxNumber={props.maxNumber}
|
||||
maxSize={props.maxSize}
|
||||
multiple={props.multiple}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,8 +11,10 @@ import formCreate from '@form-create/ant-design-vue';
|
||||
import { apiSelectRule } from '#/components/form-create/rules/data';
|
||||
|
||||
import {
|
||||
useAreaSelectRule,
|
||||
useDictSelectRule,
|
||||
useEditorRule,
|
||||
useIframeRule,
|
||||
useSelectRule,
|
||||
useUploadFileRule,
|
||||
useUploadImageRule,
|
||||
@@ -160,6 +162,8 @@ export async function useFormCreateDesigner(designer: Ref) {
|
||||
const uploadFileRule = useUploadFileRule();
|
||||
const uploadImageRule = useUploadImageRule();
|
||||
const uploadImagesRule = useUploadImagesRule();
|
||||
const iframeRule = useIframeRule();
|
||||
const areaSelectRule = useAreaSelectRule();
|
||||
|
||||
/** 构建表单组件 */
|
||||
function buildFormComponents() {
|
||||
@@ -172,6 +176,8 @@ export async function useFormCreateDesigner(designer: Ref) {
|
||||
uploadFileRule,
|
||||
uploadImageRule,
|
||||
uploadImagesRule,
|
||||
iframeRule,
|
||||
areaSelectRule,
|
||||
];
|
||||
components.forEach((component) => {
|
||||
// 插入组件规则
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
const selectRule = [
|
||||
{
|
||||
type: 'select',
|
||||
@@ -134,7 +133,7 @@ const apiSelectRule = [
|
||||
type: 'input',
|
||||
field: 'labelField',
|
||||
title: 'label 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
info: `可以使用 el 表达式:\${属性},来实现复杂数据组合。如:\${nickname}-\${id}`,
|
||||
props: {
|
||||
placeholder: 'nickname',
|
||||
},
|
||||
@@ -143,7 +142,7 @@ const apiSelectRule = [
|
||||
type: 'input',
|
||||
field: 'valueField',
|
||||
title: 'value 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
info: `可以使用 el 表达式:\${属性},来实现复杂数据组合。如:\${nickname}-\${id}`,
|
||||
props: {
|
||||
placeholder: 'id',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function useUploadFileRule() {
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '文件类型',
|
||||
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
|
||||
options: [
|
||||
@@ -40,12 +40,6 @@ export function useUploadFileRule() {
|
||||
mode: 'multiple',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'autoUpload',
|
||||
title: '是否在选取文件后立即进行上传',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
@@ -54,23 +48,23 @@ export function useUploadFileRule() {
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'isShowTip',
|
||||
field: 'showDescription',
|
||||
title: '是否显示提示',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
field: 'maxNumber',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
props: { min: 1 },
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
|
||||
@@ -24,15 +24,9 @@ export function useUploadImageRule() {
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
@@ -52,40 +46,16 @@ export function useUploadImageRule() {
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否显示删除按钮',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'showBtnText',
|
||||
title: '是否显示按钮文字',
|
||||
value: true,
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -24,15 +24,9 @@ export function useUploadImagesRule() {
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
@@ -48,40 +42,27 @@ export function useUploadImagesRule() {
|
||||
],
|
||||
props: {
|
||||
mode: 'multiple',
|
||||
maxNumber: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
field: 'maxNumber',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
props: { min: 1 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ async function handlePreview(file: UploadFile) {
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
// oxlint-disable-next-line no-unused-expressions
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
@@ -350,6 +351,8 @@ function getValue() {
|
||||
|
||||
<style>
|
||||
.ant-upload-select-picture-card {
|
||||
@apply flex items-center justify-center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
import { overridesPreferences, preferencesExtension } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
@@ -15,6 +15,7 @@ async function initApplication() {
|
||||
|
||||
// app偏好设置初始化
|
||||
await initPreferences({
|
||||
extension: preferencesExtension,
|
||||
namespace,
|
||||
overrides: overridesPreferences,
|
||||
});
|
||||
|
||||
@@ -34,8 +34,10 @@ import {
|
||||
|
||||
// ======================= 自定义组件 =======================
|
||||
import { useApiSelect } from '#/components/form-create';
|
||||
import AreaSelect from '#/components/form-create/components/area-select.vue';
|
||||
import DeptSelect from '#/components/form-create/components/dept-select.vue';
|
||||
import DictSelect from '#/components/form-create/components/dict-select.vue';
|
||||
import IframeComponent from '#/components/form-create/components/iframe.vue';
|
||||
import { useImagesUpload } from '#/components/form-create/components/use-images-upload';
|
||||
import { Tinymce } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
@@ -84,6 +86,8 @@ const components = [
|
||||
Tinymce,
|
||||
ImageUpload,
|
||||
FileUpload,
|
||||
IframeComponent,
|
||||
AreaSelect,
|
||||
];
|
||||
|
||||
// 参考 https://www.form-create.com/v3/ant-design-vue/auto-import 文档
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
import {
|
||||
defineOverridesPreferences,
|
||||
definePreferencesExtension,
|
||||
} from '@vben/preferences';
|
||||
|
||||
interface WebAntdPreferencesExtension {
|
||||
defaultTableSize: number;
|
||||
enableFormFullscreen: boolean;
|
||||
reportTitle: string;
|
||||
tenantMode: 'multi' | 'single';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
@@ -23,3 +33,52 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||
companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
|
||||
},
|
||||
});
|
||||
|
||||
export const preferencesExtension =
|
||||
definePreferencesExtension<WebAntdPreferencesExtension>({
|
||||
tabLabel: 'preferences.antd.tabLabel',
|
||||
title: 'preferences.antd.title',
|
||||
fields: [
|
||||
{
|
||||
component: 'switch',
|
||||
defaultValue: true,
|
||||
key: 'enableFormFullscreen',
|
||||
label: 'preferences.antd.fields.enableFormFullscreen.label',
|
||||
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
|
||||
},
|
||||
{
|
||||
component: 'select',
|
||||
defaultValue: 'single',
|
||||
key: 'tenantMode',
|
||||
label: 'preferences.antd.fields.tenantMode.label',
|
||||
options: [
|
||||
{
|
||||
label: 'preferences.antd.fields.tenantMode.options.single.label',
|
||||
value: 'single',
|
||||
},
|
||||
{
|
||||
label: 'preferences.antd.fields.tenantMode.options.multi.label',
|
||||
value: 'multi',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
component: 'number',
|
||||
componentProps: {
|
||||
max: 200,
|
||||
min: 10,
|
||||
step: 10,
|
||||
},
|
||||
defaultValue: 20,
|
||||
key: 'defaultTableSize',
|
||||
label: 'preferences.antd.fields.defaultTableSize.label',
|
||||
},
|
||||
{
|
||||
component: 'input',
|
||||
defaultValue: '',
|
||||
key: 'reportTitle',
|
||||
label: 'preferences.antd.fields.reportTitle.label',
|
||||
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -109,6 +109,21 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
/**
|
||||
* 用于 bpm 移动端流程表单 web-view 的嵌入
|
||||
*/
|
||||
{
|
||||
component: () => import('#/views/bpm/form/mobile/index.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
ignoreAccess: true,
|
||||
title: '移动端流程表单展示',
|
||||
},
|
||||
name: 'BpmMobileFormPreview',
|
||||
path: '/bpm/mobile/form-preview',
|
||||
},
|
||||
];
|
||||
|
||||
export { coreRoutes, fallbackNotFoundRoute };
|
||||
|
||||
@@ -83,6 +83,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
// oxlint-disable-next-line no-unused-expressions
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(
|
||||
@@ -132,6 +133,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
async function fetchUserInfo() {
|
||||
// 加载
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
let authPermissionInfo: AuthPermissionInfo | null = null;
|
||||
authPermissionInfo = await getAuthPermissionInfoApi();
|
||||
// userStore
|
||||
|
||||
@@ -3,6 +3,9 @@ import type { Recordable } from '@vben/types';
|
||||
export * from './rangePickerProps';
|
||||
export * from './routerHelper';
|
||||
|
||||
// 从共享包导出 URL 工具函数
|
||||
export { isUrl } from '@vben/utils';
|
||||
|
||||
/**
|
||||
* 查找数组对象的某个下标
|
||||
* @param {Array} ary 查找的数组
|
||||
|
||||
@@ -571,6 +571,7 @@ onMounted(async () => {
|
||||
size="small"
|
||||
@click="openChatConversationUpdateForm"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="activeConversation?.modelName"></span>
|
||||
<IconifyIcon icon="lucide:settings" class="ml-2 size-4" />
|
||||
</Button>
|
||||
|
||||
@@ -21,6 +21,7 @@ const imageListRef = ref<any>(); // image 列表 ref
|
||||
const dall3Ref = ref<any>(); // dall3(openai) ref
|
||||
const midjourneyRef = ref<any>(); // midjourney ref
|
||||
const stableDiffusionRef = ref<any>(); // stable diffusion ref
|
||||
// @ts-expect-error: template ref is retained for future provider expansion
|
||||
const commonRef = ref<any>(); // stable diffusion ref
|
||||
|
||||
const selectPlatform = ref('common'); // 选中的平台
|
||||
@@ -45,7 +46,9 @@ const platformOptions = [
|
||||
const models = ref<AiModelModelApi.Model[]>([]); // 模型列表
|
||||
|
||||
/** 绘画 start */
|
||||
async function handleDrawStart() {}
|
||||
function handleDrawStart() {
|
||||
// drawing state is handled by child components
|
||||
}
|
||||
|
||||
/** 绘画 complete */
|
||||
async function handleDrawComplete() {
|
||||
|
||||
@@ -150,10 +150,12 @@ defineExpose({
|
||||
ref="mdContainerRef"
|
||||
class="wh-full overflow-y-auto"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="flex flex-col items-center justify-center"
|
||||
v-html="html"
|
||||
></div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
<div ref="mindMapRef" class="wh-full">
|
||||
<svg
|
||||
|
||||
@@ -20,6 +20,7 @@ const currentSong = inject<any>('currentSong', {});
|
||||
{{ currentSong.date }}
|
||||
</div>
|
||||
<Button size="small" shape="round" class="my-2">信息复用</Button>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="text-xs" v-html="currentSong.lyric"></div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -106,7 +106,9 @@ async function goRun() {
|
||||
try {
|
||||
convertedParams[paramKey] = convertParamValue(value, dataType);
|
||||
} catch (error: any) {
|
||||
throw new Error(`参数 ${paramKey} 转换失败: ${error.message}`);
|
||||
throw new Error(`参数 ${paramKey} 转换失败: ${error.message}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ function convertParamValue(value: string, dataType: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error: any) {
|
||||
throw new Error(`JSON格式错误: ${error.message}`);
|
||||
throw new Error(`JSON格式错误: ${error.message}`, { cause: error });
|
||||
}
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
|
||||
import { Button, ButtonGroup, message, Modal, Tooltip } from 'ant-design-vue';
|
||||
// 模拟流转流程
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: token simulation package does not ship compatible types
|
||||
import tokenSimulation from 'bpmn-js-token-simulation';
|
||||
import BpmnModeler from 'bpmn-js/lib/Modeler';
|
||||
// 代码高亮插件
|
||||
@@ -132,6 +132,7 @@ const emit = defineEmits([
|
||||
'element-click',
|
||||
]);
|
||||
|
||||
// @ts-expect-error: file input ref is set imperatively by the template
|
||||
const bpmnCanvas = ref();
|
||||
const refFile = ref();
|
||||
|
||||
@@ -178,6 +179,7 @@ const additionalModules = computed(() => {
|
||||
) {
|
||||
Modules.push(...(props.additionalModel as any[]));
|
||||
} else {
|
||||
// oxlint-disable-next-line no-unused-expressions
|
||||
props.additionalModel && Modules.push(props.additionalModel);
|
||||
}
|
||||
|
||||
@@ -417,6 +419,7 @@ const processSimulation = () => {
|
||||
// bpmnModeler.get('toggleMode', 'strict'),
|
||||
// "bpmnModeler.get('toggleMode')",
|
||||
// );
|
||||
// oxlint-disable-next-line no-unused-expressions
|
||||
props.simulation && bpmnModeler.get('toggleMode', 'strict').toggleMode();
|
||||
};
|
||||
const processRedo = () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
|
||||
/**
|
||||
* A provider for BPMN 2.0 elements context pad
|
||||
*/
|
||||
export default function ContextPadProvider(
|
||||
function ContextPadProvider(
|
||||
config,
|
||||
injector,
|
||||
eventBus,
|
||||
@@ -57,6 +57,8 @@ export default function ContextPadProvider(
|
||||
});
|
||||
}
|
||||
|
||||
export default ContextPadProvider;
|
||||
|
||||
ContextPadProvider.$inject = [
|
||||
'config.contextPad',
|
||||
'injector',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
|
||||
|
||||
export default function CustomPalette(
|
||||
function CustomPalette(
|
||||
palette,
|
||||
create,
|
||||
elementFactory,
|
||||
@@ -24,11 +24,21 @@ export default function CustomPalette(
|
||||
);
|
||||
}
|
||||
|
||||
const F = function () {}; // 核心,利用空对象作为中介;
|
||||
F.prototype = PaletteProvider.prototype; // 核心,将父类的原型赋值给空对象F;
|
||||
CustomPalette.$inject = [
|
||||
'palette',
|
||||
'create',
|
||||
'elementFactory',
|
||||
'spaceTool',
|
||||
'lassoTool',
|
||||
'handTool',
|
||||
'globalConnect',
|
||||
'translate',
|
||||
];
|
||||
|
||||
// 利用中介函数重写原型链方法
|
||||
F.prototype.getPaletteEntries = function () {
|
||||
CustomPalette.prototype = Object.create(PaletteProvider.prototype);
|
||||
CustomPalette.prototype.constructor = CustomPalette;
|
||||
|
||||
CustomPalette.prototype.getPaletteEntries = function () {
|
||||
const actions = {};
|
||||
const create = this._create;
|
||||
const elementFactory = this._elementFactory;
|
||||
@@ -94,8 +104,7 @@ F.prototype.getPaletteEntries = function () {
|
||||
'hand-tool': {
|
||||
group: 'tools',
|
||||
className: 'bpmn-icon-hand-tool',
|
||||
title: '激活抓手工具',
|
||||
// title: translate("Activate the hand tool"),
|
||||
title: translate('Activate the hand tool'),
|
||||
action: {
|
||||
click(event) {
|
||||
handTool.activateHand(event);
|
||||
@@ -219,16 +228,4 @@ F.prototype.getPaletteEntries = function () {
|
||||
return actions;
|
||||
};
|
||||
|
||||
CustomPalette.$inject = [
|
||||
'palette',
|
||||
'create',
|
||||
'elementFactory',
|
||||
'spaceTool',
|
||||
'lassoTool',
|
||||
'handTool',
|
||||
'globalConnect',
|
||||
'translate',
|
||||
];
|
||||
|
||||
CustomPalette.prototype = new F(); // 核心,将 F的实例赋值给子类;
|
||||
CustomPalette.prototype.constructor = CustomPalette; // 修复子类CustomPalette的构造器指向,防止原型链的混乱;
|
||||
export default CustomPalette;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* A palette provider for BPMN 2.0 elements.
|
||||
*/
|
||||
export default function PaletteProvider(
|
||||
function PaletteProvider(
|
||||
palette,
|
||||
create,
|
||||
elementFactory,
|
||||
@@ -23,6 +23,8 @@ export default function PaletteProvider(
|
||||
palette.registerProvider(this);
|
||||
}
|
||||
|
||||
export default PaletteProvider;
|
||||
|
||||
PaletteProvider.$inject = [
|
||||
'palette',
|
||||
'create',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
/**
|
||||
* This is a sample file that should be replaced with the actual translation.
|
||||
*
|
||||
@@ -238,10 +237,8 @@ export default {
|
||||
'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',
|
||||
[`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': '候选人起动器组',
|
||||
|
||||
@@ -39,7 +39,7 @@ watch(
|
||||
val +=
|
||||
props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || '';
|
||||
}
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: async component registry is indexed dynamically
|
||||
customConfigComponent.value = (
|
||||
CustomConfigMap as Record<string, { component: Component }>
|
||||
)[val]?.component;
|
||||
|
||||
@@ -175,7 +175,7 @@ const resetCustomConfigList = () => {
|
||||
approveType.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:ApproveType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
|
||||
value: ApproveType.USER,
|
||||
});
|
||||
@@ -184,7 +184,7 @@ const resetCustomConfigList = () => {
|
||||
assignStartUserHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
|
||||
value: 1,
|
||||
});
|
||||
@@ -194,13 +194,13 @@ const resetCustomConfigList = () => {
|
||||
rejectHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
|
||||
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
|
||||
returnNodeIdEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
|
||||
value: '',
|
||||
});
|
||||
@@ -210,7 +210,7 @@ const resetCustomConfigList = () => {
|
||||
assignEmptyHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
|
||||
value: 1,
|
||||
});
|
||||
@@ -218,7 +218,7 @@ const resetCustomConfigList = () => {
|
||||
assignEmptyUserIdsEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
|
||||
value: '',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<!-- eslint-disable no-unused-vars -->
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||
|
||||
@@ -66,13 +65,13 @@ const bpmnElement = ref<any>(null);
|
||||
const multiLoopInstance = ref<any>(null);
|
||||
declare global {
|
||||
interface Window {
|
||||
// @ts-ignore
|
||||
bpmnInstances?: () => any;
|
||||
}
|
||||
}
|
||||
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
// @ts-expect-error: retained for legacy multi-instance mode compatibility
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const getElementLoop = (businessObject: any): void => {
|
||||
if (!businessObject.loopCharacteristics) {
|
||||
@@ -141,8 +140,7 @@ const changeLoopCharacteristicsType = (type: any): void => {
|
||||
isSequential: true,
|
||||
})
|
||||
: bpmnInstances().moddle.create('bpmn:MultiInstanceLoopCharacteristics', {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
collection: '${coll_userList}',
|
||||
collection: `\${coll_userList}`,
|
||||
});
|
||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||
loopCharacteristics: toRaw(multiLoopInstance.value),
|
||||
@@ -233,7 +231,7 @@ const updateLoopAsync = (key: any): void => {
|
||||
extensionElements: null,
|
||||
};
|
||||
} else {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: dynamic async flags are assigned by runtime key
|
||||
asyncAttr[key] = loopInstanceForm.value[key];
|
||||
}
|
||||
bpmnInstances().modeling.updateModdleProperties(
|
||||
@@ -247,23 +245,23 @@ const changeConfig = (config: string): void => {
|
||||
switch (config) {
|
||||
case '会签': {
|
||||
changeLoopCharacteristicsType('ParallelMultiInstance');
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
|
||||
|
||||
updateLoopCondition(`\${ nrOfCompletedInstances >= nrOfInstances }`);
|
||||
|
||||
break;
|
||||
}
|
||||
case '依次审批': {
|
||||
changeLoopCharacteristicsType('SequentialMultiInstance');
|
||||
updateLoopCardinality('1');
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
|
||||
|
||||
updateLoopCondition(`\${ nrOfCompletedInstances >= nrOfInstances }`);
|
||||
|
||||
break;
|
||||
}
|
||||
case '或签': {
|
||||
changeLoopCharacteristicsType('ParallelMultiInstance');
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
updateLoopCondition('${ nrOfCompletedInstances > 0 }');
|
||||
|
||||
updateLoopCondition(`\${ nrOfCompletedInstances > 0 }`);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -331,8 +329,8 @@ const updateLoopCharacteristics = (): void => {
|
||||
if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
|
||||
multiLoopInstance.value = bpmnInstances().moddle.create(
|
||||
'bpmn:MultiInstanceLoopCharacteristics',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
{ isSequential: false, collection: '${coll_userList}' },
|
||||
|
||||
{ isSequential: false, collection: `\${coll_userList}` },
|
||||
);
|
||||
multiLoopInstance.value.completionCondition =
|
||||
bpmnInstances().moddle.create('bpmn:FormalExpression', {
|
||||
@@ -344,20 +342,19 @@ const updateLoopCharacteristics = (): void => {
|
||||
if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
|
||||
multiLoopInstance.value = bpmnInstances().moddle.create(
|
||||
'bpmn:MultiInstanceLoopCharacteristics',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
{ isSequential: false, collection: '${coll_userList}' },
|
||||
|
||||
{ isSequential: false, collection: `\${coll_userList}` },
|
||||
);
|
||||
multiLoopInstance.value.completionCondition =
|
||||
bpmnInstances().moddle.create('bpmn:FormalExpression', {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
body: '${ nrOfCompletedInstances > 0 }',
|
||||
body: `\${ nrOfCompletedInstances > 0 }`,
|
||||
});
|
||||
}
|
||||
if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
|
||||
multiLoopInstance.value = bpmnInstances().moddle.create(
|
||||
'bpmn:MultiInstanceLoopCharacteristics',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
{ isSequential: true, collection: '${coll_userList}' },
|
||||
|
||||
{ isSequential: true, collection: `\${coll_userList}` },
|
||||
);
|
||||
multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
|
||||
'bpmn:FormalExpression',
|
||||
@@ -367,8 +364,7 @@ const updateLoopCharacteristics = (): void => {
|
||||
);
|
||||
multiLoopInstance.value.completionCondition =
|
||||
bpmnInstances().moddle.create('bpmn:FormalExpression', {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
body: '${ nrOfCompletedInstances >= nrOfInstances }',
|
||||
body: `\${ nrOfCompletedInstances >= nrOfInstances }`,
|
||||
});
|
||||
}
|
||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||
|
||||
@@ -53,7 +53,7 @@ watch(
|
||||
() => props.type,
|
||||
() => {
|
||||
if (props.type) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: installed task component map is indexed dynamically
|
||||
witchTaskComponent.value = installedComponent[props.type].component;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -65,7 +65,7 @@ const initCallActivity = () => {
|
||||
|
||||
// 初始化所有配置项
|
||||
Object.keys(formData.value).forEach((key: string) => {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: form state is updated through dynamic schema keys
|
||||
formData.value[key] =
|
||||
bpmnElement.value.businessObject[key] ??
|
||||
formData.value[key as keyof FormData];
|
||||
@@ -183,6 +183,7 @@ const updateElementExtensions = () => {
|
||||
watch(
|
||||
() => props.id,
|
||||
(val) => {
|
||||
// oxlint-disable-next-line no-unused-expressions
|
||||
val &&
|
||||
val.length > 0 &&
|
||||
nextTick(() => {
|
||||
|
||||
@@ -82,7 +82,6 @@ onMounted(() => {
|
||||
bpmnRootElements.value
|
||||
.filter((el: any) => el.$type === 'bpmn:Message')
|
||||
.forEach((m: any) => {
|
||||
// @ts-ignore
|
||||
if (bpmnMessageRefsMap.value) {
|
||||
bpmnMessageRefsMap.value[m.id] = m;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetTaskForm = () => {
|
||||
for (const key in defaultTaskForm.value) {
|
||||
// @ts-ignore
|
||||
scriptTaskForm.value[key] =
|
||||
bpmnElement.value?.businessObject[
|
||||
key as keyof typeof defaultTaskForm.value
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable unicorn/no-nested-ternary -->
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
@@ -206,9 +207,9 @@ const updateHttpExtensions = (force = false) => {
|
||||
|
||||
const persisted = HTTP_BOOLEAN_FIELDS.has(name)
|
||||
? String(!!rawValue)
|
||||
: (rawValue === undefined
|
||||
: rawValue === undefined
|
||||
? ''
|
||||
: rawValue.toString());
|
||||
: rawValue.toString();
|
||||
|
||||
desiredEntries.push([name, persisted]);
|
||||
});
|
||||
|
||||
@@ -70,6 +70,7 @@ const deptTreeOptions = ref<any>(); // 部门树
|
||||
const postOptions = ref<SystemPostApi.Post[]>([]); // 岗位列表
|
||||
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
|
||||
const userGroupOptions = ref<BpmUserGroupApi.UserGroup[]>([]); // 用户组列表
|
||||
// @ts-expect-error: tree ref instance type is provided by the UI library at runtime
|
||||
const treeRef = ref<any>();
|
||||
|
||||
const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ);
|
||||
@@ -128,7 +129,7 @@ const resetTaskForm = () => {
|
||||
// eslint-disable-next-line unicorn/prefer-switch
|
||||
if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
|
||||
// 特殊:流程表达式,只有一个 input 输入框
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: expression strategy stores a scalar in an array-shaped field
|
||||
userTaskForm.value.candidateParam = [candidateParamStr];
|
||||
} else if (
|
||||
userTaskForm.value.candidateStrategy ===
|
||||
@@ -152,7 +153,7 @@ const resetTaskForm = () => {
|
||||
userTaskForm.value.candidateStrategy ===
|
||||
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
|
||||
) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: dynamic candidate param shape varies by strategy
|
||||
userTaskForm.value.candidateParam = +candidateParamStr;
|
||||
deptLevel.value = +candidateParamStr;
|
||||
} else if (
|
||||
@@ -303,7 +304,7 @@ const openProcessExpressionDialog = async () => {
|
||||
const selectProcessExpression = (
|
||||
expression: BpmProcessExpressionApi.ProcessExpression,
|
||||
) => {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: modal helper exposes runtime methods outside static typing
|
||||
userTaskForm.value.candidateParam = [expression.expression];
|
||||
updateElementTask();
|
||||
};
|
||||
@@ -311,7 +312,7 @@ const selectProcessExpression = (
|
||||
const handleFormUserChange = (e: any) => {
|
||||
if (e === 'PROCESS_START_USER_ID') {
|
||||
userTaskForm.value.candidateParam = [];
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: modal helper exposes runtime methods outside static typing
|
||||
userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER;
|
||||
}
|
||||
updateElementTask();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer';
|
||||
|
||||
export default function CustomRenderer(
|
||||
function CustomRenderer(
|
||||
config,
|
||||
eventBus,
|
||||
styles,
|
||||
@@ -19,12 +19,10 @@ export default function CustomRenderer(
|
||||
2000,
|
||||
);
|
||||
|
||||
this.handlers.label = function () {
|
||||
return null;
|
||||
};
|
||||
this.handlers.label = () => null;
|
||||
}
|
||||
|
||||
const F = function () {}; // 核心,利用空对象作为中介;
|
||||
F.prototype = BpmnRenderer.prototype; // 核心,将父类的原型赋值给空对象F;
|
||||
CustomRenderer.prototype = new F(); // 核心,将 F的实例赋值给子类;
|
||||
CustomRenderer.prototype.constructor = CustomRenderer; // 修复子类CustomRenderer的构造器指向,防止原型链的混乱;
|
||||
CustomRenderer.prototype = Object.create(BpmnRenderer.prototype);
|
||||
CustomRenderer.prototype.constructor = CustomRenderer;
|
||||
|
||||
export default CustomRenderer;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
|
||||
// eslint-disable-next-line n/no-extraneous-import
|
||||
import inherits from 'inherits';
|
||||
|
||||
export default function CustomRules(eventBus) {
|
||||
function CustomRules(eventBus) {
|
||||
BpmnRules.call(this, eventBus);
|
||||
}
|
||||
|
||||
@@ -14,3 +15,5 @@ CustomRules.prototype.canDrop = function () {
|
||||
CustomRules.prototype.canMove = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
export default CustomRules;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
function xmlStr2XmlObj(xmlStr) {
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
let xmlObj = {};
|
||||
if (document.all) {
|
||||
const xmlDom = new window.ActiveXObject('Microsoft.XMLDOM');
|
||||
|
||||
@@ -71,6 +71,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
// 当前节点
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
|
||||
useNodeName(BpmNodeTypeEnum.TRIGGER_NODE);
|
||||
// 触发器表单配置
|
||||
|
||||
@@ -25,12 +25,13 @@ const emits = defineEmits<{
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
|
||||
/** 监控节点的变化 */
|
||||
const currentNode = useWatchNode(props);
|
||||
|
||||
/** 节点名称编辑 */
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.CHILD_PROCESS_NODE,
|
||||
|
||||
@@ -27,10 +27,11 @@ const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.COPY_TASK_NODE,
|
||||
|
||||
@@ -25,10 +25,11 @@ const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.DELAY_TIMER_NODE,
|
||||
|
||||
@@ -20,7 +20,7 @@ const props = defineProps({
|
||||
// 监控节点变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
const processInstance = inject<Ref<any>>('processInstance', ref({}));
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
@@ -41,7 +41,7 @@ const emits = defineEmits<{
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
const currentNode = ref<SimpleFlowNode>(props.flowNode);
|
||||
|
||||
watch(
|
||||
|
||||
@@ -46,7 +46,7 @@ const emits = defineEmits<{
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
|
||||
const currentNode = ref<SimpleFlowNode>(props.flowNode);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const props = defineProps({
|
||||
|
||||
const emits = defineEmits(['update:childNode']);
|
||||
const popoverShow = ref(false);
|
||||
const readonly = inject<Boolean>('readonly'); // 是否只读
|
||||
const readonly = inject<boolean>('readonly'); // 是否只读
|
||||
|
||||
function addNode(type: number) {
|
||||
// 校验:条件分支、包容分支后面,不允许直接添加并行分支
|
||||
|
||||
@@ -36,7 +36,7 @@ const emits = defineEmits<{
|
||||
|
||||
const currentNode = ref<SimpleFlowNode>(props.flowNode);
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
|
||||
watch(
|
||||
() => props.flowNode,
|
||||
|
||||
@@ -28,10 +28,11 @@ const emits = defineEmits<{
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.ROUTER_BRANCH_NODE,
|
||||
|
||||
@@ -32,11 +32,12 @@ defineEmits<{
|
||||
'update:modelValue': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
const readonly = inject<Boolean>('readonly'); // 是否只读
|
||||
const readonly = inject<boolean>('readonly'); // 是否只读
|
||||
const tasks = inject<Ref<any[]>>('tasks', ref([]));
|
||||
// 监控节点变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.START_USER_NODE,
|
||||
|
||||
@@ -30,7 +30,7 @@ const emits = defineEmits<{
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
// 监控节点的变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
|
||||
@@ -32,11 +32,12 @@ const emits = defineEmits<{
|
||||
}>();
|
||||
|
||||
// 是否只读
|
||||
const readonly = inject<Boolean>('readonly');
|
||||
const readonly = inject<boolean>('readonly');
|
||||
const tasks = inject<Ref<any[]>>('tasks', ref([]));
|
||||
// 监控节点变化
|
||||
const currentNode = useWatchNode(props);
|
||||
// 节点名称编辑
|
||||
// @ts-expect-error: composable typing does not preserve this node schema exactly
|
||||
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
|
||||
currentNode,
|
||||
BpmNodeTypeEnum.USER_TASK_NODE,
|
||||
|
||||
@@ -7,6 +7,10 @@ interface DictDataType {
|
||||
|
||||
// 用户任务的审批类型。 【参考飞书】
|
||||
export enum ApproveType {
|
||||
/**
|
||||
* 人工审批
|
||||
*/
|
||||
USER = 1,
|
||||
/**
|
||||
* 自动通过
|
||||
*/
|
||||
@@ -15,18 +19,14 @@ export enum ApproveType {
|
||||
* 自动拒绝
|
||||
*/
|
||||
AUTO_REJECT = 3,
|
||||
/**
|
||||
* 人工审批
|
||||
*/
|
||||
USER = 1,
|
||||
}
|
||||
|
||||
// 多人审批方式类型枚举 ( 用于审批节点 )
|
||||
export enum ApproveMethodType {
|
||||
/**
|
||||
* 多人或签(通过只需一人,拒绝只需一人)
|
||||
* 随机挑选一人审批
|
||||
*/
|
||||
ANY_APPROVE = 3,
|
||||
RANDOM_SELECT_ONE_APPROVE = 1,
|
||||
|
||||
/**
|
||||
* 多人会签(按通过比例)
|
||||
@@ -34,9 +34,9 @@ export enum ApproveMethodType {
|
||||
APPROVE_BY_RATIO = 2,
|
||||
|
||||
/**
|
||||
* 随机挑选一人审批
|
||||
* 多人或签(通过只需一人,拒绝只需一人)
|
||||
*/
|
||||
RANDOM_SELECT_ONE_APPROVE = 1,
|
||||
ANY_APPROVE = 3,
|
||||
/**
|
||||
* 多人依次审批
|
||||
*/
|
||||
@@ -70,34 +70,34 @@ export enum ConditionType {
|
||||
|
||||
// 操作按钮类型枚举 (用于审批节点)
|
||||
export enum OperationButtonType {
|
||||
/**
|
||||
* 加签
|
||||
*/
|
||||
ADD_SIGN = 5,
|
||||
/**
|
||||
* 通过
|
||||
*/
|
||||
APPROVE = 1,
|
||||
/**
|
||||
* 抄送
|
||||
*/
|
||||
COPY = 7,
|
||||
/**
|
||||
* 委派
|
||||
*/
|
||||
DELEGATE = 4,
|
||||
/**
|
||||
* 拒绝
|
||||
*/
|
||||
REJECT = 2,
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
TRANSFER = 3,
|
||||
/**
|
||||
* 委派
|
||||
*/
|
||||
DELEGATE = 4,
|
||||
/**
|
||||
* 加签
|
||||
*/
|
||||
ADD_SIGN = 5,
|
||||
/**
|
||||
* 退回
|
||||
*/
|
||||
RETURN = 6,
|
||||
/**
|
||||
* 转办
|
||||
* 抄送
|
||||
*/
|
||||
TRANSFER = 3,
|
||||
COPY = 7,
|
||||
}
|
||||
|
||||
// 审批拒绝类型枚举
|
||||
@@ -114,6 +114,10 @@ export enum RejectHandlerType {
|
||||
|
||||
// 用户任务超时处理类型枚举
|
||||
export enum TimeoutHandlerType {
|
||||
/**
|
||||
* 自动提醒
|
||||
*/
|
||||
REMINDER = 1,
|
||||
/**
|
||||
* 自动同意
|
||||
*/
|
||||
@@ -122,10 +126,6 @@ export enum TimeoutHandlerType {
|
||||
* 自动拒绝
|
||||
*/
|
||||
REJECT = 3,
|
||||
/**
|
||||
* 自动提醒
|
||||
*/
|
||||
REMINDER = 1,
|
||||
}
|
||||
|
||||
// 用户任务的审批人为空时,处理类型枚举
|
||||
@@ -135,49 +135,49 @@ export enum AssignEmptyHandlerType {
|
||||
*/
|
||||
APPROVE = 1,
|
||||
/**
|
||||
* 转交给流程管理员
|
||||
* 自动拒绝
|
||||
*/
|
||||
ASSIGN_ADMIN = 4,
|
||||
REJECT = 2,
|
||||
/**
|
||||
* 指定人员审批
|
||||
*/
|
||||
ASSIGN_USER = 3,
|
||||
/**
|
||||
* 自动拒绝
|
||||
* 转交给流程管理员
|
||||
*/
|
||||
REJECT = 2,
|
||||
ASSIGN_ADMIN = 4,
|
||||
}
|
||||
|
||||
// 用户任务的审批人与发起人相同时,处理类型枚举
|
||||
export enum AssignStartUserHandlerType {
|
||||
/**
|
||||
* 转交给部门负责人审批
|
||||
* 由发起人对自己审批
|
||||
*/
|
||||
ASSIGN_DEPT_LEADER = 3,
|
||||
START_USER_AUDIT = 1,
|
||||
/**
|
||||
* 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
|
||||
*/
|
||||
SKIP = 2,
|
||||
/**
|
||||
* 由发起人对自己审批
|
||||
* 转交给部门负责人审批
|
||||
*/
|
||||
START_USER_AUDIT = 1,
|
||||
ASSIGN_DEPT_LEADER = 3,
|
||||
}
|
||||
|
||||
// 时间单位枚举
|
||||
export enum TimeUnitType {
|
||||
/**
|
||||
* 天
|
||||
* 分钟
|
||||
*/
|
||||
DAY = 3,
|
||||
MINUTE = 1,
|
||||
/**
|
||||
* 小时
|
||||
*/
|
||||
HOUR = 2,
|
||||
/**
|
||||
* 分钟
|
||||
* 天
|
||||
*/
|
||||
MINUTE = 1,
|
||||
DAY = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,14 +202,14 @@ export enum FieldPermissionType {
|
||||
* 延迟类型
|
||||
*/
|
||||
export enum DelayTypeEnum {
|
||||
/**
|
||||
* 固定日期时间
|
||||
*/
|
||||
FIXED_DATE_TIME = 2,
|
||||
/**
|
||||
* 固定时长
|
||||
*/
|
||||
FIXED_TIME_DURATION = 1,
|
||||
/**
|
||||
* 固定日期时间
|
||||
*/
|
||||
FIXED_DATE_TIME = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,35 +217,39 @@ export enum DelayTypeEnum {
|
||||
*/
|
||||
export enum TriggerTypeEnum {
|
||||
/**
|
||||
* 表单数据删除触发器
|
||||
* 发送 HTTP 请求触发器
|
||||
*/
|
||||
FORM_DELETE = 11,
|
||||
/**
|
||||
* 表单数据更新触发器
|
||||
*/
|
||||
FORM_UPDATE = 10,
|
||||
HTTP_REQUEST = 1,
|
||||
/**
|
||||
* 接收 HTTP 回调请求触发器
|
||||
*/
|
||||
HTTP_CALLBACK = 2,
|
||||
/**
|
||||
* 发送 HTTP 请求触发器
|
||||
* 表单数据更新触发器
|
||||
*/
|
||||
HTTP_REQUEST = 1,
|
||||
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,
|
||||
}
|
||||
|
||||
export enum ChildProcessStartUserEmptyTypeEnum {
|
||||
/**
|
||||
* 子流程管理员
|
||||
*/
|
||||
@@ -254,10 +258,6 @@ export enum ChildProcessStartUserEmptyTypeEnum {
|
||||
* 主流程管理员
|
||||
*/
|
||||
MAIN_PROCESS_ADMIN = 3,
|
||||
/**
|
||||
* 同主流程发起人
|
||||
*/
|
||||
MAIN_PROCESS_START_USER = 1,
|
||||
}
|
||||
|
||||
export enum ChildProcessMultiInstanceSourceTypeEnum {
|
||||
@@ -265,54 +265,50 @@ export enum ChildProcessMultiInstanceSourceTypeEnum {
|
||||
* 固定数量
|
||||
*/
|
||||
FIXED_QUANTITY = 1,
|
||||
/**
|
||||
* 多选表单
|
||||
*/
|
||||
MULTIPLE_FORM = 3,
|
||||
/**
|
||||
* 数字表单
|
||||
*/
|
||||
NUMBER_FORM = 2,
|
||||
/**
|
||||
* 多选表单
|
||||
*/
|
||||
MULTIPLE_FORM = 3,
|
||||
}
|
||||
|
||||
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
|
||||
export enum CandidateStrategy {
|
||||
/**
|
||||
* 审批人自选
|
||||
* 指定角色
|
||||
*/
|
||||
APPROVE_USER_SELECT = 34,
|
||||
/**
|
||||
* 部门的负责人
|
||||
*/
|
||||
DEPT_LEADER = 21,
|
||||
ROLE = 10,
|
||||
/**
|
||||
* 部门成员
|
||||
*/
|
||||
DEPT_MEMBER = 20,
|
||||
/**
|
||||
* 流程表达式
|
||||
* 部门的负责人
|
||||
*/
|
||||
EXPRESSION = 60,
|
||||
/**
|
||||
* 表单内部门负责人
|
||||
*/
|
||||
FORM_DEPT_LEADER = 51,
|
||||
/**
|
||||
* 表单内用户字段
|
||||
*/
|
||||
FORM_USER = 50,
|
||||
/**
|
||||
* 连续多级部门的负责人
|
||||
*/
|
||||
MULTI_LEVEL_DEPT_LEADER = 23,
|
||||
DEPT_LEADER = 21,
|
||||
/**
|
||||
* 指定岗位
|
||||
*/
|
||||
POST = 22,
|
||||
/**
|
||||
* 指定角色
|
||||
* 连续多级部门的负责人
|
||||
*/
|
||||
ROLE = 10,
|
||||
MULTI_LEVEL_DEPT_LEADER = 23,
|
||||
/**
|
||||
* 指定用户
|
||||
*/
|
||||
USER = 30,
|
||||
/**
|
||||
* 审批人自选
|
||||
*/
|
||||
APPROVE_USER_SELECT = 34,
|
||||
/**
|
||||
* 发起人自选
|
||||
*/
|
||||
START_USER_SELECT = 35,
|
||||
/**
|
||||
* 发起人自己
|
||||
*/
|
||||
@@ -325,18 +321,22 @@ export enum CandidateStrategy {
|
||||
* 发起人连续多级部门的负责人
|
||||
*/
|
||||
START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
|
||||
/**
|
||||
* 发起人自选
|
||||
*/
|
||||
START_USER_SELECT = 35,
|
||||
/**
|
||||
* 指定用户
|
||||
*/
|
||||
USER = 30,
|
||||
/**
|
||||
* 指定用户组
|
||||
*/
|
||||
USER_GROUP = 40,
|
||||
/**
|
||||
* 表单内用户字段
|
||||
*/
|
||||
FORM_USER = 50,
|
||||
/**
|
||||
* 表单内部门负责人
|
||||
*/
|
||||
FORM_DEPT_LEADER = 51,
|
||||
/**
|
||||
* 流程表达式
|
||||
*/
|
||||
EXPRESSION = 60,
|
||||
}
|
||||
|
||||
export enum BpmHttpRequestParamTypeEnum {
|
||||
@@ -767,6 +767,14 @@ export const COMPARISON_OPERATORS: DictDataType[] = [
|
||||
value: '<=',
|
||||
label: '小于等于',
|
||||
},
|
||||
{
|
||||
value: 'contain',
|
||||
label: '包含',
|
||||
},
|
||||
{
|
||||
value: '!contain',
|
||||
label: '不包含',
|
||||
},
|
||||
];
|
||||
// 审批操作按钮名称
|
||||
export const OPERATION_BUTTON_NAME = new Map<number, string>();
|
||||
|
||||
405
apps/web-antd/src/views/bpm/form/mobile/index.vue
Normal file
405
apps/web-antd/src/views/bpm/form/mobile/index.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 移动端流程表单展示页面 - Ant Design Vue 版本
|
||||
* 使用 @form-create/ant-design-vue 渲染表单
|
||||
* 用于 UniApp 通过 iframe/webview 嵌入
|
||||
*
|
||||
* URL 参数说明:
|
||||
* - type: 环境类型(必填)'miniapp' 小程序(微信/支付宝/百度等) | 'h5' H5
|
||||
* - processInstanceId: 流程实例ID(查看已有流程时使用)
|
||||
* - taskId: 任务ID(可选)
|
||||
* - activityId: 活动节点ID(可选)
|
||||
* - token: 访问令牌(用于 API 认证)
|
||||
*/
|
||||
import { computed, nextTick, onMounted, ref, toRaw } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { BpmFieldPermissionType, BpmModelFormType } from '@vben/constants';
|
||||
import { updatePreferences } from '@vben/preferences';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { Button, Empty, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getApprovalDetail } from '#/api/bpm/processInstance';
|
||||
import { setConfAndFields2 } from '#/components/form-create';
|
||||
|
||||
type EnvType = 'h5' | 'miniapp'; // 环境类型
|
||||
|
||||
// UniApp WebView 类型声明
|
||||
interface UniWebView {
|
||||
postMessage: (options: { data: any }, targetOrigin?: string) => void;
|
||||
getEnv: (callback: (res: any) => void) => void;
|
||||
navigateTo: (options: {
|
||||
fail?: () => void;
|
||||
success?: () => void;
|
||||
url: string;
|
||||
}) => void;
|
||||
navigateBack: (options?: { delta?: number }) => void;
|
||||
switchTab: (options: { url: string }) => void;
|
||||
reLaunch: (options: { url: string }) => void;
|
||||
redirectTo: (options: { url: string }) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
uni?: UniWebView;
|
||||
}
|
||||
}
|
||||
|
||||
defineOptions({ name: 'BpmMobileFormPreview' });
|
||||
|
||||
const route = useRoute();
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const envType = ref<EnvType>('h5'); // 环境类型
|
||||
const loading = ref(true); // 页面加载状态
|
||||
const error = ref<null | string>(null);
|
||||
|
||||
const processInstance = ref<any>(null);
|
||||
const processDefinition = ref<any>(null);
|
||||
|
||||
const detailForm = ref<{
|
||||
option: any;
|
||||
rule: any[];
|
||||
value: Record<string, any>;
|
||||
}>({
|
||||
option: {},
|
||||
rule: [],
|
||||
value: {},
|
||||
}); // 流程实例的表单详情
|
||||
const fApi = ref<any>(null); // form-create API 引用
|
||||
const fieldPermissions = ref<Record<string, string>>({}); // 字段权限
|
||||
|
||||
// 是否有表单内容
|
||||
const hasFormContent = computed(() => {
|
||||
return detailForm.value.rule && detailForm.value.rule.length > 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化 Token
|
||||
* 从 URL 参数获取 token 并设置到 store
|
||||
*/
|
||||
function initToken() {
|
||||
const token = route.query.token as string;
|
||||
if (token) {
|
||||
accessStore.setAccessToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并初始化环境类型
|
||||
*/
|
||||
function initEnvType(): boolean {
|
||||
const type = route.query.type as string;
|
||||
|
||||
if (!type) {
|
||||
error.value = '缺少必填参数: type';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type !== 'h5' && type !== 'miniapp') {
|
||||
error.value = 'type 参数值无效,必须是 h5 或 miniapp';
|
||||
return false;
|
||||
}
|
||||
envType.value = type as EnvType;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审批详情
|
||||
*/
|
||||
async function getDetail() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const processInstanceId = route.query.processInstanceId as string;
|
||||
const taskId = route.query.taskId as string;
|
||||
const activityId = route.query.activityId as string;
|
||||
|
||||
if (!processInstanceId) {
|
||||
throw new Error('缺少流程实例ID参数');
|
||||
}
|
||||
|
||||
const data = await getApprovalDetail({
|
||||
processInstanceId,
|
||||
taskId,
|
||||
activityId,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error('查询不到审批详情信息');
|
||||
}
|
||||
|
||||
if (!data.processDefinition || !data.processInstance) {
|
||||
throw new Error('查询不到流程信息');
|
||||
}
|
||||
|
||||
processInstance.value = data.processInstance;
|
||||
processDefinition.value = data.processDefinition;
|
||||
|
||||
// 设置普通表单信息
|
||||
if (data.processDefinition.formType === BpmModelFormType.NORMAL) {
|
||||
if (detailForm.value.rule?.length > 0) {
|
||||
// 避免刷新 form-create 显示不了
|
||||
detailForm.value.value = processInstance.value.formVariables;
|
||||
} else {
|
||||
setConfAndFields2(
|
||||
detailForm,
|
||||
processDefinition.value.formConf,
|
||||
processDefinition.value.formFields,
|
||||
processInstance.value.formVariables,
|
||||
);
|
||||
}
|
||||
await nextTick();
|
||||
fApi.value?.btn.show(false);
|
||||
fApi.value?.resetBtn.show(false);
|
||||
fApi.value?.disabled(true);
|
||||
// 设置表单字段权限
|
||||
if (data.formFieldsPermission) {
|
||||
Object.keys(data.formFieldsPermission).forEach((item) => {
|
||||
setFieldPermission(item, data.formFieldsPermission[item]);
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向父页面发送消息
|
||||
* 根据环境类型选择不同的通信方式
|
||||
*/
|
||||
function postMessageToParent(message: { data: any; type: string }) {
|
||||
const messageData = {
|
||||
source: 'bpm-mobile-form',
|
||||
type: message.type,
|
||||
data: message.data,
|
||||
};
|
||||
|
||||
// 小程序环境:使用 uni.postMessage
|
||||
if (envType.value === 'miniapp') {
|
||||
if (window.uni?.postMessage) {
|
||||
// 传递的消息信息,必须写在 data 对象中
|
||||
window.uni.postMessage({ data: message.data }, window.location.origin);
|
||||
} else {
|
||||
console.error('小程序环境下 uni 对象未定义');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// H5 环境:使用 window.postMessage
|
||||
if (envType.value === 'h5' && window.parent !== window) {
|
||||
window.parent.postMessage(messageData, '*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地克隆对象,移除不可序列化的属性
|
||||
*/
|
||||
function safeClone(obj: any): any {
|
||||
try {
|
||||
// 先使用 toRaw 移除 Vue 的响应式代理
|
||||
const raw = toRaw(obj);
|
||||
// 使用 JSON 序列化来移除函数、DOM 元素等不可序列化的内容
|
||||
// eslint-disable-next-line unicorn/prefer-structured-clone
|
||||
return JSON.parse(JSON.stringify(raw));
|
||||
} catch (error) {
|
||||
console.error('克隆对象失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置表单权限 */
|
||||
function setFieldPermission(field: string, permission: string) {
|
||||
fieldPermissions.value[field] = permission;
|
||||
if (permission === BpmFieldPermissionType.READ) {
|
||||
fApi.value?.disabled(true, field);
|
||||
}
|
||||
if (permission === BpmFieldPermissionType.WRITE) {
|
||||
fApi.value?.disabled(false, field);
|
||||
}
|
||||
if (permission === BpmFieldPermissionType.NONE) {
|
||||
fApi.value?.hidden(true, field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定按钮点击事件
|
||||
* 获取表单数据并发送给父页面
|
||||
*/
|
||||
function handleConfirm() {
|
||||
// 获取最新的表单值(转换为普通对象,避免 Proxy 序列化问题)
|
||||
const rawValue = detailForm.value.value;
|
||||
const currentValue = safeClone(rawValue);
|
||||
|
||||
// 发送表单数据给父页面
|
||||
postMessageToParent({
|
||||
type: 'FORM_SUBMIT',
|
||||
data: {
|
||||
formValue: currentValue,
|
||||
fieldPermissions: safeClone(fieldPermissions.value),
|
||||
processInstanceId: route.query.processInstanceId,
|
||||
taskId: route.query.taskId,
|
||||
},
|
||||
});
|
||||
window.uni?.navigateBack();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 验证环境类型
|
||||
if (!initEnvType()) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先加载微信 JSSDK(微信小程序需要)
|
||||
const wxScript = document.createElement('script');
|
||||
wxScript.type = 'text/javascript';
|
||||
wxScript.src = 'https://res.wx.qq.com/open/js/jweixin-1.4.0.js';
|
||||
|
||||
wxScript.addEventListener('load', () => {
|
||||
// 2. 微信 SDK 加载完成后,加载 UniApp WebView SDK
|
||||
const uniScript = document.createElement('script');
|
||||
uniScript.type = 'text/javascript';
|
||||
uniScript.src = 'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js';
|
||||
|
||||
uniScript.addEventListener('load', () => {
|
||||
// 所有 SDK 加载完成后初始化
|
||||
initApp();
|
||||
});
|
||||
|
||||
uniScript.addEventListener('error', () => {
|
||||
error.value = 'UniApp WebView SDK 加载失败';
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
document.head.append(uniScript);
|
||||
});
|
||||
|
||||
wxScript.addEventListener('error', () => {
|
||||
// 微信 SDK 加载失败,尝试只加载 UniApp SDK(可能是其他小程序)
|
||||
const uniScript = document.createElement('script');
|
||||
uniScript.type = 'text/javascript';
|
||||
uniScript.src = 'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js';
|
||||
|
||||
uniScript.addEventListener('load', () => {
|
||||
initApp();
|
||||
});
|
||||
|
||||
uniScript.addEventListener('error', () => {
|
||||
error.value = 'SDK 加载失败';
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
document.head.append(uniScript);
|
||||
});
|
||||
|
||||
document.head.append(wxScript);
|
||||
|
||||
// 初始化
|
||||
initApp();
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
function initApp() {
|
||||
// 设置主题为 light 模式
|
||||
updatePreferences({
|
||||
theme: {
|
||||
mode: 'light',
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化 token
|
||||
initToken();
|
||||
|
||||
// 加载数据
|
||||
if (route.query.processInstanceId) {
|
||||
getDetail();
|
||||
} else {
|
||||
loading.value = false;
|
||||
error.value = '缺少必要参数:processInstanceId';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-form-preview-antd">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<Spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<Empty v-else-if="error" :description="error" />
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<template v-else>
|
||||
<!-- 有表单规则时渲染 form-create -->
|
||||
<div v-if="hasFormContent" class="mt-4">
|
||||
<form-create
|
||||
v-model="detailForm.value"
|
||||
v-model:api="fApi"
|
||||
:option="detailForm.option"
|
||||
:rule="detailForm.rule"
|
||||
/>
|
||||
|
||||
<!-- 确定按钮 -->
|
||||
<div class="form-footer">
|
||||
<Button type="primary" size="large" block @click="handleConfirm">
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无表单内容时显示空状态 -->
|
||||
<Empty v-else description="暂无表单内容" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-form-preview-antd {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-form-preview-antd {
|
||||
min-height: 100px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2px 8px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.form-footer :deep(.ant-btn) {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
* - @ 自动补全:插入 mention 占位元素
|
||||
*/
|
||||
|
||||
// @ts-ignore TinyMCE 全局或通过打包器提供
|
||||
// TinyMCE 全局或通过打包器提供
|
||||
import type { Editor } from 'tinymce';
|
||||
|
||||
export interface MentionItem {
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
bpmnXml?: string;
|
||||
loading?: boolean; // 是否加载中
|
||||
modelView?: Object;
|
||||
modelView?: object;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
@@ -29,7 +29,7 @@ watch(
|
||||
async (newModelView) => {
|
||||
// 加载最新
|
||||
if (newModelView) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error: viewer instance type is broader than local ref typing
|
||||
view.value = newModelView;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useUserStore } from '@vben/stores';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import FormCreate from '@form-create/ant-design-vue';
|
||||
import { until, useDebounceFn } from '@vueuse/core';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -107,11 +108,14 @@ const nodeTypeName = ref('审批'); // 节点类型名称
|
||||
|
||||
const reasonRequire = ref();
|
||||
const approveFormRef = ref<FormInstance>(); // 审批通过意见表单
|
||||
// @ts-expect-error: template ref is retained for future provider expansion
|
||||
const approveSignFormRef = ref();
|
||||
const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
|
||||
[],
|
||||
); // 下一个审批节点信息
|
||||
const nextAssigneesTimelineRef = ref(); // 下一个节点审批人时间线组件的引用
|
||||
let nextApprovalRequestId = 0; // 请求序号;onChange 高频触发时,丢弃过期请求结果
|
||||
let pendingNextNodesTask: null | Promise<unknown> = null; // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成
|
||||
const approveReasonForm: any = reactive({
|
||||
reason: '',
|
||||
signPicUrl: '',
|
||||
@@ -255,7 +259,6 @@ async function openPopover(type: string) {
|
||||
message.warning('表单校验不通过,请先完善表单!!');
|
||||
return;
|
||||
}
|
||||
await initNextAssigneesFormField();
|
||||
}
|
||||
if (type === 'return') {
|
||||
// 获取退回节点
|
||||
@@ -268,6 +271,20 @@ async function openPopover(type: string) {
|
||||
Object.keys(popOverVisible.value).forEach((item) => {
|
||||
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
|
||||
});
|
||||
if (type === 'approve') {
|
||||
// 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点;
|
||||
// 没有节点表单时,approveFormFApi 永远不会被赋值,跳过等待
|
||||
if (runningTask.value?.formId > 0) {
|
||||
// 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行
|
||||
await until(
|
||||
() => typeof approveFormFApi.value?.validate === 'function',
|
||||
)
|
||||
.toBeTruthy({ timeout: 1000 })
|
||||
.catch(() => {});
|
||||
}
|
||||
// 初始化下一个审批人表单字段
|
||||
await initNextAssigneesFormField();
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭气泡卡 */
|
||||
@@ -285,6 +302,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
|
||||
|
||||
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
|
||||
async function initNextAssigneesFormField() {
|
||||
// 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废
|
||||
const requestId = ++nextApprovalRequestId;
|
||||
// 获取修改的流程变量, 暂时只支持流程表单
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
const data = await getNextApprovalNodes({
|
||||
@@ -292,6 +311,12 @@ async function initNextAssigneesFormField() {
|
||||
taskId: runningTask.value.id,
|
||||
processVariablesStr: JSON.stringify(variables),
|
||||
});
|
||||
// 已有更新的请求发出,丢弃本次过期结果,避免把旧分支节点回写到当前列表
|
||||
if (requestId !== nextApprovalRequestId) {
|
||||
return;
|
||||
}
|
||||
// 在最新结果到达时再清空,避免请求期间出现节点信息抖动
|
||||
nextAssigneesActivityNode.value = [];
|
||||
if (data && data.length > 0) {
|
||||
const customApproveUsersData: Record<string, any[]> = {}; // 用于收集需要设置到 Timeline 组件的自定义审批人数据
|
||||
data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
||||
@@ -326,6 +351,12 @@ async function initNextAssigneesFormField() {
|
||||
}
|
||||
}
|
||||
|
||||
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
|
||||
const debouncedInitNextAssigneesFormField = useDebounceFn(
|
||||
initNextAssigneesFormField,
|
||||
300,
|
||||
);
|
||||
|
||||
/** 选择下一个节点的审批人 */
|
||||
function selectNextAssigneesConfirm(id: string, userList: any[]) {
|
||||
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
|
||||
@@ -361,6 +392,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
}
|
||||
|
||||
if (pass) {
|
||||
// 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交
|
||||
if (pendingNextNodesTask) {
|
||||
await pendingNextNodesTask;
|
||||
}
|
||||
const nextAssigneesValid = validateNextAssignees();
|
||||
if (!nextAssigneesValid) return;
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
@@ -375,12 +410,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
if (runningTask.value.signEnable) {
|
||||
data.signPicUrl = approveReasonForm.signPicUrl;
|
||||
}
|
||||
// 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
|
||||
// TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
|
||||
// 多表单处理:节点表单需要校验;变量已经在 getUpdatedProcessInstanceVariables 中合并到 data.variables,无需再覆盖
|
||||
const formCreateApi = approveFormFApi.value;
|
||||
if (Object.keys(formCreateApi)?.length > 0) {
|
||||
await formCreateApi.validate();
|
||||
data.variables = approveForm.value.value;
|
||||
}
|
||||
await approveTask(data);
|
||||
popOverVisible.value.approve = false;
|
||||
@@ -647,18 +680,32 @@ function loadTodoTask(task: any) {
|
||||
approveForm.value = {};
|
||||
runningTask.value = task;
|
||||
approveFormFApi.value = {};
|
||||
// 切换任务时重置请求序号与 pending 重算,避免旧任务飞行中的请求/Promise 串到新任务
|
||||
nextApprovalRequestId += 1;
|
||||
pendingNextNodesTask = null;
|
||||
reasonRequire.value = task?.reasonRequire ?? false;
|
||||
nodeTypeName.value =
|
||||
task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批';
|
||||
// 处理 approve 表单
|
||||
if (task && task.formId && task.formConf) {
|
||||
const tempApproveForm = {};
|
||||
const tempApproveForm: { option?: any; rule?: any; value?: any } = {};
|
||||
setConfAndFields2(
|
||||
tempApproveForm,
|
||||
task.formConf,
|
||||
task.formFields,
|
||||
task.formVariables,
|
||||
);
|
||||
// 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段
|
||||
tempApproveForm.option.onChange = () => {
|
||||
// 弹窗打开时,才重新计算下一个节点的信息
|
||||
if (!popOverVisible.value.approve) {
|
||||
return;
|
||||
}
|
||||
// useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled'
|
||||
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(
|
||||
() => {},
|
||||
);
|
||||
};
|
||||
approveForm.value = tempApproveForm;
|
||||
} else {
|
||||
approveForm.value = {}; // 占位,避免为空
|
||||
@@ -683,9 +730,17 @@ async function validateNormalForm() {
|
||||
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
|
||||
function getUpdatedProcessInstanceVariables() {
|
||||
const variables: any = {};
|
||||
props.writableFields.forEach((field: string) => {
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
// 从流程表单(流程定义级别)中获取变量
|
||||
if (props.writableFields?.length && props.normalFormApi) {
|
||||
props.writableFields.forEach((field: string) => {
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
}
|
||||
// 从节点表单(节点级别)中获取变量;通过 form-create 官方的 formData() 拿当前值
|
||||
const nodeFormData = approveFormFApi.value?.formData?.();
|
||||
if (nodeFormData) {
|
||||
Object.assign(variables, nodeFormData);
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useUserStore } from '@vben/stores';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
// @ts-ignore - 安装 vue3-print-nb 局部指令 v-print
|
||||
// @ts-expect-error - 安装 vue3-print-nb 局部指令 v-print
|
||||
import vPrint from 'vue3-print-nb';
|
||||
|
||||
import { getProcessInstancePrintData } from '#/api/bpm/processInstance';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user