128 Commits

Author SHA1 Message Date
YunaiV
b42e9b36e5 feat(wms):优化 antd、ele 的 order receipt 迁移 2026-05-18 01:02:09 +08:00
YunaiV
f8c2d4b1ff feat(wms):优化 antd、ele 的 order receipt 迁移 2026-05-17 23:56:05 +08:00
YunaiV
41d5aa93d6 feat(wms):新增 ele 的 order receipt 迁移 2026-05-17 23:40:00 +08:00
YunaiV
3135b28211 feat(全局):增加 number-range-input 组件 2026-05-17 23:35:31 +08:00
YunaiV
08511191f7 feat(wms):修复 print 顶部白块的问题,对齐 vue3 + ep 的样式 2026-05-17 23:15:16 +08:00
YunaiV
0246fa1ebc feat(wms):优化 order receipt 的实现,对齐 vue3 + ep 版本 2026-05-17 23:09:18 +08:00
YunaiV
0e4012c623 feat(全局):增加 barcode 二维码组件 2026-05-17 23:07:56 +08:00
YunaiV
4933180560 feat(wms):增加 receipt 功能、评审 2026-05-17 21:39:15 +08:00
YunaiV
8710da9383 feat(wms):增加 wms 工具类 2026-05-17 19:09:01 +08:00
YunaiV
5a1f4901da feat(iot):优化 iot 设备管理的样式 2026-05-17 19:07:50 +08:00
YunaiV
3da4a3f417 feat(wms):将首页的枚举值去掉,统一合并到 constants 里,更聚焦点 2026-05-17 18:17:30 +08:00
YunaiV
84b91c6795 feat(iot):优化 iot 产品管理的样式 2026-05-17 18:11:31 +08:00
YunaiV
735ff018be feat(wms):增加 home 统计的迁移 2026-05-17 17:48:53 +08:00
YunaiV
0163794e3f feat(wms):增加 category 模块的迁移 2026-05-17 16:47:27 +08:00
YunaiV
bb63ca9541 feat(wms):增加 brand 模块的迁移 2026-05-17 16:35:51 +08:00
YunaiV
6b28518165 feat(wms):迁移 api 接口 2026-05-17 16:30:50 +08:00
YunaiV
4adce844d3 feat(wms):增加 merchant 模块的迁移 2026-05-17 10:50:34 +08:00
YunaiV
19b5f38e23 feat(wms):增加 warehouse 模块的迁移 2026-05-16 23:12:33 +08:00
YunaiV
80fa8b74e8 feat:补齐 antd 的 component: 'InputNumber', 的 class full 样式 2026-05-16 22:46:16 +08:00
YunaiV
5710761dbe feat(wms):调整 README.md 2026-05-16 15:09:05 +08:00
YunaiV
877ba2727f feat(wms):调整 README.md 2026-05-16 14:56:01 +08:00
YunaiV
70a639967c feat:更新 README.md
- 提交时不再用节点表单值覆盖 data.variables;与预览阶段使用同一份合并变量
- onChange 加 useDebounceFn(300ms) + 请求序号去重,handleAudit 提交前 await 最新一轮重算
- 切换任务时重置请求序号与 pending 重算
- 改用 form-create 官方 formData() 取节点表单当前值
- 节点表单初始化等 fApi 就绪后再计算下一节点(until + 1s 兜底)

同步至 web-antd / web-ele 两端
2026-05-16 14:44:01 +08:00
xingyu
59183029b6 !341 fix: 修复禁用的删除按钮仍然可以点击的问题
Merge pull request !341 from li_shifeng/fix-dept-delete
2026-05-14 08:29:04 +00:00
xingyu
e81ca2c13f !343 fix(@vben/web-antdv-next): 修复 InputNumber 组件宽度在表单中不占满的问题
Merge pull request !343 from XuZhiqiang/feat-antdv-next
2026-05-14 08:03:20 +00:00
XuZhiqiang
dcccef1c02 fix(@vben/web-antdv-next): 修复 InputNumber 组件宽度在表单中不占满的问题 2026-05-13 15:47:52 +08:00
XuZhiqiang
9a5bee4dce fix(@vben/web-antdv-next): adapter修正组件名称TextArea一致的大小写格式 2026-05-13 15:20:15 +08:00
xingyu
29665f02bf !342 feat(@vben/web-antdv-next): migrate ant-design-vue to antdv-next
Merge pull request !342 from XuZhiqiang/feat-antdv-next
2026-05-12 13:46:18 +00:00
XuZhiqiang
06f776d1ef feat(@vben/web-antdv-next): 替换 antdv-next 中不可用的 List 组件,手动迁移为 div 结构,后续组件库新增 Listy 组件在进行替换 2026-05-12 16:37:42 +08:00
XuZhiqiang
40f0ba71f5 feat(@vben/web-antdv-next): migrate ant-design-vue to antdv-next
Migration Summary: ant-design-vue → antdv-next
Core Changes
package.json - Replaced "ant-design-vue": "catalog:" with "antdv-next": "catalog:"

bootstrap.ts - Changed @vben/styles/antd to @vben/styles/antdv-next

adapter/component/index.ts - Major rewrite:

Removed dynamic defineAsyncComponent imports from ant-design-vue/es/...
Added static imports from antdv-next main entry
Renamed RangePicker → DateRangePicker, Textarea → TextArea
Defined local types for Rule, Locale, UploadRequestOption, FileType, Key
Bulk Import Replacements (100+ files)
from ant-design-vue → from antdv-next
from ant-design-vue/es/locale/... → from antdv-next/locale/...
from ant-design-vue/es/... → removed (use main entry)
from ant-design-vue/lib/... → removed (use main entry)
Component API Differences Handled
ant-design-vue	antdv-next	Files affected
Form.Item	FormItem	475 references
Tabs.TabPane	TabPane	240 references
Select.Option	SelectOption	151 references
Descriptions.Item	DescriptionsItem	2 references
Timeline.Item	TimelineItem	2 references
Radio.Group	RadioGroup	20 references
Collapse.Panel	CollapsePanel	9 references
Layout.Content/Sider/Header/Footer	LayoutContent/LayoutSider/...	14 references
Dropdown#overlay slot	Dropdown#popupRender	6 references
RangePicker	DateRangePicker	15+ references
Textarea	TextArea	37 references
ButtonGroup	Space (fallback)	12 references
Known Issues (requires manual attention)
List component - Not available in antdv-next. 4 files have TODO comments where List/List.Item/List.Item.Meta are used
@form-create/ant-design-vue - Kept as-is (compatible with antdv-next at runtime)
Type errors - ~366 type errors remain (vs 189 in web-antd), mostly pre-existing business logic issues and minor API differences
2026-05-12 15:30:08 +08:00
XuZhiqiang
0fced45a9c refactor(@vben/web-antdv-next): 根据web-antd初始化web-antdv-next 2026-05-12 12:14:32 +08:00
li_shifeng
2ea7da06c5 fix: 修复禁用的删除任然可以点击的问题 2026-05-11 14:13:23 +08:00
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
雪忆天堂
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
xingyu
53c5ccc00a !336 chore: vite 8.0
Merge pull request !336 from xingyu/vite8
2026-03-14 05:40:10 +00: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
YunaiV
a8f67ab717 !335 fix: 修复上传头像时,如果图片加载失败,弹框一直loading的问题,针对 ele 版本 2026-03-07 11:21:03 +08:00
liubei
e136679934 fix: [bpm][antd&ele] 修复流程设计器自定义配置编辑后丢失的问题 2026-02-09 15:35:53 +08:00
2051 changed files with 241745 additions and 5539 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: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -33,7 +33,7 @@ jobs:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
run_install: false

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

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

2
.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

9
.vscode/launch.json vendored
View File

@@ -2,6 +2,15 @@
"$schema": "https://json.schemastore.org/launchsettings.json",
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"name": "vben admin antd dev",
"request": "launch",
"url": "http://localhost:5999",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-antdv-next"
},
{
"type": "chrome",
"name": "vben admin antd dev",

10
.vscode/settings.json vendored
View File

@@ -181,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",
@@ -236,6 +237,5 @@
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib"
"vue.server.hybridMode": true
}

View File

