mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-04-02 23:13:22 +08:00
Compare commits
226 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78f6a6a3f2 | ||
|
|
87a2071dab | ||
|
|
81442f7b8d | ||
|
|
c8d60e7e27 | ||
|
|
c48943bc67 | ||
|
|
7680b33b99 | ||
|
|
bb6bd2ea2e | ||
|
|
bb5d75bc7e | ||
|
|
349b518a31 | ||
|
|
528395e2c3 | ||
|
|
6a9012e5e4 | ||
|
|
6e8315ab40 | ||
|
|
7cb2699f19 | ||
|
|
4b5da81ba6 | ||
|
|
6aca9a9c99 | ||
|
|
fa195fde8e | ||
|
|
1057f2932b | ||
|
|
b9224fc379 | ||
|
|
57dd818170 | ||
|
|
49256ec1b7 | ||
|
|
f6f92e5403 | ||
|
|
613c311076 | ||
|
|
9ee7a7d9ff | ||
|
|
44f8aed06d | ||
|
|
d5d4a5c591 | ||
|
|
74381aa8c1 | ||
|
|
203ee9b623 | ||
|
|
6c8c49966a | ||
|
|
3862942e9f | ||
|
|
8571fc43b0 | ||
|
|
59aabd956d | ||
|
|
9b09ba4483 | ||
|
|
686c3f9208 | ||
|
|
8a7e2bd8e4 | ||
|
|
174c4ae749 | ||
|
|
67da9417a8 | ||
|
|
f4a4ced88d | ||
|
|
19b2d7af41 | ||
|
|
343d8a1c1e | ||
|
|
9480f8272a | ||
|
|
0d9e260a6a | ||
|
|
51bca25345 | ||
|
|
694396dcfb | ||
|
|
1cb53e943e | ||
|
|
13c8318adc | ||
|
|
48ed797055 | ||
|
|
1383f63361 | ||
|
|
dbe8beb7f9 | ||
|
|
3c2285141c | ||
|
|
49b884c0b1 | ||
|
|
24d20ca9ee | ||
|
|
6f02181024 | ||
|
|
3566191cd1 | ||
|
|
ed3353a271 | ||
|
|
965f5f96b7 | ||
|
|
61d9df7f58 | ||
|
|
231a5169ec | ||
|
|
ce7b7b910a | ||
|
|
81a61558cb | ||
|
|
7d2bc2e885 | ||
|
|
89b237f6b4 | ||
|
|
a1bb132233 | ||
|
|
022d538940 | ||
|
|
ccf70a1b76 | ||
|
|
af3fe53ec8 | ||
|
|
e981fb159f | ||
|
|
79b9d55854 | ||
|
|
3dca100349 | ||
|
|
b2055a4457 | ||
|
|
1479f159aa | ||
|
|
7bf7e09002 | ||
|
|
de8d39ffed | ||
|
|
543a7e3962 | ||
|
|
9dfe3f5af8 | ||
|
|
f11b08d8cb | ||
|
|
45b6f08984 | ||
|
|
92a4676f8d | ||
|
|
7bf7c0bb06 | ||
|
|
8f8cf5b704 | ||
|
|
6be238430d | ||
|
|
f77216d8f4 | ||
|
|
d42a9b2409 | ||
|
|
6753834054 | ||
|
|
77a4a64eb4 | ||
|
|
49db40d557 | ||
|
|
0032c608f1 | ||
|
|
fa603b32b1 | ||
|
|
9105d4d14a | ||
|
|
8c08fd683d | ||
|
|
5fee909fa3 | ||
|
|
c76db7d8d1 | ||
|
|
880a768f34 | ||
|
|
e595a9712f | ||
|
|
6f39e9136e | ||
|
|
1f1ba16ead | ||
|
|
b6edc5f574 | ||
|
|
8b0f138100 | ||
|
|
a810cd0b48 | ||
|
|
b17fec41b0 | ||
|
|
5fcfabf1d4 | ||
|
|
1d77b018bb | ||
|
|
f7d9d1b1af | ||
|
|
aaf0274fe9 | ||
|
|
c142af482b | ||
|
|
cd7c11c7d0 | ||
|
|
fb8f36eeec | ||
|
|
13f5e949a9 | ||
|
|
9a4f0f6ab1 | ||
|
|
bed52983a0 | ||
|
|
655ce2c2e8 | ||
|
|
f841568e5a | ||
|
|
469697845c | ||
|
|
273f3cbaf8 | ||
|
|
96f671908e | ||
|
|
f09aace765 | ||
|
|
772529e2fb | ||
|
|
eeff017b9f | ||
|
|
22394ebdeb | ||
|
|
ffbc9b22a7 | ||
|
|
c3a7562e2c | ||
|
|
605e0ea128 | ||
|
|
c89a83f980 | ||
|
|
cfad88666b | ||
|
|
f918dc24c6 | ||
|
|
e898993fe8 | ||
|
|
5cafcb4a01 | ||
|
|
03154cde88 | ||
|
|
a74bf7b63f | ||
|
|
0bc7169698 | ||
|
|
24b6e7a835 | ||
|
|
f59e33682c | ||
|
|
d811af37dd | ||
|
|
0e1a7d61f3 | ||
|
|
573637222d | ||
|
|
05e9d65251 | ||
|
|
0bbb20fee0 | ||
|
|
dbc5ea32ae | ||
|
|
0319604863 | ||
|
|
acf99f2441 | ||
|
|
cbf2a02877 | ||
|
|
226d9bd1ad | ||
|
|
48b3d30088 | ||
|
|
e311cfb8da | ||
|
|
4347fba80a | ||
|
|
7dc68ed368 | ||
|
|
eb2e2c6f15 | ||
|
|
1e853b7347 | ||
|
|
a4aa133db5 | ||
|
|
6bbe523f6f | ||
|
|
fef1e35c54 | ||
|
|
20410aeb03 | ||
|
|
2bc495d601 | ||
|
|
aebabdc21f | ||
|
|
60f03602ae | ||
|
|
5e4cc5729b | ||
|
|
83ea27af3e | ||
|
|
b7bfd69788 | ||
|
|
99d663a6f2 | ||
|
|
1af11240a6 | ||
|
|
3e8e8690e3 | ||
|
|
be843300be | ||
|
|
dc77721c11 | ||
|
|
a38d081f17 | ||
|
|
1e09fa4642 | ||
|
|
bd8ff73f9e | ||
|
|
15dc0c121f | ||
|
|
57620dc2ea | ||
|
|
b472fbb72f | ||
|
|
04321b16c1 | ||
|
|
28b4e907a8 | ||
|
|
8c42c8cc70 | ||
|
|
7268824612 | ||
|
|
23c41a8059 | ||
|
|
fdc78faa13 | ||
|
|
c8af580866 | ||
|
|
348c97710f | ||
|
|
a854760d26 | ||
|
|
c7c39047de | ||
|
|
f8a7a0a9a2 | ||
|
|
2cb3dcf499 | ||
|
|
b36d32b66d | ||
|
|
639ea96bb9 | ||
|
|
565be77e96 | ||
|
|
ac6de0324c | ||
|
|
f46ae023ba | ||
|
|
212144feca | ||
|
|
3eed51fd3e | ||
|
|
4d713db546 | ||
|
|
4c39bef181 | ||
|
|
86bcceaa84 | ||
|
|
1980a2482d | ||
|
|
0cd9f4615c | ||
|
|
cfbce0d737 | ||
|
|
1d9cd88dd7 | ||
|
|
0e62862119 | ||
|
|
6b6cdef42d | ||
|
|
33b7a605c0 | ||
|
|
a38cf80ea4 | ||
|
|
a986e1a2ab | ||
|
|
9822d2af8a | ||
|
|
b51f5d1fa6 | ||
|
|
e01803ce9d | ||
|
|
e3e5755903 | ||
|
|
61ce53b686 | ||
|
|
b029f77b6a | ||
|
|
0a8339a405 | ||
|
|
56104b2abf | ||
|
|
b4ca3f43a9 | ||
|
|
0666483c58 | ||
|
|
2264eaae18 | ||
|
|
77c45d855b | ||
|
|
8ce52eef51 | ||
|
|
738a918df6 | ||
|
|
d9131cbe22 | ||
|
|
968a2eb7b6 | ||
|
|
9b59a8acdb | ||
|
|
38f91da5af | ||
|
|
8edea3aee5 | ||
|
|
08b6e7713e | ||
|
|
6a89814b83 | ||
|
|
52d3aa9315 | ||
|
|
4e264c503a | ||
|
|
32051e9ca0 | ||
|
|
2b0079580b | ||
|
|
fddfc6d494 | ||
|
|
df655015b1 |
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
|
||||
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -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
|
||||
|
||||
20
.github/workflows/deploy.yml
vendored
20
.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
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
run: pnpm build:play
|
||||
|
||||
- name: Sync Playground files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
run: pnpm build:docs
|
||||
|
||||
- name: Sync Docs files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
|
||||
@@ -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
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
run: pnpm run build:antd
|
||||
|
||||
- name: Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
|
||||
@@ -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
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
run: pnpm run build:ele
|
||||
|
||||
- name: Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
|
||||
@@ -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
|
||||
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
run: pnpm run build:naive
|
||||
|
||||
- name: Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.6
|
||||
with:
|
||||
server: ${{ secrets.PRO_FTP_HOST }}
|
||||
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
|
||||
|
||||
6
.github/workflows/release-tag.yml
vendored
6
.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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.1.0
|
||||
22.22.0
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
||||
registry = "https://registry.npmmirror.com"
|
||||
registry=https://registry.npmmirror.com
|
||||
public-hoist-pattern[]=lefthook
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -181,7 +181,8 @@
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"json5"
|
||||
"json5",
|
||||
"yaml"
|
||||
],
|
||||
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,3 +1,38 @@
|
||||
# 1.5.3
|
||||
|
||||
对应后端版本 单体/微服务: 5.5.3/2.5.3(向后兼容5.5.x版本)
|
||||
|
||||
全是纯前端更新 没有后端功能变动
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 使用antd组件替换密码登录表单
|
||||
|
||||
**FEATURES**
|
||||
|
||||
- 合并官方更新 将Radix(v1)替换为Reka UI(v2)
|
||||
|
||||
# 1.5.2
|
||||
|
||||
对应后端版本 单体/微服务: 5.5.1/2.5.1
|
||||
|
||||
该版本后端功能值包含一个`同步租户参数配置`功能 旧版本也能升级(使用)
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 流程相关代码重构 移除之前的历史代码
|
||||
|
||||
**FEATURES**
|
||||
|
||||
- 修改流程变量
|
||||
- 租户管理 同步租户参数配置
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- 菜单管理 新增没有加载下拉选择api
|
||||
- v-access:role指令错误判断code而非role
|
||||
- modal/drawer里使用列配置 重置列弹窗被遮挡
|
||||
|
||||
# 1.5.1
|
||||
|
||||
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml)
|
||||
|
||||
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
||||
|
||||
目前对应后端版本: **分布式5.5.0/微服务2.5.0**
|
||||
目前对应后端版本: **分布式5.5.1/微服务2.5.1**
|
||||
|
||||
V1.1.0版本已支持离线图标
|
||||
|
||||
@@ -32,11 +32,19 @@ V1.2.0版本对接warmflow工作流
|
||||
|
||||
admin 账号: admin admin123
|
||||
|
||||
[预览地址点这里](http://vben5.dapdap.top)
|
||||
[dev分支预览](http://vben5.dapdap.top)
|
||||
|
||||
## WX Group
|
||||
[antdv-next分支预览](http://antdv-next.dapdap.top)
|
||||
|
||||
演示站 - 微信群菜单
|
||||
## 分支说明
|
||||
|
||||
- `main` 主分支 稳定分支(基于ant-design-vue)
|
||||
- `dev` 开发分支 包含前端/后端新功能 没问题后合并到`main`分支(基于ant-design-vue) [预览地址](http://vben5.dapdap.top)
|
||||
- `antdv-next`分支 使用`antdv-next`替换已经不维护的`ant-design-vue` 分支 且包含破坏性更新 (基于antv-next) [预览地址](http://antdv-next.dapdap.top)
|
||||
|
||||
antdv-next开发完毕且测试正常后 会作为主分支更新(v2) 原先基于`ant-design-vue`的版本会作为归档(v1)
|
||||
|
||||
antdv-next目前为阿尔法版本
|
||||
|
||||
## 文档
|
||||
|
||||
@@ -82,7 +90,7 @@ pnpm install
|
||||
|
||||
- 关于代码生成
|
||||
|
||||
V5版本代码生成模板为付费功能 [详见](https://dapdap.top/other/template.html)
|
||||
原先为付费功能 现由于某些原因不再放出 建议用AI照着抄 [详见](https://dapdap.top/other/template.html)
|
||||
|
||||
- 关于一些监控的地址配置(微服务版本可以跳过这一小节)
|
||||
|
||||
|
||||
12
apps/backend-mock/api/timezone/getTimezone.ts
Normal file
12
apps/backend-mock/api/timezone/getTimezone.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { getTimezone } from '~/utils/timezone-utils';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
return useResponseSuccess(getTimezone());
|
||||
});
|
||||
11
apps/backend-mock/api/timezone/getTimezoneOptions.ts
Normal file
11
apps/backend-mock/api/timezone/getTimezoneOptions.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default eventHandler(() => {
|
||||
const data = TIME_ZONE_OPTIONS.map((o) => ({
|
||||
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
|
||||
value: o.timezone,
|
||||
}));
|
||||
return useResponseSuccess(data);
|
||||
});
|
||||
22
apps/backend-mock/api/timezone/setTimezone.ts
Normal file
22
apps/backend-mock/api/timezone/setTimezone.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { eventHandler, readBody } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { setTimezone } from '~/utils/timezone-utils';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const body = await readBody<{ timezone?: unknown }>(event);
|
||||
const timezone =
|
||||
typeof body?.timezone === 'string' ? body.timezone : undefined;
|
||||
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
|
||||
if (!timezone || !allowed) {
|
||||
setResponseStatus(event, 400);
|
||||
return useResponseError('Bad Request', 'Invalid timezone');
|
||||
}
|
||||
setTimezone(timezone);
|
||||
return useResponseSuccess({});
|
||||
});
|
||||
@@ -7,6 +7,11 @@ export interface UserInfo {
|
||||
homePath?: string;
|
||||
}
|
||||
|
||||
export interface TimezoneOption {
|
||||
offset: number;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export const MOCK_USERS: UserInfo[] = [
|
||||
{
|
||||
id: 0,
|
||||
@@ -276,7 +281,7 @@ export const MOCK_MENU_LIST = [
|
||||
children: [
|
||||
{
|
||||
id: 20_401,
|
||||
pid: 201,
|
||||
pid: 202,
|
||||
name: 'SystemDeptCreate',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
@@ -285,7 +290,7 @@ export const MOCK_MENU_LIST = [
|
||||
},
|
||||
{
|
||||
id: 20_402,
|
||||
pid: 201,
|
||||
pid: 202,
|
||||
name: 'SystemDeptEdit',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
@@ -294,7 +299,7 @@ export const MOCK_MENU_LIST = [
|
||||
},
|
||||
{
|
||||
id: 20_403,
|
||||
pid: 201,
|
||||
pid: 202,
|
||||
name: 'SystemDeptDelete',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
@@ -388,3 +393,29 @@ export function getMenuIds(menus: any[]) {
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时区选项
|
||||
*/
|
||||
export const TIME_ZONE_OPTIONS: TimezoneOption[] = [
|
||||
{
|
||||
offset: -5,
|
||||
timezone: 'America/New_York',
|
||||
},
|
||||
{
|
||||
offset: 0,
|
||||
timezone: 'Europe/London',
|
||||
},
|
||||
{
|
||||
offset: 8,
|
||||
timezone: 'Asia/Shanghai',
|
||||
},
|
||||
{
|
||||
offset: 9,
|
||||
timezone: 'Asia/Tokyo',
|
||||
},
|
||||
{
|
||||
offset: 9,
|
||||
timezone: 'Asia/Seoul',
|
||||
},
|
||||
];
|
||||
|
||||
9
apps/backend-mock/utils/timezone-utils.ts
Normal file
9
apps/backend-mock/utils/timezone-utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
let mockTimeZone: null | string = null;
|
||||
|
||||
export const setTimezone = (timeZone: string) => {
|
||||
mockTimeZone = timeZone;
|
||||
};
|
||||
|
||||
export const getTimezone = () => {
|
||||
return mockTimeZone;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "1.5.1",
|
||||
"version": "1.5.3",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -27,8 +27,8 @@
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@tinymce/tinymce-vue": "^6.0.1",
|
||||
"@ant-design/icons-vue": "catalog:",
|
||||
"@tinymce/tinymce-vue": "catalog:",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
@@ -45,18 +45,18 @@
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"ant-design-vue": "catalog:",
|
||||
"cropperjs": "^1.6.2",
|
||||
"cropperjs": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"echarts": "^5.5.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"echarts": "catalog:conflicts_echarts_h5_5_1",
|
||||
"lodash-es": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"tinymce": "7.9.1",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"tinymce": "catalog:",
|
||||
"unplugin-vue-components": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
"vue3-colorpicker": "^2.3.0"
|
||||
"vue3-colorpicker": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.12"
|
||||
"@types/lodash-es": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiCascader'
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
@@ -166,34 +167,28 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
|
||||
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Cascader,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
AutoComplete,
|
||||
Cascader: withDefaultPlaceholder(Cascader, 'select'),
|
||||
Checkbox,
|
||||
|
||||
@@ -78,9 +78,10 @@ setupVbenVxeTable({
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
return h(Image, { src: row[column.field], ...props });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -125,3 +125,13 @@ export function dictSyncTenant(tenantId?: string) {
|
||||
successMessageMode: 'message',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步租户配置
|
||||
* @returns void
|
||||
*/
|
||||
export function syncTenantConfig() {
|
||||
return requestClient.get<void>('/system/tenant/syncTenantConfig', {
|
||||
successMessageMode: 'message',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TaskInfo } from '../task/model';
|
||||
import type { FlowInfoResponse } from './model';
|
||||
import type { FlowInfoResponse, FlowInstanceVariableResp } from './model';
|
||||
|
||||
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
|
||||
|
||||
@@ -104,8 +104,8 @@ export function flowInfo(businessId: string) {
|
||||
* @returns Map<string,any>
|
||||
*/
|
||||
export function instanceVariable(instanceId: string) {
|
||||
return requestClient.get<Record<string, any>>(
|
||||
`/workflow/instance/variable/${instanceId}`,
|
||||
return requestClient.get<FlowInstanceVariableResp>(
|
||||
`/workflow/instance/instanceVariable/${instanceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,3 +118,22 @@ export function workflowInstanceInvalid(data: {
|
||||
}) {
|
||||
return requestClient.postWithMsg<void>('/workflow/instance/invalid', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改流程参数
|
||||
* @param data 参数
|
||||
* @param data.instanceId 实例ID
|
||||
* @param data.key 参数key
|
||||
* @param data.value 值
|
||||
* @returns void
|
||||
*/
|
||||
export function updateFlowVariable(data: {
|
||||
instanceId: string;
|
||||
key: string;
|
||||
value: any;
|
||||
}) {
|
||||
return requestClient.putWithMsg<void>(
|
||||
'/workflow/instance/updateVariable',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export {};
|
||||
|
||||
export interface Flow {
|
||||
id: string;
|
||||
createTime: string;
|
||||
@@ -39,3 +41,14 @@ export interface FlowInfoResponse {
|
||||
instanceId: string;
|
||||
list: Flow[];
|
||||
}
|
||||
|
||||
export interface FlowInstanceVariableResp {
|
||||
/**
|
||||
* json字符串 流程变量
|
||||
*/
|
||||
variable: string;
|
||||
variableList: {
|
||||
key: string;
|
||||
value: any;
|
||||
}[];
|
||||
}
|
||||
|
||||
27
apps/web-antd/src/components/global/slot.ts
Normal file
27
apps/web-antd/src/components/global/slot.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
/**
|
||||
* 使用默认插槽来自定义组件
|
||||
* 给vbenForm的components使用
|
||||
*/
|
||||
export const DefaultSlot = defineComponent({
|
||||
name: 'DefaultSlot',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
/**
|
||||
* 绑定到根节点的div上的属性
|
||||
*/
|
||||
rootDivAttrs: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
render() {
|
||||
/**
|
||||
* 获取属性 传递给作用域插槽供外部使用
|
||||
*/
|
||||
const attrs = this.$attrs;
|
||||
|
||||
return h('div', { ...this.rootDivAttrs }, this.$slots.default?.(attrs));
|
||||
},
|
||||
});
|
||||
@@ -133,6 +133,11 @@ const initOptions = computed((): InitOptions => {
|
||||
toolbar_mode: 'sliding',
|
||||
// 隐藏下面的 按xxx获取帮助
|
||||
help_accessibility: false,
|
||||
// https://blog.csdn.net/qq_46380656/article/details/122171418
|
||||
// 避免图片地址和链接地址转换成相对路径
|
||||
relative_urls: false,
|
||||
remove_script_host: false,
|
||||
convert_urls: false,
|
||||
...options,
|
||||
/**
|
||||
* 覆盖默认的base64行为
|
||||
|
||||
@@ -1,215 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
||||
import type { DataNode } from 'ant-design-vue/es/tree';
|
||||
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
|
||||
|
||||
import type { PropType, SetupContext } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue';
|
||||
|
||||
import { findGroupParentIds, treeToList } from '@vben/utils';
|
||||
import { treeToList } from '@vben/utils';
|
||||
|
||||
import { Checkbox, Tree } from 'ant-design-vue';
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
/** 需要禁止透传 */
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
checkStrictly: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
expandAllOnInit: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
fieldNames: {
|
||||
default: () => ({ key: 'id', title: 'label' }),
|
||||
type: Object as PropType<{ key: string; title: string }>,
|
||||
},
|
||||
/** 点击节点关联/独立时 清空已勾选的节点 */
|
||||
resetOnStrictlyChange: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
treeData: {
|
||||
default: () => [],
|
||||
type: Array as PropType<DataNode[]>,
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
expandAllOnInit: false,
|
||||
fieldNames: () => ({ key: 'id', title: 'label' }),
|
||||
resetOnStrictlyChange: true,
|
||||
treeData: () => [],
|
||||
});
|
||||
const emit = defineEmits<{ checkStrictlyChange: [boolean] }>();
|
||||
|
||||
const expandStatus = ref(false);
|
||||
const selectAllStatus = ref(false);
|
||||
interface Props {
|
||||
/**
|
||||
* 是否展开所有节点 mount
|
||||
*/
|
||||
expandAllOnInit?: boolean;
|
||||
/**
|
||||
* 自定义字段
|
||||
*/
|
||||
fieldNames?: { key: string; title: string };
|
||||
/**
|
||||
* 点击节点关联/独立时 清空已勾选的节点
|
||||
*/
|
||||
resetOnStrictlyChange?: boolean;
|
||||
/**
|
||||
* 树结构数据
|
||||
*/
|
||||
treeData?: DataNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台的这个字段跟antd/ele是反的
|
||||
* 组件库这个字段代表不关联
|
||||
* 后台这个代表关联
|
||||
* 展开的状态
|
||||
*/
|
||||
const innerCheckedStrictly = computed(() => {
|
||||
return !props.checkStrictly;
|
||||
});
|
||||
const expandStatus = ref(false);
|
||||
/**
|
||||
* 全选状态
|
||||
*/
|
||||
const selectAllStatus = ref(false);
|
||||
|
||||
const associationText = computed(() => {
|
||||
return props.checkStrictly ? '父子节点关联' : '父子节点独立';
|
||||
return checkStrictly.value ? '父子节点关联' : '父子节点独立';
|
||||
});
|
||||
|
||||
/**
|
||||
* 这个只用于界面显示
|
||||
* 关联情况下 只会有最末尾的节点被选中
|
||||
*/
|
||||
const checkedKeys = defineModel('value', {
|
||||
const checkedKeys = defineModel<(number | string)[]>('value', {
|
||||
default: () => [],
|
||||
type: Array as PropType<(number | string)[]>,
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否节点关联 后端字段跟前端字段是反的
|
||||
*/
|
||||
const checkStrictly = defineModel<boolean>('checkStrictly', {
|
||||
default: () => true,
|
||||
});
|
||||
|
||||
const computedCheckedKeys = computed<any>({
|
||||
get() {
|
||||
/**
|
||||
* 严格模式(节点不关联) 需要返回{checked: string[] | number[], halfChecked: string[]}
|
||||
* @see https://www.antdv.com/components/tree-cn#tree-props
|
||||
*/
|
||||
if (!checkStrictly.value) {
|
||||
return {
|
||||
checked: [...checkedKeys.value],
|
||||
halfChecked: [],
|
||||
};
|
||||
}
|
||||
return checkedKeys.value;
|
||||
},
|
||||
set(v) {
|
||||
if (!checkStrictly.value) {
|
||||
checkedKeys.value = [...v.checked, ...v.halfChecked];
|
||||
return;
|
||||
}
|
||||
checkedKeys.value = v;
|
||||
},
|
||||
});
|
||||
|
||||
// 所有节点的ID
|
||||
const allKeys = computed(() => {
|
||||
const idField = props.fieldNames.key;
|
||||
return treeToList(props.treeData).map((item: any) => item[idField]);
|
||||
});
|
||||
|
||||
/** 已经选择的所有节点 包括子/父节点 用于提交 */
|
||||
const checkedRealKeys = ref<(number | string)[]>([]);
|
||||
|
||||
/**
|
||||
* 取第一次的menuTree id 设置到checkedMenuKeys
|
||||
* 主要为了解决没有任何修改 直接点击保存的情况
|
||||
*
|
||||
* length为0情况(即新增时候没有勾选节点) 勾选这里会延迟触发 节点会拼接上父节点 导致ID重复
|
||||
*/
|
||||
const stop = watch([checkedKeys, () => props.treeData], () => {
|
||||
if (
|
||||
props.checkStrictly &&
|
||||
checkedKeys.value.length > 0 &&
|
||||
props.treeData.length > 0
|
||||
) {
|
||||
/** 找到父节点 添加上 */
|
||||
const parentIds = findGroupParentIds(
|
||||
props.treeData,
|
||||
checkedKeys.value as any,
|
||||
{ id: props.fieldNames.key },
|
||||
);
|
||||
/**
|
||||
* uniq 解决上面的id重复问题
|
||||
*/
|
||||
checkedRealKeys.value = uniq([...parentIds, ...checkedKeys.value]);
|
||||
stop();
|
||||
}
|
||||
if (!props.checkStrictly && checkedKeys.value.length > 0) {
|
||||
/** 节点独立 这里是全部的节点 */
|
||||
checkedRealKeys.value = checkedKeys.value;
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param checkedStateKeys 已经选中的子节点的ID
|
||||
* @param info info.halfCheckedKeys为父节点的ID
|
||||
*/
|
||||
type CheckedState<T = number | string> =
|
||||
| T[]
|
||||
| { checked: T[]; halfChecked: T[] };
|
||||
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
|
||||
// 数组的话为节点关联
|
||||
if (Array.isArray(checkedStateKeys)) {
|
||||
const halfCheckedKeys: number[] = (info.halfCheckedKeys || []) as number[];
|
||||
checkedRealKeys.value = [...halfCheckedKeys, ...checkedStateKeys];
|
||||
} else {
|
||||
checkedRealKeys.value = [...checkedStateKeys.checked];
|
||||
// fix: Invalid prop: type check failed for prop "value". Expected Array, got Object
|
||||
checkedKeys.value = [...checkedStateKeys.checked];
|
||||
}
|
||||
}
|
||||
|
||||
function handleExpandChange(e: CheckboxChangeEvent) {
|
||||
function handleCheckedAllChange(e: CheckboxChangeEvent) {
|
||||
// 这个用于展示
|
||||
checkedKeys.value = e.target.checked ? allKeys.value : [];
|
||||
// 这个用于提交
|
||||
checkedRealKeys.value = e.target.checked ? allKeys.value : [];
|
||||
}
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
function handleExpandOrCollapseAll(e: CheckboxChangeEvent) {
|
||||
const expand = e.target.checked;
|
||||
expandedKeys.value = expand ? allKeys.value : [];
|
||||
function handleExpandOrCollapseAll() {
|
||||
expandStatus.value = !expandStatus.value;
|
||||
expandedKeys.value = expandStatus.value ? allKeys.value : [];
|
||||
}
|
||||
|
||||
function handleCheckStrictlyChange(e: CheckboxChangeEvent) {
|
||||
emit('checkStrictlyChange', e.target.checked);
|
||||
function handleCheckStrictlyChange() {
|
||||
if (props.resetOnStrictlyChange) {
|
||||
checkedKeys.value = [];
|
||||
checkedRealKeys.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露方法来获取用于提交的全部节点
|
||||
* uniq去重(保险方案)
|
||||
*/
|
||||
defineExpose({
|
||||
getCheckedKeys: () => uniq(checkedRealKeys.value),
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.expandAllOnInit) {
|
||||
await nextTick();
|
||||
expandedKeys.value = allKeys.value;
|
||||
}
|
||||
});
|
||||
|
||||
const slots = useSlots() as SetupContext['slots'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background w-full rounded-lg border-[1px] p-[12px]">
|
||||
<!-- <div class="flex flex-col gap-6 text-[13px]">
|
||||
<div>computedCheckedKeys {{ computedCheckedKeys }}</div>
|
||||
<div>checkedKeys {{ checkedKeys }}</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex items-center justify-between gap-2 border-b-[1px] pb-2">
|
||||
<div>
|
||||
<div class="opacity-75">
|
||||
<span>节点状态: </span>
|
||||
<span :class="[props.checkStrictly ? 'text-primary' : 'text-red-500']">
|
||||
<span :class="[checkStrictly ? 'text-primary' : 'text-red-500']">
|
||||
{{ associationText }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
已选中
|
||||
<span class="text-primary mx-1 font-semibold">
|
||||
{{ checkedRealKeys.length }}
|
||||
</span>
|
||||
个节点
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between border-b-[1px] py-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:checked="expandStatus"
|
||||
@change="handleExpandOrCollapseAll"
|
||||
>
|
||||
<a-button size="small" @click="handleExpandOrCollapseAll">
|
||||
展开/折叠全部
|
||||
</Checkbox>
|
||||
<Checkbox v-model:checked="selectAllStatus" @change="handleExpandChange">
|
||||
</a-button>
|
||||
<Checkbox
|
||||
v-model:checked="selectAllStatus"
|
||||
@change="handleCheckedAllChange"
|
||||
>
|
||||
全选/取消全选
|
||||
</Checkbox>
|
||||
<Checkbox :checked="checkStrictly" @change="handleCheckStrictlyChange">
|
||||
<Checkbox
|
||||
v-model:checked="checkStrictly"
|
||||
@change="handleCheckStrictlyChange"
|
||||
>
|
||||
父子节点关联
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<Tree
|
||||
v-if="treeData.length > 0"
|
||||
v-model:check-strictly="innerCheckedStrictly"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
:check-strictly="!checkStrictly"
|
||||
v-model:checked-keys="computedCheckedKeys"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
:checkable="true"
|
||||
:field-names="fieldNames"
|
||||
:selectable="false"
|
||||
:tree-data="treeData"
|
||||
@check="handleChecked"
|
||||
>
|
||||
<template
|
||||
v-for="slotName in Object.keys(slots)"
|
||||
v-for="slotName in Object.keys($slots)"
|
||||
:key="slotName"
|
||||
#[slotName]="data"
|
||||
>
|
||||
@@ -219,3 +174,20 @@ const slots = useSlots() as SetupContext['slots'];
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-tree) {
|
||||
// 勾选框居中
|
||||
& .ant-tree-checkbox {
|
||||
margin: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
// 展开图标居中
|
||||
& .ant-tree-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,12 +8,14 @@ import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => preferences.app.name);
|
||||
const logo = computed(() => preferences.logo.source);
|
||||
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:logo-dark="logoDark"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"document": "Document",
|
||||
"antdv": "Ant Design Vue Version",
|
||||
"naive-ui": "Naive UI Version",
|
||||
"element-plus": "Element Plus Version"
|
||||
"element-plus": "Element Plus Version",
|
||||
"tdesign": "TDesign Vue Version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password",
|
||||
"oauthLogin": "Oauth Login"
|
||||
"oauthLogin": "Oauth Login",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"document": "文档",
|
||||
"antdv": "Ant Design Vue 版本",
|
||||
"naive-ui": "Naive UI 版本",
|
||||
"element-plus": "Element Plus 版本"
|
||||
"element-plus": "Element Plus 版本",
|
||||
"tdesign": "TDesign Vue 版本"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码",
|
||||
"oauthLogin": "第三方登录"
|
||||
"profile": "个人中心"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
|
||||
@@ -85,6 +85,16 @@ const routes: RouteRecordRaw[] = [
|
||||
order: 9999,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Profile',
|
||||
path: '/profile',
|
||||
component: () => import('#/views/_core/profile/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:user',
|
||||
hideInMenu: true,
|
||||
title: $t('page.auth.profile'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { Input } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
captcha?: string;
|
||||
label?: string;
|
||||
loading?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
captcha: '',
|
||||
label: '验证码',
|
||||
loading: false,
|
||||
placeholder: '请输入验证码',
|
||||
});
|
||||
|
||||
defineEmits<{ captchaClick: [] }>();
|
||||
|
||||
const modelValue = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<!-- 图片验证码 -->
|
||||
<template>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<Input
|
||||
size="large"
|
||||
id="code"
|
||||
name="code"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
v-model:value="modelValue"
|
||||
:class="$attrs?.class ?? {}"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="captcha-image--container relative">
|
||||
<img
|
||||
:src="captcha"
|
||||
class="h-[40px] w-[115px] cursor-pointer rounded-r-lg"
|
||||
:class="{ 'pointer-events-none': loading }"
|
||||
@click="$emit('captchaClick')"
|
||||
/>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute inset-0 flex cursor-not-allowed items-center justify-center rounded-r-lg bg-black/30"
|
||||
>
|
||||
<span class="captcha-loading"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes loading-rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-loading {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #fff;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: loading-rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
/**
|
||||
验证码输入框样式
|
||||
去除右边的圆角
|
||||
*/
|
||||
input[id='code'] {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -4,12 +4,13 @@ import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';
|
||||
import type { TenantResp } from '#/api';
|
||||
import type { CaptchaResponse } from '#/api/core/captcha';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { computed, markRaw, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Input, Select } from 'ant-design-vue';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { tenantList } from '#/api';
|
||||
@@ -17,6 +18,7 @@ import { captchaImage } from '#/api/core/captcha';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { useLoginTenantId } from '../oauth-common';
|
||||
import InputCaptcha from './input-captcha.vue';
|
||||
import OAuthLogin from './oauth-login.vue';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
@@ -73,15 +75,18 @@ const { loginTenantId } = useLoginTenantId();
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
component: markRaw(Select),
|
||||
modelPropName: 'value',
|
||||
componentProps: {
|
||||
class: 'bg-background h-[40px] focus:border-primary',
|
||||
contentClass: 'max-h-[256px] overflow-y-auto',
|
||||
class: 'w-full',
|
||||
size: 'large',
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
options: tenantInfo.value.voList?.map((item) => ({
|
||||
label: item.companyName,
|
||||
value: item.tenantId,
|
||||
})),
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
placeholder: $t('ui.formRules.selectRequired'),
|
||||
},
|
||||
defaultValue: DEFAULT_TENANT_ID,
|
||||
dependencies: {
|
||||
@@ -98,10 +103,12 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
rules: z.string().min(1, { message: $t('authentication.selectAccount') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
component: markRaw(Input),
|
||||
modelPropName: 'value',
|
||||
componentProps: {
|
||||
class: 'focus:border-primary',
|
||||
size: 'large',
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
allowClear: true,
|
||||
},
|
||||
defaultValue: 'admin',
|
||||
fieldName: 'username',
|
||||
@@ -109,10 +116,11 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
component: markRaw(Input.Password),
|
||||
modelPropName: 'value',
|
||||
componentProps: {
|
||||
class: 'focus:border-primary',
|
||||
placeholder: $t('authentication.password'),
|
||||
size: 'large',
|
||||
placeholder: $t('authentication.passwordTip'),
|
||||
},
|
||||
defaultValue: 'admin123',
|
||||
fieldName: 'password',
|
||||
@@ -120,7 +128,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
rules: z.string().min(5, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputCaptcha',
|
||||
component: markRaw(InputCaptcha),
|
||||
componentProps: {
|
||||
captcha: captchaInfo.value.img,
|
||||
class: 'focus:border-primary',
|
||||
|
||||
65
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
65
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { ProfileBaseSetting } from '@vben/common-ui';
|
||||
|
||||
import { getUserInfoApi } from '#/api';
|
||||
|
||||
const profileBaseSettingRef = ref();
|
||||
|
||||
const MOCK_ROLES_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: '管理员',
|
||||
value: 'super',
|
||||
},
|
||||
{
|
||||
label: '用户',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
label: '测试',
|
||||
value: 'test',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'realName',
|
||||
component: 'Input',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
component: 'Input',
|
||||
label: '用户名',
|
||||
},
|
||||
{
|
||||
fieldName: 'roles',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
options: MOCK_ROLES_OPTIONS,
|
||||
},
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
fieldName: 'introduction',
|
||||
component: 'Textarea',
|
||||
label: '个人简介',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await getUserInfoApi();
|
||||
profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '其他用户的消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'systemMessage',
|
||||
label: '系统消息',
|
||||
description: '系统消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'todoTask',
|
||||
label: '待办任务',
|
||||
description: '待办任务将以站内信的形式通知',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||
</template>
|
||||
63
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
63
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'oldPassword',
|
||||
label: '旧密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入旧密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'newPassword',
|
||||
label: '新密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请输入新密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'confirmPassword',
|
||||
label: '确认密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请再次输入新密码',
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { newPassword } = values;
|
||||
return z
|
||||
.string({ required_error: '请再次输入新密码' })
|
||||
.min(1, { message: '请再次输入新密码' })
|
||||
.refine((value) => value === newPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
});
|
||||
},
|
||||
triggerFields: ['newPassword'],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
message.success('密码修改成功');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ProfilePasswordSetting
|
||||
class="w-1/3"
|
||||
:form-schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileSecuritySetting } from '@vben/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '当前密码强度:强',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityPhone',
|
||||
label: '密保手机',
|
||||
description: '已绑定手机:138****8293',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityQuestion',
|
||||
label: '密保问题',
|
||||
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityEmail',
|
||||
label: '备用邮箱',
|
||||
description: '已绑定邮箱:ant***sign.com',
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
fieldName: 'securityMfa',
|
||||
label: 'MFA 设备',
|
||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { DEFAULT_TENANT_ID, LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { cn } from '@vben/utils';
|
||||
|
||||
@@ -62,6 +63,9 @@ onMounted(async () => {
|
||||
if (accessStore.accessToken) {
|
||||
await authCallback(data);
|
||||
message.success(`${source}授权成功`);
|
||||
setTimeout(() => {
|
||||
router.push(preferences.app.defaultHomePath);
|
||||
}, 1500);
|
||||
} else {
|
||||
// 这里内部已经做了跳转到首页的操作
|
||||
await authStore.authLogin(data as any);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
@@ -23,7 +24,7 @@ onMounted(() => {
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
].toSorted((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
|
||||
@@ -115,9 +115,14 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
|
||||
if (id) {
|
||||
await formApi.setFieldValue('parentId', id);
|
||||
if (update) {
|
||||
// 没有依赖关系 同时加载
|
||||
const [record] = await Promise.all([menuInfo(id), setupMenuSelect()]);
|
||||
// 创建元组(不是数组 元素位置固定)
|
||||
const promise = [
|
||||
update ? menuInfo(id) : null,
|
||||
setupMenuSelect(),
|
||||
] as const;
|
||||
// 并行获取菜单树选择和菜单信息
|
||||
const [record] = await Promise.all(promise);
|
||||
if (record) {
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -97,11 +97,19 @@ async function handleClosed() {
|
||||
<BasicForm>
|
||||
<template #tip>
|
||||
<div class="ml-7 w-full">
|
||||
<Alert
|
||||
message="私有桶使用自定义域名无法预览, 但可以正常上传/下载"
|
||||
show-icon
|
||||
type="warning"
|
||||
/>
|
||||
<Alert show-icon type="warning">
|
||||
<template #message>
|
||||
私有桶(minio)使用自定义域名需要参考
|
||||
<a
|
||||
href="https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IBQIKC"
|
||||
target="_blank"
|
||||
class="text-primary"
|
||||
>
|
||||
支持minio预览私有桶
|
||||
</a>
|
||||
, 否则无法预览
|
||||
</template>
|
||||
</Alert>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import { DefaultSlot } from '#/components/global/slot';
|
||||
import { TreeSelectPanel } from '#/components/tree';
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
|
||||
/**
|
||||
@@ -177,15 +181,6 @@ export const authModalSchemas: FormSchemaGetter = () => [
|
||||
fieldName: 'roleId',
|
||||
label: '角色ID',
|
||||
},
|
||||
{
|
||||
component: 'Radio',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'deptCheckStrictly',
|
||||
label: 'deptCheckStrictly',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
@@ -214,12 +209,39 @@ export const authModalSchemas: FormSchemaGetter = () => [
|
||||
label: '权限范围',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
component: 'Radio',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'deptCheckStrictly',
|
||||
label: 'deptCheckStrictly',
|
||||
},
|
||||
{
|
||||
// 这种的场景基本上是一个组件需要绑定两个或以上的场景
|
||||
component: markRaw(DefaultSlot),
|
||||
defaultValue: [],
|
||||
componentProps: {
|
||||
rootDivAttrs: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => values.dataScope === '2',
|
||||
triggerFields: ['dataScope'],
|
||||
},
|
||||
renderComponentContent: (model) => ({
|
||||
default: (attrs: any) => {
|
||||
return (
|
||||
<TreeSelectPanel
|
||||
expand-all-on-init={true}
|
||||
treeData={attrs.treeData}
|
||||
v-model:checkStrictly={model.deptCheckStrictly}
|
||||
v-model:value={model.deptIds}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
fieldName: 'deptIds',
|
||||
help: '更改后立即生效',
|
||||
label: '部门权限',
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptOption } from '#/api/system/role/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
import { cloneDeep, findGroupParentIds } from '@vben/utils';
|
||||
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role';
|
||||
import { TreeSelectPanel } from '#/components/tree';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
|
||||
import { authModalSchemas } from './data';
|
||||
@@ -26,26 +25,32 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const deptTree = ref<DeptOption[]>([]);
|
||||
/**
|
||||
* 保存部门数据 用于获取祖先节点
|
||||
*/
|
||||
let treeData: DeptOption[] = [];
|
||||
async function setupDeptTree(id: number | string) {
|
||||
const resp = await roleDeptTree(id);
|
||||
formApi.setFieldValue('deptIds', resp.checkedKeys);
|
||||
// 设置菜单信息
|
||||
deptTree.value = resp.depts;
|
||||
}
|
||||
const { checkedKeys, depts } = resp;
|
||||
|
||||
async function customFormValueGetter() {
|
||||
const v = await defaultFormValueGetter(formApi)();
|
||||
// 获取勾选信息
|
||||
const menuIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
||||
const mixStr = v + menuIds.join(',');
|
||||
return mixStr;
|
||||
/**
|
||||
* 设置部门树数据
|
||||
*/
|
||||
formApi.updateSchema([
|
||||
{ fieldName: 'deptIds', componentProps: { treeData: depts } },
|
||||
]);
|
||||
/**
|
||||
* 设置选中 必须先传递treeData
|
||||
* Note: Tree missing follow keys: '1981565541727186945'
|
||||
*/
|
||||
await formApi.setFieldValue('deptIds', checkedKeys);
|
||||
treeData = depts;
|
||||
}
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: customFormValueGetter,
|
||||
currentGetter: customFormValueGetter,
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -56,14 +61,14 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
onConfirm: handleConfirm,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
treeData = [];
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { id } = modalApi.getData() as { id: number | string };
|
||||
|
||||
setupDeptTree(id);
|
||||
const record = await roleInfo(id);
|
||||
const [record] = await Promise.all([roleInfo(id), setupDeptTree(id)]);
|
||||
await formApi.setValues(record);
|
||||
markInitialized();
|
||||
|
||||
@@ -71,11 +76,6 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里拿到的是一个数组ref
|
||||
*/
|
||||
const deptSelectRef = ref();
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
@@ -87,7 +87,15 @@ async function handleConfirm() {
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 不为自定义权限的话 删除部门id
|
||||
if (data.dataScope === '2') {
|
||||
const deptIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
||||
let { deptIds, deptCheckStrictly } = data;
|
||||
// 节点关联 需要拼接上祖级ID(获取的是不带的)
|
||||
if (deptCheckStrictly) {
|
||||
// 找到所有父级ID
|
||||
const parentIds = findGroupParentIds(treeData, deptIds, { id: 'id' });
|
||||
// 去重
|
||||
deptIds = uniq([...parentIds, ...deptIds]);
|
||||
}
|
||||
// 赋值
|
||||
data.deptIds = deptIds;
|
||||
} else {
|
||||
data.deptIds = [];
|
||||
@@ -107,29 +115,10 @@ async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过回调更新 无法通过v-model
|
||||
* @param value 菜单选择是否严格模式
|
||||
*/
|
||||
function handleCheckStrictlyChange(value: boolean) {
|
||||
formApi.setFieldValue('deptCheckStrictly', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal class="min-h-[600px] w-[550px]" title="分配权限">
|
||||
<BasicForm>
|
||||
<template #deptIds="slotProps">
|
||||
<TreeSelectPanel
|
||||
ref="deptSelectRef"
|
||||
v-bind="slotProps"
|
||||
:check-strictly="formApi.form.values.deptCheckStrictly"
|
||||
:expand-all-on-init="true"
|
||||
:tree-data="deptTree"
|
||||
@check-strictly-change="handleCheckStrictlyChange"
|
||||
/>
|
||||
</template>
|
||||
</BasicForm>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
dictSyncTenant,
|
||||
syncTenantConfig,
|
||||
tenantExport,
|
||||
tenantList,
|
||||
tenantRemove,
|
||||
@@ -144,6 +145,18 @@ function handleSyncTenantDict() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSyncTenantConfig() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
iconType: 'warning',
|
||||
content: '确认同步租户参数配置?',
|
||||
onOk: async () => {
|
||||
await syncTenantConfig();
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -157,6 +170,12 @@ function handleSyncTenantDict() {
|
||||
>
|
||||
同步租户字典
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:tenant:edit']"
|
||||
@click="handleSyncTenantConfig"
|
||||
>
|
||||
同步租户参数配置
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:tenant:export']"
|
||||
@click="handleDownloadExcel"
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
<script setup lang="ts">
|
||||
import type { ApprovalType } from '../type';
|
||||
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, h } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MenuOutlined,
|
||||
RollbackOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Dropdown, Menu, MenuItem, Modal, Space } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '..';
|
||||
import { approveWithReasonModal } from '../helper';
|
||||
import userSelectModal from '../user-select-modal.vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 行数据的taskInfo?
|
||||
*/
|
||||
task?: TaskInfo;
|
||||
/**
|
||||
* 审批类型 根据不同类型显示按钮
|
||||
*/
|
||||
type: ApprovalType;
|
||||
/**
|
||||
* 为审批类型时候 显示的按钮(按钮权限)
|
||||
*/
|
||||
buttonPermissions: Record<string, boolean>;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
reload: [];
|
||||
}>();
|
||||
|
||||
// 是否显示 `其他` 按钮
|
||||
const showButtonOther = computed(() => {
|
||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||
return Object.keys(props.buttonPermissions).some(
|
||||
(key) => moreCollections.has(key) && props.buttonPermissions[key],
|
||||
);
|
||||
});
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
const { buttonPermissions } = props;
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions?.copy ?? false;
|
||||
// 是否具有选人权限
|
||||
const assignPermission = buttonPermissions?.pop ?? false;
|
||||
approvalModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
copyPermission,
|
||||
assignPermission,
|
||||
});
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
if (Number(props.task.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute bottom-0 left-0',
|
||||
'border-t-solid border-t-[1px]',
|
||||
'bg-background w-full p-3',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(RollbackOutlined)"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
v-if="editableAndRemoveable"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleEdit"
|
||||
>
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
:icon="h(CheckOutlined)"
|
||||
@click="handleApproval"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ExclamationCircleOutlined)"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ArrowLeftOutlined)"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
<UserOutlined class="mr-2" />委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
<RollbackOutlined class="mr-2" /> 转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupAddOutlined class="mr-2" /> 加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupDeleteOutlined class="mr-2" /> 减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther" :icon="h(MenuOutlined)">
|
||||
其他
|
||||
</a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FlowActions } from './flow-actions.vue';
|
||||
@@ -43,6 +43,8 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId } = modalApi.getData() as ModalProps;
|
||||
|
||||
// 查询是否有按钮权限
|
||||
@@ -63,6 +65,8 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,6 +112,11 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
// 默认不显示
|
||||
dependencies: {
|
||||
if: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
|
||||
@@ -20,8 +20,6 @@ defineOptions({
|
||||
|
||||
defineProps<{
|
||||
currentFlowInfo: FlowInfoResponse;
|
||||
iframeHeight: number;
|
||||
iframeLoaded: boolean;
|
||||
task: TaskInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -1,117 +1,72 @@
|
||||
<!-- 该文件需要重构 但我没空 -->
|
||||
<!--
|
||||
TODO: 优化项
|
||||
会先加载流程信息 再加载业务表单信息
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { ApprovalType } from './type';
|
||||
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, h, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
import { Fallback, VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
import { cn } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MenuOutlined,
|
||||
RollbackOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useClipboard, useEventListener } from '@vueuse/core';
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Card, Divider, message, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
flowInfo,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
import { flowInfo } from '#/api/workflow/instance';
|
||||
import { getTaskByTaskId } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
||||
import { FlowActions } from './actions';
|
||||
import ApprovalDetails from './approval-details.vue';
|
||||
import FlowPreview from './flow-preview.vue';
|
||||
import { approveWithReasonModal } from './helper';
|
||||
import userSelectModal from './user-select-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalPanel',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
/**
|
||||
* 下面按钮点击后会触发的事件
|
||||
*/
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
defineEmits<{ reload: [] }>();
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 行数据(list)的info
|
||||
*/
|
||||
task?: TaskInfo;
|
||||
/**
|
||||
* 审批类型
|
||||
*/
|
||||
type: ApprovalType;
|
||||
}
|
||||
|
||||
const currentTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
* 目前的作用只为了获取按钮权限 因为list接口(行数据)获取为空
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!currentTask.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(currentTask.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const onlyForBtnPermissionTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 按钮权限
|
||||
*/
|
||||
const buttonPermissions = computed(() => {
|
||||
const record: Record<string, boolean> = {};
|
||||
if (!currentTask.value) {
|
||||
if (!onlyForBtnPermissionTask.value) {
|
||||
return record;
|
||||
}
|
||||
currentTask.value.buttonList.forEach((item) => {
|
||||
onlyForBtnPermissionTask.value.buttonList.forEach((item) => {
|
||||
record[item.code] = item.show;
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
// 是否显示 `其他` 按钮
|
||||
const showButtonOther = computed(() => {
|
||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||
return Object.keys(buttonPermissions.value).some(
|
||||
(key) => moreCollections.has(key) && buttonPermissions.value[key],
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
const showFooter = computed(() => {
|
||||
if (props.type === 'readonly') {
|
||||
return false;
|
||||
@@ -131,36 +86,34 @@ const currentFlowInfo = ref<FlowInfoResponse>();
|
||||
* card的loading状态
|
||||
*/
|
||||
const loading = ref(false);
|
||||
const iframeLoaded = ref(false);
|
||||
const iframeHeight = ref(300);
|
||||
useEventListener('message', (event) => {
|
||||
const data = event.data as { [key: string]: any; type: string };
|
||||
if (!isObject(data)) return;
|
||||
/**
|
||||
* iframe通信 加载完毕后才显示表单 解决卡顿问题
|
||||
*/
|
||||
if (data.type === 'mounted') {
|
||||
iframeLoaded.value = true;
|
||||
}
|
||||
/**
|
||||
* 高度与表单高度保持一致
|
||||
*/
|
||||
if (data.type === 'height') {
|
||||
const height = data.height;
|
||||
iframeHeight.value = height;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!task) return null;
|
||||
loading.value = true;
|
||||
iframeLoaded.value = false;
|
||||
const resp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = resp;
|
||||
|
||||
const taskResp = await getTaskByTaskId(props.task!.id);
|
||||
currentTask.value = taskResp;
|
||||
/**
|
||||
* 不为审批不需要调用`getTaskByTaskId`接口
|
||||
*/
|
||||
if (props.type !== 'approve') {
|
||||
const flowResp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = flowResp;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* getTaskByTaskId主要为了获取按钮权限 目前没有其他功能
|
||||
* 行数据(即props.task)获取的是没有按钮权限的
|
||||
*/
|
||||
const [flowResp, taskResp] = await Promise.all([
|
||||
flowInfo(task.businessId),
|
||||
getTaskByTaskId(task.id),
|
||||
]);
|
||||
|
||||
currentFlowInfo.value = flowResp;
|
||||
onlyForBtnPermissionTask.value = taskResp;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
@@ -170,217 +123,6 @@ async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
|
||||
watch(() => props.task, handleLoadInfo);
|
||||
|
||||
onUnmounted(() => (currentFlowInfo.value = undefined));
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions.value?.copy ?? false;
|
||||
// 是否具有选人权限
|
||||
const assignPermission = buttonPermissions.value?.pop ?? false;
|
||||
approvalModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
copyPermission,
|
||||
assignPermission,
|
||||
});
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 1提取公共函数 2原版是可以填写意见的(message参数)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 不加legacy在本地开发没有问题
|
||||
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
||||
@@ -407,6 +149,7 @@ async function handleCopy(text: string) {
|
||||
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-button size="small" @click="() => handleLoadInfo(task)">
|
||||
<div class="flex items-center justify-center">
|
||||
@@ -414,6 +157,7 @@ async function handleCopy(text: string) {
|
||||
</div>
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -426,19 +170,24 @@ async function handleCopy(text: string) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="task?.createByName ?? ''"
|
||||
class="bg-primary size-[28px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
|
||||
<span>{{ task.createByName }}</span>
|
||||
|
||||
<div class="flex items-center opacity-50">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
||||
流程分类: {{ task.categoryName }}
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
提交时间: {{ task.createTime }}
|
||||
@@ -446,154 +195,32 @@ async function handleCopy(text: string) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<ApprovalDetails
|
||||
:current-flow-info="currentFlowInfo"
|
||||
:iframe-loaded="iframeLoaded"
|
||||
:iframe-height="iframeHeight"
|
||||
:task="task"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="2" tab="审批流程图">
|
||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
|
||||
<!-- 固定底部 占位高度 -->
|
||||
<div class="h-[58px]"></div>
|
||||
<div
|
||||
<FlowActions
|
||||
v-if="showFooter"
|
||||
:class="
|
||||
cn(
|
||||
'absolute bottom-0 left-0',
|
||||
'border-t-solid border-t-[1px]',
|
||||
'bg-background w-full p-3',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(RollbackOutlined)"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
v-if="editableAndRemoveable"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleEdit"
|
||||
>
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
:icon="h(CheckOutlined)"
|
||||
@click="handleApproval"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ExclamationCircleOutlined)"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ArrowLeftOutlined)"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
<UserOutlined class="mr-2" />委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
<RollbackOutlined class="mr-2" /> 转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupAddOutlined class="mr-2" /> 加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupDeleteOutlined class="mr-2" /> 减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther" :icon="h(MenuOutlined)">
|
||||
其他
|
||||
</a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
:type="type"
|
||||
:task="task"
|
||||
:button-permissions="buttonPermissions"
|
||||
@reload="$emit('reload')"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<slot v-else name="empty">
|
||||
<Fallback title="点击左侧选择" />
|
||||
</slot>
|
||||
|
||||
@@ -45,11 +45,8 @@ onMounted(async () => {
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里无法处理昵称中带,的情况
|
||||
*/
|
||||
const isMultiplePerson = computed(
|
||||
() => props.item.approveName?.split(',').length > 1,
|
||||
() => props.item.approver?.split(',').length > 1,
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -87,6 +84,7 @@ const isMultiplePerson = computed(
|
||||
</div>
|
||||
|
||||
<div :class="cn('mt-2 flex flex-wrap gap-2')" v-if="isMultiplePerson">
|
||||
<!-- 如果昵称中带, 这里的处理是不准确的 -->
|
||||
<div
|
||||
:class="cn('bg-foreground/5 flex items-center rounded-full', 'p-1')"
|
||||
v-for="(name, index) in item.approveName.split(',')"
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
import { Empty, Timeline } from 'ant-design-vue';
|
||||
|
||||
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
list: Flow[];
|
||||
}>();
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Timeline v-if="props.list.length > 0">
|
||||
<ApprovalTimelineItem
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
/>
|
||||
<Timeline v-if="list.length > 0">
|
||||
<ApprovalTimelineItem v-for="item in list" :key="item.id" :item="item" />
|
||||
</Timeline>
|
||||
<Empty v-else />
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- 流程图预览组件 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
@@ -7,7 +9,14 @@ import { useWarmflowIframe } from './hook';
|
||||
|
||||
defineOptions({ name: 'FlowPreview' });
|
||||
|
||||
const props = defineProps<{ instanceId: string }>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 流程实例ID
|
||||
*/
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
@@ -21,6 +30,7 @@ const params = {
|
||||
|
||||
/**
|
||||
* iframe地址
|
||||
* 后端地址 + 固定flow地址拼接
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
|
||||
@@ -28,5 +38,9 @@ const { iframeRef } = useWarmflowIframe();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe ref="iframeRef" :src="url" class="h-[500px] w-full border"></iframe>
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="url"
|
||||
class="h-[600px] w-full rounded-[6px] border"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
@@ -10,20 +10,26 @@ export function useWarmflowIframe() {
|
||||
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
|
||||
const { isDark } = usePreferences();
|
||||
|
||||
async function iframeLoadEvent() {
|
||||
/**
|
||||
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
|
||||
*/
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const theme = isDark.value ? 'theme-dark' : 'theme-light';
|
||||
iframeRef.value?.contentWindow?.postMessage({ type: theme });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/**
|
||||
* load只是iframe加载完 而非vue加载完
|
||||
*/
|
||||
iframeRef.value?.addEventListener('load', async () => {
|
||||
/**
|
||||
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
|
||||
*/
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const theme = isDark.value ? 'theme-dark' : 'theme-light';
|
||||
iframeRef.value?.contentWindow?.postMessage({ type: theme });
|
||||
});
|
||||
iframeRef.value?.addEventListener('load', iframeLoadEvent);
|
||||
});
|
||||
|
||||
// onBeforeUnmount(() => {
|
||||
// iframeRef.value?.removeEventListener('load', iframeLoadEvent);
|
||||
// });
|
||||
|
||||
// 监听主题切换 通知iframe切换
|
||||
watch(isDark, (dark) => {
|
||||
if (!iframeRef.value) {
|
||||
|
||||
8
apps/web-antd/src/views/workflow/components/type.d.ts
vendored
Normal file
8
apps/web-antd/src/views/workflow/components/type.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export {};
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批(我的待办)
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
export type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
@@ -150,9 +150,10 @@ const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
|
||||
connectedComponent: instanceVariableModal,
|
||||
});
|
||||
function handleVariable(row: Recordable<any>) {
|
||||
instanceVariableModalApi.setData({ record: row.variable });
|
||||
instanceVariableModalApi.setData({ instanceId: row.id });
|
||||
instanceVariableModalApi.open();
|
||||
}
|
||||
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
connectedComponent: flowInfoModal,
|
||||
});
|
||||
|
||||
@@ -1,28 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { message, Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';
|
||||
|
||||
interface ModalData {
|
||||
/**
|
||||
* 变量 json字符串
|
||||
*/
|
||||
record: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
const data = ref({});
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程变量',
|
||||
fullscreenButton: false,
|
||||
footer: false,
|
||||
onOpenChange: (visible) => {
|
||||
onOpenChange: async (visible) => {
|
||||
if (!visible) {
|
||||
data.value = {};
|
||||
return null;
|
||||
}
|
||||
const recordString = modalApi.getData().record;
|
||||
data.value = JSON.parse(recordString);
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
await loadData();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const fieldTypeColors = {
|
||||
string: 'cyan',
|
||||
number: 'blue',
|
||||
boolean: 'orange',
|
||||
object: 'purple',
|
||||
};
|
||||
function getFieldTypeColor(fieldType: string) {
|
||||
return (
|
||||
fieldTypeColors[fieldType as keyof typeof fieldTypeColors] ?? 'default'
|
||||
);
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const { instanceId } = modalApi.getData() as ModalData;
|
||||
const resp = await instanceVariable(instanceId);
|
||||
const jsonObj = JSON.parse(resp.variable);
|
||||
data.value = jsonObj;
|
||||
|
||||
// 表单
|
||||
const objEntry = Object.entries(jsonObj);
|
||||
|
||||
interface OptionsType {
|
||||
label: string;
|
||||
value: string;
|
||||
fieldType: string;
|
||||
}
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'key',
|
||||
componentProps: {
|
||||
options: objEntry.map(
|
||||
([key, value]) =>
|
||||
({
|
||||
label: key,
|
||||
value: key,
|
||||
fieldType: typeof value,
|
||||
}) as OptionsType,
|
||||
),
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
option: (option: OptionsType) => (
|
||||
<div>
|
||||
{option.label}
|
||||
<Tag class="ml-1" color={getFieldTypeColor(option.fieldType)}>
|
||||
{option.fieldType}
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'key',
|
||||
component: 'Select',
|
||||
label: '变量名称',
|
||||
rules: 'selectRequired',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'valueType',
|
||||
component: 'Select',
|
||||
label: '变量类型',
|
||||
rules: 'selectRequired',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
options: [
|
||||
{
|
||||
label: 'string',
|
||||
value: 'string',
|
||||
},
|
||||
{
|
||||
label: 'boolean | number | object (使用JSON.parse)',
|
||||
value: 'object',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
component: 'Input',
|
||||
label: '变量值',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
submitButtonOptions: {
|
||||
content: '修改',
|
||||
},
|
||||
handleSubmit: async (values) => {
|
||||
console.log(values);
|
||||
Modal.confirm({
|
||||
title: '修改流程变量',
|
||||
content: '确认修改流程变量吗?',
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await handleSubmit(values);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit(values: any) {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
|
||||
const { instanceId } = modalApi.getData() as ModalData;
|
||||
|
||||
let transformValue = values.value;
|
||||
if (values.valueType !== 'string') {
|
||||
try {
|
||||
transformValue = JSON.parse(values.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改
|
||||
const requestData = {
|
||||
instanceId,
|
||||
key: values.key,
|
||||
value: transformValue,
|
||||
};
|
||||
await updateFlowVariable(requestData);
|
||||
await formApi.resetForm();
|
||||
|
||||
// 查询修改后的
|
||||
const resp = await instanceVariable(instanceId);
|
||||
const jsonObj = JSON.parse(resp.variable);
|
||||
data.value = jsonObj;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="min-h-[400px] overflow-y-auto">
|
||||
<div
|
||||
:class="cn('min-h-[400px] overflow-y-auto border', 'rounded-[4px] p-2')"
|
||||
>
|
||||
<JsonPreview :data="data" />
|
||||
</div>
|
||||
<div class="mt-2 break-all text-sm font-medium text-orange-500">
|
||||
需要支持变量类型需要更改后端代码(原版只支持string类型)
|
||||
<div>
|
||||
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowVariableBo.java
|
||||
</div>
|
||||
将value的类型改为Object才能使用
|
||||
</div>
|
||||
<Form class="mt-2" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@@ -10,8 +10,7 @@ import { getVxePopupContainer } from '@vben/utils';
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import { configRemove } from '#/api/system/config';
|
||||
import { spelList } from '#/api/workflow/spel';
|
||||
import { spelList,spelDelete } from '#/api/workflow/spel';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import spelDrawer from './spel-drawer.vue';
|
||||
@@ -75,7 +74,7 @@ async function handleEdit(record: Spel) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Spel) {
|
||||
await configRemove([row.id]);
|
||||
await spelDelete([row.id]);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
@@ -87,7 +86,7 @@ function handleMultiDelete() {
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await configRemove(ids);
|
||||
await spelDelete(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
|
||||
25
cspell.json
25
cspell.json
@@ -7,14 +7,19 @@
|
||||
"acmr",
|
||||
"antd",
|
||||
"antdv",
|
||||
"archiver",
|
||||
"astro",
|
||||
"axios",
|
||||
"brotli",
|
||||
"clientid",
|
||||
"cascader",
|
||||
"clsx",
|
||||
"defu",
|
||||
"demi",
|
||||
"dotenv",
|
||||
"echarts",
|
||||
"ependencies",
|
||||
"esbuild",
|
||||
"esno",
|
||||
"etag",
|
||||
"execa",
|
||||
@@ -24,6 +29,8 @@
|
||||
"intlify",
|
||||
"ipaddr",
|
||||
"jsencrypt",
|
||||
"isequal",
|
||||
"jspm",
|
||||
"lockb",
|
||||
"logininfor",
|
||||
"lucide",
|
||||
@@ -32,7 +39,9 @@
|
||||
"mkdist",
|
||||
"mockjs",
|
||||
"naiveui",
|
||||
"napi",
|
||||
"nocheck",
|
||||
"nolebase",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"nprogress",
|
||||
@@ -45,11 +54,14 @@
|
||||
"Qqchat",
|
||||
"qrcode",
|
||||
"ruoyi",
|
||||
"reka",
|
||||
"rollup",
|
||||
"shadcn",
|
||||
"sonner",
|
||||
"sortablejs",
|
||||
"styl",
|
||||
"taze",
|
||||
"tdesign",
|
||||
"ui-kit",
|
||||
"uicons",
|
||||
"unplugin",
|
||||
@@ -59,19 +71,20 @@
|
||||
"vite",
|
||||
"vitejs",
|
||||
"vitepress",
|
||||
"vitest",
|
||||
"vnode",
|
||||
"vueuse",
|
||||
"yxxx"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/*-dist/**",
|
||||
"**/icons/**",
|
||||
"pnpm-lock.yaml",
|
||||
"**/*.log",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/__tests__/**"
|
||||
"**/*.test.ts",
|
||||
"**/__tests__/**",
|
||||
"**/dist/**",
|
||||
"**/icons/**",
|
||||
"**/node_modules/**",
|
||||
"pnpm-lock.yaml"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,15 +19,15 @@ const parsedFiles = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-border shadow-float relative rounded-xl border">
|
||||
<div class="relative rounded-xl border border-border shadow-float">
|
||||
<div
|
||||
class="not-prose relative w-full overflow-x-auto rounded-t-lg px-4 py-6"
|
||||
>
|
||||
<div class="flex w-full max-w-[700px] px-2">
|
||||
<ClientOnly>
|
||||
<slot v-if="parsedFiles.length > 0"></slot>
|
||||
<div v-else class="text-destructive text-sm">
|
||||
<span class="bg-destructive text-foreground rounded-sm px-1 py-1">
|
||||
<div v-else class="text-sm text-destructive">
|
||||
<span class="rounded-sm bg-destructive px-1 py-1 text-foreground">
|
||||
ERROR:
|
||||
</span>
|
||||
The preview directory does not exist. Please check the 'dir'
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TabsList,
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from 'radix-vue';
|
||||
} from 'reka-ui';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
@@ -48,15 +48,15 @@ const toggleOpen = () => {
|
||||
<template>
|
||||
<TabsRoot
|
||||
v-model="currentTab"
|
||||
class="bg-background-deep border-border overflow-hidden rounded-b-xl border-t"
|
||||
class="overflow-hidden rounded-b-xl border-t border-border bg-background-deep"
|
||||
@update:model-value="open = true"
|
||||
>
|
||||
<div class="border-border bg-background flex border-b-2 pr-2">
|
||||
<div class="flex border-b-2 border-border bg-background pr-2">
|
||||
<div class="flex w-full items-center justify-between text-[13px]">
|
||||
<TabsList class="relative flex">
|
||||
<template v-if="open">
|
||||
<TabsIndicator
|
||||
class="absolute bottom-0 left-0 h-[2px] w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] rounded-full transition-[width,transform] duration-300"
|
||||
class="absolute bottom-0 left-0 h-[2px] w-[--reka-tabs-indicator-size] translate-x-[--reka-tabs-indicator-position] rounded-full transition-[width,transform] duration-300"
|
||||
>
|
||||
<div class="size-full bg-[var(--vp-c-indigo-1)]"></div>
|
||||
</TabsIndicator>
|
||||
@@ -64,7 +64,7 @@ const toggleOpen = () => {
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:value="tab.label"
|
||||
class="border-box text-foreground px-4 py-3 data-[state=active]:text-[var(--vp-c-indigo-1)]"
|
||||
class="border-box px-4 py-3 text-foreground data-[state=active]:text-[var(--vp-c-indigo-1)]"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{ tab.label }}
|
||||
@@ -81,7 +81,7 @@ const toggleOpen = () => {
|
||||
<VbenTooltip side="top">
|
||||
<template #trigger>
|
||||
<Code
|
||||
class="hover:bg-accent size-7 cursor-pointer rounded-full p-1.5"
|
||||
class="size-7 cursor-pointer rounded-full p-1.5 hover:bg-accent"
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
@@ -101,7 +101,7 @@ const toggleOpen = () => {
|
||||
as-child
|
||||
class="rounded-xl"
|
||||
>
|
||||
<div class="text-foreground relative rounded-xl">
|
||||
<div class="relative rounded-xl text-foreground">
|
||||
<component :is="tab.component" class="border-0" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -84,7 +84,7 @@ export const demoPreviewPlugin = (md: MarkdownRenderer) => {
|
||||
return '';
|
||||
}
|
||||
const firstString = 'index.vue';
|
||||
childFiles = childFiles.sort((a, b) => {
|
||||
childFiles = childFiles.toSorted((a, b) => {
|
||||
if (a === firstString) return -1;
|
||||
if (b === firstString) return 1;
|
||||
return a.localeCompare(b, 'en', { sensitivity: 'base' });
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"ant-design-vue": "catalog:",
|
||||
"lucide-vue-next": "catalog:",
|
||||
"medium-zoom": "catalog:",
|
||||
"radix-vue": "catalog:",
|
||||
"reka-ui": "catalog:",
|
||||
"vitepress-plugin-group-icons": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -40,9 +40,10 @@ if (!import.meta.env.SSR) {
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
return h(Image, { src: row[column.field], ...props });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -335,6 +335,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
|
||||
| handleCollapsedChange | 表单收起展开状态变化回调 | `(collapsed: boolean) => void` | - |
|
||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||
|
||||
@@ -32,7 +32,7 @@ function handleUpdate(len: number) {
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
|
||||
class="flex-center h-[220px] w-full bg-muted even:bg-heavy"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ function handleUpdate(len: number) {
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
|
||||
class="flex-center h-[220px] w-full bg-muted even:bg-heavy"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@ const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
<Image :src="row.imageUrl" height="30" width="30" />
|
||||
</template>
|
||||
<template #open="{ row }">
|
||||
<Switch v-model:checked="row.open" />
|
||||
<Switch v-model="row.open" />
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.color">{{ row.status }}</Tag>
|
||||
|
||||
@@ -60,6 +60,8 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
|
||||
"build:ele": "pnpm run build --filter=@vben/web-ele",
|
||||
// Build the web-naive application separately
|
||||
"build:naive": "pnpm run build --filter=@vben/naive",
|
||||
// Build the web-tdesign application separately
|
||||
"build:tdesign": "pnpm run build --filter=@vben/web-tdesign",
|
||||
// Build the playground application separately
|
||||
"build:play": "pnpm run build --filter=@vben/playground",
|
||||
// Changeset version management
|
||||
|
||||
@@ -261,6 +261,7 @@ const defaultPreferences: Preferences = {
|
||||
enable: true,
|
||||
fit: 'contain',
|
||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
// sourceDark: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-dark.webp', // Optional: Dark theme logo
|
||||
},
|
||||
navigation: {
|
||||
accordion: true,
|
||||
@@ -457,6 +458,8 @@ interface LogoPreferences {
|
||||
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
/** Logo URL */
|
||||
source: string;
|
||||
/** Dark theme logo URL (optional, if not set, use source) */
|
||||
sourceDark?: string;
|
||||
}
|
||||
|
||||
interface NavigationPreferences {
|
||||
|
||||
@@ -56,6 +56,7 @@ After slimming down, you may need to adjust commands according to your project.
|
||||
"build:docs": "pnpm run build --filter=@vben/docs",
|
||||
"build:ele": "pnpm run build --filter=@vben/web-ele",
|
||||
"build:naive": "pnpm run build --filter=@vben/web-naive",
|
||||
"build:tdesign": "pnpm run build --filter=@vben/web-tdesign",
|
||||
"build:play": "pnpm run build --filter=@vben/playground",
|
||||
"dev:antd": "pnpm -F @vben/web-antd run dev",
|
||||
"dev:docs": "pnpm -F @vben/docs run dev",
|
||||
|
||||
@@ -60,6 +60,8 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
|
||||
"build:ele": "pnpm run build --filter=@vben/web-ele",
|
||||
// 单独构建 web-naive 应用
|
||||
"build:naive": "pnpm run build --filter=@vben/naive",
|
||||
// 单独构建 web-tdesign 应用
|
||||
"build:tdesign": "pnpm run build --filter=@vben/web-tdesign",
|
||||
// 单独构建 playground 应用
|
||||
"build:play": "pnpm run build --filter=@vben/playground",
|
||||
// changeset 版本管理
|
||||
|
||||
@@ -260,6 +260,7 @@ const defaultPreferences: Preferences = {
|
||||
enable: true,
|
||||
fit: 'contain',
|
||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
// sourceDark: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-dark.webp', // 可选:暗色主题logo
|
||||
},
|
||||
navigation: {
|
||||
accordion: true,
|
||||
@@ -457,6 +458,8 @@ interface LogoPreferences {
|
||||
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
/** logo地址 */
|
||||
source: string;
|
||||
/** 暗色主题logo地址 (可选,若不设置则使用 source) */
|
||||
sourceDark?: string;
|
||||
}
|
||||
|
||||
interface NavigationPreferences {
|
||||
|
||||
@@ -24,7 +24,7 @@ apps/web-naive
|
||||
|
||||
## 演示代码精简
|
||||
|
||||
如果你不需要演示代码,你可以直接删除的`playground`文件夹。
|
||||
如果你不需要演示代码,你可以直接删除 `playground` 文件夹。
|
||||
|
||||
## 文档精简
|
||||
|
||||
@@ -60,6 +60,7 @@ pnpm install
|
||||
"build:docs": "pnpm run build --filter=@vben/docs",
|
||||
"build:ele": "pnpm run build --filter=@vben/web-ele",
|
||||
"build:naive": "pnpm run build --filter=@vben/web-naive",
|
||||
"build:tdesign": "pnpm run build --filter=@vben/web-tdesign",
|
||||
"build:play": "pnpm run build --filter=@vben/playground",
|
||||
"dev:antd": "pnpm -F @vben/web-antd run dev",
|
||||
"dev:docs": "pnpm -F @vben/docs run dev",
|
||||
@@ -87,7 +88,7 @@ pnpm install
|
||||
|
||||
- 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。
|
||||
|
||||
- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以删除对应的页面文件,在 `src/views` 文件夹中。
|
||||
- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以在 `src/views` 文件夹中删除对应的页面文件。
|
||||
|
||||
### 删除不需要的组件
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const scopeComplete = execSync('git status --porcelain || true')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.find((r) => ~r.indexOf('M src'))
|
||||
?.replace(/(\/)/g, '%%')
|
||||
?.replaceAll(/(\/)/g, '%%')
|
||||
?.match(/src%%((\w|-)*)/)?.[1]
|
||||
?.replace(/s$/, '');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/eslint-config",
|
||||
"version": "5.0.0",
|
||||
"version": "5.5.9",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
@@ -43,14 +43,17 @@
|
||||
"eslint-plugin-n": "catalog:",
|
||||
"eslint-plugin-no-only-tests": "catalog:",
|
||||
"eslint-plugin-perfectionist": "catalog:",
|
||||
"eslint-plugin-pnpm": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-regexp": "catalog:",
|
||||
"eslint-plugin-unicorn": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vitest": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"eslint-plugin-yml": "catalog:",
|
||||
"globals": "catalog:",
|
||||
"jsonc-eslint-parser": "catalog:",
|
||||
"vue-eslint-parser": "catalog:"
|
||||
"vue-eslint-parser": "catalog:",
|
||||
"yaml-eslint-parser": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import createCommand from 'eslint-plugin-command/config';
|
||||
export async function command() {
|
||||
return [
|
||||
{
|
||||
// @ts-expect-error - no types
|
||||
...createCommand(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -46,6 +46,8 @@ export async function ignores(): Promise<Linter.Config[]> {
|
||||
'**/*.sh',
|
||||
'**/*.ttf',
|
||||
'**/*.woff',
|
||||
'**/.github',
|
||||
'**/lefthook.yml',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './jsdoc';
|
||||
export * from './jsonc';
|
||||
export * from './node';
|
||||
export * from './perfectionist';
|
||||
export * from './pnpm';
|
||||
export * from './prettier';
|
||||
export * from './regexp';
|
||||
export * from './test';
|
||||
@@ -15,3 +16,4 @@ export * from './turbo';
|
||||
export * from './typescript';
|
||||
export * from './unicorn';
|
||||
export * from './vue';
|
||||
export * from './yaml';
|
||||
|
||||
@@ -48,6 +48,7 @@ export async function jsonc(): Promise<Linter.Config[]> {
|
||||
},
|
||||
sortTsconfig(),
|
||||
sortPackageJson(),
|
||||
sortCspellJson(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -130,6 +131,21 @@ function sortPackageJson(): Linter.Config {
|
||||
};
|
||||
}
|
||||
|
||||
function sortCspellJson(): Linter.Config {
|
||||
return {
|
||||
files: ['**/cspell.json', '**/.cspell.json'],
|
||||
rules: {
|
||||
'jsonc/sort-array-values': [
|
||||
'error',
|
||||
{
|
||||
order: { type: 'asc' },
|
||||
pathPattern: '^words$|^ignorePaths$',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sortTsconfig(): Linter.Config {
|
||||
return {
|
||||
files: [
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function node(): Promise<Linter.Config[]> {
|
||||
'error',
|
||||
{
|
||||
ignores: [],
|
||||
version: '>=18.0.0',
|
||||
version: '>=20.12.0',
|
||||
},
|
||||
],
|
||||
'n/prefer-global/buffer': ['error', 'never'],
|
||||
|
||||
@@ -4,7 +4,6 @@ import { interopDefault } from '../util';
|
||||
|
||||
export async function perfectionist(): Promise<Linter.Config[]> {
|
||||
const perfectionistPlugin = await interopDefault(
|
||||
// @ts-expect-error - no types
|
||||
import('eslint-plugin-perfectionist'),
|
||||
);
|
||||
|
||||
|
||||
41
internal/lint-configs/eslint-config/src/configs/pnpm.ts
Normal file
41
internal/lint-configs/eslint-config/src/configs/pnpm.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
import { interopDefault } from '../util';
|
||||
|
||||
export async function pnpm(): Promise<Linter.Config[]> {
|
||||
const [pluginPnpm, parserPnpm, parserJsonc] = await Promise.all([
|
||||
interopDefault(import('eslint-plugin-pnpm')),
|
||||
interopDefault(import('yaml-eslint-parser')),
|
||||
interopDefault(import('jsonc-eslint-parser')),
|
||||
] as const);
|
||||
|
||||
return [
|
||||
{
|
||||
files: ['package.json', '**/package.json'],
|
||||
languageOptions: {
|
||||
parser: parserJsonc,
|
||||
},
|
||||
plugins: {
|
||||
pnpm: pluginPnpm,
|
||||
},
|
||||
rules: {
|
||||
'pnpm/json-enforce-catalog': 'error',
|
||||
'pnpm/json-prefer-workspace-settings': 'error',
|
||||
'pnpm/json-valid-catalog': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['pnpm-workspace.yaml'],
|
||||
languageOptions: {
|
||||
parser: parserPnpm,
|
||||
},
|
||||
plugins: {
|
||||
pnpm: pluginPnpm,
|
||||
},
|
||||
rules: {
|
||||
'pnpm/yaml-no-duplicate-catalog-item': 'error',
|
||||
'pnpm/yaml-no-unused-catalog-item': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { interopDefault } from '../util';
|
||||
|
||||
export async function turbo(): Promise<Linter.Config[]> {
|
||||
const [pluginTurbo] = await Promise.all([
|
||||
// @ts-expect-error - no types
|
||||
interopDefault(import('eslint-config-turbo')),
|
||||
] as const);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { interopDefault } from '../util';
|
||||
export async function typescript(): Promise<Linter.Config[]> {
|
||||
const [pluginTs, parserTs] = await Promise.all([
|
||||
interopDefault(import('@typescript-eslint/eslint-plugin')),
|
||||
// @ts-expect-error missing types
|
||||
interopDefault(import('@typescript-eslint/parser')),
|
||||
] as const);
|
||||
|
||||
@@ -27,11 +26,11 @@ export async function typescript(): Promise<Linter.Config[]> {
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': pluginTs,
|
||||
'@typescript-eslint': pluginTs as any,
|
||||
},
|
||||
rules: {
|
||||
...pluginTs.configs['eslint-recommended'].overrides?.[0].rules,
|
||||
...pluginTs.configs.strict.rules,
|
||||
...pluginTs.configs['eslint-recommended']?.overrides?.[0]?.rules,
|
||||
...pluginTs.configs.strict?.rules,
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ export async function vue(): Promise<Linter.Config[]> {
|
||||
const [pluginVue, parserVue, parserTs] = await Promise.all([
|
||||
interopDefault(import('eslint-plugin-vue')),
|
||||
interopDefault(import('vue-eslint-parser')),
|
||||
// @ts-expect-error missing types
|
||||
interopDefault(import('@typescript-eslint/parser')),
|
||||
] as const);
|
||||
|
||||
|
||||
87
internal/lint-configs/eslint-config/src/configs/yaml.ts
Normal file
87
internal/lint-configs/eslint-config/src/configs/yaml.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
import { interopDefault } from '../util';
|
||||
|
||||
export async function yaml(): Promise<Linter.Config[]> {
|
||||
const [pluginYaml, parserYaml] = await Promise.all([
|
||||
interopDefault(import('eslint-plugin-yml')),
|
||||
interopDefault(import('yaml-eslint-parser')),
|
||||
] as const);
|
||||
|
||||
return [
|
||||
{
|
||||
files: ['**/*.y?(a)ml'],
|
||||
plugins: {
|
||||
yaml: pluginYaml as any,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: parserYaml,
|
||||
},
|
||||
rules: {
|
||||
'style/spaced-comment': 'off',
|
||||
|
||||
'yaml/block-mapping': 'error',
|
||||
'yaml/block-sequence': 'error',
|
||||
'yaml/no-empty-key': 'error',
|
||||
'yaml/no-empty-sequence-entry': 'error',
|
||||
'yaml/no-irregular-whitespace': 'error',
|
||||
'yaml/plain-scalar': 'error',
|
||||
|
||||
'yaml/vue-custom-block/no-parsing-error': 'error',
|
||||
|
||||
'yaml/block-mapping-question-indicator-newline': 'error',
|
||||
'yaml/block-sequence-hyphen-indicator-newline': 'error',
|
||||
'yaml/flow-mapping-curly-newline': 'error',
|
||||
'yaml/flow-mapping-curly-spacing': 'error',
|
||||
'yaml/flow-sequence-bracket-newline': 'error',
|
||||
'yaml/flow-sequence-bracket-spacing': 'error',
|
||||
'yaml/indent': ['error', 2],
|
||||
'yaml/key-spacing': 'error',
|
||||
'yaml/no-tab-indent': 'error',
|
||||
'yaml/quotes': [
|
||||
'error',
|
||||
{
|
||||
avoidEscape: true,
|
||||
prefer: 'single',
|
||||
},
|
||||
],
|
||||
'yaml/spaced-comment': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['pnpm-workspace.yaml'],
|
||||
rules: {
|
||||
'yaml/sort-keys': [
|
||||
'error',
|
||||
{
|
||||
order: [
|
||||
'packages',
|
||||
'overrides',
|
||||
'patchedDependencies',
|
||||
'hoistPattern',
|
||||
'catalog',
|
||||
'catalogs',
|
||||
|
||||
'allowedDeprecatedVersions',
|
||||
'allowNonAppliedPatches',
|
||||
'configDependencies',
|
||||
'ignoredBuiltDependencies',
|
||||
'ignoredOptionalDependencies',
|
||||
'neverBuiltDependencies',
|
||||
'onlyBuiltDependencies',
|
||||
'onlyBuiltDependenciesFile',
|
||||
'packageExtensions',
|
||||
'peerDependencyRules',
|
||||
'supportedArchitectures',
|
||||
],
|
||||
pathPattern: '^$',
|
||||
},
|
||||
{
|
||||
order: { type: 'asc' },
|
||||
pathPattern: '.*',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
jsonc,
|
||||
node,
|
||||
perfectionist,
|
||||
pnpm,
|
||||
prettier,
|
||||
regexp,
|
||||
test,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
typescript,
|
||||
unicorn,
|
||||
vue,
|
||||
yaml,
|
||||
} from './configs';
|
||||
import { customConfig } from './custom-config';
|
||||
|
||||
@@ -48,6 +50,8 @@ async function defineConfig(config: FlatConfig[] = []) {
|
||||
regexp(),
|
||||
command(),
|
||||
turbo(),
|
||||
yaml(),
|
||||
pnpm(),
|
||||
...customConfig,
|
||||
...config,
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/prettier-config",
|
||||
"version": "5.0.0",
|
||||
"version": "5.5.9",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
||||
@@ -75,6 +75,7 @@ export default {
|
||||
'import-notation': null,
|
||||
'media-feature-range-notation': null,
|
||||
'named-grid-areas-no-invalid': null,
|
||||
'nesting-selector-no-missing-scoping-root': null,
|
||||
'no-descending-specificity': null,
|
||||
'no-empty-source': null,
|
||||
'order/order': [
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"@tailwindcss/typography": "catalog:",
|
||||
"autoprefixer": "catalog:",
|
||||
"cssnano": "catalog:",
|
||||
"jiti": "catalog:",
|
||||
"postcss": "catalog:",
|
||||
"postcss-antd-fixes": "catalog:",
|
||||
"postcss-import": "catalog:",
|
||||
|
||||
@@ -176,18 +176,18 @@ export default {
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: 'var(--reka-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
from: { height: 'var(--reka-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
'collapsible-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-collapsible-content-height)' },
|
||||
to: { height: 'var(--reka-collapsible-content-height)' },
|
||||
},
|
||||
'collapsible-up': {
|
||||
from: { height: 'var(--radix-collapsible-content-height)' },
|
||||
from: { height: 'var(--reka-collapsible-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
float: {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/node.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"composite": false,
|
||||
"lib": ["ESNext"],
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"],
|
||||
"noImplicitAny": true
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
||||
assetFileNames: '[ext]/[name]-[hash].[ext]',
|
||||
chunkFileNames: 'js/[name]-[hash].js',
|
||||
entryFileNames: 'jse/index-[name]-[hash].js',
|
||||
experimentalMinChunkSize: 20 * 1024,
|
||||
},
|
||||
},
|
||||
target: 'es2015',
|
||||
@@ -114,7 +115,7 @@ function createCssOptions(injectGlobalScss = true): CSSOptions {
|
||||
}
|
||||
return content;
|
||||
},
|
||||
api: 'modern',
|
||||
// api: 'modern',
|
||||
importers: [new NodePackageImporter()],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DefineConfig } from '../typing';
|
||||
import type { DefineConfig, VbenViteConfig } from '../typing';
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
@@ -12,7 +12,7 @@ export * from './library';
|
||||
function defineConfig(
|
||||
userConfigPromise?: DefineConfig,
|
||||
type: 'application' | 'auto' | 'library' = 'auto',
|
||||
) {
|
||||
): VbenViteConfig {
|
||||
let projectType = type;
|
||||
|
||||
// 根据包是否存在 index.html,自动判断类型
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
|
||||
import type {
|
||||
ConfigEnv,
|
||||
PluginOption,
|
||||
UserConfig,
|
||||
UserConfigFnPromise,
|
||||
} from 'vite';
|
||||
import type { PluginOptions } from 'vite-plugin-dts';
|
||||
import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
|
||||
|
||||
@@ -327,6 +332,8 @@ type DefineLibraryOptions = (config?: ConfigEnv) => Promise<{
|
||||
*/
|
||||
type DefineConfig = DefineApplicationOptions | DefineLibraryOptions;
|
||||
|
||||
type VbenViteConfig = Promise<UserConfig> | UserConfig | UserConfigFnPromise;
|
||||
|
||||
export type {
|
||||
ApplicationPluginOptions,
|
||||
ArchiverPluginOptions,
|
||||
@@ -340,4 +347,5 @@ export type {
|
||||
LibraryPluginOptions,
|
||||
NitroMockPluginOptions,
|
||||
PrintPluginOptions,
|
||||
VbenViteConfig,
|
||||
};
|
||||
|
||||
25
package.json
25
package.json
@@ -92,27 +92,8 @@
|
||||
"vue-tsc": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
"node": ">=20.19.0",
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "*"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"@ast-grep/napi": "catalog:",
|
||||
"@ctrl/tinycolor": "catalog:",
|
||||
"clsx": "catalog:",
|
||||
"esbuild": "0.25.3",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"neverBuiltDependencies": [
|
||||
"canvas",
|
||||
"node-gyp"
|
||||
]
|
||||
}
|
||||
"packageManager": "pnpm@10.28.2"
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
}
|
||||
|
||||
html {
|
||||
@apply text-foreground bg-background font-sans text-[100%];
|
||||
@apply text-foreground bg-background font-sans;
|
||||
|
||||
font-size: var(--font-size-base, 16px);
|
||||
font-variation-settings: normal;
|
||||
font-synthesis-weight: none;
|
||||
line-height: 1.15;
|
||||
text-size-adjust: 100%;
|
||||
font-synthesis-weight: none;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user