295 Commits

Author SHA1 Message Date
YunaiV
c164904a14 chore: 合并 github/master,引入 PR #259 BPMN 流程设计器审批节点自定义配置编辑后丢失修复 2026-05-04 00:36:22 +08:00
芋道源码
a0ceb45df9 Merge pull request #259 from lb1565387341/fix_bpmn_custom_user_config
fix: [bpm][antd&ele] 修复流程设计器自定义配置编辑后丢失的问题
2026-05-04 00:26:29 +08:00
YunaiV
c641542c71 fix(bpm):修正 BPM 流程实例审批弹窗网关分支重算的并发与提交问题
- 提交时不再用节点表单值覆盖 data.variables;与预览阶段使用同一份合并变量
- onChange 加 useDebounceFn(300ms) + 请求序号去重,handleAudit 提交前 await 最新一轮重算
- 切换任务时重置请求序号与 pending 重算
- 改用 form-create 官方 formData() 取节点表单当前值
- 节点表单初始化等 fApi 就绪后再计算下一节点(until + 1s 兜底)

同步至 web-antd / web-ele 两端
2026-05-03 16:35:03 +08:00
YunaiV
a3d8e4bfc1 feat: 添加包含和不包含条件选项到常量定义 2026-05-03 11:04:58 +08:00
YunaiV
e385823d46 fix: 修复 Vben5.0 form-create 多图上传校验拒绝 png/jpeg/gif,isImage 兼容 MIME 与扩展名两种 accept 写法 2026-05-02 22:56:38 +08:00
YunaiV
897220e19a fix: 修复 Vben5.0 download 接口 token 过期不触发刷新,导出/下载文件变成「账号未登录」JSON;web-antd / web-ele / web-naive / web-tdesign 加 Blob 业务错误嗅探拦截器 2026-05-02 20:36:00 +08:00
YunaiV
b293e112c6 fix: 修复 MALL 商品保存时 SKU 价格被反复 *100 的漂移 2026-05-02 20:23:43 +08:00
YunaiV
627e31f1b0 fix: 修复 Vben5.0 CRM 合同配置 / 客户公海规则配置表单 label 错用 labelClass: 'w-100',Tailwind v4 动态间距下被解析为 400px 撑爆 w-1/4 容器,挤掉 RadioGroup 输入区,改用 labelWidth: 120 2026-05-02 19:44:21 +08:00
YunaiV
8020b4b743 fix: 修复 MALL 商品列表/选择器「价格」列展示原始的「分」(web-antd / web-ele)
商品列表 [mall/product/spu/data.ts] 与商品选择器 [mall/product/spu/components/spu-select-data.ts]
的「价格」列原先 formatter: 'formatAmount2',只做了小数格式化、漏了「分转元」,导致
19900 直接显示成 19900.00(应为 199.00 元)。同文件的 marketPrice / costPrice 已正确使用
fenToYuan,唯独 price 漏了。

顺手将 spu/data.ts 的 price / marketPrice / costPrice 三列从手写闭包统一切到已注册的
formatFenToYuanAmount formatter,单位「元」从 cell 后缀挪进列标题(如「价格(元)」),
减少 8 处闭包并复用平台统一的 null/NaN 处理。
2026-05-02 19:38:50 +08:00
YunaiV
228c5463da fix: 修复 IoT 物模型表单 Form.Item 嵌套字段 name 误用点号字符串,事件类型等校验始终失败 / resetFields 写错路径 2026-05-02 19:27:35 +08:00
YunaiV
50ee691191 fix: 修复 web-ele 下 ApiSelect / ApiTreeSelect 误用 antd 的 fieldNames 写法导致下拉无内容
element-plus 适配器走 ApiComponent,识别的是 labelField / valueField / childrenField;
而 fieldNames 是 antd 风格写法,从 web-antd 复制过来未做适配,导致内部数据无法被映射成
{ label, value, children },下拉树/列表显示为空。

涉及:
- CRM 客户 / 联系人 / 线索 新增表单的「地址」树
- CRM 商机状态「应用部门」、产品「产品类型」树
- ERP 销售出库的 客户 / 销售人员 / 结算账户 / 产品 / 创建人 下拉
2026-05-02 18:55:48 +08:00
YunaiV
eda6ffaf1e fix: 修复 web-ele 下 ApiSelect / ApiTreeSelect 误用 antd 的 fieldNames 写法导致下拉无内容
element-plus 适配器走 ApiComponent,识别的是 labelField / valueField / childrenField;
而 fieldNames 是 antd 风格写法,从 web-antd 复制过来未做适配,导致内部数据无法被映射成
{ label, value, children },下拉树/列表显示为空。