@@ -84,7 +84,7 @@
- 通用模块(必选):系统功能、基础设施
- 通用模块(可选):工作流程、支付系统、数据报表、会员中心
- 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
- 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
### 系统功能
@@ -219,18 +219,44 @@
![功能图](/.gitee/image/common/mall-preview.png)
### 会员中心
| | 功能 | 描述 |
| --- | --- | --- |
| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 |
| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 |
| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 |
| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
### ERP 系统
演示地址:<https://doc.iocoder.cn/erp-preview/>
![功能图](/.gitee/image/common/erp-feature.png)
### WMS 系统
演示地址:<https://doc.iocoder.cn/wms-preview/>
![功能图](/.gitee/image/common/wms-feature.png)
![预览图](/.gitee/image/common/wms-preview.png)
### CRM 系统
演示地址:<https://doc.iocoder.cn/crm-preview/>
![功能图](/.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 +264,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

@@ -6,14 +6,38 @@
/* 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';
@@ -46,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'),
@@ -602,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,
@@ -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

@@ -1,53 +1,9 @@
import type { PageParam, PageResult } from '@vben/request';
import { isEmpty } from '@vben/utils';
import { requestClient } from '#/api/request';
export namespace ThingModelApi {
/** IoT 物模型数据 VO */
export interface ThingModel {
id?: number;
productId?: number;
productKey?: string;
identifier: string;
name: string;
desc?: string;
type: string;
property?: ThingModelProperty;
event?: ThingModelEvent;
service?: ThingModelService;
}
/** IoT 物模型属性 */
export interface Property {
identifier: string;
name: string;
accessMode: string;
dataType: string;
dataSpecs?: any;
dataSpecsList?: any[];
desc?: string;
}
/** IoT 物模型服务 */
export interface Service {
identifier: string;
name: string;
callType: string;
inputData?: any[];
outputData?: any[];
desc?: string;
}
/** IoT 物模型事件 */
export interface Event {
identifier: string;
name: string;
type: string;
outputData?: any[];
desc?: string;
}
}
/** IoT 物模型数据 */
export interface ThingModelData {
id?: number;
@@ -55,9 +11,9 @@ export interface ThingModelData {
productKey?: string;
identifier?: string;
name?: string;
desc?: string;
type?: string;
description?: string;
dataType?: string;
type?: number; // 参见 IoTThingModelTypeEnum 枚举类
property?: ThingModelProperty;
event?: ThingModelEvent;
service?: ThingModelService;
@@ -68,29 +24,45 @@ export interface ThingModelProperty {
identifier?: string;
name?: string;
accessMode?: string;
required?: boolean;
dataType?: string;
description?: string;
dataSpecs?: any;
dataSpecsList?: any[];
desc?: string;
}
/** IoT 物模型服务 */
export interface ThingModelService {
identifier?: string;
name?: string;
required?: boolean;
callType?: string;
inputData?: any[];
outputData?: any[];
desc?: string;
description?: string;
inputParams?: ThingModelParam[];
outputParams?: ThingModelParam[];
method?: string;
}
/** IoT 物模型事件 */
export interface ThingModelEvent {
identifier?: string;
name?: string;
required?: boolean;
type?: string;
outputData?: any[];
desc?: string;
description?: string;
outputParams?: ThingModelParam[];
method?: string;
}
/** IoT 物模型参数 */
export interface ThingModelParam {
identifier?: string;
name?: string;
direction?: string;
paraOrder?: number;
dataType?: string;
dataSpecs?: any;
dataSpecsList?: any[];
}
/** IoT 数据定义(数值型) */
@@ -108,23 +80,119 @@ export interface DataSpecsEnumOrBoolData {
name: string;
}
/** IoT 物模型表单校验规则 */
export interface ThingModelFormRules {
[key: string]: any;
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
function buildRequiredNumberValidator(label: string) {
return (_rule: any, value: any, callback: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
}
if (Number.isNaN(Number(value))) {
callback(new Error(`${label}必须是数字`));
return;
}
callback();
};
}
/** 验证布尔型名称 */
export function validateBoolName(_rule: any, value: any, callback: any) {
if (value) {
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
export function buildIdentifierLikeNameValidator(label: string) {
return (_rule: any, value: string, callback: any) => {
if (isEmpty(value)) {
callback(new Error(`${label}不能为空`));
return;
}
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
return;
}
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
callback(
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
);
return;
}
if (value.length > 20) {
callback(new Error(`${label}长度不能超过 20 个字符`));
return;
}
callback();
} else {
callback(new Error('枚举描述不能为空'));
}
};
}
/** IoT 物模型表单校验规则 */
export const ThingModelFormRules = {
name: [
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
{
pattern: /^[一-龥A-Za-z0-9][一-龥A-Za-z0-9\-_/.]{0,29}$/,
message:
'支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
trigger: 'blur',
},
],
type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
identifier: [
{ required: true, message: '标识符不能为空', trigger: 'blur' },
{
pattern: /^\w{1,50}$/,
message: '支持大小写字母、数字和下划线,不超过 50 个字符',
trigger: 'blur',
},
{
validator: (_rule: any, value: string, callback: any) => {
const reservedKeywords = [
'set',
'get',
'post',
'property',
'event',
'time',
'value',
];
if (reservedKeywords.includes(value)) {
callback(
new Error(
'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义',
),
);
return;
}
if (/^\d+$/.test(value)) {
callback(new Error('标识符不能是纯数字'));
return;
}
callback();
},
trigger: 'blur',
},
],
childDataType: [{ required: true, message: '元素类型不能为空' }],
size: [
{
required: true,
validator: buildRequiredNumberValidator('元素个数'),
trigger: 'blur',
},
],
length: [
{
required: true,
validator: buildRequiredNumberValidator('文本长度'),
trigger: 'blur',
},
],
accessMode: [{ required: true, message: '请选择读写类型', trigger: 'change' }],
callType: [{ required: true, message: '请选择调用方式', trigger: 'change' }],
eventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
};
/** 校验布尔值名称 */
export const validateBoolName = buildIdentifierLikeNameValidator('布尔值名称');
/** 查询产品物模型分页 */
export function getThingModelPage(params: PageParam) {
return requestClient.get<PageResult<ThingModelApi.ThingModel>>(
return requestClient.get<PageResult<ThingModelData>>(
'/iot/thing-model/page',
{ params },
);
@@ -132,17 +200,14 @@ export function getThingModelPage(params: PageParam) {
/** 查询产品物模型详情 */
export function getThingModel(id: number) {
return requestClient.get<ThingModelApi.ThingModel>(
`/iot/thing-model/get?id=${id}`,
);
return requestClient.get<ThingModelData>(`/iot/thing-model/get?id=${id}`);
}
/** 根据产品 ID 查询物模型列表 */
export function getThingModelListByProductId(productId: number) {
return requestClient.get<ThingModelApi.ThingModel[]>(
'/iot/thing-model/list',
{ params: { productId } },
);
return requestClient.get<ThingModelData[]>('/iot/thing-model/list', {
params: { productId },
});
}
/** 新增物模型 */
@@ -162,25 +227,7 @@ export function deleteThingModel(id: number) {
/** 获取物模型 TSL */
export function getThingModelTSL(productId: number) {
return requestClient.get<ThingModelApi.ThingModel[]>(
'/iot/thing-model/get-tsl',
{ params: { productId } },
);
}
/** 导入物模型 TSL
export function importThingModelTSL(productId: number, tslData: any) {
return requestClient.post('/iot/thing-model/import-tsl', {
productId,
tslData,
});
}
*/
/** 导出物模型 TSL
export function exportThingModelTSL(productId: number) {
return requestClient.get<any>('/iot/thing-model/export-tsl', {
return requestClient.get<any>('/iot/thing-model/get-tsl', {
params: { productId },
});
}
*/

View File

@@ -128,6 +128,49 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
},
});
// 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

@@ -0,0 +1,66 @@
import { requestClient } from '#/api/request';
export namespace WmsHomeStatisticsApi {
export interface StatisticsReq {
goodsLimit?: number;
warehouseId?: number;
warehouseLimit?: number;
}
export interface OrderStatus {
count: number;
status: number;
}
export interface OrderSummary {
statuses: OrderStatus[];
total: number;
type: number;
}
export interface OrderTrend {
checkCount: number;
movementCount: number;
receiptCount: number;
shipmentCount: number;
time: number | string;
}
export interface InventoryRankItem {
id: number;
name: string;
quantity: number;
}
export interface InventorySummary {
goodsShareList: InventoryRankItem[];
totalQuantity: number;
warehouseDistributionList: InventoryRankItem[];
}
}
export function getOrderSummary(params?: WmsHomeStatisticsApi.StatisticsReq) {
return requestClient.get<WmsHomeStatisticsApi.OrderSummary[]>(
'/wms/home-statistics/order-summary',
{ params },
);
}
export function getOrderTrend(
days?: number,
params?: WmsHomeStatisticsApi.StatisticsReq,
) {
return requestClient.get<WmsHomeStatisticsApi.OrderTrend[]>(
'/wms/home-statistics/order-trend',
{ params: { ...params, days } },
);
}
export function getInventorySummary(
params?: WmsHomeStatisticsApi.StatisticsReq,
) {
return requestClient.get<WmsHomeStatisticsApi.InventorySummary>(
'/wms/home-statistics/inventory-summary',
{ params },
);
}

View File

@@ -0,0 +1,37 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsInventoryHistoryApi {
/** WMS 库存记录 */
export interface InventoryHistory {
id?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
beforeQuantity?: number;
afterQuantity?: number;
price?: number;
totalPrice?: number;
remark?: string;
orderId?: number;
orderNo?: string;
orderType?: number;
createTime?: Date;
}
}
/** 查询库存记录分页 */
export function getInventoryHistoryPage(params: PageParam) {
return requestClient.get<PageResult<WmsInventoryHistoryApi.InventoryHistory>>(
'/wms/inventory-history/page',
{ params },
);
}

View File

@@ -0,0 +1,42 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsInventoryApi {
/** WMS 库存统计 */
export interface Inventory {
id?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
remark?: string;
createTime?: Date;
}
/** WMS 库存统计列表请求 */
export interface InventoryListReq {
warehouseId: number;
}
}
/** 查询库存统计分页 */
export function getInventoryPage(params: PageParam) {
return requestClient.get<PageResult<WmsInventoryApi.Inventory>>(
'/wms/inventory/page',
{ params },
);
}
/** 查询库存统计列表 */
export function getInventoryList(params: WmsInventoryApi.InventoryListReq) {
return requestClient.get<WmsInventoryApi.Inventory[]>('/wms/inventory/list', {
params,
});
}

View File

@@ -0,0 +1,55 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsItemBrandApi {
/** WMS 商品品牌 */
export interface ItemBrand {
id?: number;
code?: string;
name?: string;
createTime?: Date;
}
}
/** 查询商品品牌分页 */
export function getItemBrandPage(params: PageParam) {
return requestClient.get<PageResult<WmsItemBrandApi.ItemBrand>>(
'/wms/item-brand/page',
{ params },
);
}
/** 查询商品品牌精简列表 */
export function getItemBrandSimpleList() {
return requestClient.get<WmsItemBrandApi.ItemBrand[]>(
'/wms/item-brand/simple-list',
);
}
/** 查询商品品牌详情 */
export function getItemBrand(id: number) {
return requestClient.get<WmsItemBrandApi.ItemBrand>(
`/wms/item-brand/get?id=${id}`,
);
}
/** 新增商品品牌 */
export function createItemBrand(data: WmsItemBrandApi.ItemBrand) {
return requestClient.post('/wms/item-brand/create', data);
}
/** 修改商品品牌 */
export function updateItemBrand(data: WmsItemBrandApi.ItemBrand) {
return requestClient.put('/wms/item-brand/update', data);
}
/** 删除商品品牌 */
export function deleteItemBrand(id: number) {
return requestClient.delete(`/wms/item-brand/delete?id=${id}`);
}
/** 导出商品品牌 */
export function exportItemBrand(params: any) {
return requestClient.download('/wms/item-brand/export-excel', { params });
}

View File

@@ -0,0 +1,52 @@
import { requestClient } from '#/api/request';
export namespace WmsItemCategoryApi {
/** WMS 商品分类 */
export interface ItemCategory {
id?: number;
parentId?: number;
code?: string;
name?: string;
sort?: number;
status?: number;
createTime?: Date;
children?: ItemCategory[];
}
}
/** 查询商品分类列表 */
export function getItemCategoryList(params?: any) {
return requestClient.get<WmsItemCategoryApi.ItemCategory[]>(
'/wms/item-category/list',
{ params },
);
}
/** 查询商品分类精简列表 */
export function getItemCategorySimpleList() {
return requestClient.get<WmsItemCategoryApi.ItemCategory[]>(
'/wms/item-category/simple-list',
);
}
/** 查询商品分类详情 */
export function getItemCategory(id: number) {
return requestClient.get<WmsItemCategoryApi.ItemCategory>(
`/wms/item-category/get?id=${id}`,
);
}
/** 新增商品分类 */
export function createItemCategory(data: WmsItemCategoryApi.ItemCategory) {
return requestClient.post('/wms/item-category/create', data);
}
/** 修改商品分类 */
export function updateItemCategory(data: WmsItemCategoryApi.ItemCategory) {
return requestClient.put('/wms/item-category/update', data);
}
/** 删除商品分类 */
export function deleteItemCategory(id: number) {
return requestClient.delete(`/wms/item-category/delete?id=${id}`);
}

View File

@@ -0,0 +1,61 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsItemSkuApi } from './sku';
import { requestClient } from '#/api/request';
export namespace WmsItemApi {
/** WMS 商品 */
export interface Item {
id?: number;
code?: string;
name?: string;
categoryId?: number;
categoryName?: string;
unit?: string;
brandId?: number;
brandName?: string;
remark?: string;
skus?: WmsItemSkuApi.ItemSku[];
createTime?: Date;
}
}
/** 查询商品分页 */
export function getItemPage(params: PageParam) {
return requestClient.get<PageResult<WmsItemApi.Item>>('/wms/item/page', {
params,
});
}
/** 查询商品精简列表 */
export function getItemSimpleList(params?: any) {
return requestClient.get<WmsItemApi.Item[]>('/wms/item/simple-list', {
params,
});
}
/** 查询商品详情 */
export function getItem(id: number) {
return requestClient.get<WmsItemApi.Item>(`/wms/item/get?id=${id}`);
}
/** 新增商品 */
export function createItem(data: WmsItemApi.Item) {
return requestClient.post('/wms/item/create', data);
}
/** 修改商品 */
export function updateItem(data: WmsItemApi.Item) {
return requestClient.put('/wms/item/update', data);
}
/** 删除商品 */
export function deleteItem(id: number) {
return requestClient.delete(`/wms/item/delete?id=${id}`);
}
/** 导出商品 */
export function exportItem(params: any) {
return requestClient.download('/wms/item/export-excel', { params });
}

View File

@@ -0,0 +1,37 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsItemSkuApi {
/** WMS 商品 SKU */
export interface ItemSku {
id?: number;
name?: string;
itemId?: number;
itemCode?: string;
itemName?: string;
categoryId?: number;
categoryName?: string;
unit?: string;
brandId?: number;
brandName?: string;
barCode?: string;
code?: string;
length?: number;
width?: number;
height?: number;
grossWeight?: number;
netWeight?: number;
costPrice?: number;
sellingPrice?: number;
createTime?: Date;
}
}
/** 按 SKU 维度分页(支持商品 / 品牌 / 分类多表联查筛选) */
export function getItemSkuPage(params: PageParam) {
return requestClient.get<PageResult<WmsItemSkuApi.ItemSku>>(
'/wms/item-sku/page',
{ params },
);
}

View File

@@ -0,0 +1,73 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsMerchantApi {
/** WMS 往来企业 */
export interface Merchant {
id?: number;
code?: string;
name?: string;
type?: number;
level?: string;
bankName?: string;
bankAccount?: string;
address?: string;
mobile?: string;
telephone?: string;
contact?: string;
email?: string;
remark?: string;
createTime?: Date;
}
/** WMS 往来企业精简列表请求 */
export interface MerchantSimpleListReq {
types?: number[];
}
}
/** 查询往来企业分页 */
export function getMerchantPage(params: PageParam) {
return requestClient.get<PageResult<WmsMerchantApi.Merchant>>(
'/wms/merchant/page',
{ params },
);
}
/** 查询往来企业精简列表 */
export function getMerchantSimpleList(
params?: WmsMerchantApi.MerchantSimpleListReq,
) {
return requestClient.get<WmsMerchantApi.Merchant[]>(
'/wms/merchant/simple-list',
{ params },
);
}
/** 查询往来企业详情 */
export function getMerchant(id: number) {
return requestClient.get<WmsMerchantApi.Merchant>(
`/wms/merchant/get?id=${id}`,
);
}
/** 新增往来企业 */
export function createMerchant(data: WmsMerchantApi.Merchant) {
return requestClient.post('/wms/merchant/create', data);
}
/** 修改往来企业 */
export function updateMerchant(data: WmsMerchantApi.Merchant) {
return requestClient.put('/wms/merchant/update', data);
}
/** 删除往来企业 */
export function deleteMerchant(id: number) {
return requestClient.delete(`/wms/merchant/delete?id=${id}`);
}
/** 导出往来企业 */
export function exportMerchant(params: any) {
return requestClient.download('/wms/merchant/export-excel', { params });
}

View File

@@ -0,0 +1,57 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace WmsWarehouseApi {
/** WMS 仓库 */
export interface Warehouse {
id?: number;
code?: string;
name?: string;
remark?: string;
sort?: number;
createTime?: Date;
}
}
/** 查询仓库分页 */
export function getWarehousePage(params: PageParam) {
return requestClient.get<PageResult<WmsWarehouseApi.Warehouse>>(
'/wms/warehouse/page',
{ params },
);
}
/** 查询仓库精简列表 */
export function getWarehouseSimpleList() {
return requestClient.get<WmsWarehouseApi.Warehouse[]>(
'/wms/warehouse/simple-list',
);
}
/** 查询仓库详情 */
export function getWarehouse(id: number) {
return requestClient.get<WmsWarehouseApi.Warehouse>(
`/wms/warehouse/get?id=${id}`,
);
}
/** 新增仓库 */
export function createWarehouse(data: WmsWarehouseApi.Warehouse) {
return requestClient.post('/wms/warehouse/create', data);
}
/** 修改仓库 */
export function updateWarehouse(data: WmsWarehouseApi.Warehouse) {
return requestClient.put('/wms/warehouse/update', data);
}
/** 删除仓库 */
export function deleteWarehouse(id: number) {
return requestClient.delete(`/wms/warehouse/delete?id=${id}`);
}
/** 导出仓库 */
export function exportWarehouse(params: any) {
return requestClient.download('/wms/warehouse/export-excel', { params });
}

View File

@@ -0,0 +1,23 @@
export namespace WmsCheckOrderDetailApi {
/** WMS 盘库单明细 */
export interface CheckOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
inventoryId?: number;
warehouseId?: number;
warehouseName?: string;
receiptTime?: Date;
quantity?: number;
checkQuantity?: number;
availableQuantity?: number;
price?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,71 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsCheckOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsCheckOrderApi {
/** WMS 盘库单 */
export interface CheckOrder {
id?: number;
no?: string;
orderTime?: string;
status?: number;
remark?: string;
warehouseId?: number;
warehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
actualPrice?: number;
details?: WmsCheckOrderDetailApi.CheckOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getCheckOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsCheckOrderApi.CheckOrder>>(
'/wms/check-order/page',
{ params },
);
}
export function getCheckOrder(id: number) {
return requestClient.get<WmsCheckOrderApi.CheckOrder>(
`/wms/check-order/get?id=${id}`,
);
}
export function getCheckOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsCheckOrderDetailApi.CheckOrderDetail[]>(
`/wms/check-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createCheckOrder(data: WmsCheckOrderApi.CheckOrder) {
return requestClient.post('/wms/check-order/create', data);
}
export function updateCheckOrder(data: WmsCheckOrderApi.CheckOrder) {
return requestClient.put('/wms/check-order/update', data);
}
export function completeCheckOrder(id: number) {
return requestClient.put(`/wms/check-order/complete?id=${id}`);
}
export function cancelCheckOrder(id: number) {
return requestClient.put(`/wms/check-order/cancel?id=${id}`);
}
export function deleteCheckOrder(id: number) {
return requestClient.delete(`/wms/check-order/delete?id=${id}`);
}
export function exportCheckOrder(params: any) {
return requestClient.download('/wms/check-order/export-excel', { params });
}

View File

@@ -0,0 +1,23 @@
export namespace WmsMovementOrderDetailApi {
/** WMS 移库单明细 */
export interface MovementOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
sourceWarehouseId?: number;
sourceWarehouseName?: string;
targetWarehouseId?: number;
targetWarehouseName?: string;
quantity?: number;
availableQuantity?: number;
price?: number;
totalPrice?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,72 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsMovementOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsMovementOrderApi {
/** WMS 移库单 */
export interface MovementOrder {
id?: number;
no?: string;
orderTime?: string;
status?: number;
remark?: string;
sourceWarehouseId?: number;
sourceWarehouseName?: string;
targetWarehouseId?: number;
targetWarehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
details?: WmsMovementOrderDetailApi.MovementOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getMovementOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsMovementOrderApi.MovementOrder>>(
'/wms/movement-order/page',
{ params },
);
}
export function getMovementOrder(id: number) {
return requestClient.get<WmsMovementOrderApi.MovementOrder>(
`/wms/movement-order/get?id=${id}`,
);
}
export function getMovementOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsMovementOrderDetailApi.MovementOrderDetail[]>(
`/wms/movement-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createMovementOrder(data: WmsMovementOrderApi.MovementOrder) {
return requestClient.post('/wms/movement-order/create', data);
}
export function updateMovementOrder(data: WmsMovementOrderApi.MovementOrder) {
return requestClient.put('/wms/movement-order/update', data);
}
export function completeMovementOrder(id: number) {
return requestClient.put(`/wms/movement-order/complete?id=${id}`);
}
export function cancelMovementOrder(id: number) {
return requestClient.put(`/wms/movement-order/cancel?id=${id}`);
}
export function deleteMovementOrder(id: number) {
return requestClient.delete(`/wms/movement-order/delete?id=${id}`);
}
export function exportMovementOrder(params: any) {
return requestClient.download('/wms/movement-order/export-excel', { params });
}

View File

@@ -0,0 +1,20 @@
export namespace WmsReceiptOrderDetailApi {
/** WMS 入库单明细 */
export interface ReceiptOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
price?: number;
totalPrice?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,74 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsReceiptOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsReceiptOrderApi {
/** WMS 入库单 */
export interface ReceiptOrder {
id?: number;
no?: string;
type?: number;
orderTime?: string;
status?: number;
bizOrderNo?: string;
merchantId?: number;
merchantName?: string;
remark?: string;
warehouseId?: number;
warehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
details?: WmsReceiptOrderDetailApi.ReceiptOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getReceiptOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsReceiptOrderApi.ReceiptOrder>>(
'/wms/receipt-order/page',
{ params },
);
}
export function getReceiptOrder(id: number) {
return requestClient.get<WmsReceiptOrderApi.ReceiptOrder>(
`/wms/receipt-order/get?id=${id}`,
);
}
export function getReceiptOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsReceiptOrderDetailApi.ReceiptOrderDetail[]>(
`/wms/receipt-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createReceiptOrder(data: WmsReceiptOrderApi.ReceiptOrder) {
return requestClient.post('/wms/receipt-order/create', data);
}
export function updateReceiptOrder(data: WmsReceiptOrderApi.ReceiptOrder) {
return requestClient.put('/wms/receipt-order/update', data);
}
export function completeReceiptOrder(id: number) {
return requestClient.put(`/wms/receipt-order/complete?id=${id}`);
}
export function cancelReceiptOrder(id: number) {
return requestClient.put(`/wms/receipt-order/cancel?id=${id}`);
}
export function deleteReceiptOrder(id: number) {
return requestClient.delete(`/wms/receipt-order/delete?id=${id}`);
}
export function exportReceiptOrder(params: any) {
return requestClient.download('/wms/receipt-order/export-excel', { params });
}

View File

@@ -0,0 +1,21 @@
export namespace WmsShipmentOrderDetailApi {
/** WMS 出库单明细 */
export interface ShipmentOrderDetail {
id?: number;
orderId?: number;
itemId?: number;
itemCode?: string;
itemName?: string;
unit?: string;
skuId?: number;
skuCode?: string;
skuName?: string;
warehouseId?: number;
warehouseName?: string;
quantity?: number;
availableQuantity?: number;
price?: number;
totalPrice?: number;
createTime?: Date;
}
}

View File

@@ -0,0 +1,74 @@
import type { PageParam, PageResult } from '@vben/request';
import type { WmsShipmentOrderDetailApi } from './detail';
import { requestClient } from '#/api/request';
export namespace WmsShipmentOrderApi {
/** WMS 出库单 */
export interface ShipmentOrder {
id?: number;
no?: string;
type?: number;
orderTime?: string;
status?: number;
bizOrderNo?: string;
merchantId?: number;
merchantName?: string;
remark?: string;
warehouseId?: number;
warehouseName?: string;
totalQuantity?: number;
totalPrice?: number;
details?: WmsShipmentOrderDetailApi.ShipmentOrderDetail[];
createTime?: Date;
creator?: string;
creatorName?: string;
updateTime?: Date;
updater?: string;
updaterName?: string;
}
}
export function getShipmentOrderPage(params: PageParam) {
return requestClient.get<PageResult<WmsShipmentOrderApi.ShipmentOrder>>(
'/wms/shipment-order/page',
{ params },
);
}
export function getShipmentOrder(id: number) {
return requestClient.get<WmsShipmentOrderApi.ShipmentOrder>(
`/wms/shipment-order/get?id=${id}`,
);
}
export function getShipmentOrderDetailListByOrderId(orderId: number) {
return requestClient.get<WmsShipmentOrderDetailApi.ShipmentOrderDetail[]>(
`/wms/shipment-order-detail/list-by-order-id?orderId=${orderId}`,
);
}
export function createShipmentOrder(data: WmsShipmentOrderApi.ShipmentOrder) {
return requestClient.post('/wms/shipment-order/create', data);
}
export function updateShipmentOrder(data: WmsShipmentOrderApi.ShipmentOrder) {
return requestClient.put('/wms/shipment-order/update', data);
}
export function completeShipmentOrder(id: number) {
return requestClient.put(`/wms/shipment-order/complete?id=${id}`);
}
export function cancelShipmentOrder(id: number) {
return requestClient.put(`/wms/shipment-order/cancel?id=${id}`);
}
export function deleteShipmentOrder(id: number) {
return requestClient.delete(`/wms/shipment-order/delete?id=${id}`);
}
export function exportShipmentOrder(params: any) {
return requestClient.download('/wms/shipment-order/export-excel', { params });
}

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

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

@@ -0,0 +1,39 @@
import type { VbenFormSchema } from '#/adapter/form';
import { markRaw } from 'vue';
import NumberRangeInput from './number-range-input.vue';
export { default as NumberRangeInput } from './number-range-input.vue';
export type NumberRangeValue = [number | undefined, number | undefined];
function splitNumberRange(minFieldName: string, maxFieldName: string) {
return (
value: NumberRangeValue | undefined,
setValue: (fieldName: string, value: number | undefined) => void,
) => {
setValue(minFieldName, value?.[0]);
setValue(maxFieldName, value?.[1]);
return undefined;
};
}
export function buildNumberRangeSchema(
label: string,
fieldName: string,
minFieldName: string,
maxFieldName: string,
precision: number,
): VbenFormSchema {
return {
component: markRaw(NumberRangeInput),
componentProps: {
min: 0,
precision,
},
fieldName,
label,
valueFormat: splitNumberRange(minFieldName, maxFieldName),
};
}

View File

@@ -0,0 +1,73 @@
<script lang="ts" setup>
import { InputNumber } from 'ant-design-vue';
type NumberRangeValue = [number | undefined, number | undefined];
const props = withDefaults(
defineProps<{
maxPlaceholder?: string;
min?: number;
minPlaceholder?: string;
precision?: number;
value?: NumberRangeValue;
}>(),
{
maxPlaceholder: '最大值',
min: undefined,
minPlaceholder: '最小值',
precision: 2,
value: undefined,
},
);
const emit = defineEmits<{
'update:value': [value: NumberRangeValue | undefined];
}>();
function normalizeValue(value: unknown) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value === 'string' && value.trim() !== '') {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : undefined;
}
return undefined;
}
function updateValue(index: 0 | 1, value: unknown) {
const next: NumberRangeValue = [
props.value?.[0] ?? undefined,
props.value?.[1] ?? undefined,
];
next[index] = normalizeValue(value);
emit(
'update:value',
next[0] === undefined && next[1] === undefined ? undefined : next,
);
}
</script>
<template>
<div class="flex w-full items-center gap-2">
<InputNumber
:controls="false"
:min="min"
:placeholder="minPlaceholder"
:precision="precision"
:value="value?.[0]"
class="min-w-0 flex-1"
@update:value="updateValue(0, $event)"
/>
<span class="shrink-0 text-muted-foreground"></span>
<InputNumber
:controls="false"
:min="min"
:placeholder="maxPlaceholder"
:precision="precision"
:value="value?.[1]"
class="min-w-0 flex-1"
@update:value="updateValue(1, $event)"
/>
</div>
</template>

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

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

@@ -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 查找的数组
@@ -27,14 +30,3 @@ export const findIndex = <T = Recordable<any>>(
});
return index;
};
/**
* URL 验证
* @param path URL 路径
*/
export const isUrl = (path: string): boolean => {
// fix:修复hash路由无法跳转的问题
const reg =
/(((^https?:(?:\/\/)?)(?:[-:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%#/.\w-]*)?\??[-+=&%@.\w]*(?:#\w*)?)?)$/;
return reg.test(path);
};

View File

@@ -41,6 +41,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '温度参数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入温度参数',
precision: 2,
min: 0,
@@ -53,6 +54,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '回复数 Token 数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入回复数 Token 数',
min: 0,
max: 8192,
@@ -64,6 +66,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '上下文数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入上下文数量',
min: 0,
max: 20,

View File

@@ -52,6 +52,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索 topK',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索 topK',
min: 0,
max: 10,
@@ -63,6 +64,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索相似度阈值',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索相似度阈值',
min: 0,
max: 1,

View File

@@ -55,6 +55,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索 topK',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索 topK',
min: 0,
max: 10,
@@ -66,6 +67,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '检索相似度阈值',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入检索相似度阈值',
min: 0,
max: 1,

View File

@@ -154,6 +154,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '角色排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入角色排序',
},
dependencies: {

View File

@@ -84,6 +84,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '模型排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入模型排序',
},
rules: 'required',
@@ -104,6 +105,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '温度参数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入温度参数',
min: 0,
max: 2,
@@ -121,6 +123,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '回复数 Token 数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 8192,
placeholder: '请输入回复数 Token 数',
@@ -138,6 +141,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '上下文数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
max: 20,
placeholder: '请输入上下文数量',

View File

@@ -60,6 +60,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '分类排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入分类排序',
},

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,5 +1,4 @@
import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules';
// eslint-disable-next-line n/no-extraneous-import
import inherits from 'inherits';
function CustomRules(eventBus) {

View File

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

@@ -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,
@@ -113,6 +114,8 @@ 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: '',
@@ -256,7 +259,6 @@ async function openPopover(type: string) {
message.warning('表单校验不通过,请先完善表单!!');
return;
}
await initNextAssigneesFormField();
}
if (type === 'return') {
// 获取退回节点
@@ -269,6 +271,18 @@ 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();
}
}
/** 关闭气泡卡 */
@@ -286,6 +300,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
async function initNextAssigneesFormField() {
// 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废
const requestId = ++nextApprovalRequestId;
// 获取修改的流程变量, 暂时只支持流程表单
const variables = getUpdatedProcessInstanceVariables();
const data = await getNextApprovalNodes({
@@ -293,6 +309,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) => {
@@ -327,6 +349,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);
@@ -362,6 +390,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();
@@ -376,12 +408,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;
@@ -648,18 +678,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 = {}; // 占位,避免为空
@@ -684,9 +728,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

@@ -118,6 +118,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品总金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
disabled: true,
@@ -130,6 +131,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '整单折扣(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入整单折扣',
@@ -141,6 +143,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '折扣后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
disabled: true,

View File

@@ -17,6 +17,7 @@ export const schema: VbenFormSchema[] = [
component: 'InputNumber',
fieldName: 'notifyDays',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},

View File

@@ -18,7 +18,7 @@ import { schema } from './data';
const [Form, formApi] = useVbenForm({
commonConfig: {
labelClass: 'w-100',
labelWidth: 120,
},
layout: 'horizontal',
schema,

View File

@@ -198,6 +198,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品总金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入产品总金额',
@@ -209,6 +210,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '整单折扣(%',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
placeholder: '请输入整单折扣',
@@ -220,6 +222,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '折扣后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
disabled: true,

View File

@@ -65,6 +65,7 @@ export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
: '锁定客户数上限',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: `请输入${
LimitConfType.CUSTOMER_QUANTITY_LIMIT === confType
? '拥有客户数上限'

View File

@@ -16,6 +16,7 @@ export const schema: VbenFormSchema[] = [
component: 'InputNumber',
fieldName: 'contactExpireDays',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
@@ -35,6 +36,7 @@ export const schema: VbenFormSchema[] = [
addonAfter: () => '天未成交',
}),
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
@@ -63,6 +65,7 @@ export const schema: VbenFormSchema[] = [
component: 'InputNumber',
fieldName: 'notifyDays',
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},

View File

@@ -18,7 +18,7 @@ import { schema } from './data';
const [Form, formApi] = useVbenForm({
commonConfig: {
labelClass: 'w-100',
labelWidth: 120,
},
layout: 'horizontal',
schema,

View File

@@ -92,6 +92,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '价格(元)',
rules: 'required',
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
step: 0.1,

View File

@@ -141,6 +141,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
rules: 'required',
componentProps: {
class: '!w-full',
placeholder: '请输入回款金额',
min: 0,
precision: 2,

View File

@@ -96,6 +96,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
rules: 'required',
componentProps: {
class: '!w-full',
placeholder: '请输入计划回款金额',
min: 0,
precision: 2,
@@ -119,6 +120,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '提前几天提醒',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入提前几天提醒',
min: 0,
},

View File

@@ -44,6 +44,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
precision: 0,
},

View File

@@ -129,6 +129,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '合计付款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '合计付款',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -140,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入优惠金额',
precision: 2,
@@ -151,6 +153,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '实际付款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '实际付款',
precision: 2,
formatter: erpPriceInputFormatter,

View File

@@ -129,6 +129,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '合计收款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '合计收款',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -140,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入优惠金额',
precision: 2,
@@ -151,6 +153,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '实际收款',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '实际收款',
precision: 2,
formatter: erpPriceInputFormatter,

View File

@@ -65,6 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '显示顺序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入显示顺序',
},

View File

@@ -92,6 +92,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '保质期天数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入保质期天数',
},
},
@@ -100,6 +101,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '重量kg',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重量kg',
},
},
@@ -108,6 +110,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '采购价格',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入采购价格,单位:元',
precision: 2,
min: 0,
@@ -119,6 +122,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '销售价格',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入销售价格,单位:元',
precision: 2,
min: 0,
@@ -130,6 +134,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '最低价格',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入最低价格,单位:元',
precision: 2,
min: 0,

View File

@@ -117,6 +117,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -129,6 +130,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '付款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -140,6 +142,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -160,6 +163,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -184,6 +188,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应付金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -103,6 +103,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -115,6 +116,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '付款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -126,6 +128,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -148,6 +151,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
{
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入支付订金',
precision: 2,
min: 0,

View File

@@ -117,6 +117,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -129,6 +130,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '退款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
@@ -139,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -159,6 +162,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -183,6 +187,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应退金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -82,6 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
},
rules: 'required',
@@ -99,6 +100,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '税率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入税率',
min: 0,
precision: 2,

View File

@@ -82,6 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
precision: 0,
},
@@ -100,6 +101,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '税率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入税率',
precision: 2,
},

View File

@@ -116,6 +116,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -128,6 +129,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '付款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '收款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -139,6 +141,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -161,6 +164,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
{
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入收取订金',
precision: 2,
min: 0,

View File

@@ -134,6 +134,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -146,6 +147,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '收款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -157,6 +159,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -177,6 +180,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -204,6 +208,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应收金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -130,6 +130,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠率(%)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入优惠率',
min: 0,
max: 100,
@@ -142,6 +143,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '退款优惠',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
@@ -152,6 +154,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '优惠后金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
@@ -172,6 +175,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '其他费用',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
placeholder: '请输入其他费用',
precision: 2,
@@ -197,6 +201,7 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
label: '应收金额',
component: 'InputNumber',
componentProps: {
class: '!w-full',
precision: 2,
min: 0,
disabled: true,

View File

@@ -51,6 +51,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '仓储费(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入仓储费,单位:元/天/KG',
min: 0,
precision: 2,
@@ -61,6 +62,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '搬运费(元)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入搬运费,单位:元',
min: 0,
precision: 2,
@@ -79,6 +81,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入排序',
precision: 0,
},

View File

@@ -82,6 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '主机端口',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入主机端口',
},

View File

@@ -68,6 +68,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '重试次数',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重试次数。设置为 0 时,不进行重试',
min: 0,
},
@@ -78,6 +79,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '重试间隔',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔',
min: 0,
},
@@ -88,6 +90,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '监控超时时间',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入监控超时时间,单位:毫秒',
min: 0,
},