涉及:
- CRM 客户 / 联系人 / 线索 新增表单的「地址」树
- CRM 商机状态「应用部门」、产品「产品类型」树
- ERP 销售出库的 客户 / 销售人员 / 结算账户 / 产品 / 创建人 下拉
2026-05-02 18:53:11 +08:00
xingyu
f542db27f9 !340 feat: 商城订单发货后可再修改发货信息
Merge pull request !340 from hice/master
2026-04-13 08:47:29 +00:00
xingyu4j
b2cf1646a4 fix: lint 2026-04-13 16:46:44 +08:00
xingyu4j
adecddae67 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin 2026-04-13 16:46:22 +08:00
xingyu4j
a653e428f3 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin 2026-04-13 16:45:32 +08:00
hice
eb62e63a04 feat: 商城订单发货后可再修改发货信息 2026-04-13 16:31:48 +08:00
Caisin
ccabbf0e97 feat: enable project-scoped preferences extension tabs (#7803)
* feat: enable project-scoped preferences extension tabs

Add a typed extension schema so subprojects can define extra settings,
render them in the shared preferences drawer only when configured, and
consume them in playground as a real feature demo. Extension labels now
follow locale keys instead of hardcoded app-specific strings.

Constraint: Reuse the shared preferences drawer and field blocks
Rejected: Add app-specific fields to core preferences | too tightly coupled
Rejected: Inline localized label objects | breaks existing locale-key flow
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep extension labels as locale keys rendered via $t in UI
Tested: Vitest preferences tests
Tested: Turbo typecheck for preferences, layouts, web-antd, and playground
Tested: ESLint for touched preferences and playground files
Not-tested: Manual browser interaction in playground preferences drawer

* fix: satisfy lint formatting for preferences extension demo

Adjust the playground preferences extension demo template so formatter and
Vue template lint rules agree on the rendered markup. This keeps CI green
without changing runtime behavior.

Constraint: Must preserve the existing demo behavior while fixing CI only
Rejected: Disable the Vue newline rule | would weaken shared lint guarantees
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer computed/template structures that avoid formatter-vs-lint conflicts
Tested: pnpm run lint
Not-tested: Manual browser interaction in playground preferences extension demo

* fix: harden custom preferences validation and i18n labels

Tighten custom preferences handling so numeric extension fields respect
min, max, and step constraints. Number inputs now ignore NaN values,
and web-antd extension metadata uses locale keys instead of raw strings.
Also align tip-based hover guards in shared preference inputs/selects.

Constraint: Keep fixes scoped to verified findings only
Rejected: Broader refactor of preferences field components | not needed for these fixes
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Reuse the same validation path for updates and cache hydration
Tested: Vitest preferences tests
Tested: ESLint for touched preferences and widget files
Tested: Typecheck for web-antd, layouts, and core preferences
Not-tested: Manual browser interaction for all preference field variants

* fix: remove localized default from playground extension config

Drop the hardcoded Chinese default value from the playground extension
report title field and fall back to an empty string instead. This keeps
extension config locale-neutral while preserving localized labels and
placeholders through translation keys.

Constraint: Keep the fix limited to the verified localized default issue
Rejected: Compute the default from runtime locale in config | unnecessary for this finding
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Avoid embedding localized literals in extension default values
Tested: ESLint for playground/src/preferences.ts
Tested: Oxfmt check for playground/src/preferences.ts
Not-tested: Manual playground preferences interaction

* docs: document project-scoped preferences extension workflow

Add Chinese and English guide sections explaining how to define,
initialize, read, and update project-scoped preferences extensions.
Also document numeric field validation and point readers to the
playground demo for a complete example.

Constraint: Keep this docs-only and aligned with the shipped API
Rejected: Update only Chinese docs | would leave English docs inconsistent
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep zh/en examples and playground demo paths synchronized
Tested: git diff --check; pnpm build:docs
Not-tested: Manual browser review of the rendered docs site

* fix: harden custom preferences defaults and baselines

Use a locale-neutral default for the web-antd report title.
Also stop preference getters from exposing mutable baseline
or extension schema objects, and add a regression test for
external mutation attempts.

Constraint: Keep behavior compatible with the shipped preferences API
Rejected: Return raw refs with readonly typing only | callers could still mutate internals
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep defensive copies for baseline and schema getters unless storage semantics change
Tested: eslint, oxlint, targeted vitest, filtered typecheck, git diff --check
Not-tested: Full monorepo typecheck and test suite

* test: relax custom preference cache key matching

Avoid coupling the custom-number cache test to one exact
localStorage key string. Match the intended cache lookup
more loosely so the test still verifies filtering behavior
without depending on the full namespaced cache key.

Constraint: Focus the test on cache filtering behavior
Rejected: Assert one exact key | brittle with namespace changes
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer behavior tests over literal storage keys
Tested: targeted vitest, eslint, git diff --check
Not-tested: Full monorepo test suite

---------

Co-authored-by: caisin <caisin@caisins-Mac-mini.local>
2026-04-13 15:11:57 +08:00
Caisin
5b84ac5b13 feat(form-ui): support schema valueFormat for getValues payload shaping (#7804)
* feat(@vben-core/form-ui): support schema valueFormat on getValues

Some form fields emit UI-friendly structures such as time-range arrays,
while consumers and backend APIs often need a different payload shape.
This adds schema-level `valueFormat` hooks so `getValues()` can
normalize field output at read time without forcing callers to
post-process every submission path.

Constraint: Must preserve existing range-time mapping and nested field behavior
Constraint: Must not mutate live vee-validate form state while formatting output
Rejected: Global formatter config | too coarse for per-field payload shaping
Rejected: Post-submit-only transform | misses reset/query/change handlers
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep `getValues()` output derivation side-effect free
Directive: Clone raw form values before formatting derived payloads
Tested: vitest form-api test for valueFormat and existing getValues paths
Tested: oxlint on changed form-ui source and test files
Not-tested: Full repo typecheck baseline has unrelated .vue module resolution errors

* fix(@vben-core/form-ui): restore mount compatibility and share field path parsing

Follow-up review found two real regressions and one missing assertion in the
new value formatting flow. `FormApi.mount()` had become breaking by requiring
`componentRefMap`, and delete path resolution duplicated field-name parsing
instead of sharing the reader grammar. This patch restores backward
compatibility, centralizes field-name path parsing, and extends the test to
prove formatting does not mutate live form values.

Constraint: Must preserve current valueFormat behavior and nested field support
Constraint: Must not reintroduce mutation of live vee-validate values
Rejected: Keep duplicated delete parsing | risks grammar drift from read path
Rejected: Only loosen mount tests | would leave consumer-facing API breakage
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Reuse shared field-name parsing for read/delete semantics in form-ui
Tested: vitest form-api test suite
Tested: oxlint on changed form-ui files
Not-tested: Full repo typecheck baseline has unrelated .vue module resolution errors
EOF && git push hekx feature-form-value-format

* fix(@vben-core/form-ui): clear stale component refs on unmount

A follow-up review found that `unmount()` left the private component ref map
populated. Because `mount()` now accepts an optional `componentRefMap`, a later
mount without a new map could silently reuse stale refs from a prior form
instance. This change clears the ref map on unmount and adds a regression test
covering remount behavior without a new ref map.

Constraint: Must preserve backward-compatible optional `mount()` ref map behavior
Constraint: Focus and field-ref lookups must not observe stale refs after unmount
Rejected: Clear refs only during next mount | stale state would still leak between lifecycle calls
Rejected: Remove mount fallback entirely | would undo the compatibility fix
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When mount falls back to internal refs, unmount must always reset that cache
Tested: vitest form-api test suite
Tested: oxlint on changed form-api source and test files
Not-tested: Full repo typecheck baseline has unrelated .vue module resolution errors

* refactor(@vben-core/form-ui): trim redundant valueFormat plumbing

Review feedback identified a few small cleanups in the value formatting path.
This removes an unnecessary shallow clone in `getValues()`, reuses the
already-parsed `rawKey` from `resolveFieldNamePath()` instead of re-resolving
it in multiple helpers, and clarifies the `FormValueFormat` contract for
undefined-as-delete decomposition behavior.

Constraint: Must not change runtime valueFormat behavior or payload shape
Constraint: Documentation and helper cleanup should stay behavior-preserving
Rejected: Leave duplicate raw-key resolution in place | adds needless parsing churn
Rejected: Expand the formatter API further | outside the scope of this cleanup
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep read/format helper plumbing lean and avoid duplicate field-name parsing
Tested: vitest form-api test suite
Tested: oxlint on changed form-ui source and test files
Not-tested: Full repo typecheck baseline has unrelated .vue module resolution errors

* feat(@vben-core/form-ui): document valueFormat with live examples

The new `valueFormat` feature needed a concrete usage path in both the
playground and the docs so users can understand how raw component values differ
from the final payload returned by `getValues()`. This adds a dedicated form
example, wires it into the playground menu, and documents the API with an
interactive docs demo. The preview panels now stay in sync when values are set,
reset, or submitted.

Constraint: Must demonstrate both return-value and setValue decomposition flows
Constraint: Example previews must react to setValues, reset, and manual edits
Rejected: Only document via markdown snippet | insufficient for verifying live payload behavior
Rejected: Reuse an existing basic form page | would bury feature-specific behavior
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep playground and docs demos behaviorally aligned when extending valueFormat examples
Tested: eslint on playground/docs valueFormat demo files and route module
Tested: oxlint on playground route module
Not-tested: Full docs/playground app runtime was not launched in this session

* chore(@vben-core/form-ui): normalize valueFormat demo formatting

The previous feature/docs commit left a few formatter-only adjustments unstaged
after hooks rewrote line wrapping in the new demo and docs pages. This commit
captures those final non-behavioral formatting updates so the branch matches the
current working tree.

Constraint: Must not change runtime behavior or docs meaning
Rejected: Leave post-hook diffs unstaged | branch would not reflect local state
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: After hook-driven rewrites, verify the working tree is clean before final push
Tested: Git diff inspection of remaining changes
Not-tested: No additional runtime verification needed; formatting-only follow-up
EOF && git push hekx feature-form-value-format

* fix(@vben-core/form-ui): remove docs demo dayjs dependency

The docs valueFormat demo imported `dayjs` directly even though the docs
package does not declare it as a dependency. That caused `@vben/docs:build`
to fail in CI during VitePress bundling. This change removes the direct
import, keeps the preview formatter generic for day-like values, and drops
the docs-only preset button that required constructing dayjs instances.

Constraint: Docs build must succeed without adding new package dependencies
Constraint: Playground example should remain unchanged and fully interactive
Rejected: Add dayjs to docs dependencies | unnecessary for a small display demo
Rejected: Externalize dayjs in VitePress build | hides a package boundary issue
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Docs demos should avoid imports only available through transitive deps
Tested: pnpm exec eslint docs/src/demos/vben-form/value-format/index.vue
Tested: pnpm --dir docs run build
Not-tested: No browser-side manual verification of the docs demo in this session

---------

Co-authored-by: caisin <caisin@caisins-Mac-mini.local>
2026-04-13 11:22:04 +08:00
芋道源码
f610bd690b !339 增加 iot、mes 的说明
Merge pull request !339 from 芋道源码/dev
2026-04-12 13:29:10 +00:00
YunaiV
76f9d3d9fc merge: 合并 master 分支,解决 isUrl 冲突(保留从 @vben/utils 导出的方式)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:27:05 +08:00
YunaiV
1e6c39a4c6 feat:增加 iot 模块 2026-04-12 21:24:37 +08:00
YunaiV
7a1f8da68f feat:增加 iot 模块 2026-04-12 21:24:19 +08:00
YunaiV
51cae9b00c feat(mes):增加 mes 模块 2026-04-12 16:45:38 +08:00
YunaiV
7cbeaa8390 feat(mes):增加 mes 模块 2026-04-12 16:44:53 +08:00
Caisin
6be3a0e204 feat(common-ui): add labelFn support to ApiComponent (#7801)
* feat: allow api-component labels to be derived from option data

ApiComponent already normalizes option records into the label/value shape used by
consuming controls, but label text could only come from a single field. Add
labelFn so callers can build labels from the full option record while keeping
labelField as the fallback path.

This keeps the change inside the existing component instead of introducing a
wrapper, and it also normalizes direct options through the same transform path
as API-loaded options for consistent behavior.

Constraint: Must extend the existing ApiComponent API instead of adding a second
Constraint: wrapper component
Rejected: Add a separate ApiLabelComponent wrapper |
Rejected: extra surface area for one option-mapping concern
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep labelFn as a presentation transform and preserve labelField
Directive: fallback for existing callers
Tested: pnpm exec eslint api-component.vue index.ts types.ts
Tested: pnpm exec vue-tsc --noEmit -p packages/effects/common-ui/tsconfig.json
Not-tested: runtime integration in consuming select/tree-select components

* Update packages/effects/common-ui/src/components/api-component/api-component.vue

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-12 14:29:18 +08:00
过冬
a9b76ba2ed fix: antdv-next message/notification 跟随暗色主题 (#7799) 2026-04-12 11:51:23 +08:00
Jin Mao
53ccec1d80 fix: 修复VITE_APP_TITLE变量替换语法
- 将index.html中的<%= VITE_APP_TITLE %>替换为%VITE_APP_TITLE%
- 更新web-antd、web-antdv-next、web-ele、web-naive、web-tdesign应用
- 修改文档中loading组件的VITE_APP_TITLE引用方式
- 修复vite-config插件中默认加载模板的变量语法
- 统一所有应用和模板中的环境变量引用格式
2026-04-11 14:29:18 +08:00
Jin Mao
4af5d6152b chore: fix actions error 2026-04-11 14:13:13 +08:00
xingyu4j
307781f437 chore: update deps 2026-04-10 22:16:33 +08:00
xingyu4j
1326994d8e fix: tailwindcss lint config 2026-04-10 22:14:09 +08:00
xingyu4j
fd70a3f3e0 fix: lint 2026-04-10 22:01:01 +08:00
xingyu4j
298930b0d7 chore: remove vite-plugin-html 2026-04-10 22:00:33 +08:00
xingyu4j
54d95b8761 fix: check deps 2026-04-10 21:23:24 +08:00
xingyu4j
4a16040d3e chore: update deps 2026-04-10 21:18:26 +08:00
xingyu4j
ee95548340 fix: tailwindcss lint 2026-04-10 21:13:04 +08:00
xingyu4j
320e687bad fix: ts config 2026-04-10 21:08:54 +08:00
Jin Mao
ad43c6817e fix: 配置 TypeScript 构建根目录
- 添加 rootDir 编译选项指向 ./src 目录
- 保持现有编译配置不变
- 排除测试文件和 node_modules 目录
2026-04-09 14:48:26 +08:00
Jin Mao
c8747c079d chore: update deps 2026-04-08 18:25:39 +08:00
dullathanol
224bfe7fcb chore: 修正注释 2026-04-08 10:19:53 +08:00
dullathanol
f443bfbc7b fix: tailwindcss config 2026-04-08 10:11:05 +08:00
过冬
195b2ea0d2 Merge branch 'vbenjs:main' into main 2026-04-08 09:34:24 +08:00
Jin Mao
4150479549 chore: fix lint 2026-04-08 07:20:52 +08:00
dullathanol
5ebf513498 fix: 修正 Modal/Drawer 中 loading 属性注释 2026-04-07 12:28:57 +08:00
dullathanol
4e4ffc439c feat: 支持 overflow 配置以允许拖拽超出可视区 2026-04-07 11:41:39 +08:00
dullathanol
ad7ed50b52 fix: 弹窗组件拖拽后全屏位置异常 2026-04-06 22:26:27 +08:00
Jin Mao
92f8916225 chore: fix lint
- 关闭 vitest/require-mock-type-parameters 规则
2026-04-06 21:20:53 +08:00
dullathanol
7e4edd270d fix: 补全 ComponentPropsMap 与 Vxe 表格表单链路的类型 2026-04-05 19:03:03 +08:00
dullathanol
332ff44219 fix: 修复 FormField 在 SFC 中的运行时异常 2026-04-05 03:05:01 +08:00
dullathanol
834ce3efc0 fix: 修复部分情况 component 类型丢失问题 2026-04-05 01:59:17 +08:00
dullathanol
5211f5065d feat: 表单 Schema 支持组件 Props 映射泛型,同步适配VxeGrid 2026-04-04 23:40:27 +08:00
dullathanol
96d6f89732 refactor: 简化 componentProps 回调的类型写法 2026-04-03 15:02:32 +08:00
dullathanol
6ab06584eb fix: 函数式 componentProps 按已注册 component 的 Props 校验返回值 2026-04-03 13:36:03 +08:00
dullathanol
a6433c2b50 feat: Schema 中 componentProps 随注册组件联动类型提示 2026-04-03 01:39:49 +08:00
墨苒孤
128a131797 fix(form): 修复表单示例中 switch 组件无法切换的问题 (#7636) (#7763) 2026-04-02 18:18:56 +08:00
橙子
c775d7ed80 fix: interface DropdownMenuProps don‘t have key prop (#7757) 2026-04-02 08:33:26 +08:00
HaroldZhangCode91
b8b4308e1c feat: fix oxlint error for oxlint upgrade (#7756)
1. remove unknown rule out of oxlint
2. add the corresponding back to eslint-config
3. fixed the eslint error for package.json
2026-04-01 19:28:57 +08:00
墨苒孤
80d6e2255f fix: make search case-insensitive (#7689) (#7755) 2026-04-01 19:17:36 +08:00
橙子
4e0968d4b7 perf: replace onUnMounted to tryOnScopeDispose (#7747)
* perf: replace `onUnMounted` to `tryOnScopeDispose`

* perf: replace `onUnMounted` to `tryOnScopeDispose`
2026-04-01 10:30:54 +08:00
Jin Mao
44a5809a46 chore: update deps 2026-04-01 08:10:49 +08:00
xingyu4j
2428fb1407 fix: extension-document 2026-03-30 19:50:44 +08:00
xingyu4j
bb78882f72 feat(@vben/plugins): add tiptap rich text editor 2026-03-30 19:36:29 +08:00
xingyu4j
df88a23102 chore: typescript config is expired‌ 2026-03-30 18:26:07 +08:00
xingyu4j
ca5f360231 chore: update deps 2026-03-30 18:24:25 +08:00
Anonymouscen
147b50ec45 chore: 修复名称错误问题,帐户改为账户 (#7735) 2026-03-29 14:17:00 +08:00
橙子
34439dce4e fix: history search can not remove (#7732) 2026-03-29 14:16:32 +08:00
Jin Mao
9a22027b35 chore: 更新 GitHub Actions 依赖版本
- 将 pnpm/action-setup 从 v4 升级到 v5
- 将 release-drafter/release-drafter 从 v6 升级到 v7
- 更新所有工作流中的依赖版本以确保兼容性
2026-03-25 17:58:47 +08:00
Jin Mao
282a102826 chore: fix lint 2026-03-25 17:31:33 +08:00
Jin Mao
417e6c2ade chore: fix lint && typecheck 2026-03-25 16:33:41 +08:00
Jin Mao
9d69d7f46c Merge branch 'main' into chore/plugins 2026-03-25 15:19:21 +08:00
Jin Mao
87d1593a1f refactor(effects): 扩展 echarts 类型定义并优化插件配置合并逻辑
- 添加 PieSeriesOption 和 RadarSeriesOption 到 echarts 类型定义
- 添加 LegendComponentOption 和 ToolboxComponentOption 组件选项
- 重构 providePluginsOptions 函数实现深合并逻辑
- 优化 vxe-table 初始化中的表单工厂优先级处理
- 调整 playground 中的 import 语句顺序和格式
2026-03-25 15:16:24 +08:00
过冬
7fbdf3d914 fix(@vben/common-ui): 修复 JsonViewer 在 Vite 下因 CJS 默认导出未解包导致的渲染失败 (#7728)
* fix: lint

* fix(@vben/common-ui): 修复 JsonViewer 在 Vite 下因 CJS 默认导出未解包导致的渲染失败
2026-03-25 14:54:14 +08:00
JyQAQ
65287cf4b7 feat: Dockerfile构建调整 (#7727)
Co-authored-by: 吉远 <jiyuan@txhmo.com>
2026-03-25 14:53:26 +08:00
Jin Mao
6da3017dcf feat: 插件新增依赖注入功能 2026-03-25 14:46:55 +08:00
Jin Mao
5c02057198 refactor(effects): 替换上下文创建逻辑为全局选项管理
- 移除 createContext 依赖并实现全局插件选项存储
- 添加 providePluginsOptions 函数用于提供插件配置
- 添加 injectPluginsOptions 函数用于注入插件配置
- 添加 resetPluginsOptions 函数用于重置插件配置
- 更新 package.json 导出配置添加主入口点定义
2026-03-25 14:42:40 +08:00
Jin Mao
a7ca7cdb9f refactor(vxe-table): 重构 useTableForm 函数实现并优化初始化逻辑
- 将 useTableForm 从箭头函数改为普通函数声明
- 简化表单工厂函数的获取逻辑,支持上下文注入
- 移除初始化时的重复上下文注入代码
- 改进错误提示信息的准确性
- 调整代码结构以提高可读性和维护性
- 将 SetupVxeTable 接口中的 useVbenForm 字段改为可选参数
2026-03-25 14:42:31 +08:00
Jin Mao
79408d406d feat: 添加插件模块导出
- 导出 types 模块
- 导出 plugins-context 模块
2026-03-25 13:33:12 +08:00
Jin Mao
e555f71bf8 feat(vxe-table): 集成表格插件并优化初始化配置
- 添加了完整的 vxe-table 插件功能实现
- 实现了插件上下文选项注入机制
- 重构了 useTableForm 工厂函数的初始化逻辑
- 支持通过参数或上下文注入 useVbenForm 函数
- 优化了组件导入和类型定义
- 添加了插件使用文档说明
- 移除了未使用的组件注释代码
- 统一了字符串引号格式为双引号
2026-03-25 13:32:21 +08:00
Jin Mao
4c320346c3 docs(motion): 添加 motion 插件文档
- 创建了 Motion 插件的 README.md 文件
- 添加了插件导出组件和类型的说明表格
- 提供了插件的基本使用示例代码
- 包含了 MotionOptions 和 MotionVariants 类型导入说明
2026-03-25 13:27:10 +08:00
Jin Mao
e5ec88169a refactor: 重构 ECharts 插件类型定义和导出结构
- 将 ECOption 类型定义移至独立的 types.ts 文件
- 修改 echarts.ts 文件导入 ECOption 类型而不是定义
- 更新 index.ts 添加 types 导出
- 移除 echarts.ts 中冗余的类型导入和定义
- 添加完整的 README.md 文档说明插件使用方法
- 优化代码组织结构提高可维护性
2026-03-25 13:27:02 +08:00
Jin Mao
914711ae04 feat: 添加插件上下文和类型定义
- 创建了插件选项的 createContext 功能
- 定义了 VbenPluginsOptions 接口结构
- 添加了表单、模态框、消息和组件的插件选项接口
- 提供了插件选项的注入和提供功能
2026-03-25 13:25:19 +08:00
Jin Mao
4c1e3b9548 Merge branch 'fork/Voidlurk/fix-default' 2026-03-24 10:36:40 +08:00
Jin Mao
9cd3987475 Merge branch 'main' into fix 2026-03-24 10:24:18 +08:00
xueyitt
47a853330d feat: ApiSelect增加shouldFetch控制,在api请求之前的判断是否允许请求的回调函数 (#7713) 2026-03-24 10:22:02 +08:00
xueyitt
2aced2f659 feat: 增加table 帮助信息help通过表单values动态展示内容 (#7712) 2026-03-24 10:20:43 +08:00
Jin Mao
cd955df02f chore: fix lint 2026-03-24 10:19:24 +08:00
Bk201
0a819df2bf fix bug
[Vue warn]: Invalid prop: custom validator check failed for prop "variant".
2026-03-24 03:01:00 +08:00
xingyu4j
67afcadcf0 fix: rollup -> rolldown 2026-03-23 17:51:46 +08:00
xingyu4j
1128ef5acd chore: update deps 2026-03-23 17:13:39 +08:00
xingyu
ca39b8d0c9 !337 Merge branch 'main' of <a href="https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fvbenjs%2Fvue-vben-admin">https://github.com/vbenjs/vue-vben-admin</a> into vite8
Merge pull request !337 from xingyu/vite8
2026-03-23 08:57:12 +00:00
xingyu4j
fece74f744 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into vite8 2026-03-23 16:55:27 +08:00
雪忆天堂
6b3506f128 feat: table 类型VxeTableGridColumns替代VxeTableGridOptions['columns']魔法值写法 2026-03-23 10:06:35 +08:00
雪忆天堂
5613dcef99 feat: table允许通过props动态变化data数据 2026-03-23 10:05:32 +08:00
MistyMoon
3528517fe0 fix(project): fix sub sidebar theme in two-column menu (#7708)
- Reset light tokens for nested sub sidebars.
- Align the extra title logo theme with sidebarThemeSub.
2026-03-22 09:15:56 +08:00
Jin Mao
2a5b520ec9 chore: update deps 2026-03-22 09:15:27 +08:00
lmx
e2fb3602f1 fix: 修复菜单项外部链接跳转问题
- 引入 isHttpUrl 工具函数用于判断 URL 类型
- 添加 isHttp 计算属性检测父路径是否为 HTTP 链接
- 修改菜单项渲染逻辑,对外部链接直接使用原地址跳转
- 调整 HTML 结构,将链接属性移到正确位置
- 确保内部路由和外部链接都能正常工作
2026-03-19 23:56:13 +08:00
lmx
da3580cbd7 Merge remote-tracking branch 'origin/main' 2026-03-19 22:07:20 +08:00
xingyu
0c300d040c fix: tailwindcss config (#7693)
* chore: update deps

* fix: tailwindcss config

* fix: lint

* fix: lint

* chore: update deps
2026-03-19 15:51:09 +08:00
橙子
d43a3729c3 fix: playground(drawer) comment (#7695) 2026-03-19 09:03:40 +08:00
Jin Mao
bed97a84d8 revert: "fix: sass-embedded@1.98.0 在 macOS 13 会直接崩 (#7692)" 2026-03-19 09:02:28 +08:00
afe1
b908076846 fix: sass-embedded@1.98.0 在 macOS 13 会直接崩 (#7692)
* fix: catelog

* fix: system
2026-03-18 20:18:25 +08:00
Noah Lan
885a0a9a00 fix: build.mjs commands could not be executed correctly. (#7682)
* fix: Fix issue where commands could not be executed correctly when they contained spaces

* chore: oxfmt
2026-03-17 18:59:35 +08:00
Jin Mao
340baf4f0b chore: 处理合并的一些问题 2026-03-16 20:50:01 +08:00
Jin Mao
82cda0edaa Merge branch 'fork/xingyu4j/tsdown'
# Conflicts:
#	pnpm-lock.yaml
2026-03-16 20:36:27 +08:00
Jin Mao
9fe875355a fix: 修复构建脚本中的进程执行问题
- 移除平台特定的 pnpm 命令路径检测逻辑
- 统一使用 'pnpm' 命令执行
- 启用 shell 模式以正确处理命令执行
- 确保子进程继承正确的标准输入输出流
2026-03-16 20:30:27 +08:00
Jin Mao
5f21bd2036 Merge branch 'fork/jyqwq/feature/antd上传组件支持拖拽排序' 2026-03-16 19:54:08 +08:00
Sun
5b5ea6d2d8 chore: Configure the ESLint auto-repair feature (#7670) 2026-03-16 18:26:42 +08:00
JyQAQ
3dcfd23036 perf: 裁剪组件默认输出格式调整 (#7673)
Co-authored-by: 吉远 <jiyuan@txhmo.com>
2026-03-16 18:25:04 +08:00
xingyu
186914bcac fix: vxe i18n (#7675)
* chore: engines node

* chore: update deps

* fix: oxlint

* chore: fix lint issues

* chore: update deps

* fix: vxe i18n
2026-03-16 18:24:28 +08:00
吉远
4b3205fee8 feat: antd Upload 组件上传文件组支持拖拽排序 2026-03-16 15:01:43 +08:00
lmx
e4453841db fix: 修复在vue-router 的模式hash复制路径不对 2026-03-16 01:32:11 +08:00
xingyu4j
32db4cbd11 fix: build warn 2026-03-15 23:11:59 +08:00
xingyu4j
5558249cd3 chore: unbuild 2026-03-15 23:11:41 +08:00
xingyu4j
86b636ae54 fix(@vben/vite-config): externalize node utils dependency 2026-03-15 22:47:28 +08:00
xingyu4j
c9f7154524 chore(tsdown): remove unbuild leftovers 2026-03-15 22:04:48 +08:00
xingyu4j
d72f872369 refactor(tsdown): migrate styled ui-kit packages 2026-03-15 21:42:44 +08:00
xingyu4j
b300011d07 refactor(tsdown): migrate ui-kit vue packages 2026-03-15 21:30:13 +08:00
xingyu4j
3946253d6e chore(tsdown): align stub scripts and package exports 2026-03-15 21:17:41 +08:00
xingyu4j
11fc367845 chore: migrate vite build to tsdown 2026-03-15 21:04:12 +08:00
xingyu4j
bdc65cc250 chore: update deps 2026-03-15 21:03:41 +08:00
xingyu4j
70dad0f600 fix(node-utils): avoid find-up sync API in monorepo root lookup 2026-03-15 20:50:07 +08:00
xingyu4j
26e9aa244b fix(vite): adapt rolldown workspace resolution 2026-03-15 20:27:01 +08:00
xingyu4j
913f77fd2f chore(pnpm): sync lockfile for tsdown migration 2026-03-15 19:41:17 +08:00
xingyu4j
dba774e1c7 chore(vsh): migrate build to tsdown 2026-03-15 19:41:00 +08:00
xingyu4j
af09d652a3 chore(turbo-run): migrate build to tsdown 2026-03-15 19:40:54 +08:00
xingyu4j
0babdfbc44 chore(core-preferences): migrate build to tsdown 2026-03-15 19:40:48 +08:00
xingyu4j
f154d53be9 chore(core-composables): migrate build to tsdown 2026-03-15 19:40:42 +08:00
xingyu4j
ed3cd2fe3b chore(core-typings): migrate build to tsdown 2026-03-15 19:40:36 +08:00
xingyu4j
59912a00bc chore(core-shared): migrate build to tsdown 2026-03-15 19:40:29 +08:00
xingyu4j
675d8b0179 chore(core-icons): migrate build to tsdown 2026-03-15 19:40:17 +08:00
xingyu4j
a1ca296fc0 chore(node-utils): migrate build to tsdown 2026-03-15 19:40:09 +08:00
xingyu4j
c1b1fe90fd chore(oxlint-config): migrate build to tsdown 2026-03-15 19:39:59 +08:00
xingyu4j
30b5610a73 chore(oxfmt-config): migrate build to tsdown 2026-03-15 19:39:51 +08:00
xingyu4j
db9b9df8f7 chore(eslint-config): migrate build to tsdown 2026-03-15 19:39:43 +08:00
xingyu4j
ae6a75e913 build: migrate core ts packages to tsdown 2026-03-15 18:13:49 +08:00
xingyu
37d72c1628 fix: oxlint config (#7661)
* chore: engines node

* chore: update deps

* fix: oxlint

* chore: fix lint issues
2026-03-15 17:41:47 +08:00
xingyu4j
ab3e6bb37c chore: fix lint issues 2026-03-15 16:35:34 +08:00
xingyu4j
9ddb899a1a fix: oxlint 2026-03-15 16:23:18 +08:00
xingyu4j
1f0cda8aee chore: update deps 2026-03-15 12:35:42 +08:00
xingyu4j
90ae85317c chore: engines node 2026-03-15 12:33:23 +08:00
Jin Mao
a8ae891aff chore: update pnpm-lock 2026-03-15 03:57:55 +08:00
Jin Mao
1f2df3e944 Merge branch 'fork/Lmx1220/main'
# Conflicts:
#	pnpm-lock.yaml
2026-03-15 03:53:43 +08:00
Jin Mao
e39a432210 Merge branch 'fork/caodachen/fix_useEcharts' 2026-03-15 03:50:21 +08:00
xingyu4j
6b3bcee582 docs(@vben/docs): sync component docs with current APIs 2026-03-14 21:34:48 +08:00
xingyu4j
6c274b75b8 docs(@vben/docs): align guide docs with current tooling 2026-03-14 21:33:55 +08:00
xingyu4j
278032c94b docs: remove docs 2026-03-14 20:29:49 +08:00
xingyu4j
23a8982f5c chore: add prepare 2026-03-14 20:19:34 +08:00
xingyu4j
5df6c32d04 fix: lint 2026-03-14 20:14:35 +08:00
xingyu4j
7cae330c3c chore: deps 2026-03-14 19:48:49 +08:00
xingyu4j
100aaa4cee chore: vsh lint 2026-03-14 19:48:40 +08:00
xingyu4j
ead0b73e7b fix: lint 2026-03-14 19:47:02 +08:00
xingyu4j
2ace846e38 chore: recommend vscode yaml extension 2026-03-14 19:41:28 +08:00
xingyu4j
1d98393f0c chore: recommend vscode eslint extension 2026-03-14 19:39:37 +08:00
xingyu4j
c48ee2a364 revert: restore vscode extensions comments 2026-03-14 19:38:09 +08:00
xingyu4j
95d1e8432f fix: surface eslint diagnostics in vscode 2026-03-14 19:36:27 +08:00
xingyu4j
4d59ac78bd fix: lint 2026-03-14 19:34:22 +08:00
xingyu4j
f1143e134e fix: match root json files in oxfmt overrides 2026-03-14 19:33:55 +08:00
xingyu4j
e3e869faee fix: align oxfmt json commas with lint 2026-03-14 19:31:01 +08:00
xingyu4j
8350e72393 fix: align json formatter with lint rules 2026-03-14 19:28:25 +08:00
xingyu4j
15f74b9d97 refactor: drop unused turbo eslint shim 2026-03-14 19:20:39 +08:00
xingyu4j
55b54e24fe refactor: migrate command lint to oxlint 2026-03-14 19:13:50 +08:00
xingyu4j
46b4ce81e4 refactor: shrink eslint fallback layer 2026-03-14 19:10:22 +08:00
xingyu4j
7a723d03d0 fix: tailwindcss 2026-03-14 18:48:30 +08:00
xingyu4j
9d6fbfd0d6 refactor: replace simple px utility styles 2026-03-14 18:43:00 +08:00
xingyu4j
8fd6bf47b1 revert: restore px-based calc utilities 2026-03-14 18:36:31 +08:00
xingyu4j
f25f3a34d0 refactor: replace px calc spacing utilities 2026-03-14 18:31:35 +08:00
xingyu4j
2823848fae refactor: migrate spacing utilities to tailwind v4 syntax 2026-03-14 18:27:16 +08:00
xingyu4j
b9467b2bc3 chore: configure tailwind v4 dynamic spacing 2026-03-14 18:14:44 +08:00
xingyu4j
fa190e0975 chore: checkpoint tailwind spacing updates 2026-03-14 18:11:08 +08:00
xingyu4j
90dc8cf997 chore: update deps 2026-03-14 17:52:40 +08:00
xingyu
53c5ccc00a !336 chore: vite 8.0
Merge pull request !336 from xingyu/vite8
2026-03-14 05:40:10 +00:00
xingyu4j
06c9e8d7c1 fix: type check 2026-03-14 13:39:51 +08:00
xingyu4j
f32818c6aa fix(lint): resolve shared form and utility warnings 2026-03-14 13:28:45 +08:00
xingyu4j
fb03afb6b7 fix(lint): clean up ai rich text views 2026-03-14 13:28:13 +08:00
xingyu4j
577efa56a9 fix(lint): update bpmn designer compatibility code 2026-03-14 13:27:38 +08:00
xingyu4j
cb98b3a47e fix(lint): add ts-expect-error descriptions 2026-03-14 13:27:00 +08:00
xingyu4j
8daf9a3ce5 docs: update version 2026-03-14 12:32:50 +08:00
xingyu4j
a83d8248d7 fix: lint 2026-03-14 12:27:31 +08:00
xingyu4j
4cdc92f759 fix: lint 2026-03-14 12:16:31 +08:00
xingyu4j
54c668c3f0 chore: update deps 2026-03-14 11:37:28 +08:00
xingyu4j
ac3fc6b7d3 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into vite8 2026-03-14 11:34:06 +08:00
Jin Mao
a6a6efdf59 chore: release 5.7.0
- 更新 backend-mock 包版本
- 更新 web-antd 包版本
- 更新 web-antdv-next 包版本
- 更新 web-ele 包版本
- 更新 web-naive 包版本
- 更新 web-tdesign 包版本
- 更新 docs 包版本
- 更新 commitlint-config 包版本
- 更新 eslint-config 包版本
- 更新 oxfmt-config 包版本
- 更新 oxlint-config 包版本
- 更新 stylelint-config 包版本
- 更新 node-utils 包版本
- 更新 tsconfig 包版本
- 更新 vite-config 包版本
- 更新 @core/base/design 包版本
- 更新 @core/base/icons 包版本
- 更新 @core/base/shared 包版本
- 更新 @core/base/typings 包版本
- 更新 @core/composables 包版本
- 更新 @core/preferences 包版本
- 更新 @core/ui-kit/form-ui 包版本
- 更新 @core/ui-kit/layout-ui 包版本
- 更新 @core/ui-kit/menu-ui 包版本
- 更新 @core/ui-kit/popup-ui 包版本
- 更新 @core/ui-kit/shadcn-ui 包版本
- 更新 @core/ui-kit/tabs-ui 包版本
- 更新 constants 包版本
- 更新 access 包版本
- 更新 common-ui 包版本
- 更新 hooks 包版本
- 更新 layouts 包版本
- 更新 plugins 包版本
- 更新 request 包版本
- 更新 icons 包版本
- 更新 locales 包版本
- 更新 preferences 包版本
- 更新 stores 包版本
- 更新 styles 包版本
- 更新 types 包版本
- 更新 utils 包版本
- 更新 playground 包版本
- 更新 turbo-run 包版本
- 更新 vsh 包版本
- 更新根目录包版本
2026-03-14 09:14:23 +08:00
xingyu4j
8043faf6c7 docs: remove doc 2026-03-14 00:18:54 +08:00
xingyu4j
ebed9e64ed fix: lint 2026-03-14 00:17:06 +08:00
xingyu4j
913636ae44 refactor: simplify oxc eslint compatibility 2026-03-14 00:16:27 +08:00
xingyu4j
7b064e9f33 chore: vsh lint 2026-03-13 23:41:17 +08:00
xingyu4j
16da0eaca3 fix: vsh lint 2026-03-13 23:31:19 +08:00
xingyu4j
6acfee2737 fix: vsh lint 2026-03-13 23:26:29 +08:00
xingyu4j
92abf7edaa chore: oxlint-tsgolint 2026-03-13 23:22:52 +08:00
xingyu4j
395babc1f5 feat: tsgolint 2026-03-13 23:13:10 +08:00
xingyu4j
68cde54bad feat: add tsgolint 2026-03-13 23:13:01 +08:00
xingyu4j
c7d7529c00 chore: ts 2026-03-13 23:07:04 +08:00
xingyu4j
748f60c7bb chore: lint config 2026-03-13 22:12:15 +08:00
xingyu4j
ffee62e940 chore(vscode): update workspace editor settings 2026-03-13 22:02:50 +08:00
xingyu4j
a0ea221131 style(common-ui): normalize generic formatting 2026-03-13 21:57:18 +08:00
xingyu4j
2846bcb84e fix(common-ui): guard resize drag start state 2026-03-13 21:56:53 +08:00
xingyu4j
542ed6c08f refactor: rename formatter utilities 2026-03-13 21:56:23 +08:00
xingyu4j
6dabb848a5 build: migrate formatting pipeline to oxfmt 2026-03-13 21:56:08 +08:00
xingyu4j
de0181e0d9 fix: lint 2026-03-13 20:58:07 +08:00
xingyu4j
a850d426ef chore: fix lint and typecheck issues 2026-03-13 20:57:52 +08:00
xingyu4j
771277d5d9 fix: align oxlint compat config typing 2026-03-13 20:28:56 +08:00
xingyu4j
20b4f5c99f chore: oxlint config 2026-03-13 20:26:10 +08:00
xingyu4j
e7fa87b301 chore: finalize oxlint migration config 2026-03-13 20:25:25 +08:00
xingyu4j
40c66958bc chore: remove un use deps 2026-03-13 15:59:04 +08:00
xingyu4j
600fc71aed fix: eslint 2026-03-13 15:58:53 +08:00
xingyu4j
443e4b04cd chore: update vite 8 2026-03-13 15:51:28 +08:00
Lmx1220
556a3c0fab Update pnpm-lock.yaml 2026-03-13 15:25:14 +08:00
Lmx1220
1eca52f962 Update pnpm-lock.yaml
更新到使用vue3版本
2026-03-13 15:20:58 +08:00
Lmx1220
e21adb395b Merge branch 'main' into main 2026-03-13 15:08:24 +08:00
xingyu4j
0e4bf80bf4 chore: update deps 2026-03-13 10:33:23 +08:00
cdc
107750971b fix: fix useEcharts 2026-03-10 21:48:33 +08:00
橙子
24e1be47ca fix: fix component type (#7601) 2026-03-10 05:11:06 +08:00
Mr. Xie
7e0978c764 fix: 修复验证码登录发送逻辑,未校验手机号或发送失败仍开始倒计时的问题 (#7616) 2026-03-10 05:10:34 +08:00
Leo
83a0c9662d fix: 修复路由重复 (#7590)
* fix: 修复路由重复

优化mixed模式路由合并逻辑。以backend 为基础,并从frontend补充

* fix: 修复名称冲突时子项合并顺序/覆盖(后端子项可能会丢失)。

* fix: 优化可访问性判断逻辑

* fix: error Forbidden non-null assertion

* refactor(access): 调整可访问性判断逻辑
2026-03-10 05:09:48 +08:00
xingyu
a4736a49f8 feat: migrate to Tailwind CSS v4 (#7614)
* chore: update deps

* feat: use jsonc/x language

* chore: update eslint 10.0

* fix: no-useless-assignment

* feat: add CLAUDE.md

* chore: ignore

* feat: claude

* fix: lint

* chore: suppot eslint v10

* fix: lint

* fix: lint

* fix: type check

* fix: unit test

* fix: Suggested fix

* fix: unit test

* chore: update stylelint v17

* chore: update all major deps

* fix:  echarts console warn

* chore: update vitest v4

* feat: add skills ignores

* chore: update deps

* chore: update deps

* fix: cspell

* chore: update deps

* chore: update tailwindcss v4

* chore: remove postcss config

* fix: no use catalog

* chore: tailwind v4 config

* fix: tailwindcss v4 sort

* feat: use eslint-plugin-better-tailwindcss

* fix: Interference between enforce-consistent-line-wrapping, jsx-curly-brace-presence and Prettier

* fix: Interference between enforce-consistent-line-wrapping, jsx-curly-brace-presence and Prettier

* fix(lint): resolve prettier and better-tailwindcss formatting conflicts

* fix(tailwind): update theme references and lint sources

* style(format): normalize apps docs and playground vue files

* style(format): normalize core ui-kit components

* style(format): normalize effects ui and layout components
2026-03-10 05:08:45 +08:00
YunaiV
1cbdf442ee feat: 添加 URL 验证工具函数并优化 area-select 组件的类型定义 2026-03-07 17:33:02 +08:00
YunaiV
f91a2702c9 merge: 合并 master 分支的 form-create 修复
合并 master 分支中关于 form-create 组件的修复,包括 area-select 和 iframe 组件的改进。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:32:54 +08:00
芋道源码
c885c0c71a !333 fix(form-create): 【ele/antd】完成 vue3 review c153ff93 的所有 TODO 修复
Merge pull request !333 from puhui999/master-fix
2026-03-07 03:23:42 +00:00
YunaiV
a8f67ab717 !335 fix: 修复上传头像时,如果图片加载失败,弹框一直loading的问题,针对 ele 版本 2026-03-07 11:21:03 +08:00
zouawen
aa7d8630b5 fix: 侧边菜单栏拖拽优化 (#7606)
* fix: 拖拽使用遮罩层实现,使得拖拽到min或max临界值时cursor显示not-allowed,同时拖拽线条颜色变浅,类似于disabled,提升用户体验

* fix: 修复代码审查建议;修复lint和test报错

* fix: 修复遮罩层挡住hover:bg-primary视觉效果问题
2026-03-07 05:32:09 +08:00
lmx
36313f378e chore: update package.json and cspell.json 2026-03-04 19:17:33 +08:00
lmx
45054d3238 chore: update pnpm-lock.yaml 2026-03-04 19:03:41 +08:00
lmx
173e6b08c9 fix: 修复执行: check:cspell 命令路径参数传入没有转义导致检测路径失效。添加菜单右键功能。优化:多余监听 activePath 变化,自动滚动到激活项 watch 2026-03-04 18:25:15 +08:00
xingyu
75e4d07395 !335 fix: 修复上传头像时,如果图片加载失败,弹框一直loading的问题
Merge pull request !335 from li_shifeng/fix-upload-avatar
2026-03-04 02:21:33 +00:00
zouawen
2a86404ba5 fix: 修复混合双列布局侧边栏拖拽线条位置显示bug,同步修复普通布局和混合双列布局切换时width计算导致侧边栏宽度显示异常问题,新增普通布局和混合双列布局侧边栏菜单折叠状态同步 (#7596) 2026-03-02 15:31:29 +08:00
han
b8a0199cde feat(preferences): add toggle for copy preferences button (#7594)
Co-authored-by: hl <hl@nmcsoft.com>
2026-03-02 15:30:38 +08:00
Bryan Qiu
a46ed55a86 fix: bump version to 5.6.0 (#7592) 2026-03-02 04:22:21 +08:00
Jin Mao
1a9fbddef4 Merge branch 'main' into main 2026-02-28 12:04:48 +08:00
zouawen
1209aaafb4 fix: 修复lint报错 2026-02-28 11:25:08 +08:00
zouawen
8e71261d49 fix: 侧边栏菜单拖拽功能在设置内增加开关 2026-02-28 11:19:24 +08:00
li_shifeng
586978f1b0 fix: 修复上传头像时,如果图片加载失败,弹框一直loading的问题 2026-02-28 10:00:39 +08:00
dependabot[bot]
49e45eab54 chore(deps): bump vue-router from 4.6.4 to 5.0.3 (#7583)
Bumps [vue-router](https://github.com/vuejs/router) from 4.6.4 to 5.0.3.
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v4.6.4...v5.0.3)

---
updated-dependencies:
- dependency-name: vue-router
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 17:06:18 +08:00
Jin Mao
bd22793ceb feat: add the attribute routeCached to route to control cache the DOM corresponding to the route 2026-02-27 16:04:01 +08:00
zouawen
b2013436c5 fix: 优化最大值限制 2026-02-27 11:12:51 +08:00
zouawen
cc808cb8c5 fix: 优化最小值限制 2026-02-27 11:07:28 +08:00
zouawen
afffc4b3f0 fix: 优化侧边栏拖拽逻辑,支持展开和折叠 2026-02-27 10:54:15 +08:00
zouawen
99710ef9dc feat: 优化侧边栏拖拽逻辑 2026-02-26 18:11:53 +08:00
zouawen
3d4ae04d9b Merge branch 'main' into main 2026-02-26 10:20:32 +08:00
zouawen
707b391449 feat: 侧边栏宽度拖拽改为composable实现,同时修复tabbar.ts文件lint报错 2026-02-26 10:09:57 +08:00
ming4762
45b843f344 fix: fix bug where renderEcharts gets stuck in a dead loop (#7561)
* 触发条件:echart所在页面开启keepalive 在其他页面切换颜色模式
2026-02-26 06:21:08 +08:00
Wu Clan
191fd90f06 chore: 更新表单描述显示样式 (#6938) 2026-02-26 06:17:04 +08:00
moil-xm
05920cd66d feat(vite-config): vite export typing (#7569)
* feat(vite-config): vite export typing

* feat(vite-config): add type

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-02-26 06:14:12 +08:00
Jin Mao
01508d5e42 fix: fix lint 2026-02-26 05:45:36 +08:00
zouawen
57cf6cbc9e feature: 简易版菜单宽度拖拽功能 2026-02-25 17:50:22 +08:00
芋道源码
dd69d7c1a5 !334 feat(iot):增加 modbus 配置 100%
Merge pull request !334 from 芋道源码/dev
2026-02-14 03:04:43 +00:00
YunaiV
63743b6929 feat(iot):增加 modbus 配置 100% 2026-02-14 11:02:56 +08:00
YunaiV
38597dd19d feat(iot):增加 modbus 配置 50% 2026-02-14 09:19:43 +08:00
AxiosLeo
03ebbea46a fix(menu): update hover color variable to use the correct reference (#7544)
* fix(menu): update hover color variable to use the correct reference

Medium Severity

In the horizontal .is-light menu section, --menu-item-hover-color is set to hsl(var(--menu-item-color)), but --menu-item-color is already defined as hsl(var(--accent-foreground)). This results in hsl(hsl(...)) at computed-value time, which is invalid CSS. The non-horizontal .is-light block correctly uses var(--menu-item-color) without the extra hsl() wrapper.

* fix(menu): simplify hover styles by removing redundant nested hover rules

Low Severity

The SCSS &:not(.is-active):hover { &:hover { ... } } compiles to a :hover:hover pseudo-class chain, which is functionally identical to a single :hover. The inner &:hover nesting is redundant and adds unnecessary complexity compared to placing styles directly inside the &:not(.is-active):hover block.
2026-02-12 22:22:53 +08:00
zouawen
8e7a5d1ec3 fix: Fix layout change, ensure div[ref="asideRef"] is contained within <aside> (#7551) 2026-02-12 22:22:34 +08:00
puhui999
e7365a4a00 fix(form-create): 【ele/antd】完成 vue3 review c153ff93 的所有 TODO 修复 2026-02-11 17:58:13 +08:00
AxiosLeo
aa74a2535b fix(tabbar): visitHistory field (#7543)
High Severity

The visitHistory field is a Stack<string> class instance persisted to sessionStorage via pinia-plugin-persistedstate. There's no custom serializer or hydration hook. When the page reloads, JSON.parse(JSON.stringify(stack)) produces a plain object {dedup, items, maxSize} that lacks all Stack methods (push, pop, remove, retain, etc.) and the size getter. Pinia's $patch replaces the Stack instance with this plain object, so subsequent calls like this.visitHistory.push(...) will throw a TypeError.
2026-02-11 16:09:37 +08:00
zouawen
32379ba4b7 fix: 双列菜单模式下新增深色侧边栏和深色侧边栏子栏 (#7542)
* fix: 双列菜单模式下新增深色侧边栏和深色侧边栏子栏

* fix: 修复报错 config.test.ts.snap

* fix: 修复lint报错

* fix: 修复侧边栏菜单文本内容溢出问题

* fix: 修复lint报错
2026-02-11 16:08:32 +08:00
xingyu
bf4fed78f2 !332 Merge branch 'main' of <a href="https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fvbenjs%2Fvue-vben-admin">https://github.com/vbenjs/vue-vben-admin</a> into dev
Merge pull request !332 from xingyu/dev
2026-02-11 03:12:23 +00:00
xingyu4j
722afc85df Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2026-02-11 11:01:44 +08:00
xingyu
3036596d16 !331 Merge remote-tracking branch 'yudao/dev' into dev
Merge pull request !331 from Jason/dev
2026-02-10 14:34:20 +00:00
jason
aee539f37e Merge remote-tracking branch 'yudao/dev' into dev 2026-02-10 22:08:57 +08:00
jason
05b41692ba feat: 移动端 uniapp 流程表单嵌入页面 2026-02-10 22:07:19 +08:00
moil-xm
7fe8d7b4be fix: ts 错误: 类型实例化过深,且可能无限 2026-02-10 16:13:36 +08:00
Bin
aace726a91 feat(playground): add antdv-next router link (#7532)
Co-authored-by: fuwb <fuwb@sunsharing.com.cn>
2026-02-10 13:09:34 +08:00
Jin Mao
e6f6e5464a Merge branch 'main' into main 2026-02-10 12:08:16 +08:00
Aliner
7d04b600fb fix: correct updateDate to updateData in the echarts hook (#7538)
* fix(@vben/plugins): Fixed the misspelling of the data update method name in the echarts hook

Correct updateDate to updateData, ensuring that the API method name is correct and consistent

* Revert "fix(@vben/plugins): Fixed the misspelling of the data update method name in the echarts hook"

This reverts commit 86d679cf25631bd1abd56d4f971e6db3a9b9d6d5.

* fix(@vben/plugins): fixed the misspelling of the data update method name in the echarts hook

Correct updateDate to updateData, ensuring that the API method name is correct and consistent
2026-02-10 11:19:45 +08:00
xingyu
8a622889ff !330 feat(form-create): 【ele/antd】新增 iframe 和省市区选择器组件
Merge pull request !330 from puhui999/master-fix
2026-02-10 01:32:01 +00:00
zouawen
463bfde2ac fix: config.test.ts.snap新增showRefresh参数 2026-02-10 08:50:06 +08:00
zouawen
893f74dc3e fix: 优化横向布局时菜单激活或聚焦时背景色,标签工具栏新增刷新按钮,其他样式优化 2026-02-09 16:32:02 +08:00
liubei
e136679934 fix: [bpm][antd&ele] 修复流程设计器自定义配置编辑后丢失的问题 2026-02-09 15:35:53 +08:00
Jin Mao
8a215fbcc7 chore: release 5.6.0 2026-02-09 05:09:57 +08:00
Jin Mao
ac5e4c4722 chore: update deps 2026-02-09 04:52:06 +08:00
Jin Mao
04d01b0bab chore: fix lint 2026-02-09 04:49:06 +08:00
Jin Mao
cb1d7565a3 Merge branch 'fork/ffgenius/antd-vue-next' 2026-02-09 03:09:01 +08:00
Jin Mao
1d9b6407a4 chore: 更新开发环境端口号配置
- 将 VITE_PORT 从 5555 修改为 5999
- 保持其他环境变量配置不变
2026-02-09 03:04:42 +08:00
MistyMoon
22ed522711 feat: support menuVisibleWithForbidden in generate-routes-backend (#7526)
当后端菜单项 `meta.menuVisibleWithForbidden` 为 true 时,将其路由组件替换为 403 页,菜单仍展示该项,访问时展示 403,便于用户知悉功能并申请权限。
2026-02-09 02:44:29 +08:00
Jin Mao
a3598ef859 chore: fix lint 2026-02-09 02:42:50 +08:00
Jin Mao
6fe09ec2dd perf: optimize the closing jump logic of tabs 2026-02-09 02:36:38 +08:00
Jin Mao
57911d9e09 Merge branch 'tab-2026020401' of https://github.com/ming4762/smart-boot-ui-vben into ming4762-tab-2026020401 2026-02-09 02:36:04 +08:00
Bin
3aee283495 Revert "feat(web): cancel pnpm-lock.yaml submission"
This reverts commit 54b24c2677.
2026-02-08 23:14:31 +08:00
Bin
54b24c2677 feat(web): cancel pnpm-lock.yaml submission 2026-02-08 23:12:40 +08:00
Bin
8cadad0a1e feat(web): add antdv-next model 2026-02-08 23:00:19 +08:00
zhongming4762
633c5f3cda perf: optimize the closing jump logic of tabs
* 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
 * 支持在配置中开启或关闭
2026-02-08 20:50:54 +08:00
zhongming4762
a8431e2040 perf: optimize the closing jump logic of tabs
* 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
 * 支持在配置中开启或关闭
2026-02-08 20:36:32 +08:00
zhongming4762
7a2b916387 perf: optimize the closing jump logic of tabs
* 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
 * 支持在配置中开启或关闭
2026-02-08 20:36:16 +08:00
puhui999
f3deefae56 Merge remote-tracking branch 'yudao/master' into master-fix 2026-02-08 11:57:23 +08:00
puhui999
d0a7065991 feat(form-create): 【ele/antd】新增 iframe 和省市区选择器组件
- 新增 iframe 网页嵌入组件,支持 URL 配置和实时预览
- 新增省市区三级联动选择器组件
- 支持 web-ele 和 web-antd 双版本
2026-02-08 11:56:02 +08:00
YunaiV
5b7e7c4d56 fix: 修复新增空目录菜单时 component 为 null 导致路由生成报错 2026-02-07 18:30:03 +08:00
Jin Mao
f4dfb68b7b Merge branch 'MrLeo-main' 2026-02-06 15:41:55 +08:00
Jin Mao
8f4f27d860 Merge branch 'main' of https://github.com/MrLeo/vue-vben-admin into MrLeo-main 2026-02-06 15:39:48 +08:00
tikitoki
e9eab29953 fix:fix password input icon visual bug in certain browser (#7521)
Co-authored-by: nick8799981325 <zc1078134211@163.com>
2026-02-06 15:28:48 +08:00
Leo Caan (陈栋)
4f1eeb7da5 fix: 修复设置default-expanded-level后无法check更低层级节点 logic and tree value updates (#7155)
假设缺省展开2级,当check 3级节点时,会触发effectWatch重新收缩到2级,并丢失check操作check操作andling.
2026-02-06 12:55:14 +08:00
Leo
6fd426d719 fix: 当 showToolbar 为 false 时禁用工具栏(vxe-table),减少无 Toolbar 的 Gird 留白 2026-02-04 22:20:17 +08:00
zhongming4762
331da3c8c7 perf: optimize the closing jump logic of tabs
* 依据tab访问历史回退上一个tab,原逻辑是返回一下个 或 上一个
2026-02-04 19:29:33 +08:00
ming4762
c48943bc67 fix: fix Nested Objects dependencies not effective (#7345) 2026-01-31 16:44:20 +08:00
xingyu
7680b33b99 fix: #7140 (#7153)
* chore: add yaml eslint validate

* chore: update deps

* fix: unused ts lint

* fix: 弹窗只能点击一次 #7140

* chore: update actions/checkout v6

* chore: update node version v22.22.0
2026-01-28 18:05:20 +08:00
Jin Mao
bb5d75bc7e fix: 修复表单展开无效 (#7148)
- 修正模板中 ref 属性的引用名称
2026-01-27 11:35:50 +08:00
ming4762
528395e2c3 perf: optimizing hidden fields cannot trigger dependencies (#7142) 2026-01-26 16:12:26 +08:00
programmer
6a9012e5e4 chore: 给个人中心的2个按钮加上 i18n (#7138)
* fix: 个人中心按钮i18n

* fix: eslint format

* fix: 调整label宽度让英文显示在一行

* chore: 调整label小点

* fix: import

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-01-26 16:12:09 +08:00
橙子
6e8315ab40 fix: arguments order update (#7132)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-01-26 16:11:37 +08:00
848 changed files with 25983 additions and 12390 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -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'

View File

@@ -30,7 +30,7 @@ jobs:
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -28,12 +28,12 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
run_install: false
@@ -67,7 +67,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -90,7 +90,7 @@ jobs:
- windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -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

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -20,6 +20,6 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
- uses: release-drafter/release-drafter@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -19,15 +19,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
node-version: [22]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
# - name: Checkout code
# uses: actions/checkout@v4
# uses: actions/checkout@v6
# with:
# fetch-depth: 0
@@ -58,7 +58,7 @@ jobs:
echo "version=${version}" >> $GITHUB_OUTPUT
echo "major=${major}" >> $GITHUB_OUTPUT
- uses: release-drafter/release-drafter@v6
- uses: release-drafter/release-drafter@v7
with:
version: ${{ steps.version.outputs.version }}
publish: true

9
.gitignore vendored
View File

@@ -22,7 +22,7 @@ yarn.lock
package-lock.json
.VSCodeCounter
**/backend-mock/data
.omx
# local env files
.env.local
.env.*.local
@@ -50,3 +50,10 @@ vite.config.ts.*
*.sw?
.history
.cursor
# AI
.agent
.agents
.claude
.codex
skills-lock.json

View File

@@ -1 +1 @@
22.1.0
22.22.0

4
.npmrc
View File

@@ -1,8 +1,8 @@
registry=https://registry.npmmirror.com
public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss
public-hoist-pattern[]=oxfmt
public-hoist-pattern[]=oxlint
public-hoist-pattern[]=stylelint
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=@commitlint/*

View File

@@ -1,18 +0,0 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml

View File

@@ -1 +0,0 @@
export { default } from '@vben/prettier-config';

View File

@@ -2,3 +2,7 @@ dist
public
__tests__
coverage
.codex
.claude
.agent
.agents

View File

@@ -2,14 +2,18 @@
"recommendations": [
// Vue 3 的语言支持
"Vue.volar",
// 将 ESLint JavaScript 集成到 VS Code 中。
// 将 eslint 集成到 VS Code 中。
"dbaeumer.vscode-eslint",
// 将 oxlint 集成到 VS Code 中。
"oxc.oxc-vscode",
// Visual Studio Code 的官方 Stylelint 扩展
"stylelint.vscode-stylelint",
// 使用 Prettier 的代码格式化程序
"esbenp.prettier-vscode",
// 使用 oxfmt 的代码格式化程序
"oxc.oxc-vscode",
// 支持 dotenv 文件语法
"mikestead.dotenv",
// YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
"redhat.vscode-yaml",
// 源代码的拼写检查器
"streetsidesoftware.code-spell-checker",
// Tailwind CSS 的官方 VS Code 插件

56
.vscode/settings.json vendored
View File

@@ -1,5 +1,6 @@
{
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
"tailwindCSS.lint.suggestCanonicalClasses": "ignore",
// workbench
"workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile",
@@ -31,39 +32,51 @@
"editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
"editor.quickSuggestions": {
"strings": "on"
},
// lint && format
"oxc.enable": true,
"oxc.typeAware": true,
"oxc.configPath": "oxlint.config.ts",
"oxc.fmt.configPath": "oxfmt.config.ts",
"eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "oxc.oxc-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode"
},
// extensions
"extensions.ignoreRecommendations": true,
@@ -79,6 +92,7 @@
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"files.associations": {
"*.css": "tailwindcss",
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
@@ -118,7 +132,7 @@
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
// 使用搜索功能时,将这些文件夹/文件排除在外
// 使用搜索功能时,将这些文件文件排除在外
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
@@ -159,7 +173,7 @@
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
"errorLens.excludeBySource": ["cSpell", "Grammarly"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
@@ -167,9 +181,10 @@
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "on",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.inlayHints.enumMemberValues.enabled": true,
"js/ts.preferences.preferTypeOnlyAutoImports": true,
"js/ts.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
@@ -193,7 +208,7 @@
"*": false
},
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
"cssVariables.lookupFiles": ["packages/@core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
@@ -218,12 +233,9 @@
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
"tailwind.config.mjs": "postcss.*"
"oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false
"vue.server.hybridMode": true
}

View File

@@ -9,7 +9,7 @@
## 🐶 新手必读
- nodejs > v20.19.0 | v22 | v24 && pnpm > 10.20.0 (强制使用pnpm)
- nodejs >= v20.19.0(推荐 v22 / v24 && pnpm >= 10.32.1强制使用 pnpm
- 演示地址【Vue3 + element-plus】<http://dashboard-vue3.yudao.iocoder.cn>
- 演示地址【Vue3 + vben5(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
- 演示地址【Vue2 + element-ui】<http://dashboard.yudao.iocoder.cn>
@@ -20,12 +20,12 @@
**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
- 采用最新 [vue-vben-admin](https://github.com/vbenjs/vue-vben-admin) v5 实现
- 采用最新 [vue-vben-admin](https://github.com/vbenjs/vue-vben-admin) v5.7.0 实现
- 支持 [Ant Design Vue](https://www.antdv.com/) | [Element Plus](https://element-plus.org/zh-CN/) | [Naive UI](https://www.naiveui.com/) | [TDesign](https://tdesign.tencent.com/) 多种免费开源的中后台模版,具备如下特性:
![首页](.gitee/image/demo/vben.png)
- **最新技术栈**:使用 Vue3、Vite7 等前端前沿技术开发
- **最新技术栈**:使用 Vue3、Vite8 等前端前沿技术开发
- **TypeScript**: 应用程序级 JavaScript 的语言
- **主题**: 提供多套主题色彩,可配置自定义主题
- **国际化**:内置完善的国际化方案
@@ -41,24 +41,24 @@
| 框架 | 说明 | 版本 |
| --- | --- | --- |
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.27 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.3.1 |
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.30 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 8.0.0 |
| [Ant Design Vue](https://www.antdv.com/) | Ant Design Vue | 4.2.6 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.13.1 |
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.43.2 |
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.18.0 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.13.5 |
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.44.1 |
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.18.5 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.9.3 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 3.0.4 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 14.1.0 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.2.8 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.6.4 |
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.19 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 14.2.1 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.3.0 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 5.0.3 |
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 4.2.1 |
| [Iconify](https://iconify.design/) | 图标组件 | 5.0.0 |
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.431 |
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.449 |
| [TinyMCE](https://www.tiny.cloud/) | 富文本编辑器 | 7.3.0 |
| [Echarts](https://echarts.apache.org/) | 图表库 | 6.0.0 |
| [axios](https://axios-http.com/) | http客户端 | 1.13.2 |
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.19 |
| [axios](https://axios-http.com/) | http客户端 | 1.13.6 |
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.20 |
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.1 |
| [zod](https://zod.dev/) | 数据验证 | 3.25.76 |
@@ -82,9 +82,9 @@
![功能分层](/.gitee/image/common/ruoyi-vue-pro-biz.png)
- 通用模块(必选):系统功能、基础设施
- 通用模块(可选):工作流程、支付系统、数据报表、会员中心
- 业务系统按需ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
* 通用模块(必选):系统功能、基础设施
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
* 业务系统按需ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
### 系统功能
@@ -219,6 +219,16 @@
![功能图](/.gitee/image/common/mall-preview.png)
### 会员中心
| | 功能 | 描述 |
|-----|------|----------------------------------|
| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 |
| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 |
| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 |
| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
### ERP 系统
演示地址:<https://doc.iocoder.cn/erp-preview/>
@@ -231,6 +241,14 @@
![功能图](/.gitee/image/common/crm-feature.png)
### MES 系统
演示地址:<https://doc.iocoder.cn/mes-preview/>
![功能图](/.gitee/image/common/mes-feature.png)
![功能图](/.gitee/image/common/mes-preview.png)
### AI 大模型
演示地址:<https://doc.iocoder.cn/ai-preview/>
@@ -238,3 +256,11 @@
![功能图](/.gitee/image/common/ai-feature.png)
![功能图](/.gitee/image/common/ai-preview.gif)
### IoT 物联网
演示地址:<https://doc.iocoder.cn/iot/build>
![功能图](/.gitee/image/common/iot-feature.png)
![预览图](/.gitee/image/common/iot-preview.png)

View File

@@ -12,7 +12,7 @@
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title>
<title>%VITE_APP_TITLE%</title>
<link rel="icon" href="/favicon.ico" />
<script>
var HM_ID = '<%= VITE_APP_BAIDU_CODE %>';

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.5.9",
"version": "5.7.0",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1 +0,0 @@
export { default } from '@vben/tailwind-config/postcss';

View File

@@ -2537,12 +2537,12 @@ interface EditorSelection {
normalize: () => Range;
selectorChanged: (selector: string, callback: (active: boolean, args: {
node: Node;
selector: String;
selector: string;
parents: Node[];
}) => void) => EditorSelection;
selectorChangedWithUnbind: (selector: string, callback: (active: boolean, args: {
node: Node;
selector: String;
selector: string;
parents: Node[];
}) => void) => {
unbind: () => void;
@@ -3217,9 +3217,9 @@ interface Tools {
<T, R>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, R>): R[];
<T, R>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, R>): R[];
};
extend: (obj: Object, ext: Object, ...objs: Object[]) => any;
extend: (obj: object, ext: object, ...objs: object[]) => any;
walk: <T extends Record<string, any>>(obj: T, f: WalkCallback<T>, n?: keyof T, scope?: any) => void;
resolve: (path: string, o?: Object) => any;
resolve: (path: string, o?: object) => any;
explode: (s: string | string[], d?: string | RegExp) => string[];
_addCacheSuffix: (url: string) => string;
}

View File

@@ -6,14 +6,39 @@
/* eslint-disable vue/one-component-per-file */
import type {
AutoCompleteProps,
ButtonProps,
CascaderProps,
CheckboxGroupProps,
CheckboxProps,
DatePickerProps,
DividerProps,
InputNumberProps,
InputProps,
MentionsProps,
RadioGroupProps,
RadioProps,
RateProps,
SelectProps,
SpaceProps,
SwitchProps,
TextAreaProps,
TimePickerProps,
TreeSelectProps,
UploadChangeParam,
UploadFile,
UploadProps,
} from 'ant-design-vue';
import type { RangePickerProps } from 'ant-design-vue/es/date-picker';
import type { Component, Ref } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { Recordable } from '@vben/types';
import {
@@ -21,6 +46,9 @@ import {
defineAsyncComponent,
defineComponent,
h,
nextTick,
onMounted,
onUnmounted,
ref,
render,
unref,
@@ -33,6 +61,7 @@ import {
IconPicker,
VCropper,
} from '@vben/common-ui';
import { useSortable } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { isEmpty } from '@vben/utils';
@@ -41,6 +70,15 @@ import { message, Modal, notification } from 'ant-design-vue';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
draggable?: boolean;
handleChange?: (event: UploadChangeParam) => void;
maxSize?: number;
onDragSort?: (oldIndex: number, newIndex: number) => void;
onHandleChange?: (event: UploadChangeParam) => void;
};
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
@@ -132,260 +170,261 @@ const withDefaultPlaceholder = <T extends Component>(
});
};
const withPreviewUpload = () => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
const IMAGE_EXTENSIONS = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
/**
* 检查是否为图片文件
*/
function isImageFile(file: UploadFile): boolean {
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? imageExtensions.has(ext) : false;
}
return file.type.startsWith('image/');
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
* 创建默认的上传按钮插槽
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
};
// 创建默认的上传按钮插槽
const createDefaultSlotsWithUpload = (
listType: string,
placeholder: string,
) => {
switch (listType) {
case 'picture-card': {
return {
default: () => placeholder,
};
}
default: {
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
}
/**
* 获取文件的 Base64
*/
function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
* 预览图片
*/
async function previewImage(
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
current: currentIndex,
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
},
() => placeholder,
},
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
};
}
}
);
};
},
};
// 构建预览图片组
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
// 如果当前文件不是图片,直接打开
if (!isImageFile(file)) {
if (file.url) {
window.open(file.url, '_blank');
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
// 对于图片文件,继续使用预览组
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
render(h(PreviewWrapper), container);
}
const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', (error) => reject(error));
});
};
// 从fileList中过滤出所有图片文件
const imageFiles = (unref(fileList) || []).filter((element) =>
isImageFile(element),
);
// 为所有没有预览地址的图片生成预览
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
}
}
const container: HTMLElement | null = document.createElement('div');
/**
* 图片裁剪操作
*/
function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const PreviewWrapper = {
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
PreviewGroupComponent,
Modal,
{
class: 'hidden',
preview: {
visible: visible.value,
// 设置初始显示的图片索引
current: imageFiles.findIndex((f) => f.uid === file.uid),
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
}
},
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
// 渲染所有图片文件
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(PreviewWrapper), container);
};
// 图片裁剪操作
const cropImage = (file: File, aspectRatio: string | undefined) => {
return new Promise((resolve, reject) => {
const container: HTMLElement | null = document.createElement('div');
document.body.append(container);
// 用于追踪组件是否已卸载
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref<boolean>(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
// 延迟清理,确保动画完成
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
resolve(dataUrl);
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (ref: any) => (cropperRef.value = ref),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
};
render(h(CropperWrapper), container);
});
}
/**
* 带预览功能的上传组件
*/
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup: (
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
@@ -399,12 +438,14 @@ const withPreviewUpload = () => {
file: UploadFile,
originFileList: Array<File>,
) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 多选或者非图片不唤起裁剪框
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
@@ -412,14 +453,11 @@ const withPreviewUpload = () => {
isImageFile(file)
) {
file.status = 'removed';
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
const blob = await cropImage(originFileList[0], aspectRatio.value);
return new Promise((resolve, reject) => {
if (!blob) {
return reject(new Error($t('ui.crop.errorTip')));
}
resolve(blob);
});
if (!blob) {
throw new Error($t('ui.crop.errorTip'));
}
return blob;
}
return attrs.beforeUpload?.(file) ?? true;
@@ -427,12 +465,9 @@ const withPreviewUpload = () => {
const handleChange = (event: UploadChangeParam) => {
try {
// 行内写法 handleChange: (event) => {}
attrs.handleChange?.(event);
// template写法 @handle-change="(event) => {}"
attrs.onHandleChange?.(event);
} catch (error) {
// Avoid breaking internal v-model sync on user handler errors
console.error(error);
}
fileList.value = event.fileList.filter(
@@ -449,21 +484,88 @@ const withPreviewUpload = () => {
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
const renderUploadButton = () => {
if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
? createDefaultUploadSlots(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
// 拖拽排序
const draggable = computed(
() => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head.append(style);
}
}
function removeDragStyle() {
document.querySelector(`[id="${styleId}"]`)?.remove();
}
async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 监听表单值变化
watch(
() => attrs.modelValue,
(res) => {
@@ -471,18 +573,28 @@ const withPreviewUpload = () => {
},
);
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
'div',
{ 'data-upload-id': uploadId, class: 'w-full' },
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
);
},
});
@@ -523,6 +635,39 @@ export type ComponentType =
| 'Upload'
| BaseFormComponentType;
/**
* 与 {@link ComponentType} 中注册的组件名一一对应,便于 Schema 上 `component` + `componentProps` 联动提示
*/
export interface ComponentPropsMap {
ApiCascader: ApiComponentSharedProps & CascaderProps;
ApiSelect: ApiComponentSharedProps & SelectProps;
ApiTreeSelect: ApiComponentSharedProps & TreeSelectProps;
AutoComplete: AutoCompleteProps;
Cascader: CascaderProps;
Checkbox: CheckboxProps;
CheckboxGroup: CheckboxGroupProps;
DatePicker: DatePickerProps;
DefaultButton: ButtonProps;
Divider: DividerProps;
IconPicker: IconPickerProps;
Input: InputProps;
InputNumber: InputNumberProps;
InputPassword: InputProps;
Mentions: MentionsProps;
PrimaryButton: ButtonProps;
Radio: RadioProps;
RadioGroup: RadioGroupProps;
RangePicker: RangePickerProps;
Rate: RateProps;
Select: SelectProps;
Space: SpaceProps;
Switch: SwitchProps;
Textarea: TextAreaProps;
TimePicker: TimePickerProps;
TreeSelect: TreeSelectProps;
Upload: AdapterUploadProps;
}
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载

View File

@@ -1,9 +1,9 @@
import type {
VbenFormProps as FormProps,
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import type { ComponentPropsMap, ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -61,9 +61,9 @@ async function initSetupVbenForm() {
});
}
const useVbenForm = useForm<ComponentType>;
const useVbenForm = useForm<ComponentType, ComponentPropsMap>;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };
export type VbenFormSchema = FormSchema<ComponentType, ComponentPropsMap>;
export type VbenFormProps = FormProps<ComponentType, ComponentPropsMap>;

View File

@@ -1,6 +1,8 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { Recordable } from '@vben/types';
import type { ComponentPropsMap, ComponentType } from './component';
import { h } from 'vue';
import { IconifyIcon } from '@vben/icons';
@@ -10,7 +12,7 @@ import {
AsyncVxeTable,
createRequiredValidation,
setupVbenVxeTable,
useVbenVxeGrid,
useVbenVxeGrid as useGrid,
} from '@vben/plugins/vxe-table';
import {
erpCountInputFormatter,
@@ -199,7 +201,7 @@ setupVbenVxeTable({
vxeUI.renderer.add('CellOperation', {
renderTableDefault({ attrs, options, props }, { column, row }) {
const defaultProps = { size: 'small', type: 'link', ...props };
let align = 'end';
let align: string;
switch (column.align) {
case 'center': {
align = 'center';
@@ -363,10 +365,13 @@ setupVbenVxeTable({
useVbenForm,
});
export { createRequiredValidation, useVbenVxeGrid };
export { createRequiredValidation };
export const [VxeTable, VxeColumn] = [AsyncVxeTable, AsyncVxeColumn];
export * from '#/components/table-action';
export const useVbenVxeGrid = <T extends Record<string, any>>(
...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>>
) => useGrid<T, ComponentType, ComponentPropsMap>(...rest);
export type * from '@vben/plugins/vxe-table';

View File

@@ -16,10 +16,10 @@ export namespace CrmCustomerLimitConfigApi {
/** 客户限制配置类型 */
export enum LimitConfType {
/** 锁定客户数限制 */
CUSTOMER_LOCK_LIMIT = 2,
/** 拥有客户数限制 */
CUSTOMER_QUANTITY_LIMIT = 1,
/** 锁定客户数限制 */
CUSTOMER_LOCK_LIMIT = 2,
}
/** 查询客户限制配置列表 */

View File

@@ -35,11 +35,11 @@ export namespace CrmPermissionApi {
* CRM 业务类型枚举
*/
export enum BizTypeEnum {
CRM_BUSINESS = 4, // 商机
CRM_CLUE = 1, // 线索
CRM_CONTACT = 3, // 联系人
CRM_CONTRACT = 5, // 合同
CRM_CUSTOMER = 2, // 客户
CRM_CONTACT = 3, // 联系人
CRM_BUSINESS = 4, // 商机
CRM_CONTRACT = 5, // 合同
CRM_PRODUCT = 6, // 产品
CRM_RECEIVABLE = 7, // 回款
CRM_RECEIVABLE_PLAN = 8, // 回款计划

View File

@@ -0,0 +1,30 @@
import { requestClient } from '#/api/request';
export namespace IotDeviceModbusConfigApi {
/** Modbus 连接配置 VO */
export interface ModbusConfig {
id?: number; // 主键
deviceId: number; // 设备编号
ip: string; // Modbus 服务器 IP 地址
port: number; // Modbus 服务器端口
slaveId: number; // 从站地址
timeout: number; // 连接超时时间,单位:毫秒
retryInterval: number; // 重试间隔,单位:毫秒
mode: number; // 模式
frameFormat: number; // 帧格式
status: number; // 状态
}
}
/** 获取设备的 Modbus 连接配置 */
export function getModbusConfig(deviceId: number) {
return requestClient.get<IotDeviceModbusConfigApi.ModbusConfig>(
'/iot/device-modbus-config/get',
{ params: { deviceId } },
);
}
/** 保存 Modbus 连接配置 */
export function saveModbusConfig(data: IotDeviceModbusConfigApi.ModbusConfig) {
return requestClient.post('/iot/device-modbus-config/save', data);
}

View File

@@ -0,0 +1,52 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace IotDeviceModbusPointApi {
/** Modbus 点位配置 VO */
export interface ModbusPoint {
id?: number; // 主键
deviceId: number; // 设备编号
thingModelId?: number; // 物模型属性编号
identifier: string; // 属性标识符
name: string; // 属性名称
functionCode?: number; // Modbus 功能码
registerAddress?: number; // 寄存器起始地址
registerCount?: number; // 寄存器数量
byteOrder?: string; // 字节序
rawDataType?: string; // 原始数据类型
scale: number; // 缩放因子
pollInterval: number; // 轮询间隔,单位:毫秒
status: number; // 状态
}
}
/** 获取设备的 Modbus 点位分页 */
export function getModbusPointPage(params: PageParam) {
return requestClient.get<PageResult<IotDeviceModbusPointApi.ModbusPoint>>(
'/iot/device-modbus-point/page',
{ params },
);
}
/** 获取 Modbus 点位详情 */
export function getModbusPoint(id: number) {
return requestClient.get<IotDeviceModbusPointApi.ModbusPoint>(
`/iot/device-modbus-point/get?id=${id}`,
);
}
/** 创建 Modbus 点位配置 */
export function createModbusPoint(data: IotDeviceModbusPointApi.ModbusPoint) {
return requestClient.post('/iot/device-modbus-point/create', data);
}
/** 更新 Modbus 点位配置 */
export function updateModbusPoint(data: IotDeviceModbusPointApi.ModbusPoint) {
return requestClient.put('/iot/device-modbus-point/update', data);
}
/** 删除 Modbus 点位配置 */
export function deleteModbusPoint(id: number) {
return requestClient.delete(`/iot/device-modbus-point/delete?id=${id}`);
}

View File

@@ -10,7 +10,7 @@ export namespace IotProductApi {
productKey?: string; // 产品标识
productSecret?: string; // 产品密钥
protocolId?: number; // 协议编号
protocolType?: number; // 接入协议类型
protocolType?: string; // 协议类型
categoryId?: number; // 产品所属品类标识符
categoryName?: string; // 产品所属品类名称
icon?: string; // 产品图标
@@ -19,7 +19,7 @@ export namespace IotProductApi {
status?: number; // 产品状态
deviceType?: number; // 设备类型
netType?: number; // 联网方式
codecType?: string; // 数据格式(编解码器类型
serializeType?: string; // 序列化类型
dataFormat?: number; // 数据格式
validateType?: number; // 认证方式
registerEnabled?: boolean; // 是否开启动态注册
@@ -28,6 +28,25 @@ export namespace IotProductApi {
}
}
// IoT 协议类型枚举
export enum ProtocolTypeEnum {
COAP = 'coap',
EMQX = 'emqx',
HTTP = 'http',
MODBUS_TCP_CLIENT = 'modbus_tcp_client',
MODBUS_TCP_SERVER = 'modbus_tcp_server',
MQTT = 'mqtt',
TCP = 'tcp',
UDP = 'udp',
WEBSOCKET = 'websocket',
}
// IoT 序列化类型枚举
export enum SerializeTypeEnum {
BINARY = 'binary',
JSON = 'json',
}
/** 查询产品分页 */
export function getProductPage(params: PageParam) {
return requestClient.get<PageResult<IotProductApi.Product>>(

View File

@@ -119,13 +119,58 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
response.data = apiEncrypt.decryptResponse(response.data);
} catch (error) {
console.error('响应数据解密失败:', error);
throw new Error(`响应数据解密失败: ${(error as Error).message}`);
throw new Error(`响应数据解密失败: ${(error as Error).message}`, {
cause: error,
});
}
}
return response;
},
});
// add by 芋艿:对应 https://t.zsxq.com/SHqWw 反馈
// 处理 Blob 响应中的业务错误(如 401后端把「账号未登录」包成 HTTP 200 + body {code: 401, msg: ...}
// download 强制 responseType: 'blob' 后被 axios 包成 application/json 的 BlobdefaultResponseInterceptor 走
// responseReturn === 'body' 分支直接返回,绕过了 authenticateResponseInterceptor 的 401 token 刷新;
// 这里把这种 Blob 解析回 JSON再以 axios 风格抛出,让后续拦截器接管
client.addResponseInterceptor({
fulfilled: async (response) => {
const blob = response.data;
if (!(blob instanceof Blob)) {
return response;
}
// Blob.type 在部分环境可能为空或大小写不一,叠加 response header 一起判断更稳
const blobType = (blob.type || '').toLowerCase();
const headerType = String(
response.headers?.['content-type'] ??
response.headers?.['Content-Type'] ??
'',
).toLowerCase();
if (
!blobType.includes('application/json') &&
!headerType.includes('application/json')
) {
return response;
}
let parsed: any;
try {
parsed = JSON.parse(await blob.text());
} catch {
return response;
}
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
response.data = parsed;
throw Object.assign(new Error(parsed.msg ?? 'Request failed'), {
config: response.config,
response,
data: parsed,
isAxiosError: true,
});
}
return response;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({

View File

@@ -41,8 +41,8 @@ const [Modal, modalApi] = useVbenModal({
onConfirm: handleOk,
onOpenChange(isOpen) {
if (isOpen) {
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading通过 handleReady
modalLoading(true);
// 只有存在可加载图片时才显示 loading避免空图或异常链接导致一直 loading
modalLoading(!!src.value);
} else {
// 关闭时,清空右侧预览
previewSource.value = '';
@@ -65,10 +65,14 @@ function handleBeforeUpload(file: File) {
reader.readAsDataURL(file);
src.value = '';
previewSource.value = '';
modalLoading(true);
reader.addEventListener('load', (e) => {
src.value = (e.target?.result as string) ?? '';
filename = file.name;
});
reader.addEventListener('error', () => {
modalLoading(false);
});
return false;
}
@@ -82,6 +86,10 @@ function handleReady(cropperInstance: CropperType) {
modalLoading(false);
}
function handleCropperError() {
modalLoading(false);
}
function handlerToolbar(event: string, arg?: number) {
if (event === 'scaleX') {
scaleX = arg = scaleX === -1 ? 1 : -1;
@@ -133,6 +141,7 @@ async function handleOk() {
:src="src"
height="300px"
@cropend="handleCropend"
@cropend-error="handleCropperError"
@ready="handleReady"
/>
</div>

View File

@@ -143,6 +143,10 @@ function getRoundedCanvas() {
context.fill();
return canvas;
}
function handleImageError() {
emit('cropendError');
}
</script>
<template>
@@ -154,6 +158,7 @@ function getRoundedCanvas() {
:crossorigin="crossorigin"
:src="src"
:style="getImageStyle"
@error="handleImageError"
class="h-auto max-w-full"
/>
</div>

View File

@@ -21,7 +21,7 @@ export function useDescription(options?: Partial<DescriptionProps>) {
inheritAttrs: false,
setup(_props, { attrs, slots }) {
return () => {
// @ts-ignore - 避免类型实例化过深
// @ts-expect-error - 避免类型实例化过深
return h(Description, { ...propsState, ...attrs }, slots);
};
},

View File

@@ -0,0 +1,155 @@
<!-- 省市区选择器 (Ant Design Vue 版本) -->
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { AreaLevelEnum } from '@vben/constants';
import { Cascader } from 'ant-design-vue';
import { getAreaTree } from '#/api/system/area';
defineOptions({ name: 'AreaSelect' });
const props = withDefaults(defineProps<Props>(), {
modelValue: undefined,
value: undefined,
level: AreaLevelEnum.DISTRICT,
disabled: false,
placeholder: '请选择省市区',
clearable: true,
showAllLevels: true,
separator: '/',
});
const emit = defineEmits<{
(e: 'update:modelValue', value: number[] | string[] | undefined): void;
(e: 'update:value', value: number[] | string[] | undefined): void;
}>();
// 地区数据接口
interface AreaVO {
id: number;
name: string;
code: string;
parentId?: number;
sort?: number;
status?: number;
children?: AreaVO[];
}
// 接受父组件参数
interface Props {
modelValue?: number[] | string[];
value?: number[] | string[];
level?: (typeof AreaLevelEnum)[keyof typeof AreaLevelEnum];
disabled?: boolean;
placeholder?: string;
clearable?: boolean;
showAllLevels?: boolean;
separator?: string;
// eslint-disable-next-line vue/require-default-prop
formCreateInject?: any;
}
// Ant Design Vue Cascader 的 fieldNames 配置
const fieldNames = {
label: 'name',
value: 'id',
children: 'children',
};
// 地区树形数据
const areaTree = ref<AreaVO[]>([]);
// 当前选中值
const selectedValue = ref<number[] | undefined>();
// 加载状态
const loading = ref(false);
// 加载地区树形数据
async function loadAreaTree(): Promise<void> {
try {
loading.value = true;
const data = await getAreaTree();
// 根据 level 限制层级
areaTree.value = filterTreeByLevel((data || []) as AreaVO[], props.level);
} catch (error) {
console.warn('[AreaSelect] 加载地区数据失败:', error);
areaTree.value = [];
} finally {
loading.value = false;
}
}
// 根据层级过滤树形数据
function filterTreeByLevel(tree: AreaVO[], maxLevel: number): AreaVO[] {
if (maxLevel <= 0) return [];
return tree.map((node) => {
const newNode = { ...node };
// 如果当前是最后一层,移除 children
if (maxLevel === 1) {
delete newNode.children;
} else if (node.children && node.children.length > 0) {
// 递归处理子节点
newNode.children = filterTreeByLevel(node.children, maxLevel - 1);
}
return newNode;
});
}
// 处理选中值变化
function handleChange(value: any): void {
if (value === undefined || value === null) {
emit('update:modelValue', undefined);
emit('update:value', undefined);
return;
}
emit('update:modelValue', value);
emit('update:value', value);
}
// 同步 modelValue 或 value 到内部选中值
function syncSelectedValue(): void {
const newValue = props.modelValue || props.value;
if (newValue === undefined || newValue === null) {
selectedValue.value = undefined;
return;
}
// 确保是数组格式
selectedValue.value = Array.isArray(newValue)
? (newValue as number[])
: [newValue as number];
}
// 监听 modelValue 和 value 变化
watch(() => props.modelValue || props.value, syncSelectedValue, {
immediate: true,
});
// 组件挂载时加载数据
onMounted(async () => {
await loadAreaTree();
});
</script>
<template>
<Cascader
v-model:value="selectedValue"
class="w-full"
:options="areaTree"
:field-names="fieldNames"
:disabled="disabled"
:placeholder="placeholder"
:allow-clear="clearable"
:show-search="true"
:change-on-select="true"
:loading="loading"
@change="handleChange"
/>
</template>

View File

@@ -39,15 +39,16 @@ interface DeptVO {
status?: number;
}
// TODO @puhui999linter 报错;
/** 接受父组件参数 */
interface Props {
// eslint-disable-next-line vue/require-default-prop
modelValue?: number | number[] | string | string[];
multiple?: boolean;
returnType?: 'id' | 'name';
defaultCurrentDept?: boolean;
disabled?: boolean;
placeholder?: string;
// eslint-disable-next-line vue/require-default-prop
formCreateInject?: any;
}

View File

@@ -0,0 +1,95 @@
<!-- 网页 iframe 组件 (Ant Design Vue 版本) -->
<script lang="ts" setup>
import { computed } from 'vue';
import { isUrl } from '#/utils';
defineOptions({ name: 'IframeComponent' });
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
value: '',
url: '',
height: '500px',
width: '100%',
frameborder: '0',
allowfullscreen: true,
loading: 'lazy',
sandbox: '',
});
// 接受父组件参数
interface Props {
modelValue?: string;
value?: string;
url?: string;
height?: string;
width?: string;
frameborder?: string;
allowfullscreen?: boolean;
loading?: 'eager' | 'lazy';
sandbox?: string;
// eslint-disable-next-line vue/require-default-prop
formCreateInject?: any;
}
// 显示的 URL优先使用 url prop其次使用 value 或 modelValue
const displayUrl = computed(
() => props.url || props.value || props.modelValue || '',
);
// 是否显示预览
const showPreview = computed(() => {
return displayUrl.value && isUrl(displayUrl.value);
});
</script>
<template>
<div class="iframe-component">
<!-- iframe 预览 -->
<div v-if="showPreview" class="iframe-preview">
<iframe
:src="displayUrl"
:width="width"
:height="height"
:frameborder="frameborder"
:allowfullscreen="allowfullscreen"
:loading="loading"
:sandbox="sandbox || undefined"
class="iframe-content"
></iframe>
</div>
<!-- URL 或无效 URL 提示 -->
<div v-else class="iframe-placeholder">
<a-empty description="请在右侧属性面板配置 URL 地址" />
</div>
</div>
</template>
<style scoped>
.iframe-component {
width: 100%;
}
.iframe-preview {
overflow: hidden;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
.iframe-content {
display: block;
border: none;
}
.iframe-placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
background-color: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 4px;
}
</style>

View File

@@ -193,7 +193,8 @@ export function useApiSelect(option: ApiSelectProps) {
let parse: any = null;
if (props.parseFunc) {
// 解析字符串函数
// eslint-disable-next-line no-new-func
// oxlint-disable-next-line typescript/no-implied-eval
// oxlint-disable-next-line no-new-func, typescript/no-implied-eval
parse = new Function(`return ${props.parseFunc}`)();
}
return parse;

View File

@@ -6,18 +6,36 @@ export function useImagesUpload() {
return defineComponent({
name: 'ImagesUpload',
props: {
multiple: {
accept: {
type: Array,
default: () => ['image/jpeg', 'image/png', 'image/gif'],
},
disabled: {
type: Boolean,
default: true,
default: false,
},
maxNumber: {
type: Number,
default: 5,
},
maxSize: {
type: Number,
default: 5,
},
multiple: {
type: Boolean,
default: true,
},
},
setup(props) {
return () => (
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
<ImageUpload
accept={props.accept as string[]}
disabled={props.disabled}
maxNumber={props.maxNumber}
maxSize={props.maxSize}
multiple={props.multiple}
/>
);
},
});

View File

@@ -11,8 +11,10 @@ import formCreate from '@form-create/ant-design-vue';
import { apiSelectRule } from '#/components/form-create/rules/data';
import {
useAreaSelectRule,
useDictSelectRule,
useEditorRule,
useIframeRule,
useSelectRule,
useUploadFileRule,
useUploadImageRule,
@@ -160,6 +162,8 @@ export async function useFormCreateDesigner(designer: Ref) {
const uploadFileRule = useUploadFileRule();
const uploadImageRule = useUploadImageRule();
const uploadImagesRule = useUploadImagesRule();
const iframeRule = useIframeRule();
const areaSelectRule = useAreaSelectRule();
/** 构建表单组件 */
function buildFormComponents() {
@@ -172,6 +176,8 @@ export async function useFormCreateDesigner(designer: Ref) {
uploadFileRule,
uploadImageRule,
uploadImagesRule,
iframeRule,
areaSelectRule,
];
components.forEach((component) => {
// 插入组件规则

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-template-curly-in-string */
const selectRule = [
{
type: 'select',
@@ -134,7 +133,7 @@ const apiSelectRule = [
type: 'input',
field: 'labelField',
title: 'label 属性',
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
info: `可以使用 el 表达式:\${},来实现复杂数据组合。如:\${nickname}-\${id}`,
props: {
placeholder: 'nickname',
},
@@ -143,7 +142,7 @@ const apiSelectRule = [
type: 'input',
field: 'valueField',
title: 'value 属性',
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
info: `可以使用 el 表达式:\${},来实现复杂数据组合。如:\${nickname}-\${id}`,
props: {
placeholder: 'id',
},

View File

@@ -1,5 +1,7 @@
export { useAreaSelectRule } from './use-area-select-rule';
export { useDictSelectRule } from './use-dict-select';
export { useEditorRule } from './use-editor-rule';
export { useIframeRule } from './use-iframe-rule';
export { useSelectRule } from './use-select-rule';
export { useUploadFileRule } from './use-upload-file-rule';
export { useUploadImageRule } from './use-upload-image-rule';

View File

@@ -0,0 +1,77 @@
import { AreaLevelEnum } from '@vben/constants';
import {
localeProps,
makeRequiredRule,
} from '#/components/form-create/helpers';
/** 省市区选择器规则 */
export function useAreaSelectRule() {
const label = '省市区选择器';
const name = 'AreaSelect';
return {
icon: 'icon-location',
label,
name,
rule() {
return {
type: name,
field: `area_${Date.now()}`,
title: label,
info: '',
$required: false,
modelField: 'value', // 特殊ele 里是 model-valueantd 里是 value
};
},
props(_: any, { t }: any) {
return localeProps(t, `${name}.props`, [
makeRequiredRule(),
{
type: 'select',
field: 'level',
title: '选择层级',
value: AreaLevelEnum.DISTRICT,
options: [
{ label: '省', value: AreaLevelEnum.PROVINCE },
{ label: '省/市', value: AreaLevelEnum.CITY },
{ label: '省/市/区', value: AreaLevelEnum.DISTRICT },
],
info: '限制可选择的地区层级',
},
{
type: 'input',
field: 'placeholder',
title: '占位符',
value: '请选择省市区',
},
{
type: 'switch',
field: 'clearable',
title: '是否可清空',
value: true,
},
{
type: 'switch',
field: 'showAllLevels',
title: '显示完整路径',
value: true,
info: '输入框中是否显示选中值的完整路径',
},
{
type: 'input',
field: 'separator',
title: '分隔符',
value: '/',
info: '选项分隔符',
},
{
type: 'switch',
field: 'disabled',
title: '是否禁用',
value: false,
},
]);
},
};
}

View File

@@ -0,0 +1,77 @@
import { buildUUID } from '@vben/utils';
import {
localeProps,
makeRequiredRule,
} from '#/components/form-create/helpers';
/** iframe 组件规则 */
export function useIframeRule() {
const label = '网页 iframe';
const name = 'IframeComponent';
return {
icon: 'icon-link',
label,
name,
rule() {
return {
type: name,
field: buildUUID(),
title: label,
info: '',
$required: false,
modelField: 'value', // 特殊ele 里是 model-valueantd 里是 value
};
},
props(_: any, { t }: any) {
return localeProps(t, `${name}.props`, [
makeRequiredRule(),
{
type: 'input',
field: 'url',
title: 'URL 地址',
value: '',
info: '请输入完整的 HTTP 或 HTTPS 地址',
},
{
type: 'input',
field: 'height',
title: 'iframe 高度',
value: '500px',
info: '支持 px、%、vh 等单位',
},
{
type: 'input',
field: 'width',
title: 'iframe 宽度',
value: '100%',
info: '支持 px、%、vw 等单位',
},
{
type: 'select',
field: 'loading',
title: '加载方式',
value: 'lazy',
options: [
{ label: '懒加载', value: 'lazy' },
{ label: '立即加载', value: 'eager' },
],
},
{
type: 'switch',
field: 'allowfullscreen',
title: '允许全屏',
value: true,
},
{
type: 'input',
field: 'sandbox',
title: 'sandbox 属性',
value: '',
info: '安全沙箱限制allow-scripts allow-same-origin',
},
]);
},
};
}

View File

@@ -26,7 +26,7 @@ export function useUploadFileRule() {
makeRequiredRule(),
{
type: 'select',
field: 'fileType',
field: 'accept',
title: '文件类型',
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
options: [
@@ -40,12 +40,6 @@ export function useUploadFileRule() {
mode: 'multiple',
},
},
{
type: 'switch',
field: 'autoUpload',
title: '是否在选取文件后立即进行上传',
value: true,
},
{
type: 'switch',
field: 'drag',
@@ -54,23 +48,23 @@ export function useUploadFileRule() {
},
{
type: 'switch',
field: 'isShowTip',
field: 'showDescription',
title: '是否显示提示',
value: true,
},
{
type: 'inputNumber',
field: 'fileSize',
field: 'maxSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 },
},
{
type: 'inputNumber',
field: 'limit',
field: 'maxNumber',
title: '数量限制',
value: 5,
props: { min: 0 },
props: { min: 1 },
},
{
type: 'switch',

View File

@@ -24,15 +24,9 @@ export function useUploadImageRule() {
props(_: any, { t }: any) {
return localeProps(t, `${name}.props`, [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false,
},
{
type: 'select',
field: 'fileType',
field: 'accept',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
@@ -52,40 +46,16 @@ export function useUploadImageRule() {
},
{
type: 'inputNumber',
field: 'fileSize',
field: 'maxSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 },
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px',
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px',
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px',
},
{
type: 'switch',
field: 'disabled',
title: '是否显示删除按钮',
value: true,
},
{
type: 'switch',
field: 'showBtnText',
title: '是否显示按钮文字',
value: true,
title: '是否禁用',
value: false,
},
]);
},

View File

@@ -24,15 +24,9 @@ export function useUploadImagesRule() {
props(_: any, { t }: any) {
return localeProps(t, `${name}.props`, [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false,
},
{
type: 'select',
field: 'fileType',
field: 'accept',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
@@ -48,40 +42,27 @@ export function useUploadImagesRule() {
],
props: {
mode: 'multiple',
maxNumber: 5,
},
},
{
type: 'inputNumber',
field: 'fileSize',
field: 'maxSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 },
},
{
type: 'inputNumber',
field: 'limit',
field: 'maxNumber',
title: '数量限制',
value: 5,
props: { min: 0 },
props: { min: 1 },
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px',
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px',
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px',
type: 'switch',
field: 'disabled',
title: '是否禁用',
value: false,
},
]);
},

View File

@@ -47,6 +47,7 @@ onMounted(async () => {
</script>
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
</template>

View File

@@ -146,6 +146,7 @@ async function handlePreview(file: UploadFile) {
async function handleRemove(file: UploadFile) {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
// oxlint-disable-next-line no-unused-expressions
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
@@ -350,6 +351,8 @@ function getValue() {
<style>
.ant-upload-select-picture-card {
@apply flex items-center justify-center;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,7 +1,7 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
import { overridesPreferences, preferencesExtension } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
@@ -15,6 +15,7 @@ async function initApplication() {
// app偏好设置初始化
await initPreferences({
extension: preferencesExtension,
namespace,
overrides: overridesPreferences,
});

View File

@@ -34,8 +34,10 @@ import {
// ======================= 自定义组件 =======================
import { useApiSelect } from '#/components/form-create';
import AreaSelect from '#/components/form-create/components/area-select.vue';
import DeptSelect from '#/components/form-create/components/dept-select.vue';
import DictSelect from '#/components/form-create/components/dict-select.vue';
import IframeComponent from '#/components/form-create/components/iframe.vue';
import { useImagesUpload } from '#/components/form-create/components/use-images-upload';
import { Tinymce } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
@@ -84,6 +86,8 @@ const components = [
Tinymce,
ImageUpload,
FileUpload,
IframeComponent,
AreaSelect,
];
// 参考 https://www.form-create.com/v3/ant-design-vue/auto-import 文档

View File

@@ -1,4 +1,14 @@
import { defineOverridesPreferences } from '@vben/preferences';
import {
defineOverridesPreferences,
definePreferencesExtension,
} from '@vben/preferences';
interface WebAntdPreferencesExtension {
defaultTableSize: number;
enableFormFullscreen: boolean;
reportTitle: string;
tenantMode: 'multi' | 'single';
}
/**
* @description 项目配置文件
@@ -23,3 +33,52 @@ export const overridesPreferences = defineOverridesPreferences({
companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
},
});
export const preferencesExtension =
definePreferencesExtension<WebAntdPreferencesExtension>({
tabLabel: 'preferences.antd.tabLabel',
title: 'preferences.antd.title',
fields: [
{
component: 'switch',
defaultValue: true,
key: 'enableFormFullscreen',
label: 'preferences.antd.fields.enableFormFullscreen.label',
tip: 'preferences.antd.fields.enableFormFullscreen.tip',
},
{
component: 'select',
defaultValue: 'single',
key: 'tenantMode',
label: 'preferences.antd.fields.tenantMode.label',
options: [
{
label: 'preferences.antd.fields.tenantMode.options.single.label',
value: 'single',
},
{
label: 'preferences.antd.fields.tenantMode.options.multi.label',
value: 'multi',
},
],
},
{
component: 'number',
componentProps: {
max: 200,
min: 10,
step: 10,
},
defaultValue: 20,
key: 'defaultTableSize',
label: 'preferences.antd.fields.defaultTableSize.label',
},
{
component: 'input',
defaultValue: '',
key: 'reportTitle',
label: 'preferences.antd.fields.reportTitle.label',
placeholder: 'preferences.antd.fields.reportTitle.placeholder',
},
],
});

View File

@@ -109,6 +109,21 @@ const coreRoutes: RouteRecordRaw[] = [
},
],
},
/**
* 用于 bpm 移动端流程表单 web-view 的嵌入
*/
{
component: () => import('#/views/bpm/form/mobile/index.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
ignoreAccess: true,
title: '移动端流程表单展示',
},
name: 'BpmMobileFormPreview',
path: '/bpm/mobile/form-preview',
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@@ -83,6 +83,7 @@ export const useAuthStore = defineStore('auth', () => {
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
// oxlint-disable-next-line no-unused-expressions
onSuccess
? await onSuccess?.()
: await router.push(
@@ -132,6 +133,7 @@ export const useAuthStore = defineStore('auth', () => {
async function fetchUserInfo() {
// 加载
// eslint-disable-next-line no-useless-assignment
let authPermissionInfo: AuthPermissionInfo | null = null;
authPermissionInfo = await getAuthPermissionInfoApi();
// userStore

View File

@@ -3,6 +3,9 @@ import type { Recordable } from '@vben/types';
export * from './rangePickerProps';
export * from './routerHelper';
// 从共享包导出 URL 工具函数
export { isUrl } from '@vben/utils';
/**
* 查找数组对象的某个下标
* @param {Array} ary 查找的数组

View File

@@ -571,6 +571,7 @@ onMounted(async () => {
size="small"
@click="openChatConversationUpdateForm"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="activeConversation?.modelName"></span>
<IconifyIcon icon="lucide:settings" class="ml-2 size-4" />
</Button>

View File

@@ -21,6 +21,7 @@ const imageListRef = ref<any>(); // image 列表 ref
const dall3Ref = ref<any>(); // dall3(openai) ref
const midjourneyRef = ref<any>(); // midjourney ref
const stableDiffusionRef = ref<any>(); // stable diffusion ref
// @ts-expect-error: template ref is retained for future provider expansion
const commonRef = ref<any>(); // stable diffusion ref
const selectPlatform = ref('common'); // 选中的平台
@@ -45,7 +46,9 @@ const platformOptions = [
const models = ref<AiModelModelApi.Model[]>([]); // 模型列表
/** 绘画 start */
async function handleDrawStart() {}
function handleDrawStart() {
// drawing state is handled by child components
}
/** 绘画 complete */
async function handleDrawComplete() {

View File

@@ -150,10 +150,12 @@ defineExpose({
ref="mdContainerRef"
class="wh-full overflow-y-auto"
>
<!-- eslint-disable vue/no-v-html -->
<div
class="flex flex-col items-center justify-center"
v-html="html"
></div>
<!-- eslint-enable vue/no-v-html -->
</div>
<div ref="mindMapRef" class="wh-full">
<svg

View File

@@ -20,6 +20,7 @@ const currentSong = inject<any>('currentSong', {});
{{ currentSong.date }}
</div>
<Button size="small" shape="round" class="my-2">信息复用</Button>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="text-xs" v-html="currentSong.lyric"></div>
</Card>
</template>

View File

@@ -106,7 +106,9 @@ async function goRun() {
try {
convertedParams[paramKey] = convertParamValue(value, dataType);
} catch (error: any) {
throw new Error(`参数 ${paramKey} 转换失败: ${error.message}`);
throw new Error(`参数 ${paramKey} 转换失败: ${error.message}`, {
cause: error,
});
}
}
@@ -175,7 +177,7 @@ function convertParamValue(value: string, dataType: string) {
try {
return JSON.parse(value);
} catch (error: any) {
throw new Error(`JSON格式错误: ${error.message}`);
throw new Error(`JSON格式错误: ${error.message}`, { cause: error });
}
}
default: {

View File

@@ -22,7 +22,7 @@ import {
import { Button, ButtonGroup, message, Modal, Tooltip } from 'ant-design-vue';
// 模拟流转流程
// @ts-ignore
// @ts-expect-error: token simulation package does not ship compatible types
import tokenSimulation from 'bpmn-js-token-simulation';
import BpmnModeler from 'bpmn-js/lib/Modeler';
// 代码高亮插件
@@ -132,6 +132,7 @@ const emit = defineEmits([
'element-click',
]);
// @ts-expect-error: file input ref is set imperatively by the template
const bpmnCanvas = ref();
const refFile = ref();
@@ -178,6 +179,7 @@ const additionalModules = computed(() => {
) {
Modules.push(...(props.additionalModel as any[]));
} else {
// oxlint-disable-next-line no-unused-expressions
props.additionalModel && Modules.push(props.additionalModel);
}
@@ -417,6 +419,7 @@ const processSimulation = () => {
// bpmnModeler.get('toggleMode', 'strict'),
// "bpmnModeler.get('toggleMode')",
// );
// oxlint-disable-next-line no-unused-expressions
props.simulation && bpmnModeler.get('toggleMode', 'strict').toggleMode();
};
const processRedo = () => {

View File

@@ -7,7 +7,7 @@ import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
/**
* A provider for BPMN 2.0 elements context pad
*/
export default function ContextPadProvider(
function ContextPadProvider(
config,
injector,
eventBus,
@@ -57,6 +57,8 @@ export default function ContextPadProvider(
});
}
export default ContextPadProvider;
ContextPadProvider.$inject = [
'config.contextPad',
'injector',

View File

@@ -1,6 +1,6 @@
import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
export default function CustomPalette(
function CustomPalette(
palette,
create,
elementFactory,
@@ -24,11 +24,21 @@ export default function CustomPalette(
);
}
const F = function () {}; // 核心,利用空对象作为中介;
F.prototype = PaletteProvider.prototype; // 核心将父类的原型赋值给空对象F
CustomPalette.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate',
];
// 利用中介函数重写原型链方法
F.prototype.getPaletteEntries = function () {
CustomPalette.prototype = Object.create(PaletteProvider.prototype);
CustomPalette.prototype.constructor = CustomPalette;
CustomPalette.prototype.getPaletteEntries = function () {
const actions = {};
const create = this._create;
const elementFactory = this._elementFactory;
@@ -94,8 +104,7 @@ F.prototype.getPaletteEntries = function () {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: '激活抓手工具',
// title: translate("Activate the hand tool"),
title: translate('Activate the hand tool'),
action: {
click(event) {
handTool.activateHand(event);
@@ -219,16 +228,4 @@ F.prototype.getPaletteEntries = function () {
return actions;
};
CustomPalette.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate',
];
CustomPalette.prototype = new F(); // 核心,将 F的实例赋值给子类
CustomPalette.prototype.constructor = CustomPalette; // 修复子类CustomPalette的构造器指向防止原型链的混乱
export default CustomPalette;

View File

@@ -1,7 +1,7 @@
/**
* A palette provider for BPMN 2.0 elements.
*/
export default function PaletteProvider(
function PaletteProvider(
palette,
create,
elementFactory,
@@ -23,6 +23,8 @@ export default function PaletteProvider(
palette.registerProvider(this);
}
export default PaletteProvider;
PaletteProvider.$inject = [
'palette',
'create',

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-template-curly-in-string */
/**
* This is a sample file that should be replaced with the actual translation.
*
@@ -238,10 +237,8 @@ export default {
'Due Date': '到期时间',
'Follow Up Date': '跟踪日期',
Priority: '优先级',
'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
[`The follow up date as an EL expression (e.g. \${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)`]: `跟踪日期必须符合EL表达式 \${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00`,
[`The due date as an EL expression (e.g. \${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)`]: `跟踪日期必须符合EL表达式 \${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00`,
Variables: '变量',
'Candidate Starter Configuration': '候选人起动器配置',
'Candidate Starter Groups': '候选人起动器组',

View File

@@ -39,7 +39,7 @@ watch(
val +=
props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || '';
}
// @ts-ignore
// @ts-expect-error: async component registry is indexed dynamically
customConfigComponent.value = (
CustomConfigMap as Record<string, { component: Component }>
)[val]?.component;

View File

@@ -175,7 +175,7 @@ const resetCustomConfigList = () => {
approveType.value =
elExtensionElements.value.values?.find(
(ex: any) => ex.$type === `${prefix}:ApproveType`,
)?.[0] ||
) ||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
value: ApproveType.USER,
});
@@ -184,7 +184,7 @@ const resetCustomConfigList = () => {
assignStartUserHandlerTypeEl.value =
elExtensionElements.value.values?.find(
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
)?.[0] ||
) ||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
value: 1,
});
@@ -194,13 +194,13 @@ const resetCustomConfigList = () => {
rejectHandlerTypeEl.value =
elExtensionElements.value.values?.find(
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
)?.[0] ||
) ||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
returnNodeIdEl.value =
elExtensionElements.value.values?.find(
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
)?.[0] ||
) ||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
value: '',
});
@@ -210,7 +210,7 @@ const resetCustomConfigList = () => {
assignEmptyHandlerTypeEl.value =
elExtensionElements.value.values?.find(
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
)?.[0] ||
) ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
value: 1,
});
@@ -218,7 +218,7 @@ const resetCustomConfigList = () => {
assignEmptyUserIdsEl.value =
elExtensionElements.value.values?.find(
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
)?.[0] ||
) ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
value: '',
});

View File

@@ -1,4 +1,3 @@
<!-- eslint-disable no-unused-vars -->
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
@@ -66,13 +65,13 @@ const bpmnElement = ref<any>(null);
const multiLoopInstance = ref<any>(null);
declare global {
interface Window {
// @ts-ignore
bpmnInstances?: () => any;
}
}
const bpmnInstances = () => (window as any)?.bpmnInstances;
// @ts-expect-error: retained for legacy multi-instance mode compatibility
// eslint-disable-next-line unused-imports/no-unused-vars
const getElementLoop = (businessObject: any): void => {
if (!businessObject.loopCharacteristics) {
@@ -141,8 +140,7 @@ const changeLoopCharacteristicsType = (type: any): void => {
isSequential: true,
})
: bpmnInstances().moddle.create('bpmn:MultiInstanceLoopCharacteristics', {
// eslint-disable-next-line no-template-curly-in-string
collection: '${coll_userList}',
collection: `\${coll_userList}`,
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value),
@@ -233,7 +231,7 @@ const updateLoopAsync = (key: any): void => {
extensionElements: null,
};
} else {
// @ts-ignore
// @ts-expect-error: dynamic async flags are assigned by runtime key
asyncAttr[key] = loopInstanceForm.value[key];
}
bpmnInstances().modeling.updateModdleProperties(
@@ -247,23 +245,23 @@ const changeConfig = (config: string): void => {
switch (config) {
case '会签': {
changeLoopCharacteristicsType('ParallelMultiInstance');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
updateLoopCondition(`\${ nrOfCompletedInstances >= nrOfInstances }`);
break;
}
case '依次审批': {
changeLoopCharacteristicsType('SequentialMultiInstance');
updateLoopCardinality('1');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
updateLoopCondition(`\${ nrOfCompletedInstances >= nrOfInstances }`);
break;
}
case '或签': {
changeLoopCharacteristicsType('ParallelMultiInstance');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances > 0 }');
updateLoopCondition(`\${ nrOfCompletedInstances > 0 }`);
break;
}
@@ -331,8 +329,8 @@ const updateLoopCharacteristics = (): void => {
if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: false, collection: '${coll_userList}' },
{ isSequential: false, collection: `\${coll_userList}` },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
@@ -344,20 +342,19 @@ const updateLoopCharacteristics = (): void => {
if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: false, collection: '${coll_userList}' },
{ isSequential: false, collection: `\${coll_userList}` },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
// eslint-disable-next-line no-template-curly-in-string
body: '${ nrOfCompletedInstances > 0 }',
body: `\${ nrOfCompletedInstances > 0 }`,
});
}
if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: true, collection: '${coll_userList}' },
{ isSequential: true, collection: `\${coll_userList}` },
);
multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
@@ -367,8 +364,7 @@ const updateLoopCharacteristics = (): void => {
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
// eslint-disable-next-line no-template-curly-in-string
body: '${ nrOfCompletedInstances >= nrOfInstances }',
body: `\${ nrOfCompletedInstances >= nrOfInstances }`,
});
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {

View File

@@ -53,7 +53,7 @@ watch(
() => props.type,
() => {
if (props.type) {
// @ts-ignore
// @ts-expect-error: installed task component map is indexed dynamically
witchTaskComponent.value = installedComponent[props.type].component;
}
},

View File

@@ -65,7 +65,7 @@ const initCallActivity = () => {
// 初始化所有配置项
Object.keys(formData.value).forEach((key: string) => {
// @ts-ignore
// @ts-expect-error: form state is updated through dynamic schema keys
formData.value[key] =
bpmnElement.value.businessObject[key] ??
formData.value[key as keyof FormData];
@@ -183,6 +183,7 @@ const updateElementExtensions = () => {
watch(
() => props.id,
(val) => {
// oxlint-disable-next-line no-unused-expressions
val &&
val.length > 0 &&
nextTick(() => {

View File

@@ -82,7 +82,6 @@ onMounted(() => {
bpmnRootElements.value
.filter((el: any) => el.$type === 'bpmn:Message')
.forEach((m: any) => {
// @ts-ignore
if (bpmnMessageRefsMap.value) {
bpmnMessageRefsMap.value[m.id] = m;
}

View File

@@ -34,7 +34,6 @@ const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (const key in defaultTaskForm.value) {
// @ts-ignore
scriptTaskForm.value[key] =
bpmnElement.value?.businessObject[
key as keyof typeof defaultTaskForm.value

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable unicorn/no-nested-ternary -->
<!-- eslint-disable prettier/prettier -->
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, ref, watch } from 'vue';
@@ -206,9 +207,9 @@ const updateHttpExtensions = (force = false) => {
const persisted = HTTP_BOOLEAN_FIELDS.has(name)
? String(!!rawValue)
: (rawValue === undefined
: rawValue === undefined
? ''
: rawValue.toString());
: rawValue.toString();
desiredEntries.push([name, persisted]);
});

View File

@@ -70,6 +70,7 @@ const deptTreeOptions = ref<any>(); // 部门树
const postOptions = ref<SystemPostApi.Post[]>([]); // 岗位列表
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
const userGroupOptions = ref<BpmUserGroupApi.UserGroup[]>([]); // 用户组列表
// @ts-expect-error: tree ref instance type is provided by the UI library at runtime
const treeRef = ref<any>();
const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ);
@@ -128,7 +129,7 @@ const resetTaskForm = () => {
// eslint-disable-next-line unicorn/prefer-switch
if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
// 特殊:流程表达式,只有一个 input 输入框
// @ts-ignore
// @ts-expect-error: expression strategy stores a scalar in an array-shaped field
userTaskForm.value.candidateParam = [candidateParamStr];
} else if (
userTaskForm.value.candidateStrategy ===
@@ -152,7 +153,7 @@ const resetTaskForm = () => {
userTaskForm.value.candidateStrategy ===
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
// @ts-ignore
// @ts-expect-error: dynamic candidate param shape varies by strategy
userTaskForm.value.candidateParam = +candidateParamStr;
deptLevel.value = +candidateParamStr;
} else if (
@@ -303,7 +304,7 @@ const openProcessExpressionDialog = async () => {
const selectProcessExpression = (
expression: BpmProcessExpressionApi.ProcessExpression,
) => {
// @ts-ignore
// @ts-expect-error: modal helper exposes runtime methods outside static typing
userTaskForm.value.candidateParam = [expression.expression];
updateElementTask();
};
@@ -311,7 +312,7 @@ const selectProcessExpression = (
const handleFormUserChange = (e: any) => {
if (e === 'PROCESS_START_USER_ID') {
userTaskForm.value.candidateParam = [];
// @ts-ignore
// @ts-expect-error: modal helper exposes runtime methods outside static typing
userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER;
}
updateElementTask();

View File

@@ -1,6 +1,6 @@
import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer';
export default function CustomRenderer(
function CustomRenderer(
config,
eventBus,
styles,
@@ -19,12 +19,10 @@ export default function CustomRenderer(
2000,
);
this.handlers.label = function () {
return null;
};
this.handlers.label = () => null;
}
const F = function () {}; // 核心,利用空对象作为中介;
F.prototype = BpmnRenderer.prototype; // 核心将父类的原型赋值给空对象F
CustomRenderer.prototype = new F(); // 核心,将 F的实例赋值给子类
CustomRenderer.prototype.constructor = CustomRenderer; // 修复子类CustomRenderer的构造器指向防止原型链的混乱
CustomRenderer.prototype = Object.create(BpmnRenderer.prototype);
CustomRenderer.prototype.constructor = CustomRenderer;
export default CustomRenderer;

View File

@@ -1,7 +1,8 @@
import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
// eslint-disable-next-line n/no-extraneous-import
import inherits from 'inherits';
export default function CustomRules(eventBus) {
function CustomRules(eventBus) {
BpmnRules.call(this, eventBus);
}
@@ -14,3 +15,5 @@ CustomRules.prototype.canDrop = function () {
CustomRules.prototype.canMove = function () {
return false;
};
export default CustomRules;

View File

@@ -1,4 +1,5 @@
function xmlStr2XmlObj(xmlStr) {
// eslint-disable-next-line no-useless-assignment
let xmlObj = {};
if (document.all) {
const xmlDom = new window.ActiveXObject('Microsoft.XMLDOM');

View File

@@ -71,6 +71,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
// 当前节点
const currentNode = useWatchNode(props);
// 节点名称
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.TRIGGER_NODE);
// 触发器表单配置

View File

@@ -25,12 +25,13 @@ const emits = defineEmits<{
}>();
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
/** 监控节点的变化 */
const currentNode = useWatchNode(props);
/** 节点名称编辑 */
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
BpmNodeTypeEnum.CHILD_PROCESS_NODE,

View File

@@ -27,10 +27,11 @@ const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined];
}>();
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
BpmNodeTypeEnum.COPY_TASK_NODE,

View File

@@ -25,10 +25,11 @@ const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined];
}>();
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
BpmNodeTypeEnum.DELAY_TIMER_NODE,

View File

@@ -20,7 +20,7 @@ const props = defineProps({
// 监控节点变化
const currentNode = useWatchNode(props);
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
const processInstance = inject<Ref<any>>('processInstance', ref({}));
const [Modal, modalApi] = useVbenModal({

View File

@@ -41,7 +41,7 @@ const emits = defineEmits<{
const { proxy } = getCurrentInstance() as any;
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
const currentNode = ref<SimpleFlowNode>(props.flowNode);
watch(

View File

@@ -46,7 +46,7 @@ const emits = defineEmits<{
const { proxy } = getCurrentInstance() as any;
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
const currentNode = ref<SimpleFlowNode>(props.flowNode);

View File

@@ -36,7 +36,7 @@ const props = defineProps({
const emits = defineEmits(['update:childNode']);
const popoverShow = ref(false);
const readonly = inject<Boolean>('readonly'); // 是否只读
const readonly = inject<boolean>('readonly'); // 是否只读
function addNode(type: number) {
// 校验:条件分支、包容分支后面,不允许直接添加并行分支

View File

@@ -36,7 +36,7 @@ const emits = defineEmits<{
const currentNode = ref<SimpleFlowNode>(props.flowNode);
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
watch(
() => props.flowNode,

View File

@@ -28,10 +28,11 @@ const emits = defineEmits<{
}>();
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
BpmNodeTypeEnum.ROUTER_BRANCH_NODE,

View File

@@ -32,11 +32,12 @@ defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined];
}>();
const readonly = inject<Boolean>('readonly'); // 是否只读
const readonly = inject<boolean>('readonly'); // 是否只读
const tasks = inject<Ref<any[]>>('tasks', ref([]));
// 监控节点变化
const currentNode = useWatchNode(props);
// 节点名称编辑
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
BpmNodeTypeEnum.START_USER_NODE,

View File

@@ -30,7 +30,7 @@ const emits = defineEmits<{
}>();
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑

View File

@@ -32,11 +32,12 @@ const emits = defineEmits<{
}>();
// 是否只读
const readonly = inject<Boolean>('readonly');
const readonly = inject<boolean>('readonly');
const tasks = inject<Ref<any[]>>('tasks', ref([]));
// 监控节点变化
const currentNode = useWatchNode(props);
// 节点名称编辑
// @ts-expect-error: composable typing does not preserve this node schema exactly
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
BpmNodeTypeEnum.USER_TASK_NODE,

View File

@@ -7,6 +7,10 @@ interface DictDataType {
// 用户任务的审批类型。 【参考飞书】
export enum ApproveType {
/**
* 人工审批
*/
USER = 1,
/**
* 自动通过
*/
@@ -15,18 +19,14 @@ export enum ApproveType {
* 自动拒绝
*/
AUTO_REJECT = 3,
/**
* 人工审批
*/
USER = 1,
}
// 多人审批方式类型枚举 用于审批节点
export enum ApproveMethodType {
/**
* 多人或签(通过只需一人,拒绝只需一人)
* 随机挑选一人审批
*/
ANY_APPROVE = 3,
RANDOM_SELECT_ONE_APPROVE = 1,
/**
* 多人会签(按通过比例)
@@ -34,9 +34,9 @@ export enum ApproveMethodType {
APPROVE_BY_RATIO = 2,
/**
* 随机挑选一人审批
* 多人或签(通过只需一人,拒绝只需一人)
*/
RANDOM_SELECT_ONE_APPROVE = 1,
ANY_APPROVE = 3,
/**
* 多人依次审批
*/
@@ -70,34 +70,34 @@ export enum ConditionType {
// 操作按钮类型枚举 (用于审批节点)
export enum OperationButtonType {
/**
* 加签
*/
ADD_SIGN = 5,
/**
* 通过
*/
APPROVE = 1,
/**
* 抄送
*/
COPY = 7,
/**
* 委派
*/
DELEGATE = 4,
/**
* 拒绝
*/
REJECT = 2,
/**
* 转办
*/
TRANSFER = 3,
/**
* 委派
*/
DELEGATE = 4,
/**
* 加签
*/
ADD_SIGN = 5,
/**
* 退回
*/
RETURN = 6,
/**
* 转办
* 抄送
*/
TRANSFER = 3,
COPY = 7,
}
// 审批拒绝类型枚举
@@ -114,6 +114,10 @@ export enum RejectHandlerType {
// 用户任务超时处理类型枚举
export enum TimeoutHandlerType {
/**
* 自动提醒
*/
REMINDER = 1,
/**
* 自动同意
*/
@@ -122,10 +126,6 @@ export enum TimeoutHandlerType {
* 自动拒绝
*/
REJECT = 3,
/**
* 自动提醒
*/
REMINDER = 1,
}
// 用户任务的审批人为空时,处理类型枚举
@@ -135,49 +135,49 @@ export enum AssignEmptyHandlerType {
*/
APPROVE = 1,
/**
* 转交给流程管理员
* 自动拒绝
*/
ASSIGN_ADMIN = 4,
REJECT = 2,
/**
* 指定人员审批
*/
ASSIGN_USER = 3,
/**
* 自动拒绝
* 转交给流程管理员
*/
REJECT = 2,
ASSIGN_ADMIN = 4,
}
// 用户任务的审批人与发起人相同时,处理类型枚举
export enum AssignStartUserHandlerType {
/**
* 转交给部门负责人审批
* 由发起人对自己审批
*/
ASSIGN_DEPT_LEADER = 3,
START_USER_AUDIT = 1,
/**
* 自动跳过【参考飞书】1如果当前节点还有其他审批人则交由其他审批人进行审批2如果当前节点没有其他审批人则该节点自动通过
*/
SKIP = 2,
/**
* 由发起人对自己审批
* 转交给部门负责人审批
*/
START_USER_AUDIT = 1,
ASSIGN_DEPT_LEADER = 3,
}
// 时间单位枚举
export enum TimeUnitType {
/**
*
* 分钟
*/
DAY = 3,
MINUTE = 1,
/**
* 小时
*/
HOUR = 2,
/**
* 分钟
*
*/
MINUTE = 1,
DAY = 3,
}
/**
@@ -202,14 +202,14 @@ export enum FieldPermissionType {
* 延迟类型
*/
export enum DelayTypeEnum {
/**
* 固定日期时间
*/
FIXED_DATE_TIME = 2,
/**
* 固定时长
*/
FIXED_TIME_DURATION = 1,
/**
* 固定日期时间
*/
FIXED_DATE_TIME = 2,
}
/**
@@ -217,35 +217,39 @@ export enum DelayTypeEnum {
*/
export enum TriggerTypeEnum {
/**
* 表单数据删除触发器
* 发送 HTTP 请求触发器
*/
FORM_DELETE = 11,
/**
* 表单数据更新触发器
*/
FORM_UPDATE = 10,
HTTP_REQUEST = 1,
/**
* 接收 HTTP 回调请求触发器
*/
HTTP_CALLBACK = 2,
/**
* 发送 HTTP 请求触发器
* 表单数据更新触发器
*/
HTTP_REQUEST = 1,
FORM_UPDATE = 10,
/**
* 表单数据删除触发器
*/
FORM_DELETE = 11,
}
export enum ChildProcessStartUserTypeEnum {
/**
* 同主流程发起人
*/
MAIN_PROCESS_START_USER = 1,
/**
* 表单
*/
FROM_FORM = 2,
}
export enum ChildProcessStartUserEmptyTypeEnum {
/**
* 同主流程发起人
*/
MAIN_PROCESS_START_USER = 1,
}
export enum ChildProcessStartUserEmptyTypeEnum {
/**
* 子流程管理员
*/
@@ -254,10 +258,6 @@ export enum ChildProcessStartUserEmptyTypeEnum {
* 主流程管理员
*/
MAIN_PROCESS_ADMIN = 3,
/**
* 同主流程发起人
*/
MAIN_PROCESS_START_USER = 1,
}
export enum ChildProcessMultiInstanceSourceTypeEnum {
@@ -265,54 +265,50 @@ export enum ChildProcessMultiInstanceSourceTypeEnum {
* 固定数量
*/
FIXED_QUANTITY = 1,
/**
* 多选表单
*/
MULTIPLE_FORM = 3,
/**
* 数字表单
*/
NUMBER_FORM = 2,
/**
* 多选表单
*/
MULTIPLE_FORM = 3,
}
// 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy {
/**
* 审批人自选
* 指定角色
*/
APPROVE_USER_SELECT = 34,
/**
* 部门的负责人
*/
DEPT_LEADER = 21,
ROLE = 10,
/**
* 部门成员
*/
DEPT_MEMBER = 20,
/**
* 流程表达式
* 部门的负责人
*/
EXPRESSION = 60,
/**
* 表单内部门负责人
*/
FORM_DEPT_LEADER = 51,
/**
* 表单内用户字段
*/
FORM_USER = 50,
/**
* 连续多级部门的负责人
*/
MULTI_LEVEL_DEPT_LEADER = 23,
DEPT_LEADER = 21,
/**
* 指定岗位
*/
POST = 22,
/**
* 指定角色
* 连续多级部门的负责人
*/
ROLE = 10,
MULTI_LEVEL_DEPT_LEADER = 23,
/**
* 指定用户
*/
USER = 30,
/**
* 审批人自选
*/
APPROVE_USER_SELECT = 34,
/**
* 发起人自选
*/
START_USER_SELECT = 35,
/**
* 发起人自己
*/
@@ -325,18 +321,22 @@ export enum CandidateStrategy {
* 发起人连续多级部门的负责人
*/
START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
/**
* 发起人自选
*/
START_USER_SELECT = 35,
/**
* 指定用户
*/
USER = 30,
/**
* 指定用户组
*/
USER_GROUP = 40,
/**
* 表单内用户字段
*/
FORM_USER = 50,
/**
* 表单内部门负责人
*/
FORM_DEPT_LEADER = 51,
/**
* 流程表达式
*/
EXPRESSION = 60,
}
export enum BpmHttpRequestParamTypeEnum {
@@ -767,6 +767,14 @@ export const COMPARISON_OPERATORS: DictDataType[] = [
value: '<=',
label: '小于等于',
},
{
value: 'contain',
label: '包含',
},
{
value: '!contain',
label: '不包含',
},
];
// 审批操作按钮名称
export const OPERATION_BUTTON_NAME = new Map<number, string>();

View File

@@ -0,0 +1,405 @@
<script lang="ts" setup>
/**
* 移动端流程表单展示页面 - Ant Design Vue 版本
* 使用 @form-create/ant-design-vue 渲染表单
* 用于 UniApp 通过 iframe/webview 嵌入
*
* URL 参数说明:
* - type: 环境类型(必填)'miniapp' 小程序(微信/支付宝/百度等) | 'h5' H5
* - processInstanceId: 流程实例ID查看已有流程时使用
* - taskId: 任务ID可选
* - activityId: 活动节点ID可选
* - token: 访问令牌(用于 API 认证)
*/
import { computed, nextTick, onMounted, ref, toRaw } from 'vue';
import { useRoute } from 'vue-router';
import { BpmFieldPermissionType, BpmModelFormType } from '@vben/constants';
import { updatePreferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { Button, Empty, Spin } from 'ant-design-vue';
import { getApprovalDetail } from '#/api/bpm/processInstance';
import { setConfAndFields2 } from '#/components/form-create';
type EnvType = 'h5' | 'miniapp'; // 环境类型
// UniApp WebView 类型声明
interface UniWebView {
postMessage: (options: { data: any }, targetOrigin?: string) => void;
getEnv: (callback: (res: any) => void) => void;
navigateTo: (options: {
fail?: () => void;
success?: () => void;
url: string;
}) => void;
navigateBack: (options?: { delta?: number }) => void;
switchTab: (options: { url: string }) => void;
reLaunch: (options: { url: string }) => void;
redirectTo: (options: { url: string }) => void;
}
declare global {
interface Window {
uni?: UniWebView;
}
}
defineOptions({ name: 'BpmMobileFormPreview' });
const route = useRoute();
const accessStore = useAccessStore();
const envType = ref<EnvType>('h5'); // 环境类型
const loading = ref(true); // 页面加载状态
const error = ref<null | string>(null);
const processInstance = ref<any>(null);
const processDefinition = ref<any>(null);
const detailForm = ref<{
option: any;
rule: any[];
value: Record<string, any>;
}>({
option: {},
rule: [],
value: {},
}); // 流程实例的表单详情
const fApi = ref<any>(null); // form-create API 引用
const fieldPermissions = ref<Record<string, string>>({}); // 字段权限
// 是否有表单内容
const hasFormContent = computed(() => {
return detailForm.value.rule && detailForm.value.rule.length > 0;
});
/**
* 初始化 Token
* 从 URL 参数获取 token 并设置到 store
*/
function initToken() {
const token = route.query.token as string;
if (token) {
accessStore.setAccessToken(token);
}
}
/**
* 验证并初始化环境类型
*/
function initEnvType(): boolean {
const type = route.query.type as string;
if (!type) {
error.value = '缺少必填参数: type';
return false;
}
if (type !== 'h5' && type !== 'miniapp') {
error.value = 'type 参数值无效,必须是 h5 或 miniapp';
return false;
}
envType.value = type as EnvType;
return true;
}
/**
* 获取审批详情
*/
async function getDetail() {
loading.value = true;
error.value = null;
try {
const processInstanceId = route.query.processInstanceId as string;
const taskId = route.query.taskId as string;
const activityId = route.query.activityId as string;
if (!processInstanceId) {
throw new Error('缺少流程实例ID参数');
}
const data = await getApprovalDetail({
processInstanceId,
taskId,
activityId,
});
if (!data) {
throw new Error('查询不到审批详情信息');
}
if (!data.processDefinition || !data.processInstance) {
throw new Error('查询不到流程信息');
}
processInstance.value = data.processInstance;
processDefinition.value = data.processDefinition;
// 设置普通表单信息
if (data.processDefinition.formType === BpmModelFormType.NORMAL) {
if (detailForm.value.rule?.length > 0) {
// 避免刷新 form-create 显示不了
detailForm.value.value = processInstance.value.formVariables;
} else {
setConfAndFields2(
detailForm,
processDefinition.value.formConf,
processDefinition.value.formFields,
processInstance.value.formVariables,
);
}
await nextTick();
fApi.value?.btn.show(false);
fApi.value?.resetBtn.show(false);
fApi.value?.disabled(true);
// 设置表单字段权限
if (data.formFieldsPermission) {
Object.keys(data.formFieldsPermission).forEach((item) => {
setFieldPermission(item, data.formFieldsPermission[item]);
});
}
}
} finally {
loading.value = false;
}
}
/**
* 向父页面发送消息
* 根据环境类型选择不同的通信方式
*/
function postMessageToParent(message: { data: any; type: string }) {
const messageData = {
source: 'bpm-mobile-form',
type: message.type,
data: message.data,
};
// 小程序环境:使用 uni.postMessage
if (envType.value === 'miniapp') {
if (window.uni?.postMessage) {
// 传递的消息信息,必须写在 data 对象中
window.uni.postMessage({ data: message.data }, window.location.origin);
} else {
console.error('小程序环境下 uni 对象未定义');
}
return;
}
// H5 环境:使用 window.postMessage
if (envType.value === 'h5' && window.parent !== window) {
window.parent.postMessage(messageData, '*');
}
}
/**
* 安全地克隆对象,移除不可序列化的属性
*/
function safeClone(obj: any): any {
try {
// 先使用 toRaw 移除 Vue 的响应式代理
const raw = toRaw(obj);
// 使用 JSON 序列化来移除函数、DOM 元素等不可序列化的内容
// eslint-disable-next-line unicorn/prefer-structured-clone
return JSON.parse(JSON.stringify(raw));
} catch (error) {
console.error('克隆对象失败:', error);
return {};
}
}
/** 设置表单权限 */
function setFieldPermission(field: string, permission: string) {
fieldPermissions.value[field] = permission;
if (permission === BpmFieldPermissionType.READ) {
fApi.value?.disabled(true, field);
}
if (permission === BpmFieldPermissionType.WRITE) {
fApi.value?.disabled(false, field);
}
if (permission === BpmFieldPermissionType.NONE) {
fApi.value?.hidden(true, field);
}
}
/**
* 确定按钮点击事件
* 获取表单数据并发送给父页面
*/
function handleConfirm() {
// 获取最新的表单值(转换为普通对象,避免 Proxy 序列化问题)
const rawValue = detailForm.value.value;
const currentValue = safeClone(rawValue);
// 发送表单数据给父页面
postMessageToParent({
type: 'FORM_SUBMIT',
data: {
formValue: currentValue,
fieldPermissions: safeClone(fieldPermissions.value),
processInstanceId: route.query.processInstanceId,
taskId: route.query.taskId,
},
});
window.uni?.navigateBack();
}
onMounted(() => {
// 验证环境类型
if (!initEnvType()) {
loading.value = false;
return;
}
// 1. 先加载微信 JSSDK微信小程序需要
const wxScript = document.createElement('script');
wxScript.type = 'text/javascript';
wxScript.src = 'https://res.wx.qq.com/open/js/jweixin-1.4.0.js';
wxScript.addEventListener('load', () => {
// 2. 微信 SDK 加载完成后,加载 UniApp WebView SDK
const uniScript = document.createElement('script');
uniScript.type = 'text/javascript';
uniScript.src = 'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js';
uniScript.addEventListener('load', () => {
// 所有 SDK 加载完成后初始化
initApp();
});
uniScript.addEventListener('error', () => {
error.value = 'UniApp WebView SDK 加载失败';
loading.value = false;
});
document.head.append(uniScript);
});
wxScript.addEventListener('error', () => {
// 微信 SDK 加载失败,尝试只加载 UniApp SDK可能是其他小程序
const uniScript = document.createElement('script');
uniScript.type = 'text/javascript';
uniScript.src = 'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js';
uniScript.addEventListener('load', () => {
initApp();
});
uniScript.addEventListener('error', () => {
error.value = 'SDK 加载失败';
loading.value = false;
});
document.head.append(uniScript);
});
document.head.append(wxScript);
// 初始化
initApp();
});
/**
* 初始化应用
*/
function initApp() {
// 设置主题为 light 模式
updatePreferences({
theme: {
mode: 'light',
},
});
// 初始化 token
initToken();
// 加载数据
if (route.query.processInstanceId) {
getDetail();
} else {
loading.value = false;
error.value = '缺少必要参数processInstanceId';
}
}
</script>
<template>
<div class="mobile-form-preview-antd">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<Spin size="large" tip="加载中..." />
</div>
<!-- 错误状态 -->
<Empty v-else-if="error" :description="error" />
<!-- 表单内容 -->
<template v-else>
<!-- 有表单规则时渲染 form-create -->
<div v-if="hasFormContent" class="mt-4">
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
<!-- 确定按钮 -->
<div class="form-footer">
<Button type="primary" size="large" block @click="handleConfirm">
确定
</Button>
</div>
</div>
<!-- 无表单内容时显示空状态 -->
<Empty v-else description="暂无表单内容" />
</template>
</div>
</template>
<style scoped>
/* 响应式适配 */
@media (max-width: 768px) {
.mobile-form-preview-antd {
padding: 12px;
}
:deep(.ant-form-item) {
margin-bottom: 20px;
}
}
.mobile-form-preview-antd {
min-height: 100px;
padding: 16px;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 60px 0;
}
.form-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 16px;
border-top: 1px solid #f0f0f0;
box-shadow: 0 -2px 8px rgb(0 0 0 / 5%);
}
.form-footer :deep(.ant-btn) {
height: 48px;
font-size: 16px;
font-weight: 500;
border-radius: 8px;
}
</style>

View File

@@ -3,7 +3,7 @@
* - @ 自动补全:插入 mention 占位元素
*/
// @ts-ignore TinyMCE 全局或通过打包器提供
// TinyMCE 全局或通过打包器提供
import type { Editor } from 'tinymce';
export interface MentionItem {

View File

@@ -9,7 +9,7 @@ const props = withDefaults(
defineProps<{
bpmnXml?: string;
loading?: boolean; // 是否加载中
modelView?: Object;
modelView?: object;
}>(),
{
loading: false,
@@ -29,7 +29,7 @@ watch(
async (newModelView) => {
// 加载最新
if (newModelView) {
// @ts-ignore
// @ts-expect-error: viewer instance type is broader than local ref typing
view.value = newModelView;
}
},

View File

@@ -24,6 +24,7 @@ import { useUserStore } from '@vben/stores';
import { isEmpty } from '@vben/utils';
import FormCreate from '@form-create/ant-design-vue';
import { until, useDebounceFn } from '@vueuse/core';
import {
Alert,
Button,
@@ -107,11 +108,14 @@ const nodeTypeName = ref('审批'); // 节点类型名称
const reasonRequire = ref();
const approveFormRef = ref<FormInstance>(); // 审批通过意见表单
// @ts-expect-error: template ref is retained for future provider expansion
const approveSignFormRef = ref();
const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
[],
); // 下一个审批节点信息
const nextAssigneesTimelineRef = ref(); // 下一个节点审批人时间线组件的引用
let nextApprovalRequestId = 0; // 请求序号onChange 高频触发时,丢弃过期请求结果
let pendingNextNodesTask: null | Promise<unknown> = null; // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成
const approveReasonForm: any = reactive({
reason: '',
signPicUrl: '',
@@ -255,7 +259,6 @@ async function openPopover(type: string) {
message.warning('表单校验不通过,请先完善表单!!');
return;
}
await initNextAssigneesFormField();
}
if (type === 'return') {
// 获取退回节点
@@ -268,6 +271,20 @@ async function openPopover(type: string) {
Object.keys(popOverVisible.value).forEach((item) => {
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
});
if (type === 'approve') {
// 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点;
// 没有节点表单时approveFormFApi 永远不会被赋值,跳过等待
if (runningTask.value?.formId > 0) {
// 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行
await until(
() => typeof approveFormFApi.value?.validate === 'function',
)
.toBeTruthy({ timeout: 1000 })
.catch(() => {});
}
// 初始化下一个审批人表单字段
await initNextAssigneesFormField();
}
}
/** 关闭气泡卡 */
@@ -285,6 +302,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
async function initNextAssigneesFormField() {
// 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废
const requestId = ++nextApprovalRequestId;
// 获取修改的流程变量, 暂时只支持流程表单
const variables = getUpdatedProcessInstanceVariables();
const data = await getNextApprovalNodes({
@@ -292,6 +311,12 @@ async function initNextAssigneesFormField() {
taskId: runningTask.value.id,
processVariablesStr: JSON.stringify(variables),
});
// 已有更新的请求发出,丢弃本次过期结果,避免把旧分支节点回写到当前列表
if (requestId !== nextApprovalRequestId) {
return;
}
// 在最新结果到达时再清空,避免请求期间出现节点信息抖动
nextAssigneesActivityNode.value = [];
if (data && data.length > 0) {
const customApproveUsersData: Record<string, any[]> = {}; // 用于收集需要设置到 Timeline 组件的自定义审批人数据
data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
@@ -326,6 +351,12 @@ async function initNextAssigneesFormField() {
}
}
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
const debouncedInitNextAssigneesFormField = useDebounceFn(
initNextAssigneesFormField,
300,
);
/** 选择下一个节点的审批人 */
function selectNextAssigneesConfirm(id: string, userList: any[]) {
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
@@ -361,6 +392,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
}
if (pass) {
// 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交
if (pendingNextNodesTask) {
await pendingNextNodesTask;
}
const nextAssigneesValid = validateNextAssignees();
if (!nextAssigneesValid) return;
const variables = getUpdatedProcessInstanceVariables();
@@ -375,12 +410,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
if (runningTask.value.signEnable) {
data.signPicUrl = approveReasonForm.signPicUrl;
}
// 多表单处理,并且有额外的 approveForm 表单需要校验 + 拼接到 data 表单里提交
// TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
// 多表单处理:节点表单需要校验;变量已经在 getUpdatedProcessInstanceVariables 中合并到 data.variables无需再覆盖
const formCreateApi = approveFormFApi.value;
if (Object.keys(formCreateApi)?.length > 0) {
await formCreateApi.validate();
data.variables = approveForm.value.value;
}
await approveTask(data);
popOverVisible.value.approve = false;
@@ -647,18 +680,32 @@ function loadTodoTask(task: any) {
approveForm.value = {};
runningTask.value = task;
approveFormFApi.value = {};
// 切换任务时重置请求序号与 pending 重算,避免旧任务飞行中的请求/Promise 串到新任务
nextApprovalRequestId += 1;
pendingNextNodesTask = null;
reasonRequire.value = task?.reasonRequire ?? false;
nodeTypeName.value =
task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批';
// 处理 approve 表单
if (task && task.formId && task.formConf) {
const tempApproveForm = {};
const tempApproveForm: { option?: any; rule?: any; value?: any } = {};
setConfAndFields2(
tempApproveForm,
task.formConf,
task.formFields,
task.formVariables,
);
// 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段
tempApproveForm.option.onChange = () => {
// 弹窗打开时,才重新计算下一个节点的信息
if (!popOverVisible.value.approve) {
return;
}
// useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled'
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(
() => {},
);
};
approveForm.value = tempApproveForm;
} else {
approveForm.value = {}; // 占位,避免为空
@@ -683,9 +730,17 @@ async function validateNormalForm() {
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
function getUpdatedProcessInstanceVariables() {
const variables: any = {};
props.writableFields.forEach((field: string) => {
variables[field] = props.normalFormApi.getValue(field);
});
// 从流程表单(流程定义级别)中获取变量
if (props.writableFields?.length && props.normalFormApi) {
props.writableFields.forEach((field: string) => {
variables[field] = props.normalFormApi.getValue(field);
});
}
// 从节点表单(节点级别)中获取变量;通过 form-create 官方的 formData() 拿当前值
const nodeFormData = approveFormFApi.value?.formData?.();
if (nodeFormData) {
Object.assign(variables, nodeFormData);
}
return variables;
}

View File

@@ -10,7 +10,7 @@ import { useUserStore } from '@vben/stores';
import { formatDate } from '@vben/utils';
import { Button } from 'ant-design-vue';
// @ts-ignore - 安装 vue3-print-nb 局部指令 v-print
// @ts-expect-error - 安装 vue3-print-nb 局部指令 v-print
import vPrint from 'vue3-print-nb';
import { getProcessInstancePrintData } from '#/api/bpm/processInstance';

Some files were not shown because too many files have changed in this diff Show More