View File

@@ -122,8 +122,8 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
label: '设备经度',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入设备经度',
class: 'w-full',
min: -180,
max: 180,
precision: 6,
@@ -140,8 +140,8 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
label: '设备纬度',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入设备纬度',
class: 'w-full',
min: -90,
max: 90,
precision: 6,

View File

@@ -62,7 +62,7 @@ function openEditForm(row: IotDeviceApi.Device) {
<div>
<h2 class="text-xl font-bold">{{ device.deviceName }}</h2>
</div>
<div class="space-x-2">
<div class="flex gap-2">
<Button
v-if="product.status === 0"
v-access:code="['iot:device:update']"

View File

@@ -70,6 +70,7 @@ const [Form, formApi] = useVbenForm({
label: '端口',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入端口',
min: 1,
max: 65_535,
@@ -86,6 +87,7 @@ const [Form, formApi] = useVbenForm({
label: '从站地址',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入从站地址,范围 1-247',
min: 1,
max: 247,
@@ -98,6 +100,7 @@ const [Form, formApi] = useVbenForm({
label: '连接超时(ms)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入连接超时时间',
min: 1000,
step: 1000,
@@ -114,6 +117,7 @@ const [Form, formApi] = useVbenForm({
label: '重试间隔(ms)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入重试间隔',
min: 1000,
step: 1000,

View File

@@ -111,6 +111,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '寄存器地址',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入寄存器地址',
min: 0,
max: 65_535,
@@ -133,6 +134,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '寄存器数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入寄存器数量',
min: 1,
max: 125,
@@ -177,6 +179,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '缩放因子',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入缩放因子',
precision: 6,
step: 0.1,
@@ -188,6 +191,7 @@ function useFormSchema(): VbenFormSchema[] {
label: '轮询间隔(ms)',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入轮询间隔',
min: 100,
step: 1000,

View File

@@ -284,7 +284,7 @@ onMounted(async () => {
<DeviceImportFormModal @success="handleRefresh" />
<!-- 统一搜索工具栏 -->
<Card :body-style="{ padding: '16px' }" class="mb-4">
<Card :body-style="{ padding: '16px' }" class="!mb-2">
<!-- 搜索表单 -->
<div class="mb-3 flex flex-wrap items-center gap-3">
<Select

View File

@@ -35,8 +35,8 @@ export function useFormSchema(): VbenFormSchema[] {
label: '分类排序',
component: 'InputNumber',
componentProps: {
class: '!w-full',
placeholder: '请输入分类排序',
class: 'w-full',
min: 0,
precision: 0,
},

View File

@@ -147,19 +147,6 @@ export function useBasicFormSchema(
help: 'iot-gateway-server 默认根据接入的协议类型确定数据格式,仅 MQTT、EMQX 协议支持自定义序列化类型',
rules: 'required',
},
// TODO @haohao这个貌似不需要
{
fieldName: 'status',
label: '产品状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
defaultValue: 0,
rules: 'required',
},
];
}
@@ -248,15 +235,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
name: 'CellImage',
},
},
{
field: 'status',
title: '产品状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_PRODUCT_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',

View File

@@ -11,6 +11,7 @@ import { message, Tabs } from 'ant-design-vue';
import { getDeviceCount } from '#/api/iot/device/device';
import { getProduct } from '#/api/iot/product/product';
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
import ProductDetailsHeader from './modules/header.vue';
import ProductDetailsInfo from './modules/info.vue';
@@ -25,7 +26,8 @@ const loading = ref(true);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
const activeTab = ref('info');
provide('product', product); // 提供产品信息给子组件
/** 向子组件提供产品信息 */
provide(IOT_PROVIDE_KEY.PRODUCT, product);
/** 获取产品详情 */
async function getProductData(productId: number) {
@@ -82,10 +84,7 @@ onMounted(async () => {
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
</Tabs.TabPane>
<Tabs.TabPane key="thingModel" tab="物模型(功能定义)">
<IoTProductThingModel
v-if="activeTab === 'thingModel'"
:product-id="id"
/>
<IoTProductThingModel v-if="activeTab === 'thingModel'" />
</Tabs.TabPane>
</Tabs>
</Page>

View File

@@ -90,7 +90,7 @@ function handleUnpublish(product: IotProductApi.Product) {
<div>
<h2 class="text-xl font-bold">{{ product.name }}</h2>
</div>
<div class="space-x-2">
<div class="flex gap-2">
<Button
:disabled="product.status === ProductStatusEnum.PUBLISHED"
@click="openEditForm(product)"

View File

@@ -175,7 +175,7 @@ onMounted(() => {
<FormModal @success="handleRefresh" />
<!-- 统一搜索工具栏 -->
<Card :body-style="{ padding: '16px' }" class="mb-4">
<Card :body-style="{ padding: '16px' }" class="!mb-2">
<!-- 搜索表单 -->
<div class="mb-3 flex items-center gap-3">
<Input

View File

@@ -115,11 +115,6 @@ onMounted(() => {
<div class="ml-3 min-w-0 flex-1">
<div class="product-title">{{ item.name }}</div>
</div>
<DictTag
:type="DICT_TYPE.IOT_PRODUCT_STATUS"
:value="item.status"
class="status-tag"
/>
</div>
<!-- 内容区域 -->
<div class="mb-3 flex items-start">
@@ -269,11 +264,6 @@ onMounted(() => {
white-space: nowrap;
}
// 状态标签
.status-tag {
font-size: 12px;
}
// 信息列表
.info-list {
.info-item {

View File

@@ -4,6 +4,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getDataTypeOptionsLabel } from '#/views/iot/utils/constants';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@@ -27,7 +29,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
field: 'type',
title: '功能类型',
minWidth: 20,
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_THING_MODEL_TYPE },
@@ -41,17 +43,16 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
field: 'identifier',
title: '标识符',
minWidth: 20,
minWidth: 120,
},
{
field: 'dataType',
title: '数据类型',
minWidth: 50,
slots: { default: 'dataType' },
minWidth: 100,
formatter: ({ row }) =>
getDataTypeOptionsLabel(row.property?.dataType) || '-',
},
{
field: 'property',
title: '属性',
title: '数据定义',
minWidth: 200,
slots: { default: 'dataDefinition' },
},

View File

@@ -1,99 +1,90 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel';
import { onMounted, provide, ref } from 'vue';
import { computed, inject } from 'vue';
import { Page } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProduct } from '#/api/iot/product/product';
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { $t } from '#/locales';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import { DataDefinition } from './modules/components';
import ThingModelForm from './modules/thing-model-form.vue';
import ThingModelTsl from './modules/thing-model-tsl.vue';
import Form from './modules/form.vue';
import Tsl from './modules/tsl.vue';
defineOptions({ name: 'IoTThingModel' });
const props = defineProps<{
productId: number;
}>();
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
const productId = computed(() => product?.value?.id);
const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品信息
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
provide(IOT_PROVIDE_KEY.PRODUCT, product); // 提供产品信息给子组件
const [TslModal, tslModalApi] = useVbenModal({
connectedComponent: Tsl,
destroyOnClose: true,
});
// TODO @haohaoform 是不是用 web-antd/src/views/system/user/index.vue 里 open 的风格;
const thingModelFormRef = ref();
// TODO @haohaothingModelTSLRef 应该是个 modal也可以调整下风格
const thingModelTSLRef = ref();
// TODO @haohao方法的顺序、注释、调整的和别的模块一致。
// 新增功能
function handleCreate() {
thingModelFormRef.value?.open('create');
}
// 编辑功能
function handleEdit(row: any) {
thingModelFormRef.value?.open('update', row.id);
}
// 删除功能
async function handleDelete(row: any) {
// TODO @haohao应该有个 loading类似别的模块写法
try {
await deleteThingModel(row.id);
message.success('删除成功');
gridApi.reload();
} catch (error) {
console.error('删除失败:', error);
}
}
// 打开 TSL
function handleOpenTSL() {
thingModelTSLRef.value?.open();
}
// 获取数据类型标签
// TODO @haohao可以直接在 data.ts 就写掉这个逻辑;
function getDataTypeLabel(row: any) {
return getDataTypeOptionsLabel(row.property?.dataType) || '-';
}
// 刷新表格
/** 刷新表格 */
function handleRefresh() {
gridApi.reload();
gridApi.query();
}
// 获取产品信息
async function getProductData() {
/** 新增物模型 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑物模型 */
function handleEdit(row: ThingModelData) {
formModalApi.setData({ id: row.id }).open();
}
/** 删除物模型 */
async function handleDelete(row: ThingModelData) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
product.value = await getProduct(props.productId);
} catch (error) {
console.error('获取产品信息失败:', error);
await deleteThingModel(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
}
// TODO @haohao字段的顺序调整成别的模块一直
/** 打开 TSL 弹窗 */
function handleOpenTsl() {
tslModalApi.open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }: any, formValues: any) => {
query: async ({ page }, formValues) => {
return await getThingModelPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
productId: props.productId,
productId: productId.value,
...formValues,
});
},
@@ -108,64 +99,55 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true,
},
},
formOptions: {
schema: useGridFormSchema(),
},
});
// 初始化
onMounted(async () => {
await getProductData();
});
</script>
<template>
<Page auto-content-height>
<Grid>
<FormModal @success="handleRefresh" />
<TslModal />
<Grid table-title="物模型列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: '添加功能',
label: $t('ui.actionTitle.create', ['物模型']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['iot:thing-model:create'],
onClick: handleCreate,
},
{
label: 'TSL',
type: 'default',
color: 'success', // TODO @haohao貌似 color 可以去掉应该是不生效的哈。ps另外也给搞个 icon
onClick: handleOpenTSL,
type: 'primary',
auth: ['iot:thing-model:query'],
onClick: handleOpenTsl,
},
]"
/>
</template>
<!-- 数据类型列 -->
<template #dataType="{ row }">
<span>{{ getDataTypeLabel(row) }}</span>
</template>
<!-- 数据定义列 -->
<!-- TODO @haohao可以在 data.ts 就写掉这个逻辑 -->
<template #dataDefinition="{ row }">
<DataDefinition :data="row" />
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '编辑',
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['iot:thing-model:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '删除',
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['iot:thing-model:delete'],
popConfirm: {
title: '确认删除该功能吗?',
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
@@ -173,10 +155,5 @@ onMounted(async () => {
/>
</template>
</Grid>
<!-- 物模型表单 -->
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
<!-- TSL 弹窗 -->
<ThingModelTsl ref="thingModelTSLRef" />
</Page>
</template>

View File

@@ -1,4 +1,3 @@
<!-- TODO @haohao如果是模块内用的就用 modules 等后面点在看优先级 -->
<script lang="ts" setup>
import type { ThingModelData } from '#/api/iot/thingmodel';
@@ -13,83 +12,63 @@ import {
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
/** 数据定义展示组件 */
defineOptions({ name: 'DataDefinition' });
const NUMBER_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT,
]);
const PLACEHOLDER_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
IoTDataSpecsDataTypeEnum.DATE,
]);
const LIST_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.BOOL,
IoTDataSpecsDataTypeEnum.ENUM,
]);
const props = defineProps<{ data: ThingModelData }>();
const formattedDataSpecsList = computed(() => {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
if (!props.data.property?.dataSpecsList?.length) {
return '';
}
return props.data.property.dataSpecsList
.map((item) => `${item.value}-${item.name}`)
.join('、');
}); // 格式化布尔值和枚举值列表为字符串
});
const shortText = computed(() => {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
const list = props.data.property?.dataSpecsList;
if (!list?.length) {
return '-';
}
const first = props.data.property.dataSpecsList[0];
const count = props.data.property.dataSpecsList.length;
return count > 1
? `${first.value}-${first.name}${count}`
const first = list[0];
return list.length > 1
? `${first.value}-${first.name}${list.length}`
: `${first.value}-${first.name}`;
}); // 显示的简短文本(第一个值)
});
</script>
<template>
<!-- 属性 -->
<template v-if="Number(data.type) === IoTThingModelTypeEnum.PROPERTY">
<!-- 非列表型数值 -->
<div
v-if="
[
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT,
].includes(data.property?.dataType as any)
"
>
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
<div v-if="NUMBER_TYPES.has(data.property?.dataType as any)">
取值范围{{
`${data.property?.dataSpecs?.min}~${data.property?.dataSpecs?.max}`
}}
</div>
<!-- 非列表型:文本 -->
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property?.dataType">
<div v-if="data.property?.dataType === IoTDataSpecsDataTypeEnum.TEXT">
数据长度{{ data.property?.dataSpecs?.length }}
</div>
<!-- 列表型: 数组、结构、时间(特殊) -->
<div
v-if="
[
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
IoTDataSpecsDataTypeEnum.DATE,
].includes(data.property?.dataType as any)
"
>
-
</div>
<!-- 列表型: 布尔值、枚举 -->
<div
v-if="
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
data.property?.dataType as any,
)
"
>
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
<Tooltip :title="formattedDataSpecsList" placement="topLeft">
<span class="data-specs-text">
<span
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
>
{{
IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
? '布尔值'
: '枚举值'
}}{{ shortText }}
@@ -98,25 +77,12 @@ const shortText = computed(() => {
</div>
</template>
<!-- 服务 -->
<div v-if="Number(data.type) === IoTThingModelTypeEnum.SERVICE">
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE">
调用方式
{{ getThingModelServiceCallTypeLabel(data.service?.callType as any) }}
</div>
<!-- 事件 -->
<div v-if="Number(data.type) === IoTThingModelTypeEnum.EVENT">
<div v-if="data.type === IoTThingModelTypeEnum.EVENT">
事件类型{{ getEventTypeLabel(data.event?.type as any) }}
</div>
</template>
<style lang="scss" scoped>
/** TODO @haohaotindwind */
.data-specs-text {
cursor: help;
border-bottom: 1px dashed #d9d9d9;
&:hover {
color: #1890ff;
border-bottom-color: #1890ff;
}
}
</style>

View File

@@ -0,0 +1,71 @@
<!-- dataTypearray 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import {
getDataTypeOptions,
IoTDataSpecsDataTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelStructDataSpecs from './struct.vue';
/** 数组元素禁止选择的类型 */
const EXCLUDED_CHILD_TYPES = new Set<string>([
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.DATE,
]);
const childDataTypeOptions = getDataTypeOptions().filter(
(item) => !EXCLUDED_CHILD_TYPES.has(item.value),
);
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
function handleChange(val: any) {
if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
return;
}
dataSpecs.value.dataSpecsList = [];
}
</script>
<template>
<Form.Item
:name="['property', 'dataSpecs', 'childDataType']"
:rules="ThingModelFormRules.childDataType"
label="元素类型"
>
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
<Radio
v-for="item in childDataTypeOptions"
:key="item.value"
:value="item.value"
class="w-1/3"
>
{{ `${item.value}(${item.label})` }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
:name="['property', 'dataSpecs', 'size']"
:rules="ThingModelFormRules.size"
label="元素个数"
>
<Input
v-model:value="dataSpecs.size"
placeholder="请输入数组中的元素个数"
/>
</Form.Item>
<!-- Struct 型配置-->
<ThingModelStructDataSpecs
v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
v-model="dataSpecs.dataSpecsList"
/>
</template>

View File

@@ -0,0 +1,131 @@
<!-- dataTypeenum 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue';
import { buildIdentifierLikeNameValidator } from '#/api/iot/thingmodel';
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const validateEnumName = buildIdentifierLikeNameValidator('枚举描述');
/** 添加枚举项 */
function addEnum() {
dataSpecsList.value.push({ name: '', value: '' } as any);
}
/** 删除枚举项 */
function deleteEnum(index: number) {
if (dataSpecsList.value.length === 1) {
message.warning('至少需要一个枚举项');
return;
}
dataSpecsList.value.splice(index, 1);
}
/** 校验单项枚举值:必填、数字、不重复 */
function validateEnumValue(_rule: any, value: any, callback: any) {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
}
if (Number.isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'));
return;
}
const sameCount = dataSpecsList.value.filter((it) => it.value === value)
.length;
if (sameCount > 1) {
callback(new Error('枚举值不能重复'));
return;
}
callback();
}
/** 校验整个枚举列表:非空、无空项、无非法数字、无重复 */
function validateEnumList(_rule: any, _value: any, callback: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'));
return;
}
const hasEmpty = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name),
);
if (hasEmpty) {
callback(new Error('存在未填写的枚举值或描述'));
return;
}
const hasInvalidNumber = dataSpecsList.value.some((item) =>
Number.isNaN(Number(item.value)),
);
if (hasInvalidNumber) {
callback(new Error('存在非数字的枚举值'));
return;
}
const values = dataSpecsList.value.map((item) => item.value);
if (new Set(values).size !== values.length) {
callback(new Error('存在重复的枚举值'));
return;
}
callback();
}
</script>
<template>
<Form.Item
:rules="[{ validator: validateEnumList, trigger: 'change' }]"
label="枚举项"
>
<div class="flex flex-col">
<div class="flex items-center">
<span class="flex-1"> 参数值 </span>
<span class="flex-1"> 参数描述 </span>
</div>
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-[5px] flex items-center justify-between"
>
<Form.Item
:name="['property', 'dataSpecsList', index, 'value']"
:rules="[
{ required: true, message: '枚举值不能为空', trigger: 'blur' },
{ validator: validateEnumValue, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model:value="item.value" placeholder="请输入枚举值如「0」" />
</Form.Item>
<span class="mx-2">~</span>
<Form.Item
:name="['property', 'dataSpecsList', index, 'name']"
:rules="[
{ required: true, message: '枚举描述不能为空', trigger: 'blur' },
{ validator: validateEnumName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
</Form.Item>
<Button class="ml-2.5" type="link" @click="deleteEnum(index)">
删除
</Button>
</div>
<Button type="link" @click="addEnum">+ 添加枚举项</Button>
</div>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,4 @@
export { default as ThingModelArrayDataSpecs } from './array.vue';
export { default as ThingModelEnumDataSpecs } from './enum.vue';
export { default as ThingModelNumberDataSpecs } from './number.vue';
export { default as ThingModelStructDataSpecs } from './struct.vue';

View File

@@ -0,0 +1,77 @@
<!-- dataTypenumber 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { DataSpecsNumberData } from '#/api/iot/thingmodel';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(
props,
'modelValue',
emits,
) as Ref<DataSpecsNumberData>;
/** 单位下拉变化时,拆出 unitName 与 unit 回写 */
function unitChange(unitSpecs: any) {
if (!unitSpecs) {
return;
}
const [unitName, unit] = String(unitSpecs).split('-');
dataSpecs.value.unitName = unitName;
dataSpecs.value.unit = unit;
}
</script>
<template>
<Form.Item label="取值范围">
<div class="flex items-center justify-between">
<div class="flex-1">
<Input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
</div>
<span class="mx-2">~</span>
<div class="flex-1">
<Input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
</div>
</div>
</Form.Item>
<Form.Item label="步长">
<Input v-model:value="dataSpecs.step" placeholder="请输入步长" />
</Form.Item>
<Form.Item label="单位">
<Select
:model-value="
dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : ''
"
show-search
placeholder="请选择单位"
class="w-full"
@change="unitChange"
>
<Select.Option
v-for="(item, index) in getDictOptions(
DICT_TYPE.IOT_THING_MODEL_UNIT,
'string',
)"
:key="index"
:value="`${item.label}-${item.value}`"
>
{{ `${item.label}-${item.value}` }}
</Select.Option>
</Select>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

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