1183 Commits

Author SHA1 Message Date
李琦
6db736a6f4 feat:
1. 更新框架
2. 修复框架更新后导致的问题
3. 消息系统升级
2026-08-11 13:30:21 +08:00
李琦
b82754e36b feat:
1. 更新框架
2. 修复框架更新后导致的问题
3. 消息系统升级
2026-08-10 20:08:50 +08:00
李琦
f0a33a628b feat:
1. 更新框架
2. 修复框架更新后导致的问题
2026-08-10 18:44:39 +08:00
李琦
3547c53f89 feat:
1. 更新框架
2026-08-10 17:56:36 +08:00
李琦
82b10bd122 Merge remote-tracking branch 'vben/main' into vben-dev
# Conflicts:
#	.gitignore
#	.husky/pre-commit
#	.vscode/extensions.json
#	.vscode/settings.json
#	README.ja-JP.md
#	README.md
#	README.zh-CN.md
#	apps/backend-mock/api/auth/codes.ts
#	apps/backend-mock/api/auth/login.post.ts
#	apps/backend-mock/api/auth/logout.post.ts
#	apps/backend-mock/api/auth/refresh.post.ts
#	apps/backend-mock/api/menu/all.ts
#	apps/backend-mock/api/status.ts
#	apps/backend-mock/api/table/list.ts
#	apps/backend-mock/api/test.get.ts
#	apps/backend-mock/api/test.post.ts
#	apps/backend-mock/api/user/info.ts
#	apps/backend-mock/middleware/1.api.ts
#	apps/backend-mock/nitro.config.ts
#	apps/backend-mock/package.json
#	apps/backend-mock/routes/[...].ts
#	apps/backend-mock/tsconfig.json
#	apps/backend-mock/utils/cookie-utils.ts
#	apps/backend-mock/utils/jwt-utils.ts
#	apps/backend-mock/utils/mock-data.ts
#	apps/backend-mock/utils/response.ts
#	apps/web-antd/.env
#	apps/web-antd/src/adapter/component/index.ts
#	apps/web-antd/src/adapter/form.ts
#	apps/web-antd/src/api/request.ts
#	apps/web-antd/src/layouts/auth.vue
#	apps/web-antd/src/layouts/basic.vue
#	apps/web-antd/src/preferences.ts
#	apps/web-antd/src/router/guard.ts
#	apps/web-antd/src/router/routes/modules/demos.ts
#	apps/web-antd/src/store/auth.ts
#	apps/web-antd/src/views/demos/antd/index.vue
#	apps/web-antdv-next/vite.config.ts
#	apps/web-ele/.env
#	apps/web-ele/index.html
#	apps/web-ele/package.json
#	apps/web-ele/src/adapter/component/index.ts
#	apps/web-ele/src/adapter/form.ts
#	apps/web-ele/src/adapter/vxe-table.ts
#	apps/web-ele/src/api/request.ts
#	apps/web-ele/src/bootstrap.ts
#	apps/web-ele/src/layouts/auth.vue
#	apps/web-ele/src/layouts/basic.vue
#	apps/web-ele/src/locales/index.ts
#	apps/web-ele/src/locales/langs/en-US/demos.json
#	apps/web-ele/src/locales/langs/en-US/page.json
#	apps/web-ele/src/locales/langs/zh-CN/demos.json
#	apps/web-ele/src/locales/langs/zh-CN/page.json
#	apps/web-ele/src/preferences.ts
#	apps/web-ele/src/router/guard.ts
#	apps/web-ele/src/router/routes/core.ts
#	apps/web-ele/src/router/routes/modules/dashboard.ts
#	apps/web-ele/src/router/routes/modules/demos.ts
#	apps/web-ele/src/router/routes/modules/vben.ts
#	apps/web-ele/src/store/auth.ts
#	apps/web-ele/src/views/_core/authentication/code-login.vue
#	apps/web-ele/src/views/_core/authentication/forget-password.vue
#	apps/web-ele/src/views/_core/authentication/register.vue
#	apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
#	apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue
#	apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
#	apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue
#	apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue
#	apps/web-ele/src/views/dashboard/analytics/index.vue
#	apps/web-ele/src/views/demos/element/index.vue
#	apps/web-ele/src/views/demos/form/basic.vue
#	apps/web-ele/tsconfig.json
#	apps/web-ele/tsconfig.node.json
#	apps/web-naive/.env
#	apps/web-naive/index.html
#	apps/web-naive/package.json
#	apps/web-naive/src/adapter/component/index.ts
#	apps/web-naive/src/adapter/form.ts
#	apps/web-naive/src/adapter/vxe-table.ts
#	apps/web-naive/src/api/request.ts
#	apps/web-naive/src/bootstrap.ts
#	apps/web-naive/src/layouts/auth.vue
#	apps/web-naive/src/layouts/basic.vue
#	apps/web-naive/src/locales/index.ts
#	apps/web-naive/src/locales/langs/en-US/demos.json
#	apps/web-naive/src/locales/langs/en-US/page.json
#	apps/web-naive/src/locales/langs/zh-CN/demos.json
#	apps/web-naive/src/locales/langs/zh-CN/page.json
#	apps/web-naive/src/preferences.ts
#	apps/web-naive/src/router/guard.ts
#	apps/web-naive/src/router/routes/core.ts
#	apps/web-naive/src/router/routes/modules/dashboard.ts
#	apps/web-naive/src/router/routes/modules/demos.ts
#	apps/web-naive/src/router/routes/modules/vben.ts
#	apps/web-naive/src/store/auth.ts
#	apps/web-naive/src/views/_core/authentication/code-login.vue
#	apps/web-naive/src/views/_core/authentication/forget-password.vue
#	apps/web-naive/src/views/_core/authentication/register.vue
#	apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue
#	apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue
#	apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
#	apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue
#	apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue
#	apps/web-naive/src/views/dashboard/analytics/index.vue
#	apps/web-naive/src/views/demos/form/basic.vue
#	apps/web-naive/src/views/demos/naive/index.vue
#	apps/web-naive/tsconfig.json
#	apps/web-naive/tsconfig.node.json
#	apps/web-naive/vite.config.ts
#	docs/.vitepress/components/demo-preview.vue
#	docs/.vitepress/components/preview-group.vue
#	docs/.vitepress/config/en.mts
#	docs/.vitepress/config/index.mts
#	docs/.vitepress/config/plugins/demo-preview.ts
#	docs/.vitepress/config/shared.mts
#	docs/.vitepress/config/zh.mts
#	docs/.vitepress/theme/components/site-layout.vue
#	docs/.vitepress/theme/components/vben-contributors.vue
#	docs/.vitepress/theme/styles/variables.css
#	docs/package.json
#	docs/src/_env/adapter/component.ts
#	docs/src/_env/adapter/form.ts
#	docs/src/_env/adapter/vxe-table.ts
#	docs/src/commercial/community.md
#	docs/src/components/common-ui/vben-count-to-animator.md
#	docs/src/components/common-ui/vben-drawer.md
#	docs/src/components/common-ui/vben-form.md
#	docs/src/components/common-ui/vben-modal.md
#	docs/src/components/common-ui/vben-vxe-table.md
#	docs/src/components/layout-ui/page.md
#	docs/src/demos/vben-drawer/auto-height/drawer.vue
#	docs/src/demos/vben-drawer/basic/index.vue
#	docs/src/demos/vben-drawer/dynamic/index.vue
#	docs/src/demos/vben-drawer/shared-data/drawer.vue
#	docs/src/demos/vben-drawer/shared-data/index.vue
#	docs/src/demos/vben-form/api/index.vue
#	docs/src/demos/vben-form/basic/index.vue
#	docs/src/demos/vben-form/custom/index.vue
#	docs/src/demos/vben-form/dynamic/index.vue
#	docs/src/demos/vben-form/query/index.vue
#	docs/src/demos/vben-form/rules/index.vue
#	docs/src/demos/vben-modal/auto-height/modal.vue
#	docs/src/demos/vben-modal/basic/index.vue
#	docs/src/demos/vben-modal/dynamic/index.vue
#	docs/src/demos/vben-modal/shared-data/index.vue
#	docs/src/demos/vben-modal/shared-data/modal.vue
#	docs/src/demos/vben-vxe-table/basic/index.vue
#	docs/src/demos/vben-vxe-table/custom-cell/index.vue
#	docs/src/demos/vben-vxe-table/edit-row/index.vue
#	docs/src/demos/vben-vxe-table/fixed/index.vue
#	docs/src/demos/vben-vxe-table/form/index.vue
#	docs/src/demos/vben-vxe-table/remote/index.vue
#	docs/src/demos/vben-vxe-table/tree/index.vue
#	docs/src/demos/vben-vxe-table/virtual/index.vue
#	docs/src/en/guide/essentials/development.md
#	docs/src/en/guide/essentials/external-module.md
#	docs/src/en/guide/essentials/route.md
#	docs/src/en/guide/essentials/server.md
#	docs/src/en/guide/essentials/settings.md
#	docs/src/en/guide/in-depth/access.md
#	docs/src/en/guide/in-depth/loading.md
#	docs/src/en/guide/in-depth/locale.md
#	docs/src/en/guide/in-depth/theme.md
#	docs/src/en/guide/introduction/quick-start.md
#	docs/src/en/guide/introduction/thin.md
#	docs/src/en/guide/introduction/vben.md
#	docs/src/en/guide/other/faq.md
#	docs/src/en/guide/project/dir.md
#	docs/src/en/guide/project/standard.md
#	docs/src/en/guide/project/tailwindcss.md
#	docs/src/en/index.md
#	docs/src/friend-links/index.md
#	docs/src/guide/essentials/build.md
#	docs/src/guide/essentials/development.md
#	docs/src/guide/essentials/external-module.md
#	docs/src/guide/essentials/route.md
#	docs/src/guide/essentials/server.md
#	docs/src/guide/essentials/settings.md
#	docs/src/guide/in-depth/access.md
#	docs/src/guide/in-depth/check-updates.md
#	docs/src/guide/in-depth/loading.md
#	docs/src/guide/in-depth/locale.md
#	docs/src/guide/in-depth/theme.md
#	docs/src/guide/introduction/quick-start.md
#	docs/src/guide/introduction/thin.md
#	docs/src/guide/introduction/vben.md
#	docs/src/guide/introduction/why.md
#	docs/src/guide/other/faq.md
#	docs/src/guide/project/dir.md
#	docs/src/guide/project/standard.md
#	docs/src/guide/project/tailwindcss.md
#	docs/src/index.md
#	docs/tsconfig.json
#	package.json
#	packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue
#	packages/constants/src/core.ts
#	packages/effects/common-ui/src/components/index.ts
#	packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue
#	packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue
#	packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue
#	packages/effects/layouts/src/authentication/authentication.vue
#	packages/effects/layouts/src/basic/layout.vue
#	packages/effects/layouts/src/widgets/notification/notification.vue
#	packages/effects/layouts/src/widgets/notification/types.ts
#	packages/icons/src/iconify/index.ts
#	packages/icons/src/svg/index.ts
#	packages/stores/src/modules/user.ts
#	playground/src/adapter/component/index.ts
#	playground/src/store/auth.ts
#	pnpm-lock.yaml
#	vben-admin.code-workspace
2026-08-10 16:55:17 +08:00
李琦
b1f223554b feat:
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
1. 创建就诊人功能增强
2. 优化交互细节
2026-08-10 15:33:08 +08:00
李琦
871982fc07 feat:
1. 金方
2026-08-09 15:27:27 +08:00
李琦
689a0f64a6 feat:
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
1. 医生线下接诊创建就诊人、患者端扫码认领就诊人
2. 生成处方后医生代付码
2026-08-09 14:36:02 +08:00
李琦
335b3535d5 feat: 金方相关
feat: 患者信息身份证改成非必填
2026-08-08 18:36:26 +08:00
李琦
c4e833a1c4 feat: 金方相关 2026-08-08 14:53:02 +08:00
李琦
7697af2619 feat: 修复部分功能(健康信息、VIP、医生端健康信息导入) 2026-08-08 11:24:46 +08:00
李琦
396a6e5b8b feat: 修复部分功能(健康信息、VIP、医生端健康信息导入) 2026-08-08 10:33:49 +08:00
李琦
7a504433da fix: 接诊UI 2026-08-07 20:24:49 +08:00
李琦
33dcd3f5d8 fix: 接诊UI 2026-08-07 14:37:23 +08:00
李琦
efd7e8ffc8 feat: 药品别名 2026-08-07 14:25:19 +08:00
李琦
5b9f3ec3ed feat: 修复配送仓库的报备字段 2026-08-07 13:24:29 +08:00
李琦
e434aa9534 feat: 修复配送仓库的报备字段 2026-08-07 13:22:58 +08:00
李琦
32eb96b4ad feat: 中药导入模块修复、部门财务的优化、队列的优化管理 2026-08-07 13:17:07 +08:00
李琦
9f37802ba7 feat: 中药导入模块修复、部门财务的优化、队列的优化管理 2026-08-07 09:48:07 +08:00
JyQAQ
e8d35a7471 fix(@vben/styles): 修复弹窗内 tooltip 层级被遮挡问题 (#8240)
将 antd-tooltip 的 z-index 提升至 2001,避免在 Modal/弹窗等高 z-index 容器内的 tooltip 被遮挡
2026-08-07 06:08:20 +08:00
yshbyy
f038166681 chore: 更新.gitattributes文件配置 (#8178)
git 不支持 Shell 的花括号展开语法
2026-08-07 05:16:35 +08:00
dependabot[bot]
88cc837f6a build(deps): bump actions/stale from 10 to 11 (#8227)
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 05:15:36 +08:00
金毛88
82d790a6ab Merge branch 'fork/dream-weave/fix/popup-route-reentry' 2026-08-07 05:14:52 +08:00
金毛88
9c3ba53990 Merge branch 'fork/ming4762/fix-transition-20260805' 2026-08-07 05:11:42 +08:00
金毛88
cb707c8c78 Merge branch 'fork/LayHuts/fix/lint-format' 2026-08-07 05:09:58 +08:00
李琦
cfc8390573 feat: 中药导入模块修复、部门财务的优化 2026-08-06 20:15:42 +08:00
李琦
174693f6db feat: 病历模块优化、AI辅助问诊、接入deepseek 2026-08-06 17:56:28 +08:00
Dream
98761dce98 fix(@vben-core/popup-ui): stabilize content popups across routes
Mount in-content drawers and modals to the persistent main content element.

Initialize drawer open state when mounted after the API opens.

Fixes vbenjs/vue-vben-admin#8210
2026-08-06 10:17:14 +08:00
李琦
c1bc234509 feat: 病历模块优化、AI辅助问诊 2026-08-06 10:08:02 +08:00
李琦
dc9f9c5c13 feat: 病历模块优化、AI辅助问诊 2026-08-06 08:14:33 +08:00
zhongming4762
d3dc081b5d perf: 优化路由动画切换 1、切换出现双页面问题 2、切换出现横向和纵向滚动条问题 2026-08-05 19:14:12 +08:00
李琦
0331325929 feat: 病历模块优化 2026-08-05 17:54:17 +08:00
李琦
9c629ff8ee feat: 病历模块优化 2026-08-05 17:50:32 +08:00
李琦
bd5b0b2de1 feat: 病历模块优化 2026-08-05 16:22:46 +08:00
layhuts
2c1e643ab9 fix: 调整lint执行顺序
使用核心 no-nested-ternary 规则替代 Unicorn 自动修复,避免嵌套三元表达式被错误加括号
调整 oxlint、oxfmt、eslint 和 stylelint 的执行顺序,保证commit与format流程一致
2026-08-05 11:10:21 +08:00
李琦
029b45d0bb feat: VIP功能绑定、VIP病历模块、字典模块、传方按钮移动到顶部导航栏 2026-08-05 10:45:58 +08:00
Dream
0a47dc8a3d fix(@vben/playground): reset popup form state 2026-08-05 10:24:28 +08:00
Dream
d0c8c887fd docs: document typed popup data contracts 2026-08-05 09:54:01 +08:00
Dream
f16c780c2b refactor(@vben/web-naive): type modal shared data 2026-08-05 09:52:28 +08:00
Dream
ebe0eadee4 feat(@vben/playground): add typed popup data examples 2026-08-05 09:50:55 +08:00
Dream
3cbb1e8411 refactor(@vben/playground): type popup data consumers 2026-08-05 09:48:27 +08:00
Dream
21d9132703 feat(@vben-core/popup-ui)!: add typed popup data contracts
Bind shared data to modal and drawer APIs and infer it from exposed connected components.

BREAKING CHANGE: getData<T>() is replaced by getData().
Shared data defaults to unknown, and an unset payload returns undefined instead
of an empty object.
2026-08-05 09:46:37 +08:00
李琦
212b16a8ec feat: VIP模块 2026-08-04 14:53:11 +08:00
李琦
feebf4b14e feat: 1. 数脉api鉴别身份证。 2. 后台+微信登陆优化 2026-08-04 13:28:43 +08:00
李琦
3bc22d02e6 feat:诊所问题优化,气泡卡片优化,修复了导入药品的时候无法打开开方组件并且切换tab的问题 2026-08-01 08:04:31 +08:00
dream-weave
9f5b1cd9fb fix(@vben-core/form-ui)!: group field slot component props (#8215)
* refactor(@vben-core/form-ui)!: group field slot component props

* fix(@vben-core/form-ui): stabilize grouped field slot bindings

Resolve function-based common component props with field context.

Preserve internal model and event bindings when custom field slots spread componentProps.

Add regression coverage for array contexts, disabled precedence, and stale binding isolation.

* feat(@vben/vite-config): warn about field slot migration

Print the temporary field-slot migration warning when the development server starts.

Inject the same message into the browser console and keep the plugin serve-only.

* docs(@vben/docs): document field slot migration

Place the breaking field-slot notice at the start of both form documents.

Explain old and new bindings, development warnings, and planned notice removal.

* feat(@vben/playground): extend custom form example

Add a switchable input and select field to demonstrate schema component updates.

Update the composite phone field to emit new tuple values so validation observes changes.
2026-07-31 18:13:49 +08:00
dream-weave
32495e40b2 fix(@vben-core/popup-ui): rebind popup api after HMR (#8217)
* fix(@vben-core/popup-ui): rebind popup api after hmr

* fix(@vben-core/popup-ui): preserve options after remount
2026-07-31 06:13:10 +08:00
dream-weave
cb52807b27 fix(@vben-core/form-ui): correct field slot props (#8216) 2026-07-31 06:07:52 +08:00
李琦
371594bfda feat:诊所问题优化,气泡卡片优化,修复了导入药品的时候无法打开开方组件并且切换tab的问题 2026-07-30 07:37:51 +08:00
MistyMoon
418c16e093 fix(@vben/locales): 修复锁屏弹窗按钮文案歧义 (#8211)
锁屏设置弹窗(lock-screen-modal)的确认按钮作用是输入密码后锁定屏幕,
但按钮文案 i18n key `ui.widgets.lockScreen.screenButton` 原值为「解锁」/「Unlock」,
与弹窗标题「锁定屏幕」及实际动作矛盾,存在歧义。

- zh-CN: 「解锁」->「锁定」
- en-US: 「Unlock」->「Lock」

(同段另有 `unlock` key 值「点击解锁」,用于已锁定后的全屏解锁界面,语义正确,保持不变)
2026-07-30 07:12:08 +08:00
李琦
52a1cb1ce5 feat:诊所问题优化,气泡卡片优化,修复了导入药品的时候无法打开开方组件并且切换tab的问题
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
2026-07-28 16:53:31 +08:00
JyQAQ
b04d728249 fix(@vben-core/form-ui): 兼容 zod v4 的 ZodArray,避免误用 unwrap() 丢失数组外壳导致数组校验失败 (#8207) 2026-07-28 15:50:15 +08:00
李琦
5506c8917f feat:诊所问题优化,气泡卡片优化,修复了导入药品的时候无法打开开方组件并且切换tab的问题 2026-07-28 14:23:50 +08:00
李琦
05885ed405 Merge remote-tracking branch 'origin/dev/message-oa' into feature/invoice
# Conflicts:
#	apps/web-antd/src/views/system/system-config/index.vue
2026-07-28 08:25:45 +08:00
李琦
c8045e1da8 feat:优化了模态框的大小
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
2026-07-28 07:59:10 +08:00
JyQAQ
44483db9af fix(project): 修复弹窗中表单上传组件预览图片层级问题 (#8205)
将 PreviewGroupComponent 的 hidden class 替换为通过 classes.popup.root
设置更高 z-index,避免在弹窗内被遮挡
2026-07-27 22:37:33 +08:00
layhuts
fbf0cebf4b style: 优化 vxe-table 类型组织结构,将 viewed-row 功能域相关类型进行模块化拆分,由全局类型声明调整为功能目录内聚管理,避免公共 types 文件持续膨胀。 (#8204) 2026-07-27 22:36:04 +08:00
金毛88
98344dfcec Merge branch 'fork/dream-weave/fix-layout-scroll-container' 2026-07-25 20:37:20 +08:00
Dream
6d5ccc7ac8 fix(@vben/layouts): scope hash targets to layout 2026-07-24 17:32:51 +08:00
Dream
b9c6d56f74 fix(@vben/layouts): restore internal scroll navigation 2026-07-24 16:58:39 +08:00
Dream
aaf0da582c fix(@vben-core/layout-ui): stabilize internal layout scrolling 2026-07-24 16:57:32 +08:00
Dream
9a5d052fa5 fix(@vben-core/composables): lock layout scroll container 2026-07-24 16:56:35 +08:00
Dream
baea741432 feat(@vben-core/shared): add layout scroll helpers 2026-07-24 16:55:36 +08:00
xingyu4j
33f4522bff fix: 补充 i18n 缺失的英文 2026-07-24 15:26:08 +08:00
xingyu4j
cb6b0a8872 fix: constrain popper content to available viewport width 2026-07-24 15:21:07 +08:00
xingyu4j
57e2a54219 fix: add missing Symbol descriptions 2026-07-24 15:17:55 +08:00
xingyu4j
4db254cd21 fix: Tailwindcss v4 new variable shorthand syntax 2026-07-24 15:07:17 +08:00
xingyu4j
255a76d79a feat: canonical tailwind class 2026-07-24 15:04:10 +08:00
dream-weave
dcdbfe5dfc fix(@vben-core/form-ui): restore lock screen form submission (#8195)
* fix(@vben-core/form-ui): avoid stale form context access

* fix(@vben/playground): constrain date range codec fields

* test(@vben-core/form-ui): harden benchmark fixtures

* docs(@vben/docs): document form runtime and benchmarks

* fix(@vben/docs): 明确 getValues/submit 返回值及 submit 参数类型
2026-07-24 10:05:27 +08:00
李琦
14414bf9b0 feat:优化了门店部门财务功能,新增开票功能 2026-07-24 09:54:44 +08:00
xingyu
0542b509ca chore: bump versions (#8192) 2026-07-23 20:41:13 +08:00
金毛88
eaf585d03a Merge branch 'fork/dream-weave/update-zod' 2026-07-23 20:35:00 +08:00
xingyu4j
0a45cb41fb fix: 添加 StyleProvider 以支持 Tailwind 样式覆盖;移除 InputNumber 组件的宽度样式,优化组件适配;antdv 暂不支持 2026-07-23 17:50:22 +08:00
Dream
517a42ec05 perf(@vben-core/form-ui): benchmark form hot paths 2026-07-23 17:47:57 +08:00
Dream
bacfdf5e42 perf(@vben-core/form-ui): optimize array field updates 2026-07-23 17:47:57 +08:00
Dream
2e69c3fefc fix(@vben/playground): address form codec edge cases 2026-07-23 17:47:57 +08:00
Dream
43b78da024 refactor(@vben/playground): share date range codecs 2026-07-23 17:47:57 +08:00
Dream
decbed20bc fix(@vben-core/form-ui): harden codec value boundaries 2026-07-23 17:47:57 +08:00
Dream
1ac5ac3e82 fix(@vben-core/form-ui): preserve array field focus 2026-07-23 17:47:57 +08:00
zhb410181
8d5a419c50 Merge branch 'vbenjs:main' into main 2026-07-23 15:25:16 +08:00
xingyu
2d82505330 fix: dropdown menu responsiveness and improve dialog handling (#8187)
* fix: 修复用户下拉框小部件菜单行点击无响应及弹框闪退

- 主题/时区/全屏/通知菜单行补充 @select 处理器,点击行任意位置即可触发
- 时区设置弹框宿主移出 DropdownMenu(showButton 模式),修复弹框随下拉框关闭而闪退
- 通知组件暴露 toggle 供下拉框调用
- 偏好设置小部件文案精简,positionMap 顺序调整

* feat: draggable-list.vue 直接复用 SelectItem,用法和 tabbar.vue 完全一致
2026-07-23 15:14:25 +08:00
dream-weave
98dea92225 feat(@vben-core/form-ui): add typed form value codecs (#8189)
* feat(@vben-core/form-ui): add typed form value codecs

* test(@vben-core/form-ui): cover codec value boundaries

* refactor(project): propagate form codec generics

* feat(@vben/plugins): propagate form codecs through vxe grids

* refactor(@vben/playground): migrate form codec examples

* refactor(@vben/playground): migrate query forms to codecs

* docs(@vben/docs): document form value codecs
2026-07-23 15:09:31 +08:00
hiss
e6a23479ee fix: 修复锁定屏幕和退出登录小部件,位置在 user-dropdown 时,使用快捷键唤出两个模态窗 2026-07-23 14:44:32 +08:00
lq
777737d063 feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互 2026-07-23 14:15:17 +08:00
zhb410181
294e9f2966 Merge branch 'vbenjs:main' into main 2026-07-23 13:44:17 +08:00
Harold Zhang
f43de252b5 fix: fixed the icon defect which cause to langauge button can not be clicked (#8184) 2026-07-23 12:20:38 +08:00
zhb410181
ebc0f23e64 Merge branch 'vbenjs:main' into main 2026-07-22 20:35:55 +08:00
dream-weave
28757fb9c8 refactor(@vben-core/form-ui): migrate to Zod 4 and TanStack Form (#8176)
* build(@vben-core/form-ui): update form validation dependencies

* refactor(@vben-core/form-ui): replace vee-validate with TanStack Form

* test(@vben-core/form-ui): cover TanStack Form migration

* docs(@vben/docs): document Zod 4 form migration

* refactor(project): update form adapters

* refactor(project): migrate application form consumers

* refactor(@vben/playground): migrate form examples

* docs(@vben/docs): fix validate return type and required rule empty check in form docs

* fix(@vben-core/form-ui): resolve oxlint and eslint errors

- rewrite nested ternary expressions as if/else in form-field-array.vue and form-runtime.ts
- sort @tanstack/vue-form before @vben-core/composables in package.json

* chore: fix merge artifacts and formatting

- move dependencies before devDependencies in root package.json
- update preferences snapshot for widget positioning fields
- format form-array demo README

* refactor(@vben-core/form-ui): optimize form runtime and value flow

- add fine-grained field selectors and atomic dependency resolution
- expose raw and formatted value snapshots with focused regression coverage

* fix(@vben/layouts): dispose sortable instance on unmount

* docs(@vben/docs): document form runtime API changes

- list added, changed, removed, and deprecated form APIs
- document atomic dependencies and raw versus formatted values
2026-07-22 19:47:07 +08:00
zhb410181
81f69d797d Merge branch 'vbenjs:main' into main 2026-07-22 18:06:57 +08:00
xingyu
6b6708bcf2 fix: 修复点击退出图标确认后不能退出 && 图标排序等数组字段需顺序敏感比较 (#8179)
* chore: 调整 sortablejs 的位置,只在用到的模块引入

* fix: non-null assertion lint error

* fix: 修复点击退出图标确认后不能退出

* fix: 图标排序等数组字段需顺序敏感比较

* feat: 配置中增加 refresh 并统一位置
2026-07-22 17:44:10 +08:00
zhb410181
5c1d31f680 Merge branch 'vbenjs:main' into main 2026-07-22 14:59:16 +08:00
金毛88
899910547f Merge branch 'fork/xingyu4j/deps' 2026-07-22 09:55:03 +08:00
金毛88
70a1d0a2e0 Merge branch 'fork/maoxuner/fix/system-menu-form' 2026-07-21 23:05:33 +08:00
MistyMoon
24eb67224d fix: message/notification demos not auto-dismissing due to wrong duration unit (#8168)
duration was written as 2500 (intended as ms), but ant-design-vue's
message/notification duration is in seconds, so it was interpreted as
2500s (~41min), making the popups appear to never close.
Changed to 2.5 with a comment documenting the unit.

Closes #8144
2026-07-21 22:57:17 +08:00
Jin Mao
e4e25028b5 Merge branch 'main' into fix/system-menu-form 2026-07-21 22:53:06 +08:00
金毛88
ef27b4e349 Merge branch 'fork/maoxuner/fix/lang-doc' 2026-07-21 22:48:41 +08:00
金毛88
b21563a89b feat: 顶栏与用户下拉框按钮样式统一与 tooltip 修复
- 修复 VbenTooltip 的 :content prop 类型错误,改用默认插槽
- 修复 VbenIconButton 的 onClick prop 支持数组合并
- VbenFullScreen 新增 tooltip prop,支持直接设置提示
- 统一顶栏与用户下拉框所有按钮样式,使用 VbenIconButton 包装
- 移除 i18n 中多余的"启用"字样(全局搜索/语言切换)
- 修复 VbenPopover PopoverTrigger 嵌套 button 导致 tooltip 不显示

Co-Authored-By: AtomCode (GLM-5.2) <noreply@atomgit.com>
2026-07-21 22:36:42 +08:00
zhb410181
d06451cdb7 Merge branch 'vbenjs:main' into main 2026-07-21 16:31:51 +08:00
lq
2b13a19731 feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互 2026-07-21 14:01:41 +08:00
lq
4224130ad4 feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互 2026-07-21 14:01:21 +08:00
xingyu4j
5318774efe fix: 优化命令执行逻辑,使用串行执行以提高错误可见性 2026-07-20 18:50:49 +08:00
xingyu4j
68a2717a26 fix: test error 2026-07-20 18:48:20 +08:00
xingyu4j
0a2c01e453 fix: 添加 oxlint 禁用注释以避免嵌套三元运算符警告 2026-07-20 18:45:38 +08:00
xingyu4j
b330f08ef4 fix: lint 2026-07-20 18:36:48 +08:00
xingyu4j
6676d962e9 fix: 修复Vben用户信息中的 homePath 属性定义 2026-07-20 18:36:37 +08:00
xingyu4j
dd8307dfe5 chore: update dependencies and refactor command execution 2026-07-20 18:36:09 +08:00
xingyu4j
d0c327e037 chore: update pnpm v11.15.1 2026-07-20 17:40:54 +08:00
xingyu4j
901a147abb Merge branch 'main' of https://github.com/xingyu4j/vue-vben-admin into deps 2026-07-20 17:39:20 +08:00
lq
2e7bc80a11 feat: 钉钉、企业微信webbook 2026-07-20 15:05:34 +08:00
陆伯言
86e68bdde3 docs: 同步后端接入文档示例菜单地址
提升示例菜单数据的整体一致性,避免多处信息不一致的情况
2026-07-20 01:03:58 +08:00
陆伯言
054deae3bb fix: 菜单组件mock数据与实际保持一致 2026-07-20 01:03:58 +08:00
陆伯言
772286fb73 fix: 路由地址不强制以斜杠开头
兼容新dashboard路由配置
2026-07-20 00:46:27 +08:00
陆伯言
5936ff2e0f fix: 系统菜单管理上级菜单无法显示名称的问题
antdv-next TreeSelect组件没有title插槽
2026-07-20 00:20:00 +08:00
金毛88
f840d235ef feat: 偏好设置小部件支持位置选择与拖拽排序
- 顶栏各小部件按钮支持选择位置(header/user-dropdown/none),偏好设置按钮额外保留 auto/fixed 智能兜底,移动端/全屏内容模式下自动切 fixed 浮动保命,避免偏好入口死锁
- draggable-list.vue 的 listRef 未在模板绑定,Sortable 永不初始化导致小部件无法拖拽;加 ref 绑定与 data-key 让 onEnd 从 DOM 读真实新顺序
- preferences.ts 的 updatePreferences 用 defu(merge) 合并数组,对 widget.order 是 concat 语义,每次拖拽触发更新都会把当前 order 追加一次,localStorage 里膨胀到上百条;改用 mergeWithArrayOverride 整体替换,加载缓存时 sanitizeCachedArray 去重已存在的脏数据
- preferences 按钮之前在 widget.order 里因 labelMap 未登记显示成裸字符串,且 widgetChecks 缺 refresh 导致刷新按钮不按 order 渲染;统一 preferences 与其它 widget 的拖拽排序与位置 select,refresh 补进 rightSlots.widgetChecks 并补模板渲染
- 补 sortablejs 依赖、WidgetPreferences.order 类型字段、preferences-drawer.vue 的 widgetOrder 绑定
2026-07-19 21:34:11 +08:00
陆伯言
930597c605 docs: 更新新增语言包的说明
closed #8160
2026-07-19 02:27:09 +08:00
陆伯言
41c20a43af docs: 更正语言使用示例,确保可用
使用可用的语言配置
修复computed变量使用语法错误

closed #8159
2026-07-19 01:22:00 +08:00
陆伯言
fe65d548be docs: 修复英文语言文件路径错误
closed #8158
2026-07-19 01:17:22 +08:00
金毛88
0cd87c170f feat: 顶栏按钮支持位置选择(header/user-dropdown/none)
为顶栏所有部件按钮添加独立的位置配置,支持在顶栏、用户下拉菜单、不显示
三种选项间切换,每个按钮可独立设置。

- 新增 7 个位置偏好字段:globalSearchButtonPosition、
  themeToggleButtonPosition、languageToggleButtonPosition、
  fullscreenButtonPosition、notificationButtonPosition、
  refreshButtonPosition、timezoneButtonPosition
- 合并 lockScreenButtonPosition、logoutButtonPosition
- header.vue 按位置条件过滤按钮显示
- user-dropdown.vue 新增对应菜单项,支持在用户下拉菜单中显示
- 偏好设置界面仅保留位置选择器(不再需要独立的开关)
- 更新中英文国际化文案

Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
2026-07-18 22:46:31 +08:00
金毛88
b71ebbd673 Merge remote-tracking branch 'pr-8019-temp/main' 2026-07-18 19:21:36 +08:00
金毛88
31d3bd18f3 Merge branch 'fork/LayHuts/feature/form-field-arry' 2026-07-18 19:18:14 +08:00
Jin Mao
eae6771554 Merge branch 'main' into feature/fix_defects_router 2026-07-18 03:54:49 +08:00
JyQAQ
8ac66a96bc fix: 修复弹窗中使用裁剪图片会出现层级问题 (#8156)
* fix(@vben/playground): 弹窗中使用裁剪图片会出现层级问题

* fix(project): 修复弹窗中裁剪图片在 web-antd 与 web-antdv-next 的层级问题
2026-07-17 15:17:37 +08:00
zhb410181
822cec0f5c Merge branch 'vbenjs:main' into main 2026-07-16 13:13:38 +08:00
Guillermo Sagar
6910c691f7 修复动画导致的白屏Bug (#8148)
首次页面加载正常,但是当通过页面点击切换路由的时候白屏,无法正常渲染子页面

 Vue Transition + KeepAlive 空白页 bug**: `<Transition mode="out-in">` + 条件渲染会导致路由切换空白页。`mode="out-in"` 要求旧元素完成 leave 动画后新元素才进入,但路由切换时条件渲染可能在动画前就移除了旧元素,导致 `<Transition>` 永远等待一个不存在的元素 → 死锁。**解决方案**: 移除 `mode="out-in"`(保留 Transition 动画),或在 `apps/web-antd/src/preferences.ts` 设置 `transition: { enable: false }` 关闭动画走 `v-else` 分支。
2026-07-16 09:35:25 +08:00
zhb410181
71c30d3e18 Merge branch 'vbenjs:main' into main 2026-07-14 14:07:09 +08:00
Harold Zhang
263d8e5b52 Merge branch 'main' into feature/fix_defects_router 2026-07-14 09:47:34 +08:00
Harold Zhang
b79b82ecf0 feat: fixed the preference will hide all other button theme/timezone/lanague etc (#8147) 2026-07-13 21:31:06 +08:00
xingyu4j
91a2e80be8 fix: 优化代码格式,合并类型声明 2026-07-13 18:30:19 +08:00
xingyu4j
819ddd4ebb fix(style): 添加no-invalid-position-declaration规则到stylelint配置 2026-07-13 18:29:30 +08:00
xingyu4j
bb6403d513 chore: update deps 2026-07-13 18:29:01 +08:00
xingyu4j
45c3776ffd chore: update pnpm v11.12.0 2026-07-13 18:27:57 +08:00
zhb410181
1d23cd707c Merge branch 'vbenjs:main' into main 2026-07-13 11:22:06 +08:00
Harold Zhang
6eb79b8ef0 Merge branch 'main' into feature/fix_defects_router 2026-07-13 08:59:16 +08:00
zouawen
8b7c245bc7 fix: 修复首选项导航栏宽度问题 (#8136) 2026-07-11 11:43:47 +08:00
Harold Zhang
e416de3290 fix: fixed defects of router 2026-07-10 10:15:40 +08:00
zhb410181
398b445d35 Merge branch 'vbenjs:main' into main 2026-07-09 15:41:03 +08:00
luoqiz
14d03a1c2d fix: 统一BasicUserInfo (#8133) 2026-07-09 13:23:29 +08:00
zhb410181
9769c11de5 fix(style): 修复偏好设置功能tab栏宽度未自适应问题 (#8132) 2026-07-09 13:23:08 +08:00
hiss
cae47e8c7f fix(style): 修复偏好设置功能tab栏宽度未自适应问题 2026-07-08 16:42:04 +08:00
陆伯言
88918aad10 docs: fix typo of env demo (#8129)
closed #8125
2026-07-08 08:38:06 +08:00
陆伯言
75c3a7214b docs: update default home path perferences (#8121)
closed #8117
2026-07-07 21:41:51 +08:00
陆伯言
71bcabd118 docs: update nitro mock server url (#8119)
closed 8113
2026-07-03 17:12:43 +08:00
dream-weave
81619b6090 fix(lint): align eslint and oxlint rules (#8118)
* docs: 路由文档示例代码同步路由组织新规范

* fix(lint): align eslint and oxlint rules

* fix(lint): avoid control character regex
2026-07-03 17:12:13 +08:00
dream-weave
77468ce9a6 docs: 路由文档示例代码同步路由组织新规范 (#8114) 2026-07-02 18:35:54 +08:00
Evan
8779c00670 fix: 修复homePath的路径 (#8110)
* fix: 修复homePath的路径

* fix: 单元测试数据

* chore: 单元测试快照数据

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-07-01 17:31:37 +08:00
金毛88
61f1b53325 fix: 修改默认首页,以适配router重构 2026-07-01 17:02:55 +08:00
layhuts
c56bb7d320 docs: 更新文档 2026-07-01 15:07:56 +08:00
金毛88
1490a93f47 Merge branch 'fork/bnuephjx/refactor/router-redirect' 2026-07-01 13:31:27 +08:00
金毛88
e50296e3c5 Merge branch 'fork/zouawen/main' 2026-07-01 13:06:42 +08:00
陆伯言
5da38ccded fix: 调整mock默认路由 (#8108)
* fix: 调整mock默认路由

1. starting改为running 语义更明确
2. API地址修复
3. 列表顺序调整,登录接口靠前

* docs: 替换axios调用示例

原先/api/user接口地址不对,且是受认证保护的接口,响应异常容易造成误解
2026-07-01 13:02:44 +08:00
layhuts
20b69fa480 revert: 撤销修改 2026-07-01 11:49:27 +08:00
layhuts
fdf2abb702 refactor: 修改form-field-array支持dependencies 2026-07-01 11:08:35 +08:00
layhuts
9d36b34c4c docs: 修复错误的注释说明 2026-07-01 09:52:51 +08:00
dependabot[bot]
9b356d731d build(deps): bump actions/checkout from 6 to 7 (#8080)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 22:42:58 +08:00
Evan
18e2bbf969 Merge branch 'main' into refactor/router-redirect 2026-06-30 22:23:54 +08:00
layhuts
14bb94245c feat: 新增logo铺满显示和是否显示text配置 (#8102)
* feat: 新增logo铺满显示和是否显示text配置

* feat: logo.enable=false时隐藏logo区域占位;设置logoMode、showText为必填。

* style: 修复注释问题

* fix: 删除多余属性
2026-06-30 21:50:45 +08:00
Evan
feb0265744 Merge branch 'main' into refactor/router-redirect 2026-06-30 14:26:23 +08:00
xueyitt
7182405d24 fix: 修复弹框自定义宽度 class 被默认 max-w 覆盖的问题 (#8100)
将 modalClass 参数移至 cn() 末尾,确保用户通过 class 传入的宽度样式
优先于内置默认的 max-w-[calc(100vw-20px)] 生效,使 业务使用Modal 上的自定义的max-w-[50%] 等方式能正确控制弹框宽度。
2026-06-30 14:24:18 +08:00
陆伯言
d7d6d2e337 fix: lint issue from a8c3729 (#8098) 2026-06-30 14:23:52 +08:00
Harold Zhang
31c31b914b feat: fix check error (#8094) 2026-06-30 14:23:27 +08:00
Evan
f00a881295 refactor(@vben/access): 重构路由的重定向逻辑
@vben-core/shared
@vben/playground
@vben/backend-mock
@vben/web-antd
@vben/web-antdv-next
@vben/web-ele
@vben/naive
@vben/tdesign
2026-06-30 13:36:37 +08:00
zoumingjun
5ed867eb95 feat: 优化禁用的开关组件也可以查看help帮助信息 2026-06-30 09:39:32 +08:00
zoumingjun
ae5bd3afc5 feat: 首选项导航栏选项标题过长时显示省略号,且增加聚焦时文字提示 2026-06-30 09:37:28 +08:00
vben
a8c3729850 feat: add ICP filing number to copyright across app 2026-06-24 10:04:10 +08:00
gotounix
794edd419e fix: add includes in mock tsconfig (#8085) 2026-06-23 21:47:08 +08:00
layhuts
9a284ee385 style: 处理modal设置class="md:w-5/12"格式时Fullscreen时无法最大化问题 (#8084) 2026-06-23 21:46:43 +08:00
gotounix
e031457271 fix: add missing imports (#8082) 2026-06-23 21:46:19 +08:00
金毛88
0931599f96 chore: update deps 2026-06-21 14:48:51 +08:00
xingyu4j
dc3865f778 fix: 优化 lint 执行策略并修复 pre-commit 依赖解析
- vsh lint 按硬件自适应:CPU<=4 核串行(汇总错误),>4 核并行;
  CPU>4 且空闲内存>8GB 时 oxfmt/oxlint 默认线程数提升至 4,否则维持 2
- lefthook pre-commit 改为串行执行,避免与 check:type 并行导致低配机资源飙升
- pre-commit 仅对暂存文件 lint 并自动修复回填(stage_fixed)
- lint 命令统一加 pnpm exec 前缀,修复 lefthook 经 sh 调用时
  oxlint 找不到 workspace 局部依赖(eslint-plugin-eslint-comments)的问题
2026-06-18 16:35:27 +08:00
yshbyy
45399d0299 fix(@vben-core/shadcn-ui): 补全 ScrollArea onScroll 滚动事件透传 (#8069) 2026-06-18 16:10:21 +08:00
xingyu
dc3466d77a refactor: 将 unicorn / TypeScript 规则迁移至 oxlint,并完善 oxfmt 配置 (#8067)
* chore: update deps

* chore: update archiver v8

* fix: 还原 vue/html-closing-bracket-newline

* feat: 增加默认配置和配置注释

* feat: 增加oxlint默认插件配置和注释

* refactor: remove unicorn ESLint configuration and update dependencies

- Deleted the unicorn ESLint configuration file from the lint-configs package.
- Removed unicorn configuration from the main index file.
- Updated oxlint-config to include a comprehensive set of unicorn rules.
- Adjusted preferences and vxe-table files to disable specific unicorn rules.
- Removed references to eslint-plugin-unicorn from pnpm-lock.yaml and pnpm-workspace.yaml.

* refactor: 移除 eslint 的 @typescript-eslint/eslint-plugin 依赖,将 TypeScript 配置迁移到 oxlint

* feat: open typeAware

* fix: mac 和linux下报错
2026-06-18 16:10:01 +08:00
金毛88
65885ea74b chore: update lockfile 2026-06-16 16:47:33 +08:00
金毛88
769791351f chore: update deps && lint 2026-06-16 16:25:21 +08:00
金毛88
741cbb3794 Merge branch 'fork/xueyitt/feat/feat3' 2026-06-16 16:16:56 +08:00
金毛88
edd110ac4e Merge branch 'fork/xueyitt/feat/feat1' 2026-06-16 16:15:33 +08:00
xueyitt
7824219557 fix: 修正AppPreferences中defaultAvatar、dynamicTitle注释 (#8060) 2026-06-16 14:23:25 +08:00
雪忆天堂
dd4afec7ed feat: workbench公共组件增加支持slot方式进行自定义 2026-06-16 10:01:24 +08:00
雪忆天堂
ebebb42625 feat: 1、FieldMappingTime拆解独立FieldMappingTimeItem+集合形式,方便单独对象定义;
2、表单submitOnChange增加changeDebouncedTime防抖处理,允许自定义防抖周期;
3、handleValuesChange事件set增加cloneDeep,防止对象值污染;
2026-06-16 09:48:27 +08:00
雪忆天堂
d1e6d4a3da Merge remote-tracking branch 'origin/main' 2026-06-16 09:10:50 +08:00
Jefsky Wong
ad3f996bdb fix(@vben/common-ui): prevent footer slot from overlapping content when Page contains Grid (#8056)
The Page component's outer container used min-h-full, which doesn't
provide an explicit height for flex children. Combined with h-full on
the content div, this caused the footer slot to overlap table content
(Grid/vxe-table) because the content area didn't properly account for
footer space in the flex column layout.

Fix:
1. Change outer container from min-h-full to h-full — gives the flex
   container an explicit height so percentage heights resolve correctly.
2. Change content area from static h-full to flex-1 when autoContentHeight
   is false — lets the flex layout naturally distribute remaining space
   between content and footer.

Closes #5494

Co-authored-by: jefsky <jefsky@users.noreply.github.com>
2026-06-16 08:36:39 +08:00
HaroldZhangCode91
68877a7e97 Merge branch 'main' into feature/fix_confirm_regression 2026-06-15 17:46:19 +08:00
雪忆天堂
c5a839f652 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	packages/effects/plugins/src/vxe-table/use-vxe-grid.vue
2026-06-15 17:41:12 +08:00
HaroldZhangCode91
bf8c3d6411 feat: aligns ESLint with oxfmt's behavior (#8054) 2026-06-15 17:25:04 +08:00
HaroldZhangCode91
f753a3b75f Merge branch 'main' into feature/fix_confirm_regression 2026-06-15 16:45:08 +08:00
曾兴顺
fcf726f953 fix: 修复Tree组件不支持半选的问题(子节点全选时父节点选中图标为勾选状态,子节点部分选中时父节点图标为减号,也就是版勾选状态,子节点未被选中时,父节点不被选中) (#8050) 2026-06-15 16:38:21 +08:00
Harold Zhang
16ebc67796 feat: fixed useScrollLock() can not be unlock 2026-06-15 16:34:53 +08:00
Harold Zhang
6b109c9ae6 fix: fixed regression of confirm 2026-06-15 16:24:17 +08:00
xingyu
160114c5e0 fix: select and user-dropdown (#8042)
* fix: 偏好设置中下拉框无法选择的问题,原因: 同步 shadcn-vue 将 z-popup 同步为 shadcn 的 z-50

* fix: 鼠标hover到用户登录头像 下拉来回闪烁 隐藏 显示
2026-06-10 21:26:47 +08:00
金毛88
4b1fada0ab Merge branch 'fork/zhb410181/fix/style' 2026-06-09 11:22:58 +08:00
hiss
5c9daab2ff fix(style): 1.修复ToggleGroupItem边框样式问题 2.修复用户头像下拉框Badge背景颜色 2026-06-09 08:56:07 +08:00
Harold Zhang
c2a85f4ec2 feat: support unlocking scroll when modal dialog closes 2026-06-08 14:15:31 +08:00
abc_abc
e7f541f37e Merge branch 'main' into main 2026-06-08 10:31:39 +08:00
金毛88
f23c9f5e19 chore: update deps 2026-06-07 18:54:43 +08:00
ddfabc
4459389d4a feat: 将锁定屏幕和退出登录图标从用户下拉菜单移至头部栏
将锁定屏幕(LockKeyhole)和退出登录(LogOut)图标按钮从 UserDropdown 下拉菜单
移至 LayoutHeader 头部栏,位于通知图标的右侧。

变更内容:
- header.vue: 新增 lock-screen-btn 和 logout-btn 插槽,位于 notification 之后;
  添加 LockScreenModal/LogoutModal 及快捷键(Alt+L/Alt+Q)支持;
  新增 avatar/text props 用于锁屏弹窗显示
- layout.vue: 透传 avatar/text props 和 logout 事件
- user-dropdown.vue: 移除锁定屏幕和退出登录的菜单项、弹窗及快捷键逻辑
- 所有 5 个应用 basic.vue: 向 BasicLayout 传递 avatar/text,绑定 @logout 事件

头部右侧图标顺序: ... → 全屏 → 通知🔔 → 锁定🔒 → 退出🚪 → 用户头像
2026-06-06 23:24:54 +08:00
xingyu4j
8e0564cc42 chore: node v24 2026-06-06 21:07:55 +08:00
xingyu4j
017b3c3fce ci: bump actions/stale from 9 to 10 2026-06-06 20:49:34 +08:00
xingyu4j
126275b417 ci: bump dessant/lock-threads from 5 to 6 2026-06-06 20:48:51 +08:00
xingyu4j
bc908e29eb ci: bump amannn/action-semantic-pull-request from 5 to 6 2026-06-06 20:48:19 +08:00
xingyu4j
efe3c25310 chore: update deps 2026-06-06 19:36:39 +08:00
xingyu4j
d85ac2c976 feat: form 表单赋值 2026-06-06 19:33:31 +08:00
xingyu4j
c816591b69 fix: modal 下拉等不能选中 2026-06-06 19:32:31 +08:00
xingyu4j
564b349700 fix: vue/custom-event-name-casing 2026-06-06 16:21:25 +08:00
xingyu4j
ce495d67a0 feat: 同步 antdv-next 的组件 2026-06-06 16:20:36 +08:00
xingyu4j
8094d1ffb9 chore: update deps 2026-06-06 16:19:32 +08:00
afe1
748b182e44 fix: workflow (#8013)
* fix: catelog

* fix: system

* chore: sync upstream changes

* feat:  component and workflow error
2026-06-05 05:13:08 +08:00
金毛88
6544340d80 Merge branch 'fork/HaroldZhangCode91/feature/fix_freeze_scrollbar_of_modal' 2026-06-04 13:54:21 +08:00
Harold Zhang
6b7cb7b1fc feat: fixed the regression of changing Menu to MenuUI 2026-06-04 11:46:53 +08:00
Harold Zhang
d90d45a128 fix: fixed modal freeze issue 2026-06-04 11:19:38 +08:00
Harold Zhang
66077b3bcf Merge branch 'main' into feature/fix_freeze_scrollbar_of_modal 2026-06-03 18:07:37 +08:00
Harold Zhang
0328594747 feat: fixed the overlay layer can not work with reka-ui 2026-06-03 17:52:41 +08:00
金毛88
790e114d41 chore: 固定vite 8.0.10,避免编译后异常 2026-06-03 17:32:13 +08:00
Harold Zhang
cc60ca9592 feat: fixed the defects the scrollbar disapear when modal close 2026-06-03 16:10:17 +08:00
HaroldZhangCode91
79707a2ebd feat: fixed regression of modal for element plus (#7999) 2026-06-03 13:27:12 +08:00
金毛88
0eb72ca4fd feat: add useTDesignDesignTokens 2026-06-03 13:25:23 +08:00
金毛88
7fd4b1de04 Merge branch 'fork/SaleriHQ/feat/form-field-array' 2026-06-03 13:22:21 +08:00
Saleri
9ffd42f013 chore: add changeset for form field array 2026-05-31 23:06:00 +08:00
Saleri
40101911f0 style: format naive styles 2026-05-31 16:21:13 +08:00
xingyu
04fbb7a556 chore: 升级 shadcn-vue 组件到v4最新版 (#7972)
* fix: useStore is deprecated

* chore: update deps

* feat: 升级shadcn-ui v4

* fix: workbench style

* feat: 升级shadcn-ui v4 step2

* feat: 升级shadcn-ui v4 step3

* chore: 升级shadcn v4

* fix: pagination

* fix: dark style

* fix: doc import

* feat: 增加详情组件,参考 antdv-next

* docs: descriptions docs

* docs: Browser Support

* feat: add table action

* feat: icon use vbenIcon

* fix: type error

* fix: dropdown popConfirm

* feat: 使用默认的文字交互

* feat: 优化渲染性能
2026-05-31 15:18:46 +08:00
Saleri
e5f9106caa feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray 2026-05-31 15:03:55 +08:00
afe1
108d7ff335 chore: add commitlint config declaration (#7968)
* fix: catelog

* fix: system

* chore: sync upstream changes

* types: add commitlint config declaration
2026-05-28 15:50:47 +08:00
leo
1d6842fa69 build: change runtime config asset naming strategy for PWA compatibility (#7963)
replace query-based config url (_app.config.js?v=...)
with deterministic hashed filename (_app-config-v{version}-{hash}.js)
ensure service worker precache matches runtime injected asset
2026-05-28 12:53:51 +08:00
Saleri
d6a9eaf9e9 fix: 修复 web-naive 表单页 Input 输入 _ 不显示的问题 (#7962) (#7965)
* fix(@vben/web-naive): 修复 naive input 输入 _ 不显示的问题 (#7962)

* fix(@vben/web-naive): 限制 input 字体修复作用域,避免全局影响
2026-05-28 12:49:48 +08:00
boisduval
f7540c119b fix: 修复 useVbenModal useVbenDrawer 传入参数失效的问题 (#7964)
- 把provide的key值修改为与inject一致的options
2026-05-28 12:49:23 +08:00
过冬
acf289381e fix(@vben-core/popup-ui): modal 响应式适配,支持视口自适应 (#7951) 2026-05-25 13:27:17 +08:00
PanFu
f813245827 feat: 偏好设置的快捷键列表追加ESC快捷键的控制(关闭当前窗口) (#7947)
* feat: 快捷键追加ESC控制,关闭当前窗口

* feat: 偏好设置中,页面切换动画的颜色看不清的问题(使用当前主题色)

* feat: 三种弹出框支持快捷键ESC动作

* feat: 代码自动格式化(3个框架改动)

* feat: 代码自动格式化(3个框架改动)

* fix: 修正locale数据获取方式

* 单元测试问题修改

* 单元测试问题修改

* fix: 解决代码评论的问题

* fix: 解决代码评论的问题

* fix: 解决代码评论的问题

* fix: 解决代码评论的问题

* 单元测试问题修改

* fix: 解决评论问题

* fix: 解决代码格式导致pnpm run lint报错的问题

---------

Co-authored-by: PanFu <panfu@zhihuaai.com>
2026-05-23 09:50:35 +08:00
过冬
e98f0b7558 fix: 移动端禁用混合布局菜单拆分 (#7949) 2026-05-23 04:21:19 +08:00
Reese Wellin
cd86de54e1 fix(preferences): 修复主题等个性化信息持久化被初始化配置覆盖 (#7948)
调整 initPreferences 中 merge 顺序,使用户缓存优先于 initialPreferences。

Fixes #7943
2026-05-23 04:17:46 +08:00
Jin Mao
60eb1335f8 chore: vite 降级到8.0.10 2026-05-22 13:20:25 +08:00
xingyu
f2b3b12553 chore: pnpm v11 (#7942)
* chore: remove rolldown dep

* fix: issues-helper and maintain-one-comment are temporarily disabled due to security issues

* chore: update deps

* fix: change error type to TypeError for non-browser environment check in LocalStorageDriver

* chore: update pnpm v11
2026-05-22 12:02:14 +08:00
layhuts
c066889c37 Fix: 在isMobile=true时无法自定义Drawer的class问题 (#7941)
* fix: 在isMobile=true时无法自定义Drawer的class问题

* fix: 在ResizableHandle两边增加边距
2026-05-22 11:56:09 +08:00
928646789
6b81d1b0d5 fix: preferences-button click to show setting drawer (#7938) 2026-05-22 07:26:12 +08:00
afe1
126b33687a chore: vite config tsconfig include (#7936)
* fix: catelog

* fix: system

* fix(vite-config): include tsdown config in tsconfig
2026-05-22 07:25:50 +08:00
Jin Mao
5b67c2e740 chore: 更新pnpm-lock 2026-05-21 20:44:38 +08:00
Jin Mao
2d7c3c3072 chore: 更新pnpm-lock 2026-05-21 20:40:49 +08:00
Jin Mao
5cc68c9dd0 chore: 更新 vite 依赖版本
- 将 vite 版本从 ^8.0.13 降级到 ^8.0.10
- 保持其他依赖包版本不变
2026-05-21 20:22:28 +08:00
Jin Mao
63a38dce49 refactor(workflow): 优化发布工作流配置
- 简化触发条件,移除冗余的推送事件配置
- 更新标签创建输入参数描述,明确格式要求
- 移除 Husky 环境变量配置
- 重命名构建作业为发布作业,提高语义清晰度
- 移除 Node.js 版本矩阵策略配置
- 优化版本提取逻辑,支持工作流调度和推送事件
- 统一标签前缀处理,确保版本格式一致性
- 移除注释掉的冗余步骤配置
- 简化发布配置,移除不必要的发布操作注释块
2026-05-21 09:07:59 +08:00
Jin Mao
629f747795 feat(workflow): 添加手动触发release标签工作流
- 添加 workflow_dispatch 触发器支持手动运行
- 新增 tag 输入参数用于指定创建的标签
- 设置默认标签值为 staging
- 保留原有的 push tags 自动触发功能
2026-05-21 08:54:51 +08:00
Jin Mao
8585008f79 chore: release v5.7.0 2026-05-20 17:45:44 +08:00
xingyu
00990d9453 Merge branch 'main' into fix/lint 2026-05-20 17:38:19 +08:00
PanFu
2a84b590b5 feat: 偏好设置按钮,支持在右上角用户的下拉框中展示;偏好设置中支持设置时区(多一个入口); (#7931)
* feat: 偏好设置按钮支持显示在用户下拉窗中的特性

* feat: 时区设置功能支持在偏好设置列表进行配置

* feat: 偏好设置按钮支持显示在用户下拉窗中的特性 - 完善示例代码

* feat: 时区设置功能支持在偏好设置列表进行配置

* feat: 时区设置功能支持在偏好设置列表进行配置-单元测试报错问题

* feat: 时区设置功能支持在偏好设置列表进行配置-修改代码QC问题

---------

Co-authored-by: PanFu <panfu@zhihuaai.com>
2026-05-20 17:24:53 +08:00
PanFu
769c970e08 fix: 修复三个问题:偏好配置项修改后重启不会生效;锁屏弹出框的按钮文字错误;右上角偏好设置按钮排序问题优化; (#7930)
* fix: 修正偏好设置加载缓存的偏好设置并与初始配置合并的顺序

* fix: 修正锁屏弹出框的按钮文字

* fix: 偏好设置按钮排序优化(取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引)

* fix: 修正偏好设置加载缓存的偏好设置并与初始配置合并的顺序 - 代码QC问题修改

---------

Co-authored-by: PanFu <panfu@zhihuaai.com>
2026-05-20 17:24:18 +08:00
xingyu4j
4d3c481a93 fix: 当lint检查出问题后,不能在终端终止 2026-05-20 16:39:03 +08:00
xingyu4j
fda4b39c01 chore: update deps 2026-05-20 16:38:19 +08:00
xingyu4j
74d8942f39 chore: remove Unused deps 2026-05-20 16:24:59 +08:00
xingyu4j
9ec98f0846 refactor: replace depcheck with knip for dependency checking
Migrate vsh check-dep from depcheck to knip, reducing
315 transitive dependencies. Config is inlined in
DEFAULT_CONFIG, matching the check-circular pattern.
Also re-export CollapsibleParams from @vben/common-ui.
2026-05-20 16:14:21 +08:00
xingyu4j
f71094e878 refactor: migrate json-viewer from vue-json-viewer to vue-json-pretty
- Replace vue-json-viewer with vue-json-pretty (actively maintained, Vue 3 native)
- Map original props to vue-json-pretty API in bindProps for backward compatibility
- Implement copy functionality via renderNodeActions slot with i18n support
- Update style.scss from .jv-* to .vjs-* class names
2026-05-20 14:39:17 +08:00
xingyu4j
9badda3d11 chore: update deps 2026-05-20 11:30:03 +08:00
xingyu4j
03fcecadcc fix: lefthook config 2026-05-19 16:13:00 +08:00
xingyu4j
3a88adb8a6 feat: test 2026-05-19 16:10:18 +08:00
xingyu4j
c759cb2593 fix: lint config 2026-05-19 16:08:10 +08:00
xingyu4j
f77166b3a2 chore: update deps 2026-05-19 15:59:43 +08:00
xingyu4j
f7928b34b2 fix: lint 2026-05-19 15:58:34 +08:00
xingyu4j
454a5b54bd chore: lefthook config 2026-05-19 15:54:03 +08:00
Jin Mao
a4dd9d30ce chore: release 5.7.0 2026-05-19 14:39:46 +08:00
Jin Mao
76cfcda2e9 Merge branch 'fork/xingyu4j/antdv-next' 2026-05-19 14:14:28 +08:00
橙子
7ec4df4995 fix: search-panel when ArrowUp and ArrowDown (#7922) 2026-05-19 13:40:50 +08:00
leo
d71c81e8ff style: apply vsh lint formatting (#7923) 2026-05-19 13:40:13 +08:00
xingyu4j
aac626da32 feat: add system user view 2026-05-18 21:45:17 +08:00
xingyu4j
5db93a345a fix: doc build error 2026-05-18 20:17:35 +08:00
xingyu4j
7c943cc06b chore: disable oxc typeAware and add --threads option for oxlint
Type-aware linting was consuming excessive memory with nearly all
type-aware rules turned off. Add --threads CLI option to control
oxlint parallelism.
2026-05-18 18:46:13 +08:00
xingyu4j
5c8dd1a6da chore: docs config 2026-05-18 18:35:34 +08:00
xingyu4j
6592135cc5 chore: update vitepress v2 2026-05-18 18:23:10 +08:00
xingyu4j
2f16ee7cf5 docs: use antdv-next 2026-05-18 17:30:59 +08:00
xingyu4j
0c1b737325 fix: fix lint && typecheck 2026-05-18 16:50:14 +08:00
xingyu4j
5bbdcffb97 Merge branch 'main' into antdv-next 2026-05-18 16:38:56 +08:00
xingyu4j
1867015e97 Merge branch 'antdv-next' of https://github.com/xingyu4j/vue-vben-admin into antdv-next 2026-05-18 16:35:28 +08:00
xingyu
b6aeadd9a2 Merge branch 'main' into antdv-next 2026-05-18 16:35:18 +08:00
xingyu4j
1e44b87359 refactor: migrate playground deprecated antdv-next APIs
- Modal: destroyOnClose → destroyOnHidden
- Card: body-style → styles.body
- Alert: message prop/slot → title
- Spin: tip → description, wrapper-class-name → classes.root
2026-05-18 16:35:06 +08:00
xingyu4j
a1081bf7a6 fix: 修复 InputNumber 组件宽度在表单中不占满的问题 2026-05-18 16:28:19 +08:00
xingyu4j
9bb2026b4d fix: 修复 InputNumber 组件宽度在表单中不占满的问题 2026-05-18 16:25:49 +08:00
xingyu4j
4554cd016a fix: lint 2026-05-18 16:19:53 +08:00
xingyu4j
350d5ee60e refactor: playground use antdv-next 2026-05-18 16:18:46 +08:00
xingyu4j
1ec058cbe3 chore: update deps 2026-05-18 15:06:59 +08:00
xingyu4j
eb9cdbab9b fix: lint 2026-05-16 12:55:19 +08:00
xingyu4j
4907f281af chore: 更新 pnpm-workspace.yaml 配置,添加 publicHoistPattern 和 allowBuilds 设置 2026-05-16 11:56:39 +08:00
xingyu4j
aadf9b6e39 chore: update deps 2026-05-16 11:52:20 +08:00
Jin Mao
84e77f64ea Merge remote-tracking branch 'origin/main' 2026-05-16 11:18:52 +08:00
guoqiangui
3e89077d46 fix(deploy): use IMAGE_NAME variable in remove_image instead of hardcoded name (#7907)
The `remove_image` function was using a hardcoded image name `vben-admin-pro`
instead of the `$IMAGE_NAME` variable (`vben-admin-local`), so the old local
image was never actually cleaned up before each build.

Co-authored-by: guoqiangui <guoqiangui@zhongshitech.cn>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:18:09 +08:00
Jin Mao
c8c425538f Merge branch 'fork/xueyitt/main' 2026-05-16 10:49:33 +08:00
Jin Mao
b04c10aab6 Merge branch 'fork/LayHuts/feature/vxe-table-viewed' 2026-05-16 10:48:00 +08:00
JyQAQ
b5f79db321 fix(@vben/plugins): 修复 tiptap 重复注册扩展警告 (#7917)
StarterKit v3.22.0 已默认包含 Document、Link、Underline 扩展,
与单独导入产生重复注册,导致控制台警告:
[tiptap warn]: Duplicate extension names found: ['link', 'doc', 'underline']

- 移除 Document 单独导入和使用,StarterKit 已内置
- 移除 Underline 单独导入和使用,StarterKit 已内置
- StarterKit 配置中添加 link: false,禁用内置 Link,
  保留自定义配置的 Link.configure({...})
2026-05-16 10:45:30 +08:00
PanFu
42d82875ce feat: 1、完善tree组件的全选状态不正确、全选没有label、item内容超长导致复选框对齐错乱、item内容超长没有tips无法看到完整内容的问题 (#7915)
Co-authored-by: PanFu <panfu@zhihuaai.com>
2026-05-16 10:44:55 +08:00
Akuria
4d8d2de6ad fix: guard svg icon loading during docs SSR (#7912) 2026-05-16 10:43:47 +08:00
Akuria
294700a247 fix: skip fixed footer height in auto-content-height calculation (#7910)
* fix: skip fixed footer height in auto-content-height calculation

When the Page component's footer has position: fixed, it is removed
from the normal document flow and should not be subtracted from the
available content height. Previously, the footer's offsetHeight was
always subtracted, causing incorrect height calculation for fixed
footers.

Also reset shouldAutoHeight before recalculating to prevent stale
state on hot reload.

Fixes #4576

* fix: replace getComputedStyle footer height check with footerFixed prop

Use an explicit `footerFixed` boolean prop instead of runtime
getComputedStyle detection to determine whether the footer height
should be excluded from content height calculation. This avoids
unreliable style queries and makes the behavior deterministic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 10:43:10 +08:00
Akuria
f55b18ffd7 fix: update primary color when toggling dark/light mode with custom theme (#7909)
When a custom theme is selected and the user toggles between dark and
light mode, the primary color was not being recalculated. This was caused
by a guard condition in the builtin theme watcher that skipped updating
themeColorPrimary for custom themes during mode changes.

Remove the guard so that the primary color is always recalculated from
the theme preset when the mode changes, ensuring Element Plus CSS
variables stay in sync.

Fixes #6615
2026-05-16 10:42:35 +08:00
Akuria
ca5931e8c4 fix: preserve tree default value when treeData starts empty (#7908)
When treeData is initially an empty array (e.g. before async data
arrives), updateTreeValue() would clear the modelValue because no
matching items could be found in the empty flattened data. This caused
default values to be lost.

Only call updateTreeValue() when flattenData has items, so that
modelValue is preserved until the actual tree data arrives.

Fixes #6522
2026-05-16 10:41:42 +08:00
JyQAQ
aac4e88353 docs(@vben/docs): 添加 VCropper 图片裁剪组件文档 (#7904)
* docs(@vben/docs): 添加 VCropper 图片裁剪组件文档

- 新增中文文档 docs/src/components/common-ui/vben-cropper.md
- 新增英文文档 docs/src/en/components/common-ui/vben-cropper.md
- 新增基础用法示例 demos/vben-cropper/basic
- 新增固定比例裁剪示例 demos/vben-cropper/aspect-ratio
- 更新侧边栏配置添加 Cropper 入口

* fix: 更正跨域图片描述并修复 demo 内存泄漏

- 文档更正:网络图片导出裁剪结果需服务端 CORS 支持
- 修复 URL.createObjectURL 内存泄漏:添加 revokeObjectURL 释放
2026-05-11 12:50:44 +08:00
雪忆天堂
9b54e50510 fix: 修正vxtable不存在formConfig时,直接变更data失效问题 2026-05-11 10:10:34 +08:00
xueyitt
30356a24e6 fix: 修正vxtable不存在formConfig时,直接变更data失效问题
修正vxtable不存在formConfig时,直接变更data失效问题
2026-05-11 10:09:47 +08:00
MistyMoon
ba60bc3c14 fix(@vben/layouts): correct logo theme in semi-dark sidebar (#7785) (#7902) 2026-05-10 10:41:43 +08:00
JyQAQ
f5feddc6c7 feat(@vben/plugins): tiptap 组件新增 maxHeight 属性支持及文档 (#7897)
* feat(@vben/plugins): tiptap 组件新增 maxHeight 属性支持

- 新增 maxHeight prop 支持限制编辑器最大高度
- 编辑器内容区超出最大高度时自动滚动
- 移除 style.css 中未使用的 CSS 规则

* docs(@vben/plugins): 添加 tiptap 富文本编辑器组件文档

- 新增中文文档 docs/src/components/common-ui/vben-tiptap.md
- 新增英文文档 docs/src/en/components/common-ui/vben-tiptap.md
- 新增基础用法示例 demos/vben-tiptap/basic
- 新增图片上传示例 demos/vben-tiptap/image-upload
- 更新文档侧边栏配置

* fix(@vben/plugins): extract URL from response in image upload callback

The upload callback was incorrectly passing the AxiosResponse object
to resolve() instead of extracting the actual image URL string.
This caused the editor to insert [object Object] as image src,
resulting in broken images.

* chore: add changeset for tiptap maxHeight feature

* Revert "chore: add changeset for tiptap maxHeight feature"

This reverts commit a28fc4441f14641f6af6c1a143aa6959591315b2.
2026-05-10 10:38:21 +08:00
Yanghai.Lin
0868deb82b fix: 修复 Input 组件 focus 边框被裁剪问题 (#7894)
* fix(other): 修复 oxlint ignorePatterns 导致 pre-commit hook 失败

* fix(@vben-core/shadcn-ui): 修复 Input 组件 focus 时边框被父容器 overflow hidden 裁剪

* fix(@vben-core/shadcn-ui): 修复 SelectTrigger 组件 focus 时边框被父容器 overflow hidden 裁剪
2026-05-10 10:37:39 +08:00
layhuts
1299acd8f9 fix: 获取已存在的 key,避免重复写入刷新过期时间 2026-05-09 23:27:25 +08:00
layhuts
43717807a4 style: reformat code 2026-05-09 23:18:44 +08:00
layhuts
1b4e126128 fix: 处理localStorageDriver在某些受限浏览器环境下报错导致无法使用 2026-05-09 23:10:05 +08:00
layhuts
d23b246aee fix: 处理localStorage prefix === ''时提醒用户可能会全部删除数据 2026-05-09 23:04:38 +08:00
layhuts
71f2e5f504 fix: 去除onConfirm中的setTimeout 2026-05-09 22:51:39 +08:00
layhuts
4bbd34fab9 fix: 修复 labelField 和 nameField 2026-05-09 22:28:03 +08:00
layhuts
08e4bb40b4 style: reformat code 2026-05-09 18:28:23 +08:00
layhuts
9c49f4bb1e refactor: 优化viewed实例初始化,rowStyle rowClassName 从最新的配置中读取 2026-05-09 18:27:09 +08:00
layhuts
28905b0bec style: reformat code 2026-05-09 14:08:19 +08:00
layhuts
bbcad709ca style: 修改注释 2026-05-09 14:07:30 +08:00
layhuts
a5f0537cb0 fix: 处理viewedKeys由于 FIFO 顺序,显式提供的值可能会被异步存储恢复静默地移除。 2026-05-09 13:38:50 +08:00
layhuts
c78d89f549 fix: 处理非原子性的先清除后写入操作会setKeys带来永久性数据丢失的风险 2026-05-09 13:16:50 +08:00
layhuts
f2a17cbe78 fix: 处理getViewedKeys()返回对内部可变对象的直接引用问题 2026-05-09 13:10:41 +08:00
layhuts
9667232684 fix: 加上 - 分隔符来避免跨前缀误匹配 2026-05-09 12:56:50 +08:00
layhuts
1f584ff0c9 fix: 处理window不可用时降级使用Memory 2026-05-09 12:49:21 +08:00
layhuts
75ed17a1f9 docs: 更正saveToCache所属组件 2026-05-09 12:41:10 +08:00
layhuts
cc6ccaab38 docs: 为符合 Markdown lint 规范,指定代码块语言。 2026-05-09 12:38:13 +08:00
layhuts
0be4b51eaa fix: 解决window在构造函数中进行访问破坏SSR/Node环境。 2026-05-09 12:33:15 +08:00
layhuts
b40822e3ed fix: 解决indexedDB的getDB在失败重试时永远返回错误Promise问题 2026-05-09 12:02:41 +08:00
layhuts
df5cb426d1 fix: 解决indexedDB的setItem、,removeItem以及clear在请求成功时而不是事务完成时触发resolve问题 2026-05-09 11:54:11 +08:00
layhuts
2d36d6b510 style: reformat code 2026-05-09 11:03:47 +08:00
layhuts
88d5661e0c refactor: 重构ViewedRowPersistOptions处理扁平化问题
改成按 type 字段区分的联合类型
2026-05-09 11:02:16 +08:00
layhuts
8ce773f264 feat: 在storage-manager添加keys方法 2026-05-09 10:58:41 +08:00
layhuts
329fa68207 fix: 手动操作行标记示例时动态获取列表前两行 2026-05-09 09:23:10 +08:00
layhuts
e1f6449073 feat: 表格已读行操作标记 2026-05-08 20:03:05 +08:00
layhuts
51e8b27d4c style(@vben-core/shared): formatting 2026-05-08 17:36:47 +08:00
layhuts
e555ee065e feat(@vben-core/shared): 新增indexedDB
在cache下新增indexedDB缓存
2026-05-08 17:34:54 +08:00
Lin
b5dacd992f feat: add time zone option to preference settings (#7871) 2026-05-03 12:16:58 +08:00
Jin Mao
d55f17670a chore: 更新依赖包版本
- 更新 pkg-types 从 2.3.0 到 2.3.1
- 更新 rolldown 从 1.0.0-rc.15 到 1.0.0-rc.17
- 更新 typescript 从 6.0.2 到 6.0.3
- 更新 vite 从 8.0.8 到 8.0.10
2026-05-01 06:55:42 +08:00
Jin Mao
9f20d52b4e chore: 更新 FTP 部署操作版本
- 将 SamKirkland/FTP-Deploy-Action 从 v4.3.6 升级到 v4.4.0
- 所有部署任务中的 FTP 操作均已更新到新版本
- 保持了所有现有配置和凭据不变
2026-05-01 06:53:06 +08:00
Jin Mao
86445a38e4 Merge branch 'fork/jyqwq/feature/富文本支持图片上传' 2026-05-01 06:49:48 +08:00
leo
9a73e961fc fix(@vben/stores): respect base URL when opening route in new window (#7837)
* fix(@vben/stores): respect base URL when opening route in new window

* fix(@vben/stores): respect base URL when opening route in new window
2026-05-01 06:49:05 +08:00
boisduval
c0b2ef980e fix: 修复 IconPicker 在手动输入时表单值不更新的问题 (#7869)
- 修复 IconPicker.vue 在 updateCurrentSelect 时未同步更新 modelValue 的 Bug
- 优化 IconPicker 适配器配置,使其兼容 Element Plus 的 ElInput 组件
2026-04-30 15:46:07 +08:00
mew
e8dc464e79 fix: issue in tab bar chrome height is not full due to wrapper element div (#7867)
Caused by PR #7858
2026-04-30 15:45:44 +08:00
mew
99c38c93c8 feat: refactor context menu to capture native events (#7858)
* feat: refactor context menu to capture native events

prevent context-menu to show in html input fields

* fix: refactor context-menu.vue for improved structure

* chore: fix format

* chore: remove dead code

* chore: fix lint

* fix: update contenteditable selector in context menu

proposed fixed by coderabitai
2026-04-28 13:36:13 +08:00
yuan.ji
244c0a5884 fix(@vben/plugins): 根据代码审查意见修复 tiptap 图片上传
- 提取 findPlaceholderPos 辅助函数,消除重复的 descendants 查找
- 添加 editor.isDestroyed 守卫,防止操作已销毁编辑器
- renderHTML 不输出上传状态属性,防止 blob URL 泄露到序列化 HTML
- uploadImage 命令返回 boolean,添加 Commands 类型增强,移除 as any
- 拖拽/粘贴多图时仅处理第一张并提示仅支持单图上传
- 自定义 extensions 时不传 imageUpload 给工具栏,toolbar action 加运行时守卫
2026-04-27 14:33:30 +08:00
yuan.ji
c6fd599784 feat(@vben/plugins): 补充 tiptap 图片上传类型、工具栏和示例
- 新增 ImageUploadOptions 类型定义和 imageUpload 属性
- 工具栏图片按钮支持上传/URL 双模式
- playground 添加图片上传 mock 示例
2026-04-27 13:42:56 +08:00
yuan.ji
4ca2f1c6e8 feat(@vben/plugins): tiptap 支持图片上传功能
- 新增 imageUpload 配置项,支持自定义上传接口
- 支持文件选择、拖拽、粘贴三种上传方式
- 上传中显示 blob 预览图 + loading spinner / 进度条
- 支持 accept 和 maxSize 文件校验
- 支持 onUploadError 自定义错误处理
- 未配置 imageUpload 时保持原有 URL 插入行为不变
- 使用 NodeView 实现实时 DOM 控制的进度展示
2026-04-27 13:42:36 +08:00
Jin Mao
36d7dc23fa Merge branch 'fork/zilvya/ta1' 2026-04-26 16:26:30 +08:00
Lgf
42317ddf41 feat(watermark): 添加暗黑模式水印颜色适配
为水印功能添加暗黑模式适配,根据当前主题自动切换水印颜色。在暗黑模式下使用浅色水印,在亮色模式下使用深色水印,提升视觉体验。
2026-04-24 15:42:07 +08:00
boisduval
c7fd6ffb0e feat: 添加日期格式化函数的空值处理
- 当传入的时间参数为 undefined、null 或空字符串时,直接返回空字符串而不是继续执行格式化逻辑,避免无效的时间值导致程序错误。
2026-04-23 17:25:10 +08:00
AxiosLeo
3a83fb0c3e fix: ensure trigger function is awaited in useDependencies (#7830) 2026-04-22 07:09:09 +08:00
AxiosLeo
5907c04e00 fix: update selector for active menu item in useMenuScroll hook (#7829) 2026-04-22 07:08:40 +08:00
Jin Mao
6885927441 chore: fix actions error 2026-04-15 18:49:07 +08:00
2ylllll
c8c589ae7e fix: type check (#7818) 2026-04-15 18:11:06 +08:00
Jin Mao
d0889b5cc4 chore: 更新依赖包版本
- 更新 @tsdown/css 从 0.21.7 到 0.21.8
- 更新 @vitejs/plugin-vue 从 6.0.5 到 6.0.6
- 更新 dotenv 从 17.4.1 到 17.4.2
- 更新 globals 从 17.4.0 到 17.5.0
- 更新 happy-dom 从 20.8.9 到 20.9.0
- 更新 oxlint 从 1.59.0 到 1.60.0
- 更新 tsdown 从 0.21.7 到 0.21.8
2026-04-15 17:28:17 +08:00
Jin Mao
2f1a866cb4 chore: 更新 pnpm action 版本
- 将 .github/actions/setup-node/action.yml 中的 pnpm/action-setup 从 v4 升级到 v6
- 将 .github/workflows/ci.yml 中的 pnpm/action-setup 从 v5 升级到 v6
- 移除重复的 pnpm 安装步骤配置
- 统一使用最新的 pnpm action 版本以获得更好的兼容性
- 保持 run_install 配置为 false 以跳过自动安装依赖
2026-04-15 17:27:57 +08:00
Jin Mao
fee32c1d12 chore: 优化通知组件逻辑
- 移除冗余的空值检查,简化数组长度判断条件
- 统一导入语句的引号格式为双引号
- 优化 VbenScrollbar 组件的显示条件逻辑
- 更新清除按钮的禁用状态判断逻辑
2026-04-15 17:22:39 +08:00
Jin Mao
f2652833a1 Merge branch 'fork/xueyitt/main' 2026-04-15 17:20:10 +08:00
allen
c3aa63982f fix: devtools warning 2026-04-15 16:44:13 +08:00
allen
4a968d9379 fix: devtools warning 2026-04-15 16:39:56 +08:00
allen
ec9b323195 docs: demo注释修改 2026-04-15 14:54:40 +08:00
allen
d69455e8ef fix: dependencies访问extendApi 2026-04-15 14:42:21 +08:00
allen
33e2582f60 fix: fix lint and add new form-ui features
feat(form-ui): 在 dependencies 里提供访问extendApi的能力
2026-04-15 14:21:39 +08:00
allen
991408b451 fix: default precision error 2026-04-13 21:08:03 +08:00
allen
a096073a8e fix: lint 2026-04-13 20:43:29 +08:00
allen
f30157fa59 fix: class 2026-04-13 20:41:43 +08:00
allen
12a81a7a7d fix: demo validator usage & types import 2026-04-13 20:15:47 +08:00
allen
e808fe74c1 Merge branch 'feature-collapsible-component' of https://github.com/2yllll/vue-vben-admin into feature-collapsible-component 2026-04-13 19:44:45 +08:00
allen
4665a787a0 fix: collapsible component css fix 2026-04-13 19:44:21 +08:00
2ylllll
b7774fc9d9 Merge branch 'main' into feature-collapsible-component 2026-04-13 19:26:23 +08:00
allen
6f18718c87 feat: add collapsible 组件,form表单增加单项可折叠,支持schema配置默认关闭/开启
feat: add collapsible 组件,form表单增加单项可折叠,支持schema配置默认关闭/开启
- shadcn-ui 增加 collapsible组件,collapsible-params组件
- form新增支持单项折叠
- collapsible-params组件在Form表单应用
2026-04-13 19:20:01 +08:00
Caisin
2a32715c99 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 17:52:17 +08:00
Caisin
46f323431c 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 17:52:17 +08:00
Caisin
2b1fcb038b 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-13 17:52:17 +08:00
过冬
d779a39862 fix: antdv-next message/notification 跟随暗色主题 (#7799) 2026-04-13 17:52:17 +08:00
Jin Mao
0c8db0f6be 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-13 17:52:17 +08:00
Jin Mao
37393e1820 chore: fix actions error 2026-04-13 17:52:16 +08:00
xingyu4j
93e23c6b05 chore: update deps 2026-04-13 17:52:16 +08:00
xingyu4j
e023a0852a fix: tailwindcss lint config 2026-04-13 17:52:16 +08:00
xingyu4j
306739cabb fix: lint 2026-04-13 17:52:16 +08:00
xingyu4j
093479bb33 chore: remove vite-plugin-html 2026-04-13 17:52:16 +08:00
xingyu4j
bd369df3bc fix: check deps 2026-04-13 17:52:16 +08:00
xingyu4j
4f7b396ebb chore: update deps 2026-04-13 17:52:16 +08:00
xingyu4j
d700b04841 fix: tailwindcss lint 2026-04-13 17:52:16 +08:00
xingyu4j
243456d928 fix: ts config 2026-04-13 17:52:16 +08:00
Jin Mao
7e8b9c2368 fix: 配置 TypeScript 构建根目录
- 添加 rootDir 编译选项指向 ./src 目录
- 保持现有编译配置不变
- 排除测试文件和 node_modules 目录
2026-04-13 17:52:15 +08:00
Jin Mao
92298bac2b chore: update deps 2026-04-13 17:52:15 +08:00
dullathanol
436a865bef chore: 修正注释 2026-04-13 17:52:15 +08:00
dullathanol
185011ee21 fix: tailwindcss config 2026-04-13 17:52:15 +08:00
过冬
feca01484b Merge branch 'vbenjs:main' into main 2026-04-13 17:52:15 +08:00
Jin Mao
35e1166b9f chore: fix lint 2026-04-13 17:52:15 +08:00
dullathanol
d0c17090ef fix: 修正 Modal/Drawer 中 loading 属性注释 2026-04-13 17:52:15 +08:00
dullathanol
cc32ebeb12 feat: 支持 overflow 配置以允许拖拽超出可视区 2026-04-13 17:52:15 +08:00
dullathanol
02372f12ba fix: 弹窗组件拖拽后全屏位置异常 2026-04-13 17:52:15 +08:00
Jin Mao
e9bdbb610a chore: fix lint
- 关闭 vitest/require-mock-type-parameters 规则
2026-04-13 17:52:15 +08:00
dullathanol
2013ba4de4 fix: 补全 ComponentPropsMap 与 Vxe 表格表单链路的类型 2026-04-13 17:52:15 +08:00
dullathanol
e417a2c209 fix: 修复 FormField 在 SFC 中的运行时异常 2026-04-13 17:52:14 +08:00
dullathanol
f1273571c7 fix: 修复部分情况 component 类型丢失问题 2026-04-13 17:52:14 +08:00
dullathanol
064a6a20f0 feat: 表单 Schema 支持组件 Props 映射泛型,同步适配VxeGrid 2026-04-13 17:52:14 +08:00
dullathanol
cb5e7e00f1 refactor: 简化 componentProps 回调的类型写法 2026-04-13 17:52:14 +08:00
dullathanol
5ed39c03a4 fix: 函数式 componentProps 按已注册 component 的 Props 校验返回值 2026-04-13 17:52:14 +08:00
dullathanol
794b103aa9 feat: Schema 中 componentProps 随注册组件联动类型提示 2026-04-13 17:52:14 +08:00
墨苒孤
c7b1bb3492 fix(form): 修复表单示例中 switch 组件无法切换的问题 (#7636) (#7763) 2026-04-13 17:52:14 +08:00
橙子
89349f8d91 fix: interface DropdownMenuProps don‘t have key prop (#7757) 2026-04-13 17:52:14 +08:00
HaroldZhangCode91
8e0b65a0e1 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-13 17:52:13 +08:00
墨苒孤
b096e83f93 fix: make search case-insensitive (#7689) (#7755) 2026-04-13 17:52:13 +08:00
橙子
4a69c9e665 perf: replace onUnMounted to tryOnScopeDispose (#7747)
* perf: replace `onUnMounted` to `tryOnScopeDispose`

* perf: replace `onUnMounted` to `tryOnScopeDispose`
2026-04-13 17:52:13 +08:00
Jin Mao
7a60a081a2 chore: update deps 2026-04-13 17:52:13 +08:00
xingyu4j
c693c854a7 fix: extension-document 2026-04-13 17:52:13 +08:00
xingyu4j
5eca357a5c feat(@vben/plugins): add tiptap rich text editor 2026-04-13 17:52:13 +08:00
xingyu4j
2930dcd78e chore: typescript config is expired‌ 2026-04-13 17:52:13 +08:00
xingyu4j
e421d9d99b chore: update deps 2026-04-13 17:52:12 +08:00
xueyitt
6c029bfa32 Merge branch 'main' into main 2026-04-13 16:10:29 +08:00
雪忆天堂
fe77bc8bc9 feat: 通知模块自定义加强 2026-04-13 16:05:42 +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
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
雪忆天堂
6b3506f128 feat: table 类型VxeTableGridColumns替代VxeTableGridOptions['columns']魔法值写法 2026-03-23 10:06:35 +08:00
雪忆天堂
5613dcef99 feat: table允许通过props动态变化data数据 2026-03-23 10:05:32 +08:00
MistyMoon
3528517fe0 fix(project): fix sub sidebar theme in two-column menu (#7708)
- Reset light tokens for nested sub sidebars.
- Align the extra title logo theme with sidebarThemeSub.
2026-03-22 09:15:56 +08:00
Jin Mao
2a5b520ec9 chore: update deps 2026-03-22 09:15:27 +08:00
lmx
e2fb3602f1 fix: 修复菜单项外部链接跳转问题
- 引入 isHttpUrl 工具函数用于判断 URL 类型
- 添加 isHttp 计算属性检测父路径是否为 HTTP 链接
- 修改菜单项渲染逻辑,对外部链接直接使用原地址跳转
- 调整 HTML 结构,将链接属性移到正确位置
- 确保内部路由和外部链接都能正常工作
2026-03-19 23:56:13 +08:00
lmx
da3580cbd7 Merge remote-tracking branch 'origin/main' 2026-03-19 22:07:20 +08:00
xingyu
0c300d040c fix: tailwindcss config (#7693)
* chore: update deps

* fix: tailwindcss config

* fix: lint

* fix: lint

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

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

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

* chore: update deps

* fix: oxlint

* chore: fix lint issues

* chore: update deps

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

* chore: update deps

* fix: oxlint

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

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

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

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

* fix: error Forbidden non-null assertion

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

* feat: use jsonc/x language

* chore: update eslint 10.0

* fix: no-useless-assignment

* feat: add CLAUDE.md

* chore: ignore

* feat: claude

* fix: lint

* chore: suppot eslint v10

* fix: lint

* fix: lint

* fix: type check

* fix: unit test

* fix: Suggested fix

* fix: unit test

* chore: update stylelint v17

* chore: update all major deps

* fix:  echarts console warn

* chore: update vitest v4

* feat: add skills ignores

* chore: update deps

* chore: update deps

* fix: cspell

* chore: update deps

* chore: update tailwindcss v4

* chore: remove postcss config

* fix: no use catalog

* chore: tailwind v4 config

* fix: tailwindcss v4 sort

* feat: use eslint-plugin-better-tailwindcss

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

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

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

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

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

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

* style(format): normalize effects ui and layout components
2026-03-10 05:08:45 +08:00
zouawen
aa7d8630b5 fix: 侧边菜单栏拖拽优化 (#7606)
* fix: 拖拽使用遮罩层实现,使得拖拽到min或max临界值时cursor显示not-allowed,同时拖拽线条颜色变浅,类似于disabled,提升用户体验

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

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

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

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

* feat(vite-config): add type

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-02-26 06:14:12 +08:00
Jin Mao
01508d5e42 fix: fix lint 2026-02-26 05:45:36 +08:00
zouawen
57cf6cbc9e feature: 简易版菜单宽度拖拽功能 2026-02-25 17:50:22 +08:00
AxiosLeo
03ebbea46a fix(menu): update hover color variable to use the correct reference (#7544)
* fix(menu): update hover color variable to use the correct reference

Medium Severity

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

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

Low Severity

The SCSS &:not(.is-active):hover { &:hover { ... } } compiles to a :hover:hover pseudo-class chain, which is functionally identical to a single :hover. The inner &:hover nesting is redundant and adds unnecessary complexity compared to placing styles directly inside the &:not(.is-active):hover block.
2026-02-12 22:22:53 +08:00
zouawen
8e7a5d1ec3 fix: Fix layout change, ensure div[ref="asideRef"] is contained within <aside> (#7551) 2026-02-12 22:22:34 +08:00
AxiosLeo
aa74a2535b fix(tabbar): visitHistory field (#7543)
High Severity

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

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

* fix: 修复lint报错

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

* fix: 修复lint报错
2026-02-11 16:08:32 +08:00
moil-xm
7fe8d7b4be fix: ts 错误: 类型实例化过深,且可能无限 2026-02-10 16:13:36 +08:00
Bin
aace726a91 feat(playground): add antdv-next router link (#7532)
Co-authored-by: fuwb <fuwb@sunsharing.com.cn>
2026-02-10 13:09:34 +08:00
Jin Mao
e6f6e5464a Merge branch 'main' into main 2026-02-10 12:08:16 +08:00
Aliner
7d04b600fb fix: correct updateDate to updateData in the echarts hook (#7538)
* fix(@vben/plugins): Fixed the misspelling of the data update method name in the echarts hook

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

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

This reverts commit 86d679cf25631bd1abd56d4f971e6db3a9b9d6d5.

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

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

* chore: update deps

* fix: unused ts lint

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

* chore: update actions/checkout v6

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

* fix: eslint format

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

* chore: 调整label小点

* fix: import

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-01-26 16:12:09 +08:00
橙子
6e8315ab40 fix: arguments order update (#7132)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2026-01-26 16:11:37 +08:00
Jin Mao
7cb2699f19 chore: 更新pnpm工作区配置
- 移除 neverBuiltDependencies 配置
- 移除 peerDependencyRules.allowedVersions 配置
- 重新整理 overrides 和 catalog 部分
- 保持 @ast-grep/napi 等依赖的目录引用配置
- 维持 eslint 版本约束在目录顶层配置中
2026-01-25 19:55:53 +08:00
Jin Mao
4b5da81ba6 chore(deps): 更新项目依赖包版本
- 更新 @playwright/test 从 1.57.0 到 1.58.0
- 更新 @tanstack/vue-query 从 5.92.8 到 5.92.9
- 更新 cheerio 从 1.1.2 到 1.2.0
- 更新 eslint-config-turbo 从 2.7.5 到 2.7.6
- 更新 playwright 从 1.57.0 到 1.58.0
- 更新 turbo 从 2.7.5 到 2.7.6
- 更新 vxe-pc-ui 从 4.12.10 到 4.12.12
- 更新 @babel/helper-define-polyfill-provider 从 0.6.5 到 0.6.6
- 更新 @cspell/dict-fullstack 从 3.2.7 到 3.2.8
- 更新 @cspell/dict-git 从 3.0.7 到 3.1.0
- 更新 @cspell/dict-node 从 5.0.8 到 5.0.9
- 更新 @cspell/dict-npm 从 5.2.29 到 5.2.30
- 更新 @parcel/watcher 相关包从 2.5.4 到 2.5.6
- 更新 @tanstack/query-core 从 5.90.19 到 5.90.20
- 更新 babel-plugin-polyfill 相关包到最新版本
- 更新 baseline-browser-mapping 从 2.9.17 到 2.9.18
- 更新 caniuse-lite 从 1.0.30001765 到 1.0.30001766
- 更新 electron-to-chromium 从 1.5.277 到 1.5.278
- 更新 eslint-plugin-turbo 从 2.7.5 到 2.7.6
- 更新 playwright-core 从 1.57.0 到 1.58.0
- 更新 turbo 平台特定版本到 2.7.6
- 更新 undici 从 7.19.0 到 7.19.1
2026-01-25 15:05:14 +08:00
Jin Mao
6aca9a9c99 test(dom): 更新元素可见区域计算的测试用例
- 修正了getElementVisibleRect函数的期望值断言
- 将bottom值从800更正为0以匹配实际计算结果
- 将left值从1100更正为0以匹配实际计算结果
- 将right值从1000更正为0以匹配实际计算结果
- 将top值从900更正为0以匹配实际计算结果
2026-01-25 14:22:22 +08:00
xingyu4j
fa195fde8e fix: lint 2026-01-23 14:48:21 +08:00
xingyu4j
1057f2932b chore: update deps 2026-01-23 14:48:06 +08:00
Jin Mao
b9224fc379 Merge branch 'main' into fix 2026-01-23 13:48:54 +08:00
Jin Mao
57dd818170 Merge branch 'fork/kuchaguangjie/fix' 2026-01-23 13:47:54 +08:00
Jin Mao
49256ec1b7 Merge branch 'main' into fix 2026-01-23 13:46:59 +08:00
Jin Mao
f6f92e5403 Merge branch 'fork/kuchaguangjie/fix' 2026-01-23 13:45:38 +08:00
Jin Mao
613c311076 Merge branch 'fork/abcd0f/dev/sun-local' 2026-01-23 13:25:03 +08:00
Jin Mao
9ee7a7d9ff Merge branch 'fork/Child-qjj/patch-1' 2026-01-23 13:21:49 +08:00
橙子
44f8aed06d fix: timer not need reactivity (#7128) 2026-01-23 13:16:09 +08:00
Sun
d5d4a5c591 feat(effects-plugins): 添加 echarts 图表更新功能
新增 updateDate 方法用于更新 echarts 图表选项,支持合并配置、
完全替换和延迟更新等模式。该方法会在组件未初始化时自动执
行首次渲染,并能够合并全局配置如 backgroundColor 等选项。
2026-01-23 11:06:45 +08:00
yuhengshen
74381aa8c1 fix: 嵌套弹窗,错误 merge options (#7126) 2026-01-22 20:07:13 +08:00
橙子
203ee9b623 fix(@vben-core/shared): element outside viewport, the element visible rect each prop expect 0 (#7120)
* fix(@vben-core/shared): element outside viewport

* fix(@vben-core/shared): element outside viewport
2026-01-22 11:37:01 +08:00
JyQAQ
6c8c49966a Perf: 优化antd upload组件参数获取 (#7114)
* perf(antd upload params): 优化组件参数取值 确保不同调用场景配置参数可用

* perf(antd upload params): 优化组件参数取值 确保不同调用场景配置参数可用

* perf(antd upload params): 优化组件参数取值 确保不同调用场景配置参数可用

* perf(antd upload params): 优化组件参数取值 确保不同调用场景配置参数可用
2026-01-21 17:20:53 +08:00
Qiu
3862942e9f fix: chart instance disposal condition
dom has been disposed in vue3 v-if,but chartInstance exist
2026-01-21 11:47:01 +08:00
xingyu
8571fc43b0 Merge branch 'main' into fix 2026-01-19 15:28:12 +08:00
JyQAQ
59aabd956d Perf: Optimization of cropping component result acquisition & optimization of cropping frame prompts (#7111)
* perf(cropper): enhance image cropping functionality and add output type support

* perf(cropper): enhance image cropping functionality and add output type support
2026-01-19 14:18:36 +08:00
xingyu
9b09ba4483 Merge branch 'main' into fix 2026-01-19 10:54:43 +08:00
橙子
686c3f9208 docs(@vben-core/preferences): update comments (#7107) 2026-01-14 19:36:45 +08:00
MRSWC
8a7e2bd8e4 fix:修复默认默认首页如果携带参数时刷新页面参数丢失问题 (#7102)
Co-authored-by: chenwei <chenw@jiuzhekan.com>
2026-01-14 15:38:55 +08:00
JyQAQ
174c4ae749 fix(antd Upload onChange Event): rewrite onChange event to handle upl… (#7098)
* fix(antd Upload onChange Event): rewrite onChange event to handle upload success or error messages

* fix(antd Upload onChange Event): rewrite onChange event to handle upload success or error messages

* fix(antd Upload onChange Event): rewrite onChange event to handle upload success or error messages
2026-01-14 15:38:21 +08:00
JyQAQ
67da9417a8 feat(upload prop:crop,aspectRatio): from Upload component accept prop… (#7095)
* feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio

* feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio

* feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio

* feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio
2026-01-14 15:38:05 +08:00
ppxb
f4a4ced88d fix: header auto mode issue (#7096) 2026-01-12 21:40:26 +08:00
ppxb
19b2d7af41 fix: tdesign theme toggle and demos (#7087) 2026-01-10 14:11:08 +08:00
yuhengshen
343d8a1c1e fix: 切换时区后,页面不刷新 (#7085)
* fix: 切换时区后,页面不刷新

* fix: keep-alive 的页面,i18n 和 timezone 不更新
2026-01-10 14:10:27 +08:00
JyQAQ
9480f8272a feat(common-ui cropper): Implement the image cropping component VCropper (#7082)
* feat(common-ui cropper): Implement the image cropping component VCropper

* feat(common-ui cropper): Implement the image cropping component VCropper

* feat(common-ui cropper): Implement the image cropping component VCropper

* feat(common-ui cropper): Implement the image cropping component VCropper

* feat(common-ui cropper): Implement the image cropping component VCropper
2026-01-10 14:08:15 +08:00
ppxb
0d9e260a6a fix: vite.config.mts type error (#7081) 2026-01-10 14:07:28 +08:00
ppxb
51bca25345 fix(lint): pnpm format lint warning (#7080) 2026-01-10 14:06:03 +08:00
eric
694396dcfb refactor: move cleanup to finally block 2026-01-09 23:22:49 +08:00
eric
1cb53e943e refactor: 将跳转放到最后 2026-01-09 23:13:06 +08:00
eric
13c8318adc refactor: 1. 用 ref 包装 flag; 2. 在最后 清理 flag; 2026-01-09 23:05:05 +08:00
eric
48ed797055 fix: 防止 /logout 死循环 2026-01-09 22:38:11 +08:00
xingyu4j
3c2285141c chore: update deps 2026-01-06 13:58:42 +08:00
xingyu
49b884c0b1 Merge branch 'main' into fix 2026-01-06 13:50:26 +08:00
ppxb
24d20ca9ee refactor: preference manager and export (#7068)
* fix: preferences drawer outline z-index

* refactor: preferencesManager and exports
2026-01-06 12:42:32 +08:00
wyc001122
6f02181024 fix(layout): 修复双列模式下重复显示logo的问题(#7071) (#7072) 2026-01-05 21:13:06 +08:00
ppxb
ed3353a271 fix: preferences drawer outline z-index (#7067) 2026-01-04 14:44:33 +08:00
xingyu4j
965f5f96b7 chore: update deps 2026-01-04 11:03:19 +08:00
xingyu4j
61d9df7f58 feat: add cascader cspell:words 2026-01-04 11:00:07 +08:00
xingyu4j
231a5169ec fix: lint 2026-01-04 10:56:55 +08:00
xingyu4j
ce7b7b910a Merge branch 'main' of https://github.com/xingyu4j/vue-vben-admin into fix 2026-01-04 10:56:14 +08:00
JyQAQ
81a61558cb feat(upload prop:maxSize): from Upload component accept prop maxSize (AI prompt fixed) (#7059)
* feat(upload prop:maxSize): from component accept prop maxSize

* feat(upload prop:maxSize): from component accept prop maxSize

* feat(upload prop:maxSize): from component accept prop maxSize

* feat(upload prop:maxSize): from component accept prop maxSize
2026-01-03 13:19:40 +08:00
ppxb
7d2bc2e885 fix: dropdown raido menu type error (#7062)
* fix: dropdown raido menu type

* chore: format code
2026-01-02 14:25:19 +08:00
luoqiz
89b237f6b4 feat: 添加上下文菜单演示,添加菜单项隐藏性 (#7057)
Co-authored-by: luoqiz <851092732@qq.com>
2026-01-02 14:22:19 +08:00
JyQAQ
a1bb132233 feat(api-cascader): 添加联级组件ApiCascader (#7031) 2025-12-22 20:00:31 +08:00
zhenghaoyang24
022d538940 Fix formatting in thin.md for clarity (#7008)
修改一些语句错误
2025-12-22 19:58:05 +08:00
xueyitt
ccf70a1b76 feat: 修正菜单排序在二级菜单不生效问题 (#7007)
* treeUtil增加对树形结构数据进行递归排序

* 菜单sort排序各级菜单均生效
2025-12-22 19:57:21 +08:00
xingyu4j
af3fe53ec8 fix: type error 2025-12-22 17:50:06 +08:00
xingyu4j
e981fb159f chore: update deps 2025-12-22 17:49:51 +08:00
xingyu
79b9d55854 Merge branch 'main' into fix 2025-12-22 16:42:15 +08:00
xingyu4j
b2055a4457 chore: update deps 2025-12-17 13:47:37 +08:00
JyQAQ
1479f159aa feat(CellImage): CellImage组件支持图片属性写入 (#6992) 2025-12-06 10:12:58 +08:00
xingyu4j
7bf7e09002 fix: lint 2025-12-05 15:09:43 +08:00
xingyu4j
de8d39ffed chore: workspace file is deprecated 2025-12-05 15:07:39 +08:00
xingyu4j
543a7e3962 chore: update deps 2025-12-05 14:55:44 +08:00
xingyu4j
9dfe3f5af8 fix: type not find 2025-12-04 18:03:55 +08:00
xingyu4j
f11b08d8cb chore: update deps 2025-12-04 17:59:12 +08:00
xingyu4j
45b6f08984 chore: neverBuiltDependencies 2025-12-03 16:34:14 +08:00
xingyu4j
92a4676f8d chore: update version 2025-12-03 16:27:14 +08:00
xingyu4j
7bf7c0bb06 chore: remove unused deps 2025-12-03 16:26:58 +08:00
xingyu4j
8f8cf5b704 chore: update deps 2025-12-03 16:22:14 +08:00
xingyu4j
6be238430d chore: add cspell 2025-12-03 16:12:08 +08:00
xingyu4j
f77216d8f4 feat: lint add pnpm config 2025-12-03 16:11:51 +08:00
xingyu4j
d42a9b2409 feat: lint add yaml config 2025-12-03 16:01:27 +08:00
xingyu4j
6753834054 fix: lint 2025-12-03 15:37:33 +08:00
xingyu4j
77a4a64eb4 fix: stylelint 2025-12-03 15:37:20 +08:00
xingyu4j
49db40d557 feat: cspell sort 2025-12-03 15:37:04 +08:00
xingyu4j
0032c608f1 chore: update deps 2025-12-03 15:36:43 +08:00
xingyu4j
fa603b32b1 fix: locales 2025-12-03 13:51:30 +08:00
JyQAQ
9105d4d14a feat(api-component): api-component组件的options支持指定disabled值 (#6991) 2025-12-03 10:03:23 +08:00
luoqiz
c76db7d8d1 fix: 修复icon丢失根属性导致的样式错误 (#6986) 2025-12-01 09:51:27 +08:00
Jin Mao
6f39e9136e Merge branch 'main' into feature/antd上传组件支持调用Image组件查看图片 2025-11-24 21:59:34 +08:00
milletpeak
1f1ba16ead Merge branch 'main' into milletpeak-fontsize 2025-11-24 08:55:54 +08:00
panda7
b6edc5f574 fix: centered 为 true 时设置 draggable 无法拖拽 (#6948)
Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-11-24 08:38:10 +08:00
Jin Mao
8b0f138100 Merge branch 'main' into deps 2025-11-24 08:23:02 +08:00
ming4762
a810cd0b48 fix: fix IconPicker reporting an error when using search if no icon is found (#6944)
* 修复未搜索图标时分页报错的问题
 * 优化`IconPicker` 的分页逻辑,由total触发跳转到第一页,而不是用户控制
2025-11-24 07:56:55 +08:00
aonoa
b17fec41b0 fix: tree component cannot set a value (#6941)
Signed-off-by: aonoa <1991849113@qq.com>
2025-11-24 07:56:16 +08:00
Rex
5fcfabf1d4 fix: 修复菜单管理中标题栏样式异常问题 (#6934) 2025-11-24 07:00:08 +08:00
yuan.ji
1d77b018bb feat(function): add antd上传组件支持调用Image组件查看图片 2025-11-21 17:33:59 +08:00
米山
f7d9d1b1af chore: update package.json and app.vue imports, and ensure global styles are included 2025-11-19 11:13:06 +08:00
米山
aaf0274fe9 feat: add menu font size variable and update related components
- Introduced a new CSS variable `--menu-font-size` calculated from the base font size.
- Updated `PreferenceManager` to trigger CSS variable updates when `fontSize` is modified.
- Adjusted `updateCSSVariables` to set the new `--menu-font-size` based on the theme's font size.
- Ensured that the menu components utilize the updated font size with `!important` to maintain styling consistency.
2025-11-19 10:51:10 +08:00
米山
c142af482b fix: update snapshot for defaultPreferences immutability test to reflect fontSize change
- Adjusted the snapshot to ensure consistency with the updated defaultPreferences configuration, specifically retaining the fontSize property.
2025-11-19 10:19:16 +08:00
米山
cd7c11c7d0 fix: run 'pnpm format' update various components and improve layout structure
- Updated demo-preview and preview-group components for better error handling and layout.
- Enhanced drawer and modal components for improved auto-height functionality.
- Refactored layout components including header, footer, sidebar, and tabbar for better responsiveness and usability.
- Adjusted tooltip and help tooltip components for better user guidance.
- Fixed issues in various UI components to ensure consistent styling and functionality across the application.
2025-11-19 10:14:04 +08:00
milletpeak
fb8f36eeec Merge branch 'main' into milletpeak-fontsize 2025-11-19 09:41:55 +08:00
xingyu4j
9a4f0f6ab1 feat: Consider multi-line formatting for readability 2025-11-17 14:24:43 +08:00
xingyu4j
bed52983a0 fix: lint 2025-11-17 12:15:14 +08:00
xingyu4j
655ce2c2e8 chore: update node version >=20.12.0 2025-11-17 12:11:43 +08:00
xingyu4j
f841568e5a chore: update vue-tsc 2025-11-17 12:04:30 +08:00
xingyu4j
469697845c fix: sort 2025-11-17 11:21:49 +08:00
xingyu4j
273f3cbaf8 Merge branch 'main' of https://github.com/xingyu4j/vue-vben-admin into deps 2025-11-17 11:21:16 +08:00
xingyu4j
96f671908e chore: update deps 2025-11-17 11:17:47 +08:00
Jin Mao
f09aace765 fix: 更新依赖后的表格显示问题 2025-11-14 21:44:32 +08:00
Copilot
772529e2fb fix: rollback zod-defaults version to 0.1.3 (#6925)
* Initial plan

* fix: downgrade zod-defaults from 0.2.3 to 0.1.3 for Zod v3 compatibility

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
2025-11-14 16:16:45 +08:00
xingyu4j
eeff017b9f chore: add tdesign cspell 2025-11-14 10:22:18 +08:00
xingyu4j
22394ebdeb chore: move tdesign-vue-next to workspace 2025-11-14 10:21:48 +08:00
dependabot[bot]
ffbc9b22a7 chore(deps): bump the non-breaking-changes group across 1 directory with 26 updates (#6914)
Bumps the non-breaking-changes group with 20 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [vue](https://github.com/vuejs/core) | `3.5.23` | `3.5.24` |
| [@clack/prompts](https://github.com/bombshell-dev/clack/tree/HEAD/packages/prompts) | `0.10.1` | `0.11.0` |
| [@iconify/json](https://github.com/iconify/icon-sets) | `2.2.404` | `2.2.406` |
| [@pnpm/workspace.read-manifest](https://github.com/pnpm/pnpm) | `1000.2.5` | `1000.2.6` |
| [@tanstack/vue-query](https://github.com/TanStack/query/tree/HEAD/packages/vue-query) | `5.90.7` | `5.91.0` |
| [@tanstack/vue-store](https://github.com/TanStack/store/tree/HEAD/packages/vue-store) | `0.7.7` | `0.8.0` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.46.3` | `8.46.4` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.46.3` | `8.46.4` |
| [@vue/shared](https://github.com/vuejs/core/tree/HEAD/packages/shared) | `3.5.23` | `3.5.24` |
| [commitlint-plugin-function-rules](https://github.com/vidavidorra/commitlint-plugin-function-rules) | `4.1.0` | `4.1.1` |
| [eslint-config-turbo](https://github.com/vercel/turborepo/tree/HEAD/packages/eslint-config-turbo) | `2.6.0` | `2.6.1` |
| [lucide-vue-next](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-vue-next) | `0.507.0` | `0.553.0` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.6.14` | `0.7.1` |
| [sass](https://github.com/sass/dart-sass) | `1.93.3` | `1.94.0` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.2` | `5.9.3` |
| [unplugin-element-plus](https://github.com/element-plus/unplugin-element-plus) | `0.10.0` | `0.11.1` |
| [vxe-pc-ui](https://github.com/x-extends/vxe-pc-ui) | `4.10.16` | `4.10.22` |
| [vxe-table](https://github.com/x-extends/vxe-table) | `4.17.10` | `4.17.14` |
| [zod-defaults](https://github.com/Ced-Sharp/zod-defaults) | `0.1.3` | `0.2.3` |
| [@ast-grep/napi](https://github.com/ast-grep/ast-grep) | `0.37.0` | `0.39.9` |



Updates `vue` from 3.5.23 to 3.5.24
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.23...v3.5.24)

Updates `@clack/prompts` from 0.10.1 to 0.11.0
- [Release notes](https://github.com/bombshell-dev/clack/releases)
- [Changelog](https://github.com/bombshell-dev/clack/blob/@clack/prompts@0.11.0/packages/prompts/CHANGELOG.md)
- [Commits](https://github.com/bombshell-dev/clack/commits/@clack/prompts@0.11.0/packages/prompts)

Updates `@iconify/json` from 2.2.404 to 2.2.406
- [Commits](https://github.com/iconify/icon-sets/compare/2.2.404...2.2.406)

Updates `@pnpm/workspace.read-manifest` from 1000.2.5 to 1000.2.6
- [Release notes](https://github.com/pnpm/pnpm/releases)
- [Commits](https://github.com/pnpm/pnpm/commits)

Updates `@tanstack/vue-query` from 5.90.7 to 5.91.0
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/vue-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/vue-query@5.91.0/packages/vue-query)

Updates `@tanstack/vue-store` from 0.7.7 to 0.8.0
- [Release notes](https://github.com/TanStack/store/releases)
- [Commits](https://github.com/TanStack/store/commits/v0.8.0/packages/vue-store)

Updates `@typescript-eslint/eslint-plugin` from 8.46.3 to 8.46.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.4/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.46.3 to 8.46.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.4/packages/parser)

Updates `@vue/shared` from 3.5.23 to 3.5.24
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.5.24/packages/shared)

Updates `autoprefixer` from 10.4.21 to 10.4.22
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.21...10.4.22)

Updates `commitlint-plugin-function-rules` from 4.1.0 to 4.1.1
- [Release notes](https://github.com/vidavidorra/commitlint-plugin-function-rules/releases)
- [Changelog](https://github.com/vidavidorra/commitlint-plugin-function-rules/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vidavidorra/commitlint-plugin-function-rules/compare/v4.1.0...v4.1.1)

Updates `eslint-config-turbo` from 2.6.0 to 2.6.1
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/release.md)
- [Commits](https://github.com/vercel/turborepo/commits/v2.6.1/packages/eslint-config-turbo)

Updates `lodash.clonedeep` from 4.5.0 to 
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Updates `lodash.get` from 4.4.2 to 
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Updates `lodash.isequal` from 4.5.0 to 
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Updates `lodash.set` from 4.3.2 to 
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Updates `lucide-vue-next` from 0.507.0 to 0.553.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.553.0/packages/lucide-vue-next)

Updates `prettier-plugin-tailwindcss` from 0.6.14 to 0.7.1
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.14...v0.7.1)

Updates `sass` from 1.93.3 to 1.94.0
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.93.3...1.94.0)

Updates `typescript` from 5.8.2 to 5.9.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.2...v5.9.3)

Updates `unplugin-element-plus` from 0.10.0 to 0.11.1
- [Release notes](https://github.com/element-plus/unplugin-element-plus/releases)
- [Commits](https://github.com/element-plus/unplugin-element-plus/compare/v0.10.0...v0.11.1)

Updates `vxe-pc-ui` from 4.10.16 to 4.10.22
- [Release notes](https://github.com/x-extends/vxe-pc-ui/releases)
- [Commits](https://github.com/x-extends/vxe-pc-ui/commits)

Updates `vxe-table` from 4.17.10 to 4.17.14
- [Release notes](https://github.com/x-extends/vxe-table/releases)
- [Commits](https://github.com/x-extends/vxe-table/compare/4.17.10...4.17.14)

Updates `zod-defaults` from 0.1.3 to 0.2.3
- [Commits](https://github.com/Ced-Sharp/zod-defaults/commits)

Updates `@ast-grep/napi` from 0.37.0 to 0.39.9
- [Release notes](https://github.com/ast-grep/ast-grep/releases)
- [Changelog](https://github.com/ast-grep/ast-grep/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ast-grep/ast-grep/compare/0.37.0...0.39.9)

Updates `@vue/reactivity` from 3.5.23 to 3.5.24
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.5.24/packages/reactivity)

---
updated-dependencies:
- dependency-name: vue
  dependency-version: 3.5.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@clack/prompts"
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: "@iconify/json"
  dependency-version: 2.2.406
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@pnpm/workspace.read-manifest"
  dependency-version: 1000.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@tanstack/vue-query"
  dependency-version: 5.91.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: "@tanstack/vue-store"
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.46.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.46.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: "@vue/shared"
  dependency-version: 3.5.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: autoprefixer
  dependency-version: 10.4.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: commitlint-plugin-function-rules
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: eslint-config-turbo
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: lodash.clonedeep
  dependency-version: 
  dependency-type: direct:production
  dependency-group: non-breaking-changes
- dependency-name: lodash.get
  dependency-version: 
  dependency-type: direct:production
  dependency-group: non-breaking-changes
- dependency-name: lodash.isequal
  dependency-version: 
  dependency-type: direct:production
  dependency-group: non-breaking-changes
- dependency-name: lodash.set
  dependency-version: 
  dependency-type: direct:production
  dependency-group: non-breaking-changes
- dependency-name: lucide-vue-next
  dependency-version: 0.553.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: prettier-plugin-tailwindcss
  dependency-version: 0.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: sass
  dependency-version: 1.94.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: unplugin-element-plus
  dependency-version: 0.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: vxe-pc-ui
  dependency-version: 4.10.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: vxe-table
  dependency-version: 4.17.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
- dependency-name: zod-defaults
  dependency-version: 0.2.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: "@ast-grep/napi"
  dependency-version: 0.39.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-breaking-changes
- dependency-name: "@vue/reactivity"
  dependency-version: 3.5.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-14 08:05:04 +08:00
Jin Mao
c3a7562e2c Merge branch 'main' into milletpeak-fontsize 2025-11-13 17:01:42 +08:00
Jin Mao
605e0ea128 chore: 更新依赖 2025-11-13 09:23:12 +08:00
dependabot[bot]
c89a83f980 chore(deps): bump stylelint-config-recommended from 14.0.1 to 17.0.0 (#6901)
Bumps [stylelint-config-recommended](https://github.com/stylelint/stylelint-config-recommended) from 14.0.1 to 17.0.0.
- [Release notes](https://github.com/stylelint/stylelint-config-recommended/releases)
- [Changelog](https://github.com/stylelint/stylelint-config-recommended/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint-config-recommended/compare/14.0.1...17.0.0)

---
updated-dependencies:
- dependency-name: stylelint-config-recommended
  dependency-version: 17.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-13 09:17:44 +08:00
Jin Mao
cfad88666b chore: 替换 lodash-es为 es-toolkit 并调整样式引入方式 (#6912) 2025-11-13 08:20:30 +08:00
Jin Mao
f918dc24c6 chore(@vben/utils): 移除冗余的 lodash 依赖 2025-11-13 07:47:48 +08:00
Jin Mao
e898993fe8 chore(@vben/utils): 替换 lodash 工具函数为 es-toolkit 实现 2025-11-13 07:45:10 +08:00
Jin Mao
5cafcb4a01 refactor(shared): 替换 lodash 工具函数为 es-toolkit 兼容版本
- 将 get 和 isEqual 方法从 lodash 替换为 es-toolkit/compat 导出
- 更新 package.json 添加 es-toolkit 依赖
- 注释掉原有的 lodash.get 和 lodash.isequal 导出语句- 锁定 es-toolkit 版本至1.41.0 并更新相关依赖配置
2025-11-13 07:28:52 +08:00
Jin Mao
03154cde88 chore: update depends 2025-11-13 07:16:28 +08:00
dependabot[bot]
a74bf7b63f chore(deps): bump echarts from 5.6.0 to 6.0.0 (#6859)
Bumps [echarts](https://github.com/apache/echarts) from 5.6.0 to 6.0.0.
- [Release notes](https://github.com/apache/echarts/releases)
- [Commits](https://github.com/apache/echarts/compare/5.6.0...6.0.0)

---
updated-dependencies:
- dependency-name: echarts
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-11-13 07:10:59 +08:00
米山
0bc7169698 feat: add global font size adjustment 2025-11-12 17:39:07 +08:00
米山
24b6e7a835 feat: add global font size adjustment 2025-11-12 17:38:41 +08:00
Jin Mao
f59e33682c Merge branch 'main' into main 2025-11-12 04:27:57 +08:00
Jin Mao
d811af37dd Merge branch 'main' into profile 2025-11-12 04:03:27 +08:00
Jin Mao
0e1a7d61f3 Merge branch 'main' into fix-downloader 2025-11-12 02:05:03 +08:00
panda7
573637222d feat: add form handleCollapsedChange event (#6893)
* feat: add form handleCollapsedChange event

* fix: ts lint

* fix: ts error

---------

Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-11-12 02:03:12 +08:00
shixi
05e9d65251 fix: for tdesign 2025-11-12 01:14:28 +08:00
shixi
0bbb20fee0 feat: sport notification link 2025-11-12 00:55:24 +08:00
shixi
dbc5ea32ae feat: add read and delete for each notification 2025-11-12 00:02:13 +08:00
xingyu4j
0319604863 feat: playground add profile 2025-11-10 18:00:50 +08:00
xingyu4j
acf99f2441 feat: tdesign add profile 2025-11-10 18:00:25 +08:00
xingyu4j
cbf2a02877 feat: naive add profile 2025-11-10 17:59:58 +08:00
xingyu4j
226d9bd1ad feat: ele add profile 2025-11-10 17:59:33 +08:00
xingyu4j
48b3d30088 fix: types 2025-11-10 17:54:01 +08:00
xingyu4j
e311cfb8da feat: route add profile 2025-11-10 17:51:10 +08:00
xingyu4j
4347fba80a feat: antdv add profile 2025-11-10 17:50:47 +08:00
xingyu4j
7dc68ed368 feat: add profile comps 2025-11-10 17:49:58 +08:00
Jin Mao
a4aa133db5 Merge branch 'main' into tdesign 2025-11-09 12:04:30 +08:00
Utopia
6bbe523f6f chore: 增强 util-formatDate ts 类型提示 (#6886)
* feat: 为 auth layout 添加 slot: logo, 提升组件的灵活性和可复用性

* feat: 增强 util-formatDate ts 类型提示
2025-11-09 12:00:49 +08:00
Copilot
fef1e35c54 fix: prevent JSONBigInt parsing error on non-string data (#6891)
* Initial plan

* Fix json-bigint serialization error when data is not a string

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
2025-11-09 11:49:38 +08:00
xingyu
20410aeb03 fix: updating Tailwind CSS variables from --radix-* to --reka-* (#6890)
* fix: radix => reka

* chore: add reka cspell

* fix: Composition url
2025-11-09 11:49:01 +08:00
Li Kui
83ea27af3e fix: fix lint 2025-11-06 23:16:23 +08:00
Li Kui
b7bfd69788 fix: fix lint 2025-11-06 23:10:03 +08:00
Li Kui
99d663a6f2 fix: fix typecheck 2025-11-06 23:02:24 +08:00
Li Kui
1af11240a6 chore: add tdesign logo 2025-11-06 22:37:55 +08:00
Li Kui
3e8e8690e3 chore: Merge branch 'main' into tdesign 2025-11-06 17:49:51 +08:00
Li Kui
be843300be chore: update demo routes 2025-11-06 17:44:34 +08:00
Li Kui
dc77721c11 chore: demo i18n 2025-11-06 16:42:25 +08:00
Li Kui
a38d081f17 chore: enable VITE_NITRO_MOCK 2025-11-06 16:37:15 +08:00
Copilot
1e09fa4642 feat: migrate from Radix Vue to Reka UI (#6870)
* Initial plan

* Update dependencies and imports from radix-vue to reka-ui

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

* Fix type errors after reka-ui migration

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

* Run formatter to fix code style

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
2025-11-06 16:22:24 +08:00
Li Kui
bd8ff73f9e fix: fix build error 2025-11-06 15:26:24 +08:00
Li Kui
57620dc2ea chore: apply coderabbitai suggestions 2025-11-06 15:11:28 +08:00
Li Kui
b472fbb72f Merge branch 'main' into tdesign 2025-11-06 14:36:27 +08:00
Copilot
04321b16c1 fix: replace ant-design-vue with tdesign components in web-tdesign app (#6880)
* Initial plan

* fix: replace ant-design-vue with tdesign components

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

* fix: remove trailing comma in package.json

Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
2025-11-06 14:35:50 +08:00
Olexandr88
7268824612 Update README.md (#6828)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-11-04 04:34:55 +08:00
dependabot[bot]
23c41a8059 chore(deps): bump @types/node from 12.20.55 to 24.9.2 (#6860)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.20.55 to 24.9.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.9.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 04:16:59 +08:00
dependabot[bot]
fdc78faa13 chore(deps): bump @vueuse/integrations from 12.8.2 to 14.0.0 (#6861)
Bumps [@vueuse/integrations](https://github.com/vueuse/vueuse/tree/HEAD/packages/integrations) from 12.8.2 to 14.0.0.
- [Release notes](https://github.com/vueuse/vueuse/releases)
- [Commits](https://github.com/vueuse/vueuse/commits/v14.0.0/packages/integrations)

---
updated-dependencies:
- dependency-name: "@vueuse/integrations"
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 04:16:37 +08:00
dependabot[bot]
c8af580866 chore(deps): bump SamKirkland/FTP-Deploy-Action (#6873)
Bumps the non-breaking-changes group with 1 update: [SamKirkland/FTP-Deploy-Action](https://github.com/samkirkland/ftp-deploy-action).


Updates `SamKirkland/FTP-Deploy-Action` from 4.3.5 to 4.3.6
- [Release notes](https://github.com/samkirkland/ftp-deploy-action/releases)
- [Commits](https://github.com/samkirkland/ftp-deploy-action/compare/v4.3.5...v4.3.6)

---
updated-dependencies:
- dependency-name: SamKirkland/FTP-Deploy-Action
  dependency-version: 4.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-breaking-changes
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 04:14:47 +08:00
Jin Mao
348c97710f Revert "refactor(jwt): 更新 JWT 工具函数实现方式 (#6869)"
This reverts commit a854760d26.
2025-11-03 19:34:45 +08:00
zengmingyong
a854760d26 refactor(jwt): 更新 JWT 工具函数实现方式 (#6869)
refactor(jwt): 更新 JWT 工具函数实现方式

- 将默认导入 jsonwebtoken 改为解构导入 sign 和 verify 方法
2025-11-03 13:08:06 +08:00
Copilot
c7c39047de feat: add theme-aware logo support via optional sourceDark configuration (#6866)
* Initial plan

* Initial exploration and setup

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* Add support for separate light and dark theme logos

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* Update documentation with dark theme logo configuration

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* feat: Add theme-aware logo support for authentication/login page

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* revert: .npmrc

Signed-off-by: aonoa <1991849113@qq.com>

---------

Signed-off-by: aonoa <1991849113@qq.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>
Co-authored-by: Li Kui <90845831+likui628@users.noreply.github.com>
Co-authored-by: aonoa <1991849113@qq.com>
2025-11-03 13:07:36 +08:00
Jin Mao
f8a7a0a9a2 chore: lint 2025-10-31 09:11:40 +08:00
Jin Mao
2cb3dcf499 Merge branch 'fork/ming4762/timezone-fix-20251030'
# Conflicts:
#	packages/stores/src/modules/timezone.ts
2025-10-31 09:08:08 +08:00
Jin Mao
b36d32b66d chore(timezone): 添加 $reset 方法并初始化时区存储
- 在时区存储中添加 `$reset` 方法
-修复导入语句引号为双引号
- 优化时区初始化错误处理逻辑- 导出 `$reset` 方法以便外部调用- 确保时区设置与默认选项同步
- 提升代码一致性和可维护性
2025-10-30 23:27:53 +08:00
zhongming4762
639ea96bb9 fix: resolve the issue of logout failure caused by the timezone store 2025-10-30 17:12:45 +08:00
zhongming4762
565be77e96 fix: resolve the issue of logout failure caused by the timezone store 2025-10-30 17:01:49 +08:00
Jin Mao
ac6de0324c Merge branch 'fork/ming4762/timezone-20251020' 2025-10-30 00:42:05 +08:00
zhongming4762
f46ae023ba feat: increase support for multiple time zones
* 优化实现方法
2025-10-29 20:14:48 +08:00
zhongming4762
212144feca feat: increase support for multiple time zones
* 优化实现方法
2025-10-29 20:10:25 +08:00
zhongming4762
3eed51fd3e feat: increase support for multiple time zones
* 优化实现方法
2025-10-29 20:03:21 +08:00
zhongming4762
4d713db546 feat: increase support for multiple time zones
* 优化实现方法
2025-10-29 19:47:10 +08:00
Jin Mao
0cd9f4615c Merge branch 'main' into fix-downloader 2025-10-26 15:28:34 +08:00
Jin Mao
cfbce0d737 chore: fix lint 2025-10-26 15:27:28 +08:00
Jin Mao
1d9cd88dd7 Merge branch 'main' into fix-downloader 2025-10-26 15:16:32 +08:00
Jin Mao
0e62862119 chore: fix lint 2025-10-26 15:01:58 +08:00
Jin Mao
6b6cdef42d chore: 一些调整 2025-10-26 15:00:47 +08:00
Jin Mao
33b7a605c0 chore: 一些调整和兼容性更新 2025-10-26 14:23:37 +08:00
zhongming4762
e01803ce9d feat: increase support for multiple time zones 2025-10-22 20:39:00 +08:00
zhongming4762
e3e5755903 feat: increase support for multiple time zones 2025-10-22 20:29:53 +08:00
zhongming4762
61ce53b686 feat: increase support for multiple time zones 2025-10-22 20:23:55 +08:00
zhongming4762
b029f77b6a feat: increase support for multiple time zones 2025-10-22 20:18:25 +08:00
zhongming4762
0a8339a405 feat: increase support for multiple time zones 2025-10-22 19:52:01 +08:00
Jin Mao
2264eaae18 feat: 新增集成tdesign组件的apps 2025-10-21 10:47:39 +08:00
zoumingjun
38f91da5af fix: 优化双列布局组件点击展开左侧按钮位置不居中问题 2025-10-18 21:22:58 +08:00
zoumingjun
8edea3aee5 fix: 调整zh-CN/system.json中<"title": "系统管理">配置位置,保持与en-US/system.json中位置一致 2025-10-18 21:20:54 +08:00
zoumingjun
08b6e7713e fix: 修复对话框modal组件方法名错误 2025-10-18 21:13:34 +08:00
zoumingjun
6a89814b83 chore: 优化侧边栏展开折叠图标 2025-10-18 20:59:14 +08:00
zoumingjun
52d3aa9315 fix: 优化Checkbox组件hover样式,Input组件placeholder样式,TabsList组件圆角样式 2025-10-18 20:58:48 +08:00
zoumingjun
4e264c503a fix: 锁屏和解锁密码输入框自动聚焦 2025-10-18 20:55:53 +08:00
zoumingjun
32051e9ca0 fix: 优化左侧和右侧认证面板动画效果 2025-10-18 20:52:27 +08:00
zoumingjun
2b0079580b fix: 修复mock-data.ts父id错误 2025-10-18 16:32:23 +08:00
lidongcheng88
03ce030e7c fix: 混合双列 onMounted 关闭二级侧边栏 (#6807)
Co-authored-by: ldc <lidchen@tencent.com>
2025-10-14 06:56:00 +08:00
zouawen
59097e2466 chore: 新增是否开启首选项导航栏(外观、布局、快捷键、通用)吸顶效果,调整部分样式 (#6804) 2025-10-14 06:55:11 +08:00
zouawen
2ce161e585 feat: 1、新增水印文案自定义功能;2、input-item输入框组件新增清除功能(可用于快捷清除水印文案);3、偏好设置、主题切换、语言选择、是否全屏按钮新增动画效果 (#6800)
* feature: 新增水印文案自定义功能;

* chore: 偏好设置、主题切换、语言选择、是否全屏按钮新增动画效果
2025-10-07 06:15:41 +08:00
zouawen
33306a5aff chore: 更换其他登录方式图标为彩色图标,新增其他登录方式tooltip文字提示 (#6799) 2025-10-06 06:56:54 +08:00
lw567
6e03de5011 fix: watermark create error #6788 (#6789)
Co-authored-by: liwei567 <liwei567@example.com>
2025-09-30 19:53:09 +08:00
HaHa
713281a8ba fix(theme-button): fix flicker during theme switch (#6782) 2025-09-30 05:10:51 +08:00
shierd
42e3de9e2c fix: 修复设置表单值时合并子对象的错误 (#6780)
Co-authored-by: shier <shier@shierd.com>
2025-09-30 05:10:39 +08:00
vent
1e029dbc9a fix: 修复部分场景 echarts 在使用 v-show 时获取不到 width 的问题 (#6776) 2025-09-30 05:09:33 +08:00
Cup_Of_Bread
81bb7456f8 fix: 修复注解错误 (#6763) 2025-09-23 23:49:25 +08:00
pangyajun123
26f8d2aa30 perf: 表单需要通过权限控制是否需要展示,dependencies.if 这种方式需要别triggerFields触发字段,不能满足要求 (#6756) 2025-09-23 23:48:27 +08:00
boisduval
a8b848d367 fix: 解决搜索表单中arrayToStringFields属性无法在搜索时生效的问题 (#6591)
- 合并handleArrayToStringFields与handleStringToArrayFields为handleMultiFields
- 在handleMultiFields判断arrayToString或stringToArray
- 删除两个方法在其他地方的引用 统一为在handleRangeTimeValue内调用handleMultiFields实现数据转换

Co-authored-by: 宛晴 <wanqing@mengtaigroup.com>
2025-09-23 23:45:26 +08:00
ming4762
482ce981ce chore: update deps vxe, vxe-table: 4.14.4 -> 4.16.11 vxe-pc-ui: 4.7.… (#6752)
* chore: update deps vxe, vxe-table: 4.14.4 -> 4.16.11  vxe-pc-ui: 4.7.12 -> 4.9.29

* fix: resolve the ESLint warning generated by #6317
2025-09-17 17:42:39 +08:00
Jin Mao
97ad93ab1f Merge branch 'zouawen-main' 2025-09-17 17:41:05 +08:00
Jin Mao
319b7fcdaa Merge branch 'main' of https://github.com/zouawen/vue-vben-admin into zouawen-main 2025-09-17 17:37:43 +08:00
Jin Mao
4de8220ce6 feat(common-ui): 添加 Tree 组件
- 在 common-ui 包中新增 Tree 组件
- 实现了基本的树形结构展示功能
- 集成了 VbenTree 组件和自定义样式- 添加了空数据时的占位展示
2025-09-17 17:28:39 +08:00
Jin Mao
9114692f95 Merge branch 'main' into main 2025-09-15 08:08:31 +08:00
Jin Mao
0f7731b611 Merge branch 'ming4762-stream-2025091101' 2025-09-15 08:03:52 +08:00
Jin Mao
8040bc1a22 Merge branch 'stream-2025091101' of https://github.com/ming4762/smart-boot-ui-vben into ming4762-stream-2025091101 2025-09-15 08:02:30 +08:00
oc
11d273cbb6 feat(authentication): 二维码登录和验证码登录组件增加返回按钮显隐配置 (#6713)
- 在 CodeLogin 和 QrcodeLogin 组件中添加 showBack 属性
- 根据 showBack 属性决定是否显示返回按钮
- 默认值为 true,即默认显示返回按钮
2025-09-15 07:43:18 +08:00
zouawen
a4b8f8f795 fix: 节点选中时过滤父节点禁用状态 2025-09-12 17:31:42 +08:00
zouawen
dfa2ac3435 fix: 全选时过滤disabled的节点 2025-09-11 18:16:10 +08:00
zouawen
39820c783c fix:
1、VbenTree新增是否全选、展开折叠功能;
2、解决当点击子节点label文字区域,而非checkbox时,关联父组件不能选中问题;
3、优化子节点选中时关联父节点选中功能:删除VbenTree中processParentSelection方法,改为在onSelect中实现,原因:processParentSelection在每次模型值更新时都会被调用,且计算复杂度为O(n^2),onSelect只在交互时触发,复杂度为O(n);
4、新增中间层tree组件,处理无数据时显示场景(显示图标Inbox和国际化comom.noData文本);
5、为防止父组件传值子组件boolean类型默认false问题,新增treePropsDefaults方法,为TreeProps赋默认值,Tree组件和VbenTree组件统一使用;
6、优化VbenTree组件整体样式(优化padding、margin、gap值,优化type为button时outline左右空白区域不对称问题),优化内部header、footer插槽样式。
2025-09-11 16:41:42 +08:00
zhongming4762
66822a5f95 feat: add SSE support to request-client 2025-09-11 11:22:47 +08:00
zhongming4762
eb4f1f8164 feat: add SSE support to request-client 2025-09-11 10:50:19 +08:00
谦元吉
6a85b3ab84 fix(@vben/common-ui): resolve #6698 (#6700)
* fix(@vben/common-ui): resolve #6698

* fix(@vben/common-ui): resolve #6698

* fix(@vben/common-ui): resolve conversation

* fix(@vben/common-ui): resolve #6698
2025-09-02 17:02:11 +08:00
谦元吉
c57592f3a6 fix(@vben-core/shadcn-ui): 修复VbenPinInput组件在点击获取验证码和回车同时进行时会重复触发计时器的问题 (#6705) 2025-09-02 16:56:46 +08:00
Ken Hai
b0d7b177be fix: 角色管理,授权的树形组件,取消勾选叶子节点,父级节点状态错误 (#6680)
Co-authored-by: haiyinlong <haiyinlong@uhigame.com>
2025-08-25 21:32:01 +08:00
LinaBell
566d3bdf7c fix: appendToMain warning (#6684) 2025-08-25 21:25:29 +08:00
螃蟹
adbf793e79 fix(@vben/web-ele): the main color tone for switching between dark and light themes has been reset (#6678) 2025-08-25 11:03:54 +08:00
LinaBell
cf6c4c9aae fix: cannot read properties of null (reading 'nextSibling') (#6667) 2025-08-21 22:26:10 +08:00
Ken Hai
ffaf85c8f3 fix: 修复角色修改时VbenTree组件没有回显选中 (#6662)
* fix: 修复角色修改时VbenTree组件没有回显选中

* chore: use nextTick

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

* fix: merge

* chore: 更新

---------

Co-authored-by: haiyinlong <haiyinlong@uhigame.com>
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-21 15:30:58 +08:00
panda7
2cc78f925f fix: the bug in the lock method of the vbenModal component (#6648) 2025-08-21 15:17:55 +08:00
ming4762
93f0eea4e7 fix: fix the issue of excessive line spacing in vbenForm (#6653)
* gap-2和 pb-4/2 重叠导致间距过宽,gap-x只保留列间距
2025-08-21 12:41:04 +08:00
lighua
fddfc6d494 fix: 增加对不支持的 HTTP 方法的错误处理 2025-08-21 11:13:02 +08:00
谦元吉
58e3941810 chore(docs): update the component import of the form adapter description in the document (#6656) 2025-08-19 16:48:10 +08:00
Svend
3ad433a50b fix: 修复在 hash 路由模式下无法在新窗口打开路由的问题 (#6652)
此问题是由于 PR #6583 中新增的 `resolveHref` 函数导致的。其在 hash 路由模式下,得到的 URL 会包含 #/ 前缀。在经过 openRouteInNewWindow 的逻辑后就会出现两次 /# 前缀
2025-08-19 16:47:45 +08:00
ming4762
8ac2db5b7c fix: fix the issue of VbenForm compact reactive failure (#6654) 2025-08-19 16:46:14 +08:00
Elm1992
a441dcebae fix: meta.link invalid issue 2025-08-19 16:40:16 +08:00
Vben
ff4704d5ea chore: Upgrade vite to version 7.x (#6645) 2025-08-16 22:50:31 +08:00
菠萝吹雪
6ddfbd84b0 chore: modify the contributor showcase in the README (#6636) 2025-08-16 22:47:08 +08:00
ming4762
1e6417f95b feat: vBenForm add layout: inline (#6644) 2025-08-16 22:41:08 +08:00
vben
e147a9d2fd chore: release 5.5.9 2025-08-16 22:16:02 +08:00
谦元吉
4efebb8c0b fix:update (#6635) 2025-08-14 22:01:12 +08:00
gxc685
9ce0df88ae fix: 修复mock里面eventHandler重复导致无法启动 (#6631) 2025-08-14 22:00:54 +08:00
谦元吉
3cf0c0eb04 fix(@vben/backend-mock): go back to the last modification (#6634)
* fix(@vben/backend-mock): the version went back to the last submission, and the latest submission was completely useless.

* fix: resolve conflicts
2025-08-14 12:06:41 +08:00
谦元吉
ab7e363279 fix(@vben/backend-mock): fix all ts type errors in this module (#6613)
* fix(@vben/backend-mock): 修复所有 ts 类型报错

* fix(@vben/backend-mock): 修复该模块所有 ts 类型报错

* fix(@vben/backend-mock): 解决 coderabbitai

* fix(@vben/backend-mock): 解决 coderabbitai

* fix(@vben/backend-mock): 解决 coderabbitai
2025-08-12 17:23:39 +08:00
lighua
df655015b1 fix: 优化文件下载器方法 2025-08-08 15:31:31 +08:00
xueyang
9fc594434f perf: 优化useVbenForm样式 (#6611)
* perf(style): 优化useVbenForm垂直布局 actions 样式

* perf(style): 优化useVbenForm actions 布局样式

- 操作按钮组显示位置
```
actionPosition?: 'center' | 'left' | 'right';
```
- 操作按钮组的样式
```
actionType?: 'block' | 'inline'
inline: 行类显示,block: 新一行单独显示
```

* perf: 优化useVbenForm actions 布局样式

删除 actionType
增加 actionLayout
- actionLayout?: 'inline' | 'newLine' | 'rowEnd';
- newLine: 在新行显示。rowEnd: 在行内显示,靠右对齐(默认)。inline: 使用grid默认样式
- 删除无用代码 queryFormStyle

* perf: 优化useVbenForm使用案例

* perf: 优化form组件样式

去掉padding,改为gap

* docs: update vben-form.md

* fix: 修复FormMessage位置

* perf: Avoid direct mutation of props object.

-  props.actionLayout = props.actionLayout || 'rowEnd';
-  props.actionPosition = props.actionPosition || 'right';
+  const actionLayout = props.actionLayout || 'rowEnd';
+  const actionPosition = props.actionPosition || 'right';

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

* fix: 修复 wrapperClass 权重

* fix: 全局搜索结果不匹配 #6603

* fix: 避免FormMessage溢出

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-07 23:48:34 +08:00
leo
b93e22c45a fix(@vben/layouts): respect base URL when opening route in new window (#6583)
Previously, the generated URL for opening routes in a new window did not include the router base,
which led to incorrect paths when the app was deployed under a subdirectory (e.g., /admin/).
This change ensures that the resolved path includes the configured base by using
router.resolve(path).href.
2025-07-29 13:46:05 +08:00
Jin Mao
193f5b6512 Merge branch 'main' into 2025072604 2025-07-28 15:53:04 +08:00
Jin Mao
cb3f96683f fix: 修复双列布局模式下,路由为hideInMenu时,空白右列 2025-07-28 15:50:21 +08:00
zhongming4762
06ffdf164a feat: add dingding login 2025-07-25 22:02:55 +08:00
ming4762
5b75e5e917 perf: perf the control logic of VbenModal full screen and header (#6566)
* resolve the issue of header=false and full screen button display but not operable
2025-07-25 21:45:45 +08:00
aonoa
fad0b49841 fix: adding roles does not automatically refresh (#6548)
* fix: adding roles does not automatically refresh

* style: fix code style err
2025-07-25 21:35:57 +08:00
Jin Mao
260e45cd7b Merge branch 'main' into feat/add-vben-modal-animation 2025-07-25 21:33:11 +08:00
vben
1575619d53 chore: release v5.5.8 2025-07-19 22:19:50 +08:00
Jin Mao
d5a36a167d fix: fix vxeTable commit proxy (#6536)
* fix: 修正use-vxe-grid中的代理配置提交类型

* chore: change config
2025-07-19 16:07:15 +08:00
panda7
fc9ea347ca Merge branch 'main' into feat/add-vben-modal-animation 2025-07-18 00:38:54 +08:00
 panda7
1a9b0509d5 feat: add animation effects to VbenModal component 2025-07-18 00:15:40 +08:00
panda7
07b64ad384 feat: add function support for formItemClass prop (#6511)
* feat: add function support for formItemClass prop

* feat: add try-catch to formItemClass function

* fix: formItemClass function ts error

---------

Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-07-17 09:37:39 +08:00
Jin Mao
1bc5d2986b chore: update-vxe-table (#6516)
* chore: update vxe-pc-ui,vxe-table

* fix(ui): 修复代理配置初始化方法名错误

* fix(ui): 修改远程表格刷新配置

* chroe: update vxeTable

更新到最新
2025-07-16 19:15:39 +08:00
HamWong
bb36cca315 fix: 锁定屏幕页面样式自适应 (#6480) 2025-07-15 09:08:08 +08:00
Jin Mao
b8bf482c6a Merge branch 'main' into form 2025-07-07 09:16:54 +08:00
Jin Mao
3b673ca915 Merge branch 'main' into feature/scroll_to_the_error_field 2025-07-07 08:21:50 +08:00
Jin Mao
bbf0287511 chore: fix lint warning (#6487) 2025-07-07 08:21:25 +08:00
panda7
d4786f3f75 Merge branch 'main' into feature/scroll_to_the_error_field 2025-07-06 21:19:52 +08:00
xue-jn
b333fd676d docs: update vben-drawer.md (#6478)
* docs: update vben-drawer.md

* docs: update vben-drawer.md

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-07-06 20:26:06 +08:00
sqchen
f1051c8773 feat: add scrollToFirstError to the form component 2025-07-05 00:50:53 +08:00
sqchen
243f3a201d feat: add scrollToFirstError to the form component 2025-07-05 00:19:12 +08:00
陈蔚然
2f7de243f6 Merge branch 'vbenjs:main' into slider-translate-captcha 2025-07-04 10:28:40 +08:00
chenweiran
1aafb43103 feat: 增加基于图片拼图切片平移的验证码 2025-07-04 10:26:02 +08:00
chenweiran
8ccd01ade5 feat: 增加基于图片拼图切片平移的验证码 2025-07-04 10:23:47 +08:00
Netfan
e6bfbce6cb revert: page height fixed,revert #6422 2025-07-03 22:29:48 +08:00
someone-cool
1f63aed64c fix(@vben/common-ui): add page footer's height to calc contentstyle (#6422) 2025-07-03 18:36:42 +08:00
xue-jn
253b0da7d2 fix: 接口返回子节点为空数组时,还会显示折叠箭头 (#6463) 2025-07-03 18:35:03 +08:00
RanMaoting
33a4d524db feat(@vben/plugins): 新增VxeGrid组件插槽类型定义 (#6452)
* feat(@vben/plugins): 新增VxeGrid组件插槽类型定义

Closes: #6451

* fix(@vben/plugins): 优化vxe-table组件的插槽类型定义

修复Omit导致的类型丢失
2025-07-03 18:34:37 +08:00
chenweiran
bbd8a53d9d feat: 增加基于图片拼图切片平移的验证码 2025-07-03 18:20:20 +08:00
chenweiran
8554924cb9 feat: 增加基于图片拼图切片平移的验证码 2025-07-03 17:43:19 +08:00
RanMaoting
fee811d950 fix: 优化组件方法透传并新增表单弹窗示例 (#6443) 2025-07-02 19:58:48 +08:00
Netfan
78076e70b4 chore: update deps 2025-07-02 16:55:55 +08:00
chewenye
b78bc65ce7 feat: 组件json-viewer支持bigint数据显示 cwy (#6377)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-06-29 04:32:30 +08:00
Utopia
b1fb623113 feat: 为 auth layout 添加 slot: logo, 提升组件的灵活性和可复用性 (#6442) 2025-06-27 19:23:24 +08:00
Stephen Chang
de14908fd3 fix(icon-picker): 解决icon-picker组件切换分页后,关键词检索失效问题 (#6437)
当icon-picker组件切换分页后,在输入关键词检索,但是分页没有重置,导致检索结果异常
2025-06-27 19:21:23 +08:00
Li Kui
5c3972196a fix: Add $t import to login expired modal (#6429)
closes #6230
2025-06-27 19:20:25 +08:00
CG.gatspy
3230781538 feat: [vben-tree]增加数据disabled (#6343)
* feat: [vben-tree]增加数据disabled

* Update packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue

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

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-27 19:09:30 +08:00
broBinChen
e7fd0e3b6a feat(hooks): useHoverToggle的入参refElement支持传入响应式数组 (#6333)
* feat(hooks): useHoverToggle的入参refElement支持传入响应式数组

* feat(hooks): 1、增加 useHoverToggle 中 refElement 参数关于传入响应式数组的注释说明。 2、修改 watch 监听深度,仅需浅层监听 refs 变化。 3、使用 effectScope 管理 useElementHover 实例,避免 refs 变化时事件监听器累积导致的内存泄漏问题

* feat(hooks): 在useHoverToggle中增强 updateHovers  的边界处理,优化watch方案,只监听元素数量变化而不是整个数组变化,避免过度依赖收集

---------

Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
2025-06-27 19:08:41 +08:00
yuhengshen
2f7d1f009d fix: 全屏状态下弹窗圆角优化 (#6413) 2025-06-24 23:41:54 +08:00
yuhengshen
946f91f387 feat: optimize modal dragging range(#6414)
* 当弹窗指定了容器时,拖拽将被限制在容器范围内
2025-06-24 17:05:59 +08:00
lghuahua
986eacae9a fix: improve request logic in api-component
* 修复某些情况下updateParam设置的参数可能不会提交给api的问题
* 修复在上一个请求尚未完成前如果params发生了变更,将不会再触发新的api请求
---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-06-20 08:42:45 +08:00
Netfan
97b8e28a2b docs: fix delete request usage (#6389) 2025-06-17 08:52:59 +08:00
Netfan
c0962fec18 fix: auto close popup on deactivated (#6368)
* 修复挂载到内容区域的弹窗和抽屉被意外关闭的问题
2025-06-11 12:20:52 +08:00
XiaoHetitu
8ba7bdf2bd fix(button): 为按钮添加type属性防止表单提交意外触发表单验证机制 (#6340)
在按钮组件中,按钮元素缺少type="button"属性可能导致在表单中意外提交。添加此属性以确保按钮行为符合预期。

Co-authored-by: yuanwj <ywj6792341@qq.com>
2025-06-08 17:56:24 +08:00
zyy
b015fbc9fc fix: [adapter] 表格配置类型报错 (#6327)
配置toolbarConfig中的search时会有类型报错
2025-06-08 17:53:55 +08:00
broBinChen
b69320c070 feat(hooks): support separate enter/leave delays in useHoverToggle (#6325)
Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
2025-06-08 17:53:29 +08:00
zhang
dcccc213ce fix: requestClient.upload会将vbenform中value为undefined的值转为字符串undefined’提… (#6300)
* fix: requestClient.upload会将vbenform中value为undefined的值转为字符串undefined’提交给后台保存

* fix: requestClient.upload会将vbenform中value为undefined的值转为字符串'undefined’提交给后台保存
2025-06-08 17:51:16 +08:00
ali-pay
c0e601c020 fix: menu type is not 'button' (#6277)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-06-08 17:50:44 +08:00
RanMaoting
017ed1a9e1 types: 为useVbenVxeGrid添加泛型声明,使grid实例上能正确获取到行数据的类型 (#5653)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-06-08 17:43:02 +08:00
vben
b9aef618fe chore: release 5.5.7 2025-06-04 05:33:06 +08:00
Netfan
4102cc2211 feat: improve vbenCheckButtonGroup (#6329)
* 按钮组支持单选清除和多选限制最大选项数

* 按钮组支持icon插槽来定制图标
2025-06-03 23:11:56 +08:00
chewenye
ea776aa710 types: 扩展user-dropdown组件的menus类型,支持iconify (#6283)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-06-03 06:07:06 +08:00
huanghezhen
feb96dc8ea fix: resolve onClosed method failure in connectedComponent of useVbenModal (#6309) 2025-06-02 08:16:48 +08:00
wyc001122
470fd43b49 fix: 修复使用useVbenVxeGrid配置hasEmptyText、hasEmptyRender不生效的问题 (#6310) 2025-06-02 08:16:26 +08:00
zhang
76d106e474 fix: When defaultHomePage is inconsistent with user.homePath, the pa… (#6299)
* fix:  When defaultHomePage is inconsistent with user.homePath, the page refresh route jump will be abnormal

* fix:  When defaultHomePage is inconsistent with user.homePath, the page refresh route jump will be abnormal
2025-06-02 08:07:06 +08:00
wyc001122
78c3c9da6f docs(settings): 完善'生产环境动态配置'步骤 (#6297) 2025-06-02 08:05:23 +08:00
Netfan
081d08a7f8 fix: alert width fixed in small screen (#6312) 2025-05-30 19:54:26 +08:00
Netfan
96a10ca83f style: fix lint error (#6298) 2025-05-28 19:23:21 +08:00
wyc001122
f31360ba4e feat: support for hybrid permission access control mode (#6294)
* feat: 添加混合权限访问控制模式

* feat: 文档补充
2025-05-28 17:01:58 +08:00
wyc001122
4eb16d6d3a fix: fix table-title slot not work (#6295) 2025-05-28 17:01:11 +08:00
liqiang0330
53304514b6 fix: Update index.ts (#6268)
* Update index.ts

VxeGridPropTypes.原文件缺少这个,现在补全!

* Update index.ts

增加空格!
2025-05-26 13:29:27 +08:00
Netfan
6fbf1387f5 fix: reset slider-captcha after login failed (#6275) 2025-05-25 16:04:56 +08:00
Netfan
e5c937396d fix: json-bigint parse used in vxeTable (#6271)
* 修复vxeTable不能加载json-bigint解析的数据的问题
2025-05-24 13:01:58 +08:00
littlesparklet
af186f878d fix: repair the unexpected form default value (#5567)
* fix: Fix inconsistent spacing around search form (issue #5429)

* fix: repair the unexpected default value in validated form.(issue #5451)

* Update packages/@core/ui-kit/form-ui/src/use-form-context.ts

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

---------

Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-05-23 16:05:11 +08:00
wyc001122
97894a940e feat: optimize logo display (#6267)
* feat(VbenAvatar): add fit property to VbenAvatar component

* feat(VbenLogo): add fit property to VbenLogo component

* feat(VbenLogo): add logo fit preference configuration

- Add preferences.logo.fit setting for logo display control
- Include corresponding documentation for the new preference

* feat(preferences): add default value for logo.fit preference

- Set default configuration for logo fit behavior
- Ensures consistent logo display across applications

* test(preferences): update configuration snapshots

---------

Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-23 15:24:01 +08:00
yingzi2019
48d70182b4 feat: improve check updates (#6257)
Co-authored-by: monkey <maotao@tutamail.com>
2025-05-23 15:23:06 +08:00
Netfan
a1091bad46 feat: enhances compatibility with APIs returning large numeric values (#6250) 2025-05-23 15:22:18 +08:00
zhang
9f9be21e2a fix: component Input is not registered when initialize page (#6246)
* fix: Component Input is not registered when initialize page

* fix: Component Input is not registered when initialize page
2025-05-23 15:21:09 +08:00
panda7
a2bdcd6e49 feat: ellipsis text automatically displays tooltip based on ellipsis (#6244)
* feat: ellipsis text automatically displays tooltip based on ellipsis

* feat: ellipsis text automatically displays tooltip based on ellipsis

---------

Co-authored-by: sqchen <9110848@qq.com>
Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-05-23 15:20:38 +08:00
ali-pay
11b2b5bcc2 fix: 修复菜单管理中按钮类型值错误的问题 (#6255) 2025-05-22 09:09:31 +08:00
LinaBell
ebef2c91e2 fix: tab cannot be displayed correctly after browser refresh (#6256) 2025-05-22 09:04:40 +08:00
Netfan
0c3edb10b0 fix: getFieldComponentRef will return actual ref within AsyncComponentWrapper (#6252)
修复异步加载组件时,表单的getFieldComponentRef方法没能获取到正确的组件实例
2025-05-21 14:48:51 +08:00
zwtvip
f7bae8ac0f chore: export framework components for use in independent pages 2025-05-21 00:37:26 +08:00
wyc001122
8ac97688da fix(preferences): 更新内容内边距默认值 (#6233)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-20 09:50:23 +08:00
李轻舟
2efacb3e5b docs: Update build.md (#6228) 2025-05-19 16:30:39 +08:00
wyc001122
dae46abb71 feat: additional-settings (#6225)
* feat(preferences): 补充VbenAdminLayout传入属性(来自偏好设置)

* docs(@vben/docs):update settings doc

---------

Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-19 16:29:15 +08:00
wyc001122
5ee2a74e2d fix(use-design-tokens): 完善element-plus暗色主题颜色 (#6224)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-19 16:27:34 +08:00
afe1
d0b8349a2d perf: stub unbuild params (#6210) 2025-05-18 10:35:20 +08:00
wyc001122
34c4ecb047 fix: in mixed layout mode, the sidebar does not display when the first child node is an external link (#6219)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-18 10:34:41 +08:00
ming4762
3d9dba965f perf: perf the control logic of Tab (#6220)
* perf: perf the control logic of Tab

* 每个标签页Tab使用唯一的key来控制关闭打开等逻辑
* 统一函数获取tab的key
* 通过3种方式设置tab key:1、使用router query参数pageKey 2、使用路由meta参数fullPathKey设置使用fullPath或path作为key
* 单个路由可以打开多个标签页
* 如果设置fullPathKey为false,则query变更不会打开新的标签(这很实用)

* perf: perf the control logic of Tab

* perf: perf the control logic of Tab

* 测试用例适配

* perf: perf the control logic of Tab

* 解决AI提示的警告
2025-05-18 10:33:02 +08:00
wyc001122
024c01d350 fix(@vben-core/shadcn-ui): fix disabled functionality not working in VbenTree component (#6205)
* fix(@vben-core/shadcn-ui): fix disabled functionality not working in VbenTree component

* fix(@vben-core/shadcn-ui): add cursor-not-allowed className when disabled and disable onfocus

---------

Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-05-16 14:13:43 +08:00
afe1
2adb8acd80 fix: css style (#6176) 2025-05-16 09:40:40 +08:00
panda7
a23bc4cb5c fix: the mobile terminal can wrap lines and expand slot attributes (#6165)
Co-authored-by: sqchen <chenshiqi@sshlx.com>
2025-05-16 09:40:05 +08:00
XiaoHetitu
cf17a45d8d feat(tabs): 支持计算属性作为标签标题,解决 #6170 的问题 (#6163)
* feat(tabs): 支持动态函数作为标签标题

修改 `setTabTitle` 和 `tabsView` 逻辑,允许传入函数作为标签标题,以便动态生成标题内容

* feat(tabbar): 添加动态设置标签页标题功能

允许设置静态字符串或动态函数作为标签标题,支持根据状态或语言变化动态更新标题

* refactor(tabs): 移除冗余的newTabTitle2变量并优化标题设置逻辑

移除tabs组件中冗余的newTabTitle2变量,直接使用newTabTitle作为标题来源。同时,优化use-tabs和tabbar模块的标题设置逻辑,支持ComputedRef作为动态标题,提升代码简洁性和可维护性。

---------

Co-authored-by: yuanwj <ywj6792341@qq.com>
2025-05-16 09:37:50 +08:00
chewenye
b46ebe756e types: 导出authentication组件的type,自定义toolbarList时类型使用ToolbarType (#6158)
Co-authored-by: 车文烨 <chewy@china-lehua.com>
2025-05-16 09:36:49 +08:00
Netfan
e89cf400c0 fix: refresh command of tabbar issue, fixed: #6162 (#6169) 2025-05-12 23:34:08 +08:00
anyup
9e67929ee7 feat: support to refresh the tab page by route name (#6153)
Co-authored-by: anyup <anyupxing@163.com>
2025-05-10 22:33:31 +08:00
afe1
90625782c0 fix: delete useless code (#6143) 2025-05-08 16:51:12 +08:00
wyc001122
84ef207d9c docs(@vben/docs): update settings doc (#6128)
Co-authored-by: wyc001122 <wangyongchao@testor.com.cn>
2025-05-07 12:04:48 +08:00
zyf0624
e68fff58e8 fix: tsconfig moduleResolution (#6122)
Co-authored-by: pzzyf <2279948211@qq.com>
2025-05-07 12:04:15 +08:00
Netfan
bf70539221 fix: missing argument for getPopupContainer 2025-05-06 22:48:03 +08:00
Leeson
5949c73a30 fix: delete Popconfirm being obscured by fixed columns (#6118)
* fix: delete Popconfirm being obscured by fixed columns

* fix: opened popConfirm will prevent the table from scrolling

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-05-06 22:33:17 +08:00
vben
cc6c9bf7a0 chore: release v5.5.6 2025-05-06 22:32:58 +08:00
Jin Mao
6b1aab9c67 fix: handle undefined children in generate-menus (#6117)
When children is undefined, use empty array as fallback to prevent potential runtime errors. This matches the behavior when hideChildrenInMenu is true.
2025-05-06 14:29:50 +08:00
LinaBell
8f4d3d418d fix: when keepAlive is enabled, returning directly through browser buttons/gestures will not close pop ups (#6113) 2025-05-06 14:02:23 +08:00
ming4762
3b3f8e4e44 fix: fix IconPicker props warning (#6108)
Invalid prop: type check failed for prop "onUpdate:value". Expected Function, got Array
2025-05-06 09:30:37 +08:00
vben
f94ca10adf chore: remove prepare script from package.json 2025-05-04 07:33:36 +08:00
vben
4471bc7a5d chore: update prepare script in package.json to remove lefthook installation 2025-05-04 00:05:29 +08:00
Vben
5689ac60ff feat(project): migrate from husky and lint-staged to lefthook (#6104) 2025-05-03 19:43:12 +08:00
Vben
045bc4e5ee feat: support smooth auto-scroll to active menu item (#6102) 2025-05-03 18:05:26 +08:00
Vben
17a18fc9ba chore: close eslint object sorting (#6101) 2025-05-03 16:06:36 +08:00
aonoa
41152d1722 refactor: modify the default homepage path loaded from the preference… (#6099)
* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

* refactor: modify the default homepage path loaded from the preferences.ts

Signed-off-by: aonoa <1991849113@qq.com>

---------

Signed-off-by: aonoa <1991849113@qq.com>
2025-05-03 16:03:08 +08:00
Netfan
f1af9f8f6e fix: add triggerClass binding to PopoverTrigger and update icon-picker styles (#6095)
* Popover支持设置trigger的样式
* 修正icon-picker的input值更新
2025-05-01 21:40:45 +08:00
Netfan
0517a7014f fix: add missing translation for preferences drawer (#6094) 2025-05-01 20:08:44 +08:00
Netfan
3e6d608a2f fix: destroyOnClose incorrect default value, fixed #6092 (#6093) 2025-05-01 14:09:37 +08:00
ming4762
5de954baa4 fix: fix LoginExpiredModal in some cases, message may be obscured (#6086) 2025-05-01 10:40:42 +08:00
Netfan
add1e61b6f fix: show validation message as tooltip in compact form (#6087)
* 紧凑模式表单的校验消息将显示为一个tooltip
2025-04-30 23:41:44 +08:00
Jin Mao
20c15f352f perf: page componet supports custom height offset for flexible content height … (#6081)
* perf: Page supports custom height offset for flexible content height control.

允许通过 height 属性调整页面内容高度计算。修改了 Page 组件以支持自定义高度偏移量,用于更灵活的内容高度控制。

* chore: typo

* perf(page): replace height with heightOffset for flexible content sizing

The `height` prop was replaced with `heightOffset` to better describe its purpose when used with `autoContentHeight`. The new prop allows custom offset values (in pixels) to adjust content area sizing, with clearer documentation.
2025-04-29 18:15:12 +08:00
Netfan
8aa7dabeff fix: calculation for collapsing search form is incorrect while initially hidden (#6068)
* 修复当默认隐藏搜索表单时,折叠位置的计算不正确的问题
2025-04-28 23:20:33 +08:00
vben
78c7c1589a chore: update readme.md 2025-04-28 23:11:34 +08:00
Vben
dd833ca56b chore: update dependencies and documentation, optimize build toolchain (#6060)
* chore: update packageManager version to pnpm@10.9.0 for compatibility improvements

* chore: Update dependent versions and configurations to improve compatibility and stability

- Update Node version to 22.1.0
- Updated pnpm version to 10.10.0
- Fixed syntax error in prettier command in lintstagedrc
- Update dependent versions in pnpm-lock.yaml to ensure consistency
- Update format and content in README documents to improve readability

* fix: lint error
2025-04-28 23:08:05 +08:00
vem
681c1dc267 fix: Update existing route index to prevent 404 on user switch (#6003)
Co-authored-by: tars-macmini <vem@qq.com>
2025-04-28 18:19:47 +08:00
Netfan
4545422ee0 fix: lock state will not change overflow style in drawer and modal (#6067)
* Modal和Drawer的锁定状态不再修改overflow样式
2025-04-28 17:02:54 +08:00
Gahotx
ca94ca906f fix: add rounded corners to project and quick nav items (#5296) 2025-04-27 22:50:42 +08:00
Vben
76de450c71 chore: update dependency version for improved stability and compatibility (#6023)
* chore: update dependency version for improved stability and compatibility

* fix: optimize clearPoints function in useCaptchaPoints hook to improve performance

* fix: make several props optional in various components for better flexibility
2025-04-27 22:06:49 +08:00
Trivikram Kamat
dd2b1ed580 fix: install corepack from npm (#5905)
* fix: install corepack from npm

* docs: install corepack from npm
2025-04-27 22:03:35 +08:00
ming4762
baec89f896 perf: resolve duplicate component names (#6039) 2025-04-27 22:02:38 +08:00
vben
7c7051a11e chore: release v5.5.5 2025-04-27 21:45:10 +08:00
Netfan
aa27a2f7a1 feat: encrypt the privacy data when it is persisted (#6056)
* 对私密数据持久化时执行加密
* 将锁屏密码合并到accessStore中进行加密
2025-04-27 20:59:10 +08:00
Jin Mao
9ee6d06d50 docs: add deepWiki doc link (#6057) 2025-04-27 20:54:07 +08:00
ming4762
0cc1cb5a7b perf: improve destroyOnClose for VbenDrawer&VbenModal (#6051)
* fix: fix that the default value of modal destroyOnClose does not take effect

* perf: improve destroyOnClose for VbenDrawer
2025-04-27 11:26:50 +08:00
Netfan
0a9fc4e02d fix: title of search button in vxeTable toolbar (#6046)
* 修改vxeTable工具栏里的搜索按钮的提示文案
2025-04-26 01:08:41 +08:00
Netfan
be840460d8 feat: vbenSelect support prop allowClear (#6043) 2025-04-25 23:37:03 +08:00
Netfan
cb45987fe2 docs: update example (#6036)
* 跟进后端菜单逻辑的修改,现已无需传递basicLayout布局
2025-04-25 11:44:47 +08:00
panda7
5ffd7db8e0 fix: the initial value echo for the check-button-group (#6029)
Co-authored-by: sqchen <9110848@qq.com>
2025-04-25 08:35:03 +08:00
Netfan
14377705e7 fix: alert confirm state in beforeClose callback (#6019) 2025-04-23 12:20:52 +08:00
pangyajun123
b985ff0584 fix: vxe-table theme token follow primary color (#6007) 2025-04-21 19:15:05 +08:00
wyc001122
b148b8ec92 fix: fix geader menu activation path (#5997)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-04-19 14:35:33 +08:00
Netfan
79de6bcbf7 fix: alert send wrong confirm state to beforeClose (#5991)
* 修复alert在按下Esc或者点击遮罩关闭时,可能发送错误的isConfirm状态
2025-04-17 22:23:05 +08:00
Netfan
14bd6dd25d fix: destroyOnClose works within connectedComponent (#5989)
* 修复destroyOnClose没能销毁connectedComponent自身的问题
2025-04-17 20:25:49 +08:00
PIPEDREA_WZJ
7f269e0d69 Update tailwindcss.md (#5602)
tailwindcss最新的版本已经是v4.x,vben中使用的是3.x的tailwindcss。在未进行兼容前,会出现运行失败的问题
2025-04-17 14:01:39 +08:00
yuh
4baec83db5 feat: add examples: form-upload (#5955)
* feat: add examples: form-upload

* fix: upload: accept and label

* fix: upload: 设置表单值、图片预览
2025-04-17 14:00:46 +08:00
Netfan
f7a4d13a4c fix: fixed arguments of callbacks in formApi (#5970)
* 修复 `handleValuesChange` 传递的参数不是处理后的表单值的问题

* 修复 `handleReset` 未能传递正确参数的问题
2025-04-16 14:11:04 +08:00
Netfan
0936861da1 feat: pass fieldsChanged into the handleValuesChange callback function (#5968)
* fieldsChanged(已被改变值的字段名)将传入handleValuesChange回调函数
2025-04-16 11:29:01 +08:00
ming4762
3318d76bab perf: improve destroyOnClose for VbenModal (#5964) 2025-04-16 11:28:36 +08:00
LinaBell
8f3881eabf perf: beforeClose of drawer support promise (#5932)
* perf: the beforeClose function of drawer is consistent with that of modal

* refactor: drawer test update
2025-04-16 11:27:13 +08:00
zhouda1fu
5252480b09 fix: missing await in department form(#5967) 2025-04-16 11:22:59 +08:00
Netfan
d18f56177c docs: update alert and apiComponent docs (#5961) 2025-04-15 20:52:23 +08:00
wyc001122
333998b518 fix: determine if scrollbar has been totally scrolled (#5934)
* 修复在系统屏幕缩放比例不为100%的情况下,滚动组件对是否已滚动到边界的判断可能不正确的问题
2025-04-15 20:51:38 +08:00
ming4762
3fb4fba1cb fix: modal closing animation (#5960) 2025-04-15 18:49:57 +08:00
ming4762
c7e6210c8d feat: modal&drawer support center-footer slot (#5956) 2025-04-15 16:04:44 +08:00
lztb
d864085c13 feat: vben-form添加arrayToStringFields属性 (#5957)
* feat: vben-form添加arrayToStringFields属性

* feat: 修改handleArrayToStringFields和handleStringToArrayFields中嵌套数组格式的处理不一致

---------

Co-authored-by: 米山 <17726957223@189.cn>
2025-04-15 16:03:20 +08:00
Netfan
fcdc1a1602 feat: add more expose methods for apiComponent (#5958)
* 为ApiComponent组件添加getOptions和getValue导出方法。
2025-04-15 15:32:30 +08:00
Netfan
bf7496f0d5 feat: add useAlertContext for Alert component (#5947)
* 新增Alert的子组件中获取弹窗上下文的能力
2025-04-15 00:00:05 +08:00
Netfan
9700150653 fix: table actions in fixed column (#5945) 2025-04-14 19:56:52 +08:00
Netfan
f0e9e55af2 feat: alert support customize footer (#5940)
* Alert组件支持自定义footer
2025-04-14 11:48:21 +08:00
Netfan
ff88274554 fix: long navigation menu can be scrolled (#5939)
* 修复超长的导航菜单无法纵向滚动的问题
2025-04-14 11:18:33 +08:00
ming4762
afce9dc5c0 perf: improve destroyOnClose for VbenModal (#5935)
* perf: 优化Vben Modal destroyOnClose,解决destroyOnClose=false,Modal依旧会被销毁的问题

影响范围(重要):destroyOnClose默认为true,这会导致所有的modal都会默认渲染到body
radix-vue Dialog组件默认会销毁挂载的组件,所以即使destroyOnClose=false,Modal依旧会被销毁的问题
对于一些大表单重复渲染导致卡顿,ApiComponent也会频繁的加载数据

* fix: modal closing animation

---------

Co-authored-by: Netfan <netfan@foxmail.com>
2025-04-13 23:02:07 +08:00
ming4762
b5700bd0b1 perf: improve autoSelect of ApiComponent (#5936)
* fix: 修复autoSelect不生效的问题,props.valueField已经被omit了

* feat: ApiComponent autoSelect支持使用函数,可以满足灵活性要求更高的场景
2025-04-13 20:03:18 +08:00
Netfan
a8c4786311 feat: api-component support autoSelect prop (#5931)
* feat: api-component support autoSelect prop

* docs: add version requirement
2025-04-12 14:02:35 +08:00
Netfan
2971ccc0b7 docs: docs modal z-index fixed, update alert docs (#5930) 2025-04-12 13:41:40 +08:00
Netfan
4a2c7b313f fix: alert animation (#5927) 2025-04-12 10:37:47 +08:00
Netfan
36bf6fc149 fix: builtin color change throttled in preference drawer (#5924)
修复偏好设置中的自定义主题色拖动选择颜色时页面会明显卡顿的问题
2025-04-12 01:44:08 +08:00
Netfan
f46ec30995 fix: theme mode follow the system only auto (#5923)
* 修复主题在未设置为auto时,仍然会跟随系统主题变化的问题。
2025-04-12 01:16:57 +08:00
Netfan
9bd5a190c2 fix: alert action button focus, fixed #5921 (#5922)
* 修复Alert组件的按钮焦点切换问题
2025-04-12 00:59:56 +08:00
zhang
86da3cedc2 chore: 导出框架自带的组件,方便独立页面使用 (#5876) 2025-04-09 16:16:56 +08:00
Netfan
329a176a5c perf: optimize bootstrap modules to speed up first-screen loading (#5899)
优化首屏加载速度
2025-04-09 01:05:20 +08:00
Netfan
9379093a4f feat: customizable table separator (#5898)
* 表格的分隔条支持定制背景色或完全移除
2025-04-08 20:28:50 +08:00
ming4762
c9014d0338 perf: 优化关闭页面切换动画的tab切换性能 (#5883) 2025-04-08 20:27:03 +08:00
Netfan
ed26dca64e chore: update pnpm-lock.yaml 2025-04-08 16:31:41 +08:00
Netfan
08c6496e24 chore: update deps 2025-04-08 14:56:40 +08:00
Netfan
a8c5df38e9 fix: possible circular reference issue during build (#5894)
* 修复构建期间出现的循环引用警告
2025-04-08 14:50:05 +08:00
Netfan
71e8d12b70 fix: improve prompt component (#5879)
* fix: prompt component render fixed

* fix: alert buttonAlign default value
2025-04-07 01:21:30 +08:00
Netfan
d216fdca44 feat: support logo text slot (#5872)
* 基础布局中的LOGO的文字区域允许通过插槽logo-text定制
2025-04-05 13:07:52 +08:00
wyc001122
384c5d7dbb fix: 布局为双列菜单或者水平模式下, 一级菜单高亮问题 (#5870)
Co-authored-by: 王泳超 <wangyongchao@testor.com.cn>
2025-04-05 11:04:59 +08:00
Netfan
b0ad08dbbc feat: use the not-found component instead of the invalid route component in the backend mode (#5871)
* 后端菜单模式下,使用not-found组件代替无效的路由组件
2025-04-04 15:21:09 +08:00
Rascal-Coder
3600603016 fix: vxeGrid height fixed #5861 (#5862) 2025-04-04 13:16:31 +08:00
superdl1996
cde1a85394 docs: typo (#5855) 2025-04-03 19:11:40 +08:00
Netfan
c623604ea9 docs: fix alert demo in docs 2025-04-02 20:37:41 +08:00
Netfan
7933da8f66 chore: update deps (#5854) 2025-04-02 18:07:06 +08:00
Netfan
ecf518bb02 fix: alert beforeClose callback arguments fixed (#5845) 2025-04-01 22:55:29 +08:00
ming4762
1d9f1be004 fix: 解决AccessModeType:backend登录过期,重新登录不会重新生成路由的问题,重现步骤分析: (#5830)
1、长时间未登录登录过期,再次打开页面构开始生成动态路由
2、fetchMenuListAsync后台返回401登录过期:doReAuthenticate函数跳转到登录页面
3、异常被拦截,return []
4、gurad.ts accessStore.setIsAccessChecked(true); 被错误的标识为已生成路由
5、重新登录后,accessStore.isAccessChecked=true未能正确的重新生成路由
2025-04-01 15:50:45 +08:00
Netfan
44138f578f feat: add preset alert, confirm, prompt components that can be simple called (#5843)
* feat: add preset alert, confirm, prompt components that can be simple called

* fix: type define
2025-04-01 15:10:18 +08:00
Joeshu
0e3abc2b53 docs: add third-party libraries to check update methods (#5819) 2025-03-31 19:28:28 +08:00
Arthur Darkstone
a96be3db98 docs: add explanation and related script configuration to distinguish build environment (#5826)
* docs: add explanation and related script configuration to distinguish build environment

* docs: fix spell error
2025-03-31 19:28:02 +08:00
Netfan
d6f239c564 docs: fix api-component demo link (#5828) 2025-03-31 12:08:45 +08:00
Netfan
166e9a0e82 chore: add demo for apiComponent with caching and concurrency (#5827)
* chore: add demo for apiComponent with caching and concurrency

* docs: update api component docs
2025-03-31 11:51:57 +08:00
Netfan
06ccad9db0 fix: vbenTree modelValue synchronization (#5825) 2025-03-31 10:18:35 +08:00
Jin Mao
18722ce434 feat: sidebar button config (#5818)
* feat: 新增 PreferenceCheckboxItem 组件

* feat(preferences): 添加侧边栏按钮配置功能

* feat: 新增按钮点击事件触发功能

* feat(SidebarPreferences): 新增侧边栏折叠按钮与固定按钮配置

* feat(ui): 新增侧边栏固定按钮及配置选项

* fix(test): 修正侧边栏配置项缺失问题
2025-03-31 10:17:42 +08:00
Netfan
a0feeb1966 fix: watermark settings in the preferences modified accidentally (#5823) 2025-03-31 09:06:02 +08:00
Jin Mao
df6341f0b8 feat(tabbar): 添加右键菜单过滤功能 (#5820) 2025-03-30 16:23:24 +08:00
anyup
dbc0b7e4a9 fix: route root.children duplicate problem (#5755)
Co-authored-by: anyup <anyupxing@163.com>
2025-03-29 22:38:30 +08:00
zhang
aa2907323f style: 更正引用格式 (#5784) 2025-03-29 22:29:16 +08:00
Netfan
96d2bc52e9 feat: pre-set serialization methods for request parameters (#5814)
添加快捷设置请求参数序列化方法的配置
2025-03-29 19:21:21 +08:00
Netfan
e91e4e0eea docs: fix form compact docs (#5811)
* docs: fix form `compact` docs

* docs: remove `compact` from FormCommonConfig
2025-03-28 16:15:35 +08:00
Netfan
c2b5f6497d fix: vben tree component warning (#5809) 2025-03-28 16:01:30 +08:00
Netfan
3c2d325d8c perf: improve api-component for using in form (#5796) 2025-03-27 15:51:11 +08:00
Netfan
a77bb8e68d perf: improve component packaging to enable instance method retrieval (#5795)
改进组件适配器里的包装函数,使得组件暴露的方法可以透传
2025-03-27 15:13:13 +08:00
Netfan
870cd86393 fix: auto check parent after node selected (#5794) 2025-03-27 14:22:05 +08:00
Netfan
0b650367f3 fix: popover background color in dark mode (#5783)
* 修复dark主题下的弹出层背景色在某些浏览器上表现为完全透明的问题
2025-03-25 21:17:55 +08:00
zhang
1616a06bfd perf: 优化多文件上传入参是数组的情况 (#5757)
Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com>
2025-03-25 10:26:19 +08:00
Netfan
3f0d30897f fix: menu drawer can not be closed after a successful submitting (#5770) 2025-03-23 11:34:21 +08:00
Jin Mao
66c1d390b9 feat(ui): logo icon support click events (#5725)
* feat(ui): 扩展auth页面添加点击 Logo 的事件处理

在 `authentication.vue` 中新增 `clickLogo` 属性,允许在点击 Logo 时执行自定义操作。在 `auth.vue` 中实现了一个示例的点击事件处理函数,用于测试该功能。

* feat(layout): 添加点击 logo 的事件处理函数

在 BasicLayout 组件中添加了 clickLogo 事件处理函数,并通过 emit 方法触发 clickLogo 事件。同时,在 basic.vue 中实现了 handleClickLogo 函数,用于处理 logo 点击事件。

* fix(ui): 移除logo点击事件的控制台日志
2025-03-23 10:02:22 +08:00
Netfan
03ceb2aac5 fix: default value for nested fields (#5763) 2025-03-21 16:06:14 +08:00
Netfan
39888cebaa fix: base component focus color in form (#5760) 2025-03-21 09:41:42 +08:00
Netfan
efb69fc75f feat: add form-is-required class for required items. fixed: #5739 (#5759) 2025-03-21 09:25:38 +08:00
Jin Mao
711a179c69 chore: update codeowners (#5750) 2025-03-21 08:37:21 +08:00
Netfan
3133f8f8b9 fix: table form reset will trigger reload twice while submitOnChange set (#5756)
* 修复表格的搜索表单在重置时,可能会触发2次表格刷新的问题
2025-03-20 19:42:22 +08:00
anyup
f0a43912d1 fix: sort the menu so that it doesn't get replaced with 999 when order=0 (#5753)
Co-authored-by: anyup <anyupxing@163.com>
2025-03-20 19:41:46 +08:00
vben
b92ac5c36d chore: release 5.5.4 2025-03-18 21:43:27 +08:00
chen-d-yu
504070f3eb fix: vsh stylelint script fixed (#5729) 2025-03-17 09:23:27 +08:00
Netfan
feab6b3b30 fix: form item style adjustment (#5694) 2025-03-11 02:47:06 +08:00
Netfan
2d4ac33046 fix: miss default value in vbenLayout 2025-03-10 18:59:56 +08:00
Netfan
17e2a02281 feat: auto set component name for keep-alive (#5690)
* fix: auto set component name for keep-alive

* fix: type define
2025-03-10 16:25:30 +08:00
Netfan
096545c5a1 docs: update table slots docs 2025-03-10 10:53:17 +08:00
Netfan
04dff33ac5 feat: improved formApi for component instance support
* 改进表单API以支持组件实例的获取,以及焦点字段的获取
2025-03-10 02:56:44 +08:00
Netfan
cfa18c2b8e fix: improve component repackaging 2025-03-10 02:56:44 +08:00
Netfan
13354955db chore: update depts 2025-03-10 02:56:44 +08:00
ijackwu
bb683804f4 fix: live-server set port use [--port=PORT] (#5687) 2025-03-09 08:49:06 +08:00
Netfan
e2a577de24 feat: add size prop to avatar component and update logo component for size handling (#5684) 2025-03-08 11:37:02 +08:00
Netfan
89d963c81a fix: vxeTable search button not working with slot (#5678) 2025-03-07 22:35:09 +08:00
Netfan
b37ed48b9d feat: role management page with component tree (#5675)
* feat: add shadcn tree

* fix: update vbenTree component

* feat: role management demo page

* feat: add cellSwitch renderer for vxeTable

* chore: remove tree examples
2025-03-07 16:03:08 +08:00
Netfan
4b9cfcb867 fix: demo nested menu path (#5667)
* 修复演示的嵌套菜单path配置导致的面包屑跳转问题
2025-03-06 22:48:54 +08:00
Netfan
f86c9f90ad fix: keepAlive not working for popup appendToMain (#5666)
* 修复弹窗和抽屉 `appendToMain` 时且启用`keepAlive` 时未能正确缓存的问题
2025-03-06 22:22:45 +08:00
Netfan
31a6ab59fb feat: vben checkbox support indeterminate state and transition animation (#5662) 2025-03-06 16:11:02 +08:00
Netfan
34789645f7 fix: nitro server cookie maxAge fixed 2025-03-04 22:29:27 +08:00
Netfan
f380452ef0 feat: modal and drawer locking improve (#5648)
* feat: add `unlock` for modalApi

* fix: modal's close button style in locking

* fix: fix modal's close button disabled on locking

* feat: add `lock` and `unlock` for drawerApi
2025-03-04 22:00:32 +08:00
jasonz18
decd9c55e5 docs: typo 2025-03-04 21:40:34 +08:00
Netfan
e815f0ff89 docs: vbenVxeTable slots docs update 2025-03-01 22:16:36 +08:00
Netfan
5ea6b4a8d8 fix: logo style in login page is affected by the globally-imported antd styles
* 修复登录页左上角LOGO部分的文字在全局导入antd样式的时候位置不正确的问题
2025-02-28 22:39:42 +08:00
Netfan
a53ca3faf1 chore: update depts 2025-02-28 20:30:59 +08:00
Netfan
86fdd6c93b fix: drawer close icon placement default value 2025-02-28 14:54:11 +08:00
Netfan
0e0661fe02 fix: breadcrumb style is affected by the globally-imported antd styles (#5627)
* 修复全局引入Antd时,面包屑的样式会受到影响的问题
2025-02-27 22:28:59 +08:00
Netfan
86ce65e0ea fix: hideChildrenInMenu demo code (#5626) 2025-02-27 20:21:48 +08:00
Netfan
c3eb4fab13 docs: fix zod rules docs 2025-02-27 17:27:00 +08:00
jinmao88
7a476372e1 fix: useDrawer中closeIconPlacement设置无效 (#5624) 2025-02-27 14:34:42 +08:00
Netfan
5e421ce607 chore: demo page menu management (#5619)
* 添加菜单管理演示页面
2025-02-27 01:22:25 +08:00
Netfan
1d8676f456 chore: remove sleep in department list api 2025-02-25 22:15:27 +08:00
Netfan
0c3dd92592 fix: getPopupContainer will return closet form first (#5612) 2025-02-25 22:07:56 +08:00
Netfan
d33261d0c2 chore: demo page for system/department (#5611)
* feat: department management demo

* perf: department page improve

* feat: demo api middleware

* fix: add losing import
2025-02-25 19:47:45 +08:00
Netfan
7041c6a106 chore: output console error for invalid route component (#5593) 2025-02-24 16:03:52 +08:00
littlesparklet
12ffb310bf fix: Fix inconsistent spacing around search form (issue #5429) (#5495) 2025-02-24 15:57:50 +08:00
Netfan
d9799fec70 fix: search take no effect in icon-picker with antd (#5592) 2025-02-24 14:13:53 +08:00
Netfan
4570d5b54b feat: add VbenButtonGroup and VbenCheckButtonGroup with demo (#5591)
* 添加按钮组、选择按钮组以及相应的Demo
2025-02-24 13:50:50 +08:00
Netfan
d49e3e81a4 fix: loading and spinner style fixed and improved (#5588) 2025-02-23 15:30:17 +08:00
Netfan
579b1b486c feat: loading and spinner component with directive (#5587)
* 添加loading和spinner组件,以及对应的vue指令
2025-02-23 12:41:54 +08:00
Netfan
eba372062e feat: improve form demo (#5582) 2025-02-21 12:07:32 +08:00
Netfan
c9ccd2bbab fix: form label and control style (#5580)
* fix: form label and control style

* fix: empty label mark with required rules
2025-02-21 11:14:59 +08:00
handsomeFu
5aff8bac10 fix: CountTo component resolve separator prop not taking effect (#5578) 2025-02-21 11:06:18 +08:00
Netfan
1a12687027 fix: vben count to animator event name fixed (#5573) 2025-02-20 23:53:47 +08:00
Netfan
a221d2b491 fix: form item overflow fixed and layout improved (#5572)
* fix: form item overflow fixed and layout improved

* fix: basic form demo update

* feat: form label support render

* fix: form docs update
2025-02-20 23:05:08 +08:00
anyup
ccd99eb24d fix: solve the problem of inconsistent returns of formSchema custom field names when code login (#5563) 2025-02-20 09:09:32 +08:00
Netfan
c5c6760b5d chore: eslint rules update 2025-02-18 15:41:16 +08:00
Netfan
c07281bf41 fix: form item slot context fixed (#5552)
* 修复表单插槽
2025-02-17 21:37:05 +08:00
Netfan
24bad09c74 refactor: new CountTo component with demo (#5551) 2025-02-17 21:16:10 +08:00
Netfan
cddf71e600 fix: playground route missing 2025-02-17 17:57:15 +08:00
Netfan
9f82052c71 feat: demo of motion plugin (#5550)
添加Motion的用法例子
2025-02-17 15:25:45 +08:00
Netfan
e0eb57d38d fix: nitro server cors support with cookie (#5549)
* 修复nitro server在使用cookie时的跨域配置
2025-02-17 15:17:31 +08:00
Netfan
b6b97accb1 feat: add more event for jsonViewer (#5546)
* 为JsonViewer添加事件支持
2025-02-17 10:41:09 +08:00
Netfan
799934171a style: code style fixed 2025-02-16 23:32:06 +08:00
Netfan
10ebf03698 fix: auth api definition 2025-02-16 23:06:20 +08:00
Netfan
cd258fbb52 chore: update deps 2025-02-16 23:03:41 +08:00
Netfan
6cba181fad feat: new component jsonViewer (#5544)
* 添加新组件JsonViewer用于展示JSON结构数据
2025-02-16 22:57:00 +08:00
jinmao88
f9504cece3 chore: add qq group 5 (#5530) 2025-02-13 14:45:51 +08:00
Netfan
182f1c9da8 fix: userDropdown triggered unnecessary while overlay shown (#5520)
* 修复顶部的用户资料下拉在弹窗被打开时,仍然可以被触发的问题
2025-02-12 17:59:59 +08:00
Netfan
e7b009786b fix: width for ellipsisText tooltip in popover content (#5517)
* 修复省略文本用在气泡中时,提示弹出层的宽度计算有误的问题
2025-02-12 14:25:12 +08:00
Netfan
5262233312 feat: tabbar support max count limit (#5490)
* 标签栏支持限制打开的最大数量
2025-02-06 19:33:10 +08:00
Netfan
a9f9031f49 docs: update form docs (#5485) 2025-02-06 09:45:28 +08:00
Netfan
061fcf926d chore: update deps 2025-02-04 17:14:14 +08:00
Netfan
7e7a5f3fd4 chore: remove testing code 2025-02-04 15:07:10 +08:00
Netfan
f8bb396dc4 fix: ant tag icon default style (#5473) 2025-02-04 10:19:39 +08:00
jsxz
a832edce0d docs: update request and access docs (#5468)
* fix: Update server.md

* docs: update request and access docs
2025-02-03 16:29:34 +08:00
Netfan
67d1f299b3 fix: renderComponentContent lose slot props data (#5466)
* 修复FormItem传递插槽时丢失插槽props的问题
2025-01-26 22:33:16 +08:00
Netfan
cb7c0ecaa2 fix: menu data for backend mode fixed (#5465) 2025-01-26 20:37:37 +08:00
Netfan
e225159cce fix: request download and upload not support responseReturn (#5456)
* fix: request download and upload not support `responseReturn`

* docs: update

* fix: type of request client upload result
2025-01-22 00:59:10 +08:00
vben
195ceec9b4 chore: release 5.5.3 2025-01-21 22:07:55 +08:00
Netfan
5bd73867b6 feat: auto fetch icon list in iconPicker (#5446)
* feat: auto fetch icon list in iconPicker

* fix: add timeout controller for fetching

* feat: add pending controller

* fix: icon demo prefix
2025-01-21 13:09:42 +08:00
Netfan
22e6f28464 perf: easy to define fieldName of response data (#5442) 2025-01-20 18:38:49 +08:00
Netfan
5611f6c7f5 perf: request support to set how to return response (#5436)
* feat: request support to set how to return response

* docs: typo

* fix: test unit

* test: add request responseReturn test
2025-01-19 17:41:26 +08:00
Netfan
3f0f4d50a1 fix: antd button icon style (#5421) 2025-01-17 14:30:49 +08:00
Netfan
2d0859a727 fix: mouse events ignored on modal loading (#5409) 2025-01-16 12:17:08 +08:00
Netfan
509b268fba docs: update docs (#5408) 2025-01-16 11:30:03 +08:00
Netfan
c3129663eb fix: form update state error before form mounted (#5406) 2025-01-15 20:11:32 +08:00
Netfan
816d1f5a69 fix: demos route fixed (#5405) 2025-01-15 19:24:02 +08:00
Netfan
8cc903c0e1 feat: modal state locked on submitting (#5401)
* feat: modal state locked on submitting

* docs: 更新modal文档
2025-01-15 17:00:46 +08:00
Netfan
13087a10b7 refactor: fix popup component zIndex (#5397) 2025-01-15 12:32:03 +08:00
Netfan
27a3888e35 style: element plus loading style fixed (#5393)
* Element Plus的loading组件默认zIndex太高
2025-01-15 00:49:11 +08:00
Netfan
fb0ec05ff8 perf: improve fieldMappingTime to support format function (#5392) 2025-01-14 18:15:00 +08:00
Netfan
76c4aa2c55 fix: hide root route in breadcrumb 2025-01-14 17:51:39 +08:00
Netfan
e1c503e51e feat: support set default props for drawer and modal (#5390)
* feat: support set default props for drawer and modal

* docs: fix typo
2025-01-14 17:11:18 +08:00
Netfan
5965755caa fix: root router config fixed (#5389) 2025-01-14 15:15:02 +08:00
Netfan
1ad54561b0 feat: add noBasicLayout in route meta (#5386)
所有菜单数据无需配置component为BasicLayout,它们将会默认使用基础布局,也可以通过meta.noBasicLayout来阻止这一行为
2025-01-14 12:12:08 +08:00
Netfan
42e322012c fix: spinner may stop playing animation after dismiss (#5365)
* fix: spinner may stop playing animation after dismiss

* fix: animation paused more safely
2025-01-12 15:43:44 +08:00
Netfan
8cf6e8ec75 style: popover bgColor is too close to common (#5364)
修复Dark主题下,弹出层的背景色与主体背景色太过接近的问题
2025-01-12 14:48:05 +08:00
Netfan
79d4d2fb22 ci: retry deploy while faild 2025-01-12 11:58:07 +08:00
Netfan
b785bc5704 fix: useEcharts return invalid instance (#5360) 2025-01-12 09:54:37 +08:00
Netfan
6719e2679f feat: popup component support overlay blur effect (#5359) 2025-01-11 23:37:17 +08:00
Netfan
cb9c8db5ba feat: improve tippy demo (#5357) 2025-01-11 20:42:38 +08:00
Netfan
a2637313f8 feat: integrate new component Tippy with demo (#5355)
* 添加新的工具提示组件Tippy
2025-01-11 17:35:59 +08:00
Netfan
467689525f perf: add nested modal demo (#5353) 2025-01-11 12:12:50 +08:00
Netfan
1a04a05b79 perf: modal and drawer api support chain calls (#5351)
* perf: modal and drawer api support chain calls

* fix: typo
2025-01-11 10:56:54 +08:00
Netfan
b8bffd884c feat: allow close tab when mouse middle button click (#5347)
* 偏好设置增加鼠标中键关闭标签页的设置
2025-01-10 20:52:31 +08:00
Netfan
624beb6fa0 fix: locale switching logic correction (#5344)
* 修复语言切换后的数据更新逻辑

* 表单默认按钮的content属性可提供computed类型的值
2025-01-10 14:32:21 +08:00
Netfan
7606b86854 fix: vxeGrid init without search form (#5342)
* 修复vxeGrid在未使用表单的情况下,自动加载数据失效的问题。

* 暂时将vxeGrid版本锁定在4.10.0,新版本尺寸计算尚有问题
2025-01-10 11:53:06 +08:00
Netfan
e10cbe23b9 chore: update deps 2025-01-10 09:22:32 +08:00
Netfan
d34838bdd8 fix: primaryColor calculation (#5337) 2025-01-10 01:51:38 +08:00
Netfan
c979c23e6b fix: form valid-error style in naive (#5336) 2025-01-10 01:15:30 +08:00
Netfan
516d0b8dc8 fix: form fieldMappingTime improve and modelPropName support (#5335)
* 表单的fieldMappingTime支持将格式化掩码设为null以便原值映射,这样可以支持非日期时间类型的组件;
* 表单增加modelPropName设置组件的双向绑定属性名,用于支持未提前注册的双向绑定属性为非默认名称的组件。
* 增加一些经常会有人提到的组合字段演示,
2025-01-09 22:49:28 +08:00
Netfan
99c7fd72f8 fix: code lint 2025-01-09 13:04:14 +08:00
Netfan
2828e7a7b6 fix: form fieldMappingTime is not working (#5333)
* fix: form option `fieldMappingTime` is not working

* fix: form merge support `fieldMappingTime`
2025-01-09 12:28:33 +08:00
王文庭
16162c01ed fix: download from url triggered twice sometimes (#5319)
解决Chrome、Safari通过路径一次下载两个文件的BUG
2025-01-08 16:01:23 +08:00
clddup
bbbdbfa912 feat: useEcharts exports echarts instance#5294 (#5299) 2025-01-05 15:54:13 +08:00
John
06cccc53fa chore: update quick-start.md (#5303)
change COREPACK_REGISTRY to COREPACK_NPM_REGISTRY
2025-01-05 15:52:27 +08:00
Netfan
801c640724 fix: vben select placeholder color (#5286) 2025-01-02 10:19:20 +08:00
Vben
081d2aed23 perf: format code with better style (#5283) 2025-01-01 11:39:49 +08:00
Netfan
4d81b9d18d fix: sidebar preferences fixed (#5276) 2024-12-31 12:36:45 +08:00
Netfan
e9dc613548 fix: breadcrumb setting not valid for header-sidebar-nav layout (#5275) 2024-12-31 12:00:50 +08:00
Netfan
3af22f7e91 fix: header logo may not be collapsed in header-sidebar-nav layout (#5274) 2024-12-31 11:35:58 +08:00
Netfan
2135cb8ece feat: new layout sidebar nav with full header (#5270) 2024-12-31 00:30:15 +08:00
Netfan
376aad5d26 feat: drawer close icon placement (#5269) 2024-12-30 23:30:53 +08:00
Netfan
27ba45aa75 docs: update dialog and drawer docs 2024-12-30 22:21:01 +08:00
Netfan
de17007788 feat: drawer support destroy on close 2024-12-30 22:21:01 +08:00
Netfan
e86a7906fe feat: drawer support onOpened & onClosed 2024-12-30 22:21:01 +08:00
Netfan
4a8e6abc06 feat: modal support destroy on close 2024-12-30 22:21:01 +08:00
Netfan
2eb7fed9f4 fix: header-mixed layout side-menu active (#5265)
* fix: header-mixed layout side-menu active

* fix: config test
2024-12-30 15:24:01 +08:00
Netfan
ff8d5ca351 feat: header mixed layout (#5263)
* feat: new layout header-mixed

* fix: header-mixed layout update

* feat: layout preference update

* fix: extra menus follow layout setting
2024-12-30 14:01:17 +08:00
vben
07c4ad05f4 chore: release 5.5.2 2024-12-28 22:15:00 +08:00
Netfan
548c2e5500 chore: downgrade vue-tsc version 2024-12-28 17:53:39 +08:00
Netfan
ec2c6eff6f feat: header menu align support (#5256)
* feat: header menu align support

* fix: typo
2024-12-28 16:16:48 +08:00
Netfan
15fe82c62f chore: update deps 2024-12-28 16:15:39 +08:00
Netfan
cb5ecf4a8a chore: add apiSelect remote search demo (#5246) 2024-12-26 19:23:59 +08:00
1820 changed files with 130214 additions and 22233 deletions

View File

@@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@@ -0,0 +1,7 @@
---
'@vben/styles': patch
'@vben-core/form-ui': patch
'@vben/web-naive': patch
---
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray

View File

@@ -1,10 +0,0 @@
---
description:
alwaysApply: true
---
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化

View File

@@ -1,10 +1,186 @@
--- ---
description: description: 萧康云医管理后台Vben Admin v5 + Vue 3 + TS + Ant Design Vue代码规范
alwaysApply: true alwaysApply: true
--- ---
# 基础规范(所有项目通用)
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则 1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2 2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于 2
3. 有封装好的方法、组件需要复用,不要重复造轮子 3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show 4. 小程序端的抽屉全部需要用 page-container 来防止用户意外退出页面(记得使用 v-if 而不是 v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化 5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化
# 全局架构规范
## 目录结构规范
- 业务页面统一放在 `apps/web-antd/src/views/<端>/<模块>/`system / business / doctor 等)
- 标准 CRUD 模块目录结构:
```
views/<端>/<模块>/
├── index.vue # 列表页Page + Grid + Modal
├── api/index.ts # 本模块 APICRUD
├── config/
│ ├── table.ts # vxe-grid 列定义 + proxyConfig
│ ├── search.ts # 顶部搜索表单 schema
│ └── form.ts # 新增/编辑弹窗 form schema
├── components/
│ └── modal.vue # 新增/编辑弹窗
└── utils/ # 模块工具(可选)
```
- 全局复用组件放 `apps/web-antd/src/components/`
- 表单内可用组件放 `apps/web-antd/src/components/form/components/`,文件名 kebab-case自动注册为 schema 的 `component` 名PascalCase 引用)
## 强制复用封装(禁止重复造轮子)
- 表格统一用 `useVbenVxeGrid`(来自 `#/adapter/vxe-table`**不要直接 new VxeGrid**
- 表单统一用 `useVbenForm`(来自 `#/adapter/form`**不要直接写 `<Form>` + 手动校验**
- 弹窗统一用 `useVbenModal`(来自 `@vben/common-ui`**不要用原生 `<Modal>`**
- 页面外壳统一用 `<Page>` 组件(来自 `@vben/common-ui`
- HTTP 请求统一用 `requestClient`(来自 `#/api/request`**禁止直接 axios / fetch**
- 行操作和工具栏按钮统一用 `<TableAction>` 组件(来自 `#/components/table-action`
- 业务选择器(员工、医生、门店、推广员、地区等)必须复用 `#/components/form/components/*-picker.vue`,不要重写
## API 层规范
- API 文件统一放在 `<模块>/api/index.ts`
- 函数命名约定:`get{Entity}List` / `get{Entity}Option` / `get{Entity}Info` / `create{Entity}` / `update{Entity}` / `delete{Entity}`
- URL 使用 kebab-case统一前缀变量 `const prefix = 'xxx/'`**URL 末尾不补 `/`**
- GET 用 `params`POST 用 `data`,例如:
```ts
import { requestClient } from '#/api/request';
const prefix = 'oa-robot/';
export async function getOaRobotList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
```
## 字段命名对齐后端
- 数据库字段在前端 schema 的 `field` / `fieldName` 中保持 snake_case如 `created_at`、`store_id`、`platform_code`**不要转换为 camelCase**
- 时间戳字段(`created_at` 等)后端查询器已格式化为字符串,前端**无需再次格式化**
- TS 变量用 camelCase常量用 UPPER_SNAKE_CASE类型用 PascalCase
## 表单校验
- 必填用字符串规则 `rules: 'required'`(选择项用 `'selectRequired'`),规则定义在 `#/adapter/form.ts` 的 `defineRules`
- 校验调用统一用 `formApi.validate().then(e => { if (e.valid) {...} })`
- 必填字段在 schema 中加 `rules: 'required'`,禁止散落 `validator` 写法
## 字典/枚举管理
- 模块内建 `config/constants.ts`,导出 `{ label, value }[]` 数组,禁止在 schema 中散落字面量
- 例如:
```ts
export const OA_MESSAGE_TYPE_OPTIONS = [
{ label: '文本', value: 'text' },
{ label: 'Markdown', value: 'markdown' },
];
```
## 路由规范
- 业务路由通过后端动态菜单驱动(`xk_menu` 表),**不要在前端 `router/routes/modules/` 静态注册业务路由**
- `defineOptions({ name: 'PascalCaseName' })` 必须与 `xk_menu.name` 字段一致
## Tab 用法
- 统一用 antd 原生 `Tabs` + `Tabs.TabPane`**不要找 Vben 自封装的 Tabs**
- Tab 选中状态持久化走 `localStorage`(参考 `system-config/index.vue` 的 `TAB_STORAGE_KEY` 模式)
## 抽屉用法小程序端admin 不涉及)
- 小程序端的抽屉用 `page-container` + `v-if`(不是 `v-show`)防止用户意外退出页面
## 中文注释
- 每个 `<script setup>` 顶部说明模块用途
- 每个主要函数(特别是有业务逻辑的)必须有中文注释
- 复杂的 schema 字段配置要注释意图
## 空行控制
- 上一部分代码和下一部分代码中间空行不超过 2 行
## VIP 功能判断PC 管理端强制)
- **判断方法**:统一用 `#/utils/vip` 的 `hasVipPermission` / `useVipPermission` / `VIP_FEATURE` 常量
- **显隐组件**:模板包一层 `<VipGate code="medical_record" :vip="receptionVip">``#/components/vip/VipGate.vue`
- 接诊/复诊/开方:**必须传履约诊所 vip**`get-current-store-type` 的 `vip`),禁止只靠登录态判断门店权益
- 显式传 `null` 表示「尚无履约诊所权益」,**不会**回落登录态;不传第二参才用 `userInfo.vip`
- 禁止在业务页再手写 `permissions.indexOf('medical_record')` 或复制一套 VIP 拉取逻辑
## 暗色模式适配PC 管理端强制)
后台支持亮/暗主题切换,**新增或改样式时必须同时适配暗色**,禁止只按白底写死颜色。
### 优先用法(推荐)
- 颜色、边框、背景一律用主题 CSS 变量,例如:
- 文字:`hsl(var(--foreground))` / `hsl(var(--muted-foreground))`
- 背景:`hsl(var(--background))` / `hsl(var(--card, var(--background)))` / `hsl(var(--muted) / 0.25)`
- 边框:`hsl(var(--border))`
- 强调/选中:`hsl(var(--primary))`、`hsl(var(--primary) / 0.1)`
- 警告:`hsl(var(--warning))`、`hsl(var(--warning) / 0.12)`(参考 `AiDisclaimerBanner.vue`
- **禁止**业务样式写死 `#fff`、`#000`、`#0f172a`、`#f8fafc`、`#64748b` 等仅适合亮色的色值
- **禁止**用 `:global(.dark)` 改全局变量以免污染整站配色
- 优先「一套变量样式自动跟主题」,避免再写一套 `.dark .xxx { ... }`;仅当变量无法表达(如图片反色、特殊阴影)才用局部 `.dark .xxx`(且必须 `scoped`
### 自检清单
- 弹窗 / 抽屉 / 卡片 / 预览对照区:在暗色下背景、文字、边框对比度可读
- 选中态、成功/导入高亮:用 `--primary` 透明度,不要写死浅绿底 `#f0faf8`
- 表格空态、分割线、次要文案:用 `--muted` / `--muted-foreground` / `--border`
- 改完后切换暗色主题肉眼过一遍,不要只测亮色
## Vben 组件使用规范
### VbenModal来自 @vben/common-ui
- 创建弹窗必须用 `useVbenModal`,禁止直接用 antd `Modal` 或原生 `<dialog>`
- 抽离弹窗内容到子组件时,外层用 `connectedComponent` 参数连接,内外组件共享 `modalApi`
- 底部按钮扩展 slot 优先级:`prepend-footer`(取消按钮左侧)> `center-footer`(取消/确定之间)> `append-footer`(确定右侧)
- **严禁覆盖 `#footer` slot**,会丢失默认的取消/确定按钮和 loading 行为
- 表单提交 loading 走 `modalApi.setState({ confirmLoading: true })`,禁止自己写 button loading
- 提交防抖/防重复用 `modalApi.lock()` / `unlock()`>5.5.3),禁止手动 disabled
- 拖拽开 `draggable: true`,需要拖出可视区时同时开 `overflow: true`
- 数据回填走 `modalApi.setData()` + `onOpenChange(isOpen) { const data = modalApi.getData() }`
### VbenDrawer来自 @vben/common-ui
- 创建抽屉必须用 `useVbenDrawer`,禁止直接用 antd `Drawer`
- 底部按钮扩展 slot 与 Modal 一致:`prepend-footer` / `center-footer` / `append-footer`
- **同样严禁覆盖 `#footer` slot**
- 关闭前校验用 `onBeforeClose`>5.5.2 支持 Promise禁止自己拦截
- 提交防抖/防重复用 `drawerApi.lock()` / `unlock()`>5.5.3
- 小程序端的「抽屉」概念与此无关,小程序端按现有规则用 `page-container`
### VbenVxeTable来自 #/adapter/vxe-table
- 表格必须用 `useVbenVxeGrid`,禁止直接 `new VxeGrid` 或 antd `Table`
- 远程数据加载走 `gridOptions.proxyConfig.ajax.query`**禁止自己 fetch 后 `data = []` 赋值**
- query 接口返回结构必须匹配适配器配置:`{ items: [], total: number }`(在 `apps/web-antd/src/adapter/vxe-table.ts` 的 `response` 字段统一配置)
- 分页参数从 `{ page }` 解构取:`page.currentPage` / `page.pageSize`,禁止自己读 URL
- 刷新表格用 `gridApi.query()`(保留当前页)或 `gridApi.reload()`(回到第一页),禁止整页 `window.location.reload()`
- 工具栏按钮插 slot`toolbar-actions`(标题左侧)、`toolbar-tools`(工具按钮左侧),禁止改 vxe-grid 内部模板
- 表格 loading 用 `gridApi.setLoading(bool)`,禁止自己遮罩
- 搜索表单由 `formOptions` 配置(底层是 Vben Form开关走 `gridOptions.toolbarConfig.search = true`
- 自定义列渲染走 `slots: { default: 'action' }` + Grid 子组件内 `<template #action="{ row }">`,禁止 column 渲染函数里写 JSX
### VbenForm来自 #/adapter/form
- 表单必须用 `useVbenForm`,禁止直接 `<form>` 或 antd `Form`
- 动态修改 schema 必须用 `formApi.updateSchema([...])`**vben form 没有 `setComponentProps` 这个方法**(这是已踩过的坑)
- 重置表单用 `formApi.resetForm()`**严禁 `formApi.resetFields()`**(那是 antd Form 的 APIVben Form 没有,会报 `resetFields is not a function`;已踩过坑)
- 取值/赋值用 `formApi.getValues()` / `formApi.setValues(obj)`,禁止自己 `v-model` 收集
- 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()`
- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册
- schema 中字段名(`fieldName`)必须与后端字段 snake_case 对齐,禁止前端 camelCase
- 密码/敏感字段用 `VbenInputPassword` 组件,禁止用 `VbenInput` 配 `type="password"`
- 单选/多选选项来自接口时,必须在弹窗/页面 `onMounted` 或 `onOpenChange` 里拉取后通过 `updateSchema` 注入,禁止写死常量

16
.gitattributes vendored
View File

@@ -4,8 +4,18 @@
* text=auto eol=lf * text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout. # Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf *.cmd text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf *.[cC][mM][dD] text eol=crlf
*.bat text eol=crlf
*.[bB][aA][tT] text eol=crlf
# Denote all files that are truly binary and should not be modified. # Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary *.ico binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.svg binary
*.woff binary
*.woff2 binary

18
.github/CODEOWNERS vendored
View File

@@ -1,14 +1,14 @@
# default onwer # default onwer
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com * anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
# vben core onwer # vben core onwer
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com /.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com /.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com /packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com /packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com /internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com /scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
# vben team onwer # vben team onwer
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com

View File

@@ -6,10 +6,10 @@ runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v6
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: .node-version node-version-file: .node-version
cache: 'pnpm' cache: 'pnpm'

3
.github/config.yml vendored
View File

@@ -21,12 +21,10 @@ newPRWelcomeComment: |
firstPRMergeComment: > firstPRMergeComment: >
Thanks for your contribution! 🎉🎉🎉 Thanks for your contribution! 🎉🎉🎉
# Comment to be posted to on first time issues # Comment to be posted to on first time issues
newIssueWelcomeComment: > newIssueWelcomeComment: >
Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers! Thanks for opening your first issue! Be sure to follow the issue template and provide every bit of information to help the developers!
# *OPTIONAL* default titles to check against for lack of descriptiveness # *OPTIONAL* default titles to check against for lack of descriptiveness
# MUST BE ALL LOWERCASE # MUST BE ALL LOWERCASE
requestInfoDefaultTitles: requestInfoDefaultTitles:
@@ -36,4 +34,3 @@ requestInfoDefaultTitles:
# *Required* Comment to reply with # *Required* Comment to reply with
requestInfoReplyComment: > requestInfoReplyComment: >
Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context. Thanks for filing this issue/PR! It would be much appreciated if you could provide us with more information so we can effectively analyze the situation in context.

View File

@@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject
- Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch. - Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch.
- If adding a new feature: - If adding a new feature:
- Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it. - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it.
- If fixing bug: - If fixing bug:
- Provide a detailed description of the bug in the PR. Live demo preferred. - Provide a detailed description of the bug in the PR. Live demo preferred.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging. - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.

View File

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

View File

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

View File

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

View File

@@ -57,11 +57,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -89,6 +89,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -30,7 +30,7 @@ jobs:
run: pnpm build:play run: pnpm build:play
- name: Sync Playground files - name: Sync Playground files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }} username: ${{ secrets.WEB_PLAYGROUND_FTP_ACCOUNT }}
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -54,7 +54,7 @@ jobs:
run: pnpm build:docs run: pnpm build:docs
- name: Sync Docs files - name: Sync Docs files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEBSITE_FTP_ACCOUNT }} username: ${{ secrets.WEBSITE_FTP_ACCOUNT }}
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -85,7 +85,7 @@ jobs:
run: pnpm run build:antd run: pnpm run build:antd
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }} username: ${{ secrets.WEB_ANTD_FTP_ACCOUNT }}
@@ -98,7 +98,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -116,7 +116,7 @@ jobs:
run: pnpm run build:ele run: pnpm run build:ele
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }} username: ${{ secrets.WEB_ELE_FTP_ACCOUNT }}
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -147,9 +147,26 @@ jobs:
run: pnpm run build:naive run: pnpm run build:naive
- name: Sync files - name: Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with: with:
server: ${{ secrets.PRO_FTP_HOST }} server: ${{ secrets.PRO_FTP_HOST }}
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }} username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }} password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
local-dir: ./apps/web-naive/dist/ local-dir: ./apps/web-naive/dist/
rerun-on-failure:
name: Rerun on failure
needs:
- deploy-playground-ftp
- deploy-docs-ftp
- deploy-antd-ftp
- deploy-ele-ftp
- deploy-naive-ftp
if: failure() && fromJSON(github.run_attempt) < 10
runs-on: ubuntu-latest
steps:
- name: Retry ${{ fromJSON(github.run_attempt) }} of 10
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: gh workflow run rerun.yml -F run_id=${{ github.run_id }}

View File

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

View File

@@ -19,7 +19,7 @@ jobs:
steps: steps:
# 关闭未活动的 Issues # 关闭未活动的 Issues
- name: Close Inactive Issues - name: Close Inactive Issues
uses: actions/stale@v9 uses: actions/stale@v11
with: with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically. days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: needs-reproduction # Label that flags an issue as stale. stale-issue-label: needs-reproduction # Label that flags an issue as stale.

View File

@@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: remove enhancement pending - name: remove enhancement pending
if: github.event.label.name == 'enhancement' if: github.event.label.name == 'enhancement'
uses: actions-cool/issues-helper@v3 uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
with: with:
actions: 'remove-labels' actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -27,7 +27,7 @@ jobs:
- name: remove bug pending - name: remove bug pending
if: github.event.label.name == 'bug' if: github.event.label.name == 'bug'
uses: actions-cool/issues-helper@v3 uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
with: with:
actions: 'remove-labels' actions: 'remove-labels'
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -36,7 +36,7 @@ jobs:
- name: needs reproduction - name: needs reproduction
if: github.event.label.name == 'needs reproduction' if: github.event.label.name == 'needs reproduction'
uses: actions-cool/issues-helper@v3 uses: actions-cool/issues-helper-backup@d65454423c6fbbd20026b9b499d403f79422ac69
with: with:
actions: 'create-comment, remove-labels' actions: 'create-comment, remove-labels'
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -14,7 +14,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v6
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14' issue-inactive-days: '14'

View File

@@ -3,78 +3,48 @@ name: Create Release Tag
on: on:
push: push:
tags: tags:
- 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*.*.*'
workflow_dispatch:
env: inputs:
HUSKY: '0' tag:
description: 'Tag to create (e.g. v1.2.3)'
required: true
type: string
permissions: permissions:
pull-requests: write
contents: write contents: write
jobs: jobs:
build: release:
name: Create Release name: Create Release
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
# - name: Checkout code - name: Extract version
# uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: Install pnpm
# uses: pnpm/action-setup@v4
# - name: Use Node.js ${{ matrix.node-version }}
# uses: actions/setup-node@v4
# with:
# node-version: ${{ matrix.node-version }}
# cache: "pnpm"
# - name: Install dependencies
# run: pnpm install --frozen-lockfile
# - name: Test and Build
# run: |
# pnpm run test
# pnpm run build
- name: version
id: version id: version
run: | run: |
tag=${GITHUB_REF/refs\/tags\//} if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
version=${tag#v} raw_tag="${{ inputs.tag }}"
major=${version%%.*} else
echo "tag=${tag}" >> $GITHUB_OUTPUT raw_tag="${GITHUB_REF_NAME}"
echo "version=${version}" >> $GITHUB_OUTPUT fi
echo "major=${major}" >> $GITHUB_OUTPUT # Normalize: ensure v prefix
tag="${raw_tag}"
[[ "${tag:0:1}" != "v" ]] && tag="v${tag}"
version="${tag#v}"
major="${version%%.*}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "major=${major}" >> "${GITHUB_OUTPUT}"
- uses: release-drafter/release-drafter@v6 - uses: release-drafter/release-drafter@v7
with: with:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
publish: true publish: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: force update major tag
# run: |
# git tag v${{ steps.version.outputs.major }} ${{ steps.version.outputs.tag }} -f
# git push origin refs/tags/v${{ steps.version.outputs.major }} -f
# - name: Create Release for Tag
# id: release_tag
# uses: ncipollo/release-action@v1
# with:
# token: ${{ secrets.GITHUB_TOKEN }}
# generateReleaseNotes: "true"
# body: |
# > Please refer to [CHANGELOG.md](https://github.com/vbenjs/vue-vben-admin/blob/main/CHANGELOG.md) for details.

19
.github/workflows/rerun.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Rerun workflow
on:
workflow_dispatch:
inputs:
run_id:
description: The workflow id to relanch
required: true
jobs:
rerun:
runs-on: ubuntu-latest
steps:
- name: rerun ${{ inputs.run_id }}
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
gh run watch ${{ inputs.run_id }} > /dev/null 2>&1
gh run rerun ${{ inputs.run_id }} --failed

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@v5 uses: amannn/action-semantic-pull-request@v6
with: with:
wip: true wip: true
subjectPattern: ^(?![A-Z]).+$ subjectPattern: ^(?![A-Z]).+$

View File

@@ -9,7 +9,7 @@ jobs:
if: github.repository == 'vbenjs/vue-vben-admin' if: github.repository == 'vbenjs/vue-vben-admin'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v11
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'

14
.gitignore vendored
View File

@@ -22,7 +22,8 @@ yarn.lock
package-lock.json package-lock.json
.VSCodeCounter .VSCodeCounter
**/backend-mock/data **/backend-mock/data
.omx
.pnpm-store
# local env files # local env files
.env.local .env.local
.env.*.local .env.*.local
@@ -51,3 +52,14 @@ vite.config.ts.*
.history .history
/.mimocode/ /.mimocode/
/.cursor/skills/ /.cursor/skills/
.cursor
# AI
.agent
.agents
.claude
.codex
skills-lock.json
.atomcode
datalog
.playwright-mcp

View File

@@ -2,5 +2,5 @@ ports:
- port: 5555 - port: 5555
onOpen: open-preview onOpen: open-preview
tasks: tasks:
- init: corepack enable && pnpm install - init: npm i -g corepack && pnpm install
command: pnpm run dev:play command: pnpm run dev:play

View File

@@ -1,6 +0,0 @@
echo Start running commit-msg hook...
# Check whether the git commit information is standardized
pnpm exec commitlint --edit "$1"
echo Run commit-msg hook done.

View File

@@ -1,3 +0,0 @@
# 每次 git pull 之后, 安装依赖
pnpm install

View File

@@ -1,20 +0,0 @@
export default {
'*.{js,jsx,ts,tsx}': [
'prettier --cache --ignore-unknown --write',
'eslint --cache --fix',
],
'*.{scss,less,styl,html,vue,css}': [
'prettier --cache --ignore-unknown --write',
'stylelint --fix --allow-empty-input',
],
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json',
],
'package.json': ['prettier --cache --write'],
};

View File

@@ -1 +1 @@
20.14.0 24.16.0

14
.npmrc
View File

@@ -1,13 +1 @@
registry = "https://registry.npmmirror.com" registry=https://registry.npmmirror.com
public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss
public-hoist-pattern[]=stylelint
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=@commitlint/*
public-hoist-pattern[]=czg
strict-peer-dependencies=false
auto-install-peers=true
dedupe-peer-dependents=true

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
import { eventHandler, setHeader } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const data = `
{
"code": 0,
"message": "success",
"data": [
{
"id": 123456789012345678901234567890123456789012345678901234567890,
"name": "John Doe",
"age": 30,
"email": "john-doe@demo.com"
},
{
"id": 987654321098765432109876543210987654321098765432109876543210,
"name": "Jane Smith",
"age": 25,
"email": "jane@demo.com"
}
]
}
`;
setHeader(event, 'Content-Type', 'application/json');
return data;
});

View File

@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(1000);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,16 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(2000);
return useResponseSuccess(null);
});

View File

@@ -0,0 +1,62 @@
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
pid: 0,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
),
remark: faker.lorem.sentence(),
};
if (faker.datatype.boolean()) {
dataItem.children = Array.from(
{ length: faker.number.int({ min: 1, max: 5 }) },
() => ({
id: faker.string.uuid(),
pid: dataItem.id,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
),
remark: faker.lorem.sentence(),
}),
);
}
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(10);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const listData = structuredClone(mockData);
return useResponseSuccess(listData);
});

View File

@@ -0,0 +1,13 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(MOCK_MENU_LIST);
});

View File

@@ -0,0 +1,29 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {};
function getNames(menus: any[]) {
menus.forEach((menu) => {
namesMap[menu.name] = String(menu.id);
if (menu.children) {
getNames(menu.children);
}
});
}
getNames(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, name } = getQuery(event);
return (name as string) in namesMap &&
(!id || namesMap[name as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});

View File

@@ -0,0 +1,29 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };
function getPaths(menus: any[]) {
menus.forEach((menu) => {
pathMap[menu.path] = String(menu.id);
if (menu.children) {
getPaths(menu.children);
}
});
}
getPaths(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, path } = getQuery(event);
return (path as string) in pathMap &&
(!id || pathMap[path as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});

View File

@@ -0,0 +1,84 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const menuIds = getMenuIds(MOCK_MENU_LIST);
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
permissions: faker.helpers.arrayElements(menuIds),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

View File

@@ -0,0 +1,85 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record<string, any> = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
deptId: faker.string.uuid(),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
deptId,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
if (deptId) {
listData = listData.filter((item) => item.deptId === deptId);
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

View File

@@ -0,0 +1,12 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
import { getTimezone } from '~/utils/timezone-utils';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(getTimezone());
});

View File

@@ -0,0 +1,11 @@
import { eventHandler } from 'h3';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import { useResponseSuccess } from '~/utils/response';
export default eventHandler(() => {
const data = TIME_ZONE_OPTIONS.map((o) => ({
label: `${o.timezone} (GMT${o.offset >= 0 ? `+${o.offset}` : o.offset})`,
value: o.timezone,
}));
return useResponseSuccess(data);
});

View File

@@ -0,0 +1,26 @@
import { eventHandler, readBody, setResponseStatus } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { TIME_ZONE_OPTIONS } from '~/utils/mock-data';
import {
unAuthorizedResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
import { setTimezone } from '~/utils/timezone-utils';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const body = await readBody<{ timezone?: unknown }>(event);
const timezone =
typeof body?.timezone === 'string' ? body.timezone : undefined;
const allowed = TIME_ZONE_OPTIONS.some((o) => o.timezone === timezone);
if (!timezone || !allowed) {
setResponseStatus(event, 400);
return useResponseError('Bad Request', 'Invalid timezone');
}
setTimezone(timezone);
return useResponseSuccess({});
});

View File

@@ -0,0 +1,14 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess({
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
});
// return useResponseError("test")
});

View File

@@ -0,0 +1,9 @@
let mockTimeZone: null | string = null;
export const setTimezone = (timeZone: string) => {
mockTimeZone = timeZone;
};
export const getTimezone = () => {
return mockTimeZone;
};

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" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/> />
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 --> <!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title> <title>%VITE_APP_TITLE%</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<script> <script>
// 生产环境下注入百度统计 // 生产环境下注入百度统计

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-antd",
"version": "5.5.1", "version": "5.7.0",
"homepage": "https://vben.pro", "homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "repository": {
@@ -27,6 +27,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@fortawesome/fontawesome-free": "^6.7.2",
"@vben/access": "workspace:*", "@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*", "@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*", "@vben/constants": "workspace:*",
@@ -41,15 +42,23 @@
"@vben/styles": "workspace:*", "@vben/styles": "workspace:*",
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "catalog:", "@vueuse/core": "catalog:",
"ant-design-vue": "catalog:", "ant-design-vue": "catalog:",
"axios": "^1.10.0",
"dayjs": "catalog:", "dayjs": "catalog:",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"html2canvas": "^1.4.1",
"js-base64": "^3.7.7",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.487.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"md-editor-v3": "^5.4.5",
"pinia": "catalog:", "pinia": "catalog:",
"sortablejs": "catalog:", "sortablejs": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-router": "catalog:", "vue-router": "catalog:",
"xgplayer": "^3.0.24",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

@@ -3,55 +3,616 @@
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用, * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/ */
import type { BaseFormComponentType } from '@vben/common-ui'; /* eslint-disable vue/one-component-per-file */
import type { Component, SetupContext } from 'vue'; import type {
import { h } from 'vue'; 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 { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import type { Component, Ref } from 'vue';
import { $t } from '@vben/locales';
import type {
ApiComponentSharedProps,
BaseFormComponentType,
IconPickerProps,
} from '@vben/common-ui';
import type { Sortable } from '@vben/hooks';
import type { Recordable } from '@vben/types';
import { import {
AutoComplete, computed,
Button, defineAsyncComponent,
Cascader, defineComponent,
Checkbox, h,
CheckboxGroup, nextTick,
DatePicker, onMounted,
Divider, onUnmounted,
Input, ref,
InputNumber, render,
InputPassword, unref,
Mentions, watch,
notification, } from 'vue';
Radio,
RadioGroup, import {
RangePicker, ApiComponent,
Rate, globalShareState,
Select, IconPicker,
Space, VCropper,
Switch, } from '@vben/common-ui';
Textarea, import { useSortable } from '@vben/hooks';
TimePicker, import { IconifyIcon } from '@vben/icons';
TreeSelect, import { $t } from '@vben/locales';
Upload, import { isEmpty } from '@vben/utils';
} from 'ant-design-vue';
import { message, Modal, notification } from 'ant-design-vue';
import { registerComponent } from '#/components/form/component-map'; import { registerComponent } from '#/components/form/component-map';
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;
};
// ============================================================
// 所有 antd 组件统一用 defineAsyncComponent 异步加载
// ⚠️ 不要再从 'ant-design-vue' 同步 import 这些组件!
// 否则会和下面的 const 声明冲突,导致
// "Identifier 'Xxx' has already been declared"
// ============================================================
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
const PreviewGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
);
// ============================================================
// withDefaultPlaceholder给组件注入 i18n placeholder + ref 透传
// ============================================================
const withDefaultPlaceholder = <T extends Component>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: Component,
type: 'input' | 'select', type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => { ) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => { return defineComponent({
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`); name: (component as any).name,
return h(component, { ...props, ...attrs, placeholder }, slots); inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
const IMAGE_EXTENSIONS = new Set([
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
]);
/**
* 检查是否为图片文件
*/
function isImageFile(file: UploadFile): boolean {
if (file.url) {
try {
const pathname = new URL(file.url, 'http://localhost').pathname;
const ext = pathname.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
} catch {
const ext = file.url?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
}
if (!file.type) {
const ext = file.name?.split('.').pop()?.toLowerCase();
return ext ? IMAGE_EXTENSIONS.has(ext) : false;
}
return file.type.startsWith('image/');
}
/**
* 创建默认的上传按钮插槽
*/
function createDefaultUploadSlots(listType: string, placeholder: string) {
if (listType === 'picture-card') {
return { default: () => placeholder };
}
return {
default: () =>
h(
Button,
{
icon: h(IconifyIcon, {
icon: 'ant-design:upload-outlined',
class: 'mb-1 size-4',
}),
},
() => placeholder,
),
}; };
}
/**
* 获取文件的 Base64
*/
function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => resolve(reader.result as string));
reader.addEventListener('error', reject);
});
}
/**
* 预览图片
*/
async function previewImage(
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) {
// 非图片文件直接打开链接
if (!isImageFile(file)) {
const url = file.url || file.preview;
if (url) {
window.open(url, '_blank');
} else {
message.error($t('ui.formRules.previewWarning'));
}
return;
}
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
Image,
PreviewGroup,
]);
// 过滤图片文件并生成预览
const imageFiles = (unref(fileList) || []).filter((f) => isImageFile(f));
for (const imgFile of imageFiles) {
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
imgFile.preview = await getBase64(imgFile.originFileObj);
}
}
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
const currentIndex = imageFiles.findIndex((f) => f.uid === file.uid);
const PreviewWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
return h(
PreviewGroupComponent,
{
class: 'hidden',
preview: {
visible: visible.value,
current: currentIndex,
onVisibleChange: (value: boolean) => {
visible.value = value;
if (!value) {
setTimeout(() => {
if (!isUnmounted && container) {
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
}
},
},
},
() =>
imageFiles.map((imgFile) =>
h(ImageComponent, {
key: imgFile.uid,
src: imgFile.url || imgFile.preview,
}),
),
);
};
},
};
render(h(PreviewWrapper), container);
}
/**
* 图片裁剪操作
*/
function cropImage(file: File, aspectRatio: string | undefined) {
return new Promise<Blob | string | undefined>((resolve, reject) => {
const container = document.createElement('div');
document.body.append(container);
let isUnmounted = false;
let objectUrl: null | string = null;
const open = ref(true);
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
const closeModal = () => {
open.value = false;
setTimeout(() => {
if (!isUnmounted && container) {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
isUnmounted = true;
render(null, container);
container.remove();
}
}, 300);
};
const CropperWrapper = {
setup() {
return () => {
if (isUnmounted) return null;
if (!objectUrl) {
objectUrl = URL.createObjectURL(file);
}
return h(
Modal,
{
open: open.value,
title: h('div', {}, [
$t('ui.crop.title'),
h(
'span',
{
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
},
$t('ui.crop.titleTip', [aspectRatio]),
),
]),
centered: true,
width: 548,
zIndex: 9999,
keyboard: false,
maskClosable: false,
closable: false,
cancelText: $t('common.cancel'),
okText: $t('ui.crop.confirm'),
destroyOnClose: true,
onOk: async () => {
const cropper = cropperRef.value;
if (!cropper) {
reject(new Error('Cropper not found'));
closeModal();
return;
}
try {
const dataUrl = await cropper.getCropImage();
if (dataUrl) {
resolve(dataUrl);
} else {
reject(new Error($t('ui.crop.errorTip')));
}
} catch {
reject(new Error($t('ui.crop.errorTip')));
} finally {
closeModal();
}
},
onCancel() {
resolve('');
closeModal();
},
},
() =>
h(VCropper, {
ref: (refValue: any) => (cropperRef.value = refValue),
img: objectUrl as string,
aspectRatio,
}),
);
};
},
};
render(h(CropperWrapper), container);
});
}
/**
* 带预览功能的上传组件
*/
const withPreviewUpload = () => {
return defineComponent({
name: 'Upload',
emits: ['update:modelValue'],
setup(
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t('ui.placeholder.upload');
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
const aspectRatio = computed(
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
);
const handleBeforeUpload = async (
file: UploadFile,
originFileList: Array<File>,
) => {
// 文件大小限制
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
file.status = 'removed';
return false;
}
// 图片裁剪处理
if (
attrs.crop &&
!attrs.multiple &&
originFileList[0] &&
isImageFile(file)
) {
file.status = 'removed';
const blob = await cropImage(originFileList[0], aspectRatio.value);
if (!blob) {
throw new Error($t('ui.crop.errorTip'));
}
return blob;
}
return attrs.beforeUpload?.(file) ?? true;
};
const handleChange = (event: UploadChangeParam) => {
try {
attrs.handleChange?.(event);
attrs.onHandleChange?.(event);
} catch (error) {
console.error(error);
}
fileList.value = event.fileList.filter(
(file) => file.status !== 'removed',
);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = () => {
if (attrs.disabled) return null;
return isEmpty(slots)
? createDefaultUploadSlots(listType, placeholder)
: slots;
};
// 拖拽排序
const draggable = computed(
() => (attrs.draggable ?? false) && !attrs.disabled,
);
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const sortableInstance = ref<null | Sortable>(null);
const styleId = `upload-drag-style-${uploadId}`;
function injectDragStyle() {
if (!document.querySelector(`[id="${styleId}"]`)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
[data-upload-id="${uploadId}"] .ant-upload-list-item { cursor: move; }
[data-upload-id="${uploadId}"] .ant-upload-list-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
`;
document.head?.append(style);
}
}
function removeDragStyle() {
document.querySelector(`[id="${styleId}"]`)?.remove();
}
async function initSortable(retryCount = 0) {
if (!draggable.value) return;
injectDragStyle();
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
const container = document.querySelector(
`[data-upload-id="${uploadId}"] .ant-upload-list`,
) as HTMLElement | null;
if (!container) {
if (retryCount < 5) {
setTimeout(() => initSortable(retryCount + 1), 200);
}
return;
}
const { initializeSortable } = useSortable(container, {
animation: 300,
delay: 400,
delayOnTouchOnly: true,
filter:
'.ant-upload-select, .ant-upload-list-item-error, .ant-upload-list-item-uploading',
onEnd: (evt: { oldIndex?: number; newIndex?: number }) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
const list = [...(fileList.value || [])];
const [movedItem] = list.splice(oldIndex, 1);
if (movedItem) {
list.splice(newIndex, 0, movedItem);
fileList.value = list;
}
attrs.onDragSort?.(oldIndex, newIndex);
emit('update:modelValue', fileList.value);
},
});
sortableInstance.value = await initializeSortable();
}
// 监听表单值变化
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
onMounted(initSortable);
onUnmounted(() => {
sortableInstance.value?.destroy();
removeDragStyle();
});
return () =>
h(
'div',
{ 'data-upload-id': uploadId, class: 'w-full' },
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton() as any,
),
);
},
});
}; };
// 表单组件在这里加 // 表单组件在这里加
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiCascader'
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect' | 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
@@ -61,6 +622,8 @@ export type ComponentType =
| 'DatePicker' | 'DatePicker'
| 'DefaultButton' | 'DefaultButton'
| 'Divider' | 'Divider'
/** OA 场景表单的图片/文件素材选择(透传 acceptTypes 过滤类型) */
| 'GalleryPickLink'
| 'IconPicker' | 'IconPicker'
| 'Input' | 'Input'
| 'InputNumber' | 'InputNumber'
@@ -81,68 +644,92 @@ export type ComponentType =
| 'UploadImage' | 'UploadImage'
| 'UploadImageSortable' | 'UploadImageSortable'
| 'UploadOssFile' | 'UploadOssFile'
| 'UploadDraggerPaste'
| BaseFormComponentType; | 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() { async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = { const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载 // 如果你的组件体积比较大,可以使用异步加载
// Button: () => // Button: () =>
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => {
return h( ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
ApiComponent, component: Cascader,
{ fieldNames: { label: 'label', value: 'value', children: 'children' },
placeholder: $t('ui.placeholder.select'), loadingSlot: 'suffixIcon',
...props, modelPropName: 'value',
...attrs, visibleEvent: 'onVisibleChange',
component: Select, }),
loadingSlot: 'suffixIcon', ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
visibleEvent: 'onDropdownVisibleChange', component: Select,
modelPropName: 'value', loadingSlot: 'suffixIcon',
}, modelPropName: 'value',
slots, visibleEvent: 'onVisibleChange',
); }),
}, ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
ApiTreeSelect: (props, { attrs, slots }) => { component: TreeSelect,
return h( fieldNames: { label: 'label', value: 'value', children: 'children' },
ApiComponent, loadingSlot: 'suffixIcon',
{ modelPropName: 'value',
placeholder: $t('ui.placeholder.select'), optionsPropName: 'treeData',
...props, visibleEvent: 'onVisibleChange',
...attrs, }),
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
AutoComplete, AutoComplete,
Cascader, Cascader,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,
DatePicker, DatePicker,
// 自定义默认按钮 // 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => { DefaultButton: (props: any, { attrs, slots }: any) => {
return h(Button, { ...props, attrs, type: 'default' }, slots); return h(Button, { ...props, attrs, type: 'default' }, slots);
}, },
Divider, Divider,
IconPicker: (props, { attrs, slots }) => { IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
return h( iconSlot: 'addonAfter',
IconPicker, inputComponent: Input,
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs }, modelValueProp: 'value',
slots, }),
);
},
Input: withDefaultPlaceholder(Input, 'input'), Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input', {
style: { width: '100%' },
}),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'), InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'), Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮 // 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => { PrimaryButton: (props: any, { attrs, slots }: any) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots); return h(Button, { ...props, attrs, type: 'primary' }, slots);
}, },
Radio, Radio,
@@ -155,10 +742,10 @@ async function initComponentAdapter() {
Textarea: withDefaultPlaceholder(Textarea, 'input'), Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker, TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload, Upload: withPreviewUpload(),
}; };
// 自动注册自定义组件 // 自动注册 components/form 与 views/**/components/form 下的业务表单组件(如 Editor、Avatar
registerComponent(components); registerComponent(components);
// 将组件注册到全局共享状态中 // 将组件注册到全局共享状态中
globalShareState.setComponents(components); globalShareState.setComponents(components);

View File

@@ -1,49 +1,84 @@
import type { import type {
VbenFormProps as FormProps,
VbenFormSchema as FormSchema, VbenFormSchema as FormSchema,
VbenFormProps, FormValues,
} from '@vben/common-ui'; } 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 { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({ async function initSetupVbenForm() {
config: { setupVbenForm<ComponentType>({
// ant design vue组件库默认都是 v-model:value config: {
baseModelPropName: 'value', // ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList // 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: { modelPropNameMap: {
Checkbox: 'checked', Checkbox: 'checked',
Radio: 'checked', Radio: 'checked',
Switch: 'checked', Switch: 'checked',
Upload: 'fileList', Upload: 'fileList',
UploadImage: 'modelValue', UploadImage: 'modelValue',
UploadOssFile: 'modelValue', UploadOssFile: 'modelValue',
},
}, },
}, rules: {
defineRules: { // 输入项目必填国际化适配
// 输入项目必填国际化适配 required: (value, _params, ctx) => {
required: (value, _params, ctx) => { if (value === undefined || value === null || value.length === 0) {
if (value === undefined || value === null || value.length === 0) { return $t('ui.formRules.required', [ctx.label]);
return $t('ui.formRules.required', [ctx.label]); }
} return true;
return true; },
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
/**
* 大陆手机号:必填 + 11 位且号段 13-19与后端 UtilsService::checkPhone 一致)
* 表单字段 rules: 'mobile' 即可,无需再叠 required
*/
mobile: (value, _params, ctx) => {
const phone = typeof value === 'string' ? value.trim() : value;
if (phone === undefined || phone === null || phone === '') {
return $t('ui.formRules.required', [ctx.label]);
}
if (!/^1[3456789]\d{9}$/.test(String(phone))) {
return $t('ui.formRules.mobile', [ctx.label]);
}
return true;
},
}, },
// 选择项目必填国际化适配 });
selectRequired: (value, _params, ctx) => { }
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<ComponentType>; function useVbenForm<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
options: FormProps<
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>,
) {
return useForm<TFormValues, ComponentType, ComponentPropsMap, TSubmitValues>(
options,
);
}
export { useVbenForm, z }; export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>; export type VbenFormSchema<TValues extends FormValues = FormValues> =
export type { VbenFormProps }; FormSchema<ComponentType, ComponentPropsMap, TValues>;
export type VbenFormProps<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
> = FormProps<ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>;

View File

@@ -1,6 +1,14 @@
import type { FormValues } from '@vben/common-ui';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { ComponentPropsMap, ComponentType } from './component';
import { h } from 'vue'; import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; import {
setupVbenVxeTable,
useVbenVxeGrid as useGrid,
} from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue'; import { Button, Image } from 'ant-design-vue';
@@ -33,14 +41,15 @@ setupVbenVxeTable({
round: true, round: true,
showOverflow: true, showOverflow: true,
size: 'small', size: 'small',
}, } as VxeTableGridOptions,
}); });
// 表格配置项可以用 cellRender: { name: 'CellImage' }, // 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', { vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) { renderTableDefault(renderOpts, params) {
const { props } = renderOpts;
const { column, row } = params; const { column, row } = params;
return h(Image, { src: row[column.field] }); return h(Image, { src: row[column.field], ...props });
}, },
}); });
@@ -62,6 +71,23 @@ setupVbenVxeTable({
useVbenForm, useVbenForm,
}); });
export { useVbenVxeGrid }; export const useVbenVxeGrid = <
T extends Record<string, any>,
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
...rest: Parameters<
typeof useGrid<
T,
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>
>
) =>
useGrid<T, ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>(
...rest,
);
export type * from '@vben/plugins/vxe-table'; export type * from '@vben/plugins/vxe-table';

View File

@@ -54,12 +54,15 @@ export namespace AuthApi {
* 登录 * 登录
*/ */
export async function loginApi(data: AuthApi.LoginParams) { export async function loginApi(data: AuthApi.LoginParams) {
// PC 管理端固定 web供单机登录按端区分会话
const clientType = 'web';
if (data.account) { if (data.account) {
return requestClient.post<AuthApi.LoginResult>('login', { return requestClient.post<AuthApi.LoginResult>('login', {
account: data.account, account: data.account,
password: data.password, password: data.password,
code: data.code, code: data.code,
admin_id: data.admin_id, admin_id: data.admin_id,
client_type: clientType,
}); });
} }
@@ -67,6 +70,7 @@ export async function loginApi(data: AuthApi.LoginParams) {
login_method: data.login_method || 'phone', login_method: data.login_method || 'phone',
password: data.password, password: data.password,
code: data.code, code: data.code,
client_type: clientType,
}; };
if (data.login_method === 'phone') { if (data.login_method === 'phone') {

View File

@@ -1,18 +1,19 @@
/** /**
* 该文件可自行根据业务逻辑进行调整 * 该文件可自行根据业务逻辑进行调整
*/ */
import type { HttpResponse } from '@vben/request'; import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { import {
authenticateResponseInterceptor, authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor, errorMessageResponseInterceptor,
RequestClient, RequestClient,
} from '@vben/request'; } from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
// eslint-disable-next-line n/no-extraneous-import // eslint-disable-next-line n/no-extraneous-import
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
@@ -29,14 +30,17 @@ import {downloadByData} from "#/util/tool";
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({ const client = new RequestClient({
...options,
baseURL, baseURL,
}); });
/** /**
* 重新认证逻辑 * 重新认证逻辑
*/ */
/** 防重复:并发 401 只弹一次重新登录提示 */
let authModalShowing = false;
async function doReAuthenticate() { async function doReAuthenticate() {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
@@ -47,6 +51,21 @@ function createRequestClient(baseURL: string) {
) { ) {
accessStore.setLoginExpired(true); accessStore.setLoginExpired(true);
} else { } else {
// 被踢/登录失效时用模态框提示,确认后再退出
if (!authModalShowing) {
authModalShowing = true;
await new Promise<void>((resolve) => {
Modal.warning({
title: '登录失效',
content: '账号需重新登录',
okText: '重新登录',
centered: true,
onOk: () => resolve(),
onCancel: () => resolve(),
});
});
authModalShowing = false;
}
await authStore.logout(true); await authStore.logout(true);
} }
} }
@@ -157,7 +176,9 @@ function createRequestClient(baseURL: string) {
return client; return client;
} }
export const requestClient = createRequestClient(apiURL); export const requestClient = createRequestClient(apiURL, {
responseReturn: 'result',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -9,7 +9,6 @@ import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales'; import { antdLocale } from '#/locales';
import { GlobalContextMenu } from '#/components/context-menu'; import { GlobalContextMenu } from '#/components/context-menu';
import DoctorTransferFloat from '#/components/doctor-transfer-float/DoctorTransferFloat.vue';
import { useWebSocket } from '#/views/business/chat/composables/useWebSocket'; import { useWebSocket } from '#/views/business/chat/composables/useWebSocket';
import { useChatStore } from '#/views/business/chat/stores/chat'; import { useChatStore } from '#/views/business/chat/stores/chat';
import { useUserStore } from '#/views/business/chat/stores/user'; import { useUserStore } from '#/views/business/chat/stores/user';
@@ -76,13 +75,6 @@ watch(userInfoLoaded, (loaded) => {
initWebsocket(); initWebsocket();
} }
}); });
/** 诊所医生账号显示传方悬浮窗(有 doctor_id 且绑定门店) */
const showDoctorTransferFloat = computed(() => {
const info = vbenUserStore.userInfo as Record<string, any> | null;
if (!info) return false;
return !!(info.doctor_id && info.store_id);
});
</script> </script>
<template> <template>
@@ -90,7 +82,6 @@ const showDoctorTransferFloat = computed(() => {
<App> <App>
<RouterView /> <RouterView />
<GlobalContextMenu /> <GlobalContextMenu />
<DoctorTransferFloat v-if="showDoctorTransferFloat" />
</App> </App>
</ConfigProvider> </ConfigProvider>
</template> </template>

View File

@@ -1,6 +1,7 @@
import { createApp, watchEffect } from 'vue'; import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
@@ -11,6 +12,7 @@ import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales'; import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component'; import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
@@ -18,8 +20,26 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
await initComponentAdapter(); await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App); const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置 // 国际化 i18n 配置
await setupI18n(app); await setupI18n(app);
@@ -29,9 +49,17 @@ async function bootstrap(namespace: string) {
// 安装权限指令 // 安装权限指令
registerAccessDirective(app); registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题 // 动态更新标题
watchEffect(() => { watchEffect(() => {
if (preferences.app.dynamicTitle) { if (preferences.app.dynamicTitle) {

View File

@@ -1,12 +1,12 @@
import type { SpreadsheetCommand } from '../types'; import type { SpreadsheetCommand } from '../types';
export function createSetCellValueCommand(
undo: () => void | Promise<void>,
export function createSetCellValueCommand( redo: () => void | Promise<void>,
): SpreadsheetCommand {
undo: () => void | Promise<void>, return {
label: 'setCellValue',
redo: () => void | Promise<void>, undo,
redo,
): SpreadsheetCommand { };
}

View File

@@ -0,0 +1,752 @@
<script lang="ts" setup>
/**
* 中药药名气泡选择器
* - 对齐诊断/医嘱 EntryKeywordBubbleInput 聚焦弹出 Teleport 浮层
* - 远程搜索药品getProductListDoctorReception支持药名/别名/首拼/全拼/数字
* - 列表展示拼音列;命中别名时展示匹配别名及其拼音
* - 选中后回传完整商品行,供接诊/复诊/特色方/常用方复用现有落库逻辑
*/
import {
computed,
nextTick,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue';
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { useDebounceFn } from '@vueuse/core';
import { Empty, Input, Spin } from 'ant-design-vue';
import {
rankByMatchScore,
splitHighlight,
type HighlightPart,
} from '#/utils/dictSearchRank';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
export type ChineseDrugBubbleItem = {
id: number;
drug_id: number;
price: number;
buy_price?: number;
drug: Record<string, any>;
[key: string]: any;
};
const props = withDefaults(
defineProps<{
/** 已选药名(展示回填;打开搜索时仍可编辑关键词) */
displayName?: string;
/** 药品类型1 中药,颗粒等由调用方传入 */
type?: number;
storeId?: number;
registerId?: number;
placeholder?: string;
disabled?: boolean;
/** 卡片内紧凑宽度 */
compact?: boolean;
/** 额外 class便于焦点跳转 querySelector */
inputClass?: string;
}>(),
{
displayName: '',
type: 1,
storeId: 0,
registerId: 0,
placeholder: '药名',
disabled: false,
compact: false,
inputClass: '',
},
);
const emit = defineEmits<{
select: [item: ChineseDrugBubbleItem];
}>();
const ROW_HEIGHT = 48;
const HINT_HEIGHT = 34;
const HEAD_HEIGHT = 32;
const PANEL_PAD = 8;
const PREFERRED_PANEL = 360;
const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const inputRef = ref<{ focus?: () => void } | null>(null);
const keyword = ref('');
const loading = ref(false);
const open = ref(false);
/** 是否处于「正在搜」true 时不把 displayName 强写回 keyword */
const searching = ref(false);
const highlightIndex = ref(0);
const options = ref<ChineseDrugBubbleItem[]>([]);
const scrollTop = ref(0);
const listViewportH = ref(200);
const panelStyle = ref<Record<string, string>>({});
const totalHeight = computed(() => options.value.length * ROW_HEIGHT);
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - 2),
);
const endIndex = computed(() => {
const visible = Math.ceil(listViewportH.value / ROW_HEIGHT) + 4;
return Math.min(options.value.length - 1, startIndex.value + visible);
});
const visibleRows = computed(() => {
const list = options.value;
const start = startIndex.value;
const end = endIndex.value;
if (list.length === 0 || end < start) return [];
return list.slice(start, end + 1).map((item, i) => ({
item,
index: start + i,
}));
});
const offsetY = computed(() => startIndex.value * ROW_HEIGHT);
/**
* 同步展示名:未打开搜索时用 displayName 回填输入框
*/
watch(
() => props.displayName,
(name) => {
if (!searching.value && !open.value) {
keyword.value = String(name || '');
}
},
{ immediate: true },
);
function partsOf(text: string): HighlightPart[] {
return splitHighlight(text, keyword.value.trim());
}
function drugNameOf(item: ChineseDrugBubbleItem): string {
return String(item?.drug?.drug_name || item?.drug_name || '').trim();
}
/** 关键词命中的别名(后端 matched_alias */
function matchedAliasOf(item: ChineseDrugBubbleItem): string {
return String(
item?.matched_alias || item?.drug?.matched_alias || '',
).trim();
}
/**
* 拼音展示:命中别名时用别名拼音,否则用药名首拼(全拼)
*/
function pinyinLabelOf(item: ChineseDrugBubbleItem): string {
const aliasPy = String(
item?.matched_alias_pinyin || item?.drug?.matched_alias_pinyin || '',
).trim();
if (matchedAliasOf(item) && aliasPy) return aliasPy;
const display = String(item?.drug?.pinyin_display || '').trim();
if (display) return display;
const simple = String(item?.drug?.pinyin_simple || '').trim();
const full = String(item?.drug?.pinyin_full || '').trim();
if (simple && full && simple.toLowerCase() !== full.toLowerCase()) {
return `${simple}(${full})`;
}
return simple || full || '-';
}
function unitLabelOf(item: ChineseDrugBubbleItem): string {
const unit = item?.drug?.unit || item?.unit;
if (typeof unit === 'object' && unit?.name) return String(unit.name);
return String(item?.drug?.specification || item?.specification || '').trim() || '-';
}
function priceLabelOf(item: ChineseDrugBubbleItem): string {
const p = Number(item?.price ?? 0);
return Number.isFinite(p) ? `¥${p.toFixed(2)}` : '-';
}
/**
* 面板 Teleport + fixed避免接诊/Modal overflow 裁切
*/
function updatePanelPlacement() {
const el = containerRef.value;
if (!el) return;
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const gap = 4;
const spaceBelow = vh - rect.bottom - PANEL_PAD;
const spaceAbove = rect.top - PANEL_PAD;
const placeTop = spaceBelow < 180 && spaceAbove > spaceBelow;
const avail = Math.max(140, placeTop ? spaceAbove : spaceBelow);
const panelMax = Math.min(PREFERRED_PANEL, avail);
listViewportH.value = Math.max(80, panelMax - HINT_HEIGHT - HEAD_HEIGHT);
// 含拼音列后加宽,避免别名/全拼被裁切
const PANEL_MIN_WIDTH = 560;
const width = Math.min(
Math.max(rect.width, PANEL_MIN_WIDTH),
vw - PANEL_PAD * 2,
);
let left = rect.left;
if (left + width > vw - PANEL_PAD) {
left = vw - PANEL_PAD - width;
}
if (left < PANEL_PAD) left = PANEL_PAD;
const base: Record<string, string> = {
position: 'fixed',
zIndex: '4000',
width: `${width}px`,
maxHeight: `${panelMax}px`,
left: `${left}px`,
right: 'auto',
};
if (placeTop) {
panelStyle.value = {
...base,
top: 'auto',
bottom: `${Math.max(PANEL_PAD, vh - rect.top + gap)}px`,
};
} else {
panelStyle.value = {
...base,
top: `${rect.bottom + gap}px`,
bottom: 'auto',
};
}
}
/**
* 远程搜药;空关键词不请求(与原中药 Select 一致)
*/
async function fetchDrugs(kw: string) {
const name = String(kw || '').trim();
if (!name) {
options.value = [];
loading.value = false;
return;
}
loading.value = true;
try {
const res = await getProductListDoctorReception({
name,
type: props.type,
store_id: props.storeId || 0,
...(props.registerId > 0 ? { register_id: props.registerId } : {}),
});
const list = Array.isArray(res) ? res : [];
const mapped: ChineseDrugBubbleItem[] = list.map((item: any) => {
const drug = item.drug || {};
return {
...item,
id: Number(item.id || 0),
drug_id: Number(item.drug_id || drug.id || 0),
price: Number(item.price ?? 0),
buy_price: item.buy_price,
drug: {
...drug,
drug_name: drug.drug_name || item.drug_name || '',
},
};
});
options.value = rankByMatchScore(
mapped,
name,
(it) => drugNameOf(it),
(it) =>
[
it.drug?.pinyin_simple,
it.drug?.pinyin_full,
it.drug?.pinyin_display,
it.drug?.drug_alias,
it.matched_alias || it.drug?.matched_alias,
it.matched_alias_pinyin || it.drug?.matched_alias_pinyin,
it.drug?.search_digits,
it.drug?.specification,
]
.map((v) => String(v || ''))
.join(' '),
);
highlightIndex.value = options.value.length > 0 ? 0 : -1;
scrollTop.value = 0;
if (listRef.value) listRef.value.scrollTop = 0;
} catch (e) {
console.error('中药药名搜索失败:', e);
options.value = [];
} finally {
loading.value = false;
nextTick(() => updatePanelPlacement());
}
}
const debouncedFetch = useDebounceFn((kw: string) => {
void fetchDrugs(kw);
}, 280);
function openPanel() {
if (props.disabled) return;
open.value = true;
searching.value = true;
nextTick(() => {
updatePanelPlacement();
if (keyword.value.trim()) {
debouncedFetch(keyword.value);
} else {
options.value = [];
}
});
}
function closePanel(restoreDisplay = true) {
open.value = false;
searching.value = false;
options.value = [];
highlightIndex.value = 0;
if (restoreDisplay) {
keyword.value = String(props.displayName || '');
}
}
function handleFocus() {
if (props.disabled) return;
// 聚焦时清空为搜索态,便于直接输入新药名
if (props.displayName && keyword.value === props.displayName) {
keyword.value = '';
}
openPanel();
}
function handleInput() {
searching.value = true;
if (!open.value) openPanel();
else updatePanelPlacement();
debouncedFetch(keyword.value);
}
function selectItem(item: ChineseDrugBubbleItem) {
const name = drugNameOf(item);
keyword.value = name;
searching.value = false;
open.value = false;
options.value = [];
emit('select', item);
}
/**
* 键盘高亮循环移动(与诊断 EntryKeywordBubble 一致:第一项再上键 → 最后一项)
*/
function moveHighlight(delta: number) {
if (options.value.length === 0) return;
const len = options.value.length;
let next = highlightIndex.value;
if (next < 0) next = 0;
else next = (next + delta + len) % len;
highlightIndex.value = next;
ensureRowVisible(highlightIndex.value);
}
function handleKeydown(e: KeyboardEvent) {
if (!open.value) {
if (e.key === 'ArrowDown' || e.key === 'Enter') {
e.preventDefault();
openPanel();
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
closePanel(true);
return;
}
if (options.value.length === 0) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
moveHighlight(1);
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
moveHighlight(-1);
} else if (e.key === 'Enter') {
e.preventDefault();
const hit = options.value[highlightIndex.value];
if (hit) selectItem(hit);
}
}
function ensureRowVisible(index: number) {
const el = listRef.value;
if (!el || index < 0) return;
const top = index * ROW_HEIGHT;
const bottom = top + ROW_HEIGHT;
if (top < el.scrollTop) el.scrollTop = top;
else if (bottom > el.scrollTop + el.clientHeight) {
el.scrollTop = bottom - el.clientHeight;
}
// 同步虚拟列表切片(循环跳到首/尾时必须刷新可见区)
scrollTop.value = el.scrollTop;
}
function onListScroll() {
scrollTop.value = listRef.value?.scrollTop || 0;
}
function onDocPointerDown(e: MouseEvent) {
if (!open.value) return;
const t = e.target as Node;
if (containerRef.value?.contains(t)) return;
if (panelRef.value?.contains(t)) return;
closePanel(true);
}
function onWinChange() {
if (open.value) updatePanelPlacement();
}
onMounted(() => {
document.addEventListener('mousedown', onDocPointerDown, true);
window.addEventListener('resize', onWinChange);
window.addEventListener('scroll', onWinChange, true);
});
onUnmounted(() => {
document.removeEventListener('mousedown', onDocPointerDown, true);
window.removeEventListener('resize', onWinChange);
window.removeEventListener('scroll', onWinChange, true);
});
defineExpose({
focus() {
nextTick(() => {
const el = inputRef.value as any;
el?.focus?.();
const native = containerRef.value?.querySelector(
'input',
) as HTMLInputElement | null;
native?.focus();
});
},
close() {
closePanel(true);
},
});
</script>
<template>
<div
ref="containerRef"
class="chinese-drug-name-bubble"
:class="[{ 'chinese-drug-name-bubble--compact': compact }, inputClass]"
>
<Input
ref="inputRef"
v-model:value="keyword"
:placeholder="placeholder"
:disabled="disabled"
allow-clear
size="small"
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
>
<template #prefix>
<LoadingOutlined v-if="loading" class="text-muted-foreground" />
<SearchOutlined v-else class="text-muted-foreground" />
</template>
</Input>
<Teleport to="body">
<div
v-if="open"
ref="panelRef"
class="chinese-drug-name-bubble__panel"
:style="panelStyle"
>
<div class="chinese-drug-name-bubble__hint">
<span> 循环切换 · Enter 选用 · Esc 关闭</span>
<span class="chinese-drug-name-bubble__count">
<template v-if="keyword.trim()">搜到 {{ options.length }} </template>
<template v-else>输入药名搜索</template>
</span>
</div>
<Spin :spinning="loading">
<Empty
v-if="!loading && options.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
:description="
keyword.trim()
? '未找到匹配药材'
: '请输入药名/别名/拼音/数字搜索'
"
class="py-4"
/>
<template v-else>
<div
class="chinese-drug-name-bubble__head"
:class="{ 'has-score': !!keyword.trim() }"
>
<span class="col-name">药名</span>
<span class="col-py">拼音</span>
<span class="col-spec">规格/单位</span>
<span class="col-price">价格</span>
<span v-if="keyword.trim()" class="col-score">匹配度</span>
</div>
<div
ref="listRef"
class="chinese-drug-name-bubble__list"
:style="{ height: `${listViewportH}px` }"
@scroll="onListScroll"
>
<div
class="chinese-drug-name-bubble__phantom"
:style="{ height: `${totalHeight}px` }"
/>
<div
class="chinese-drug-name-bubble__virtual"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="{ item, index } in visibleRows"
:key="`${item.drug_id}_${index}`"
class="chinese-drug-name-bubble__row"
:class="{
'is-active': index === highlightIndex,
'has-score': !!keyword.trim(),
}"
:style="{ height: `${ROW_HEIGHT}px` }"
@mousedown.prevent="selectItem(item)"
@mouseenter="highlightIndex = index"
>
<span
class="col-name"
:title="
matchedAliasOf(item)
? `${drugNameOf(item)}(别名:${matchedAliasOf(item)}`
: drugNameOf(item)
"
>
<span class="col-name__main">
<template
v-for="(p, pi) in partsOf(drugNameOf(item))"
:key="`n_${pi}`"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
</template>
</span>
<span
v-if="matchedAliasOf(item)"
class="col-name__alias"
>
别名
<template
v-for="(p, pi) in partsOf(matchedAliasOf(item))"
:key="`a_${pi}`"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
</template>
</span>
</span>
<span class="col-py" :title="pinyinLabelOf(item)">
<template v-if="pinyinLabelOf(item) === '-'">-</template>
<template v-else>
<template
v-for="(p, pi) in partsOf(pinyinLabelOf(item))"
:key="`py_${pi}`"
>
<em v-if="p.hit" class="hit">{{ p.text }}</em>
<template v-else>{{ p.text }}</template>
</template>
</template>
</span>
<span class="col-spec" :title="unitLabelOf(item)">
{{ unitLabelOf(item) }}
</span>
<span class="col-price">{{ priceLabelOf(item) }}</span>
<span v-if="keyword.trim()" class="col-score">
{{ Number(item.match_score || 0) }}%
</span>
</div>
</div>
</div>
</template>
</Spin>
</div>
</Teleport>
</div>
</template>
<style scoped>
.chinese-drug-name-bubble {
position: relative;
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
/* 锁死宽度:避免接诊/复诊/特色方/常用方 flex 布局把药名框撑满整行 */
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
overflow: hidden;
}
.chinese-drug-name-bubble--compact {
width: 100px !important;
min-width: 88px !important;
max-width: 110px !important;
flex: 0 0 100px !important;
}
.chinese-drug-name-bubble :deep(.ant-input-affix-wrapper) {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
padding-inline: 6px;
box-sizing: border-box;
}
.chinese-drug-name-bubble :deep(.ant-input-prefix) {
margin-inline-end: 2px;
}
.chinese-drug-name-bubble :deep(.ant-input) {
width: 100% !important;
min-width: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
}
.chinese-drug-name-bubble__panel {
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: 8px;
border: 1px solid hsl(var(--border));
background: hsl(var(--popover, var(--background)));
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.chinese-drug-name-bubble__hint {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 12px;
font-size: 12px;
color: hsl(var(--muted-foreground));
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.35);
}
.chinese-drug-name-bubble__count {
flex-shrink: 0;
font-weight: 600;
color: hsl(var(--foreground));
}
.chinese-drug-name-bubble__list {
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
.chinese-drug-name-bubble__phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
pointer-events: none;
}
.chinese-drug-name-bubble__virtual {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.chinese-drug-name-bubble__head,
.chinese-drug-name-bubble__row {
display: grid;
grid-template-columns: minmax(90px, 1.1fr) minmax(110px, 1.3fr) 90px 64px;
gap: 8px;
padding: 6px 12px;
font-size: 13px;
align-items: center;
box-sizing: border-box;
}
.chinese-drug-name-bubble__head.has-score,
.chinese-drug-name-bubble__row.has-score {
grid-template-columns: minmax(90px, 1.1fr) minmax(100px, 1.2fr) 80px 56px 48px;
}
.chinese-drug-name-bubble__head {
flex-shrink: 0;
font-weight: 600;
color: hsl(var(--muted-foreground));
background: hsl(var(--popover, var(--background)));
border-bottom: 1px solid hsl(var(--border));
}
.chinese-drug-name-bubble__row {
cursor: pointer;
border-bottom: 1px solid hsl(var(--border) / 0.6);
}
.chinese-drug-name-bubble__row:hover,
.chinese-drug-name-bubble__row.is-active {
background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.col-name,
.col-py,
.col-spec {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-name {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
font-weight: 500;
white-space: normal;
line-height: 1.25;
}
.col-name__main {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-name__alias {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
font-weight: 400;
color: hsl(var(--muted-foreground));
}
.col-py {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.col-spec {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.col-price {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.col-score {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: #d4380d;
white-space: nowrap;
}
.chinese-drug-name-bubble__row.is-active .col-spec,
.chinese-drug-name-bubble__row.is-active .col-py,
.chinese-drug-name-bubble__row.is-active .col-name__alias {
color: inherit;
opacity: 0.85;
}
.chinese-drug-name-bubble__row.is-active .col-score {
color: inherit;
}
.hit {
font-style: normal;
color: #cf1322;
background: rgba(255, 214, 102, 0.55);
border-radius: 2px;
padding: 0 1px;
}
</style>

View File

@@ -0,0 +1,5 @@
/**
* 中药药名气泡选择器
*/
export { default as ChineseDrugNameBubble } from './chinese-drug-name-bubble.vue';
export type { ChineseDrugBubbleItem } from './chinese-drug-name-bubble.vue';

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* 医生端 PC 传方悬浮窗:悬浮按钮 + 子窗口列表 + 状态操作 * 医生端 PC 传方入口:顶栏按钮 + 子窗口列表 + 状态操作
* 权限:有 doctor_id 且绑定 store_id 时由外层 v-if 控制,本组件只负责展示与操作
*/ */
import { import {
computed, computed,
@@ -11,6 +12,7 @@ import {
} from 'vue'; } from 'vue';
import { import {
FileTextOutlined,
ReloadOutlined, ReloadOutlined,
ZoomInOutlined, ZoomInOutlined,
ZoomOutOutlined, ZoomOutOutlined,
@@ -19,6 +21,7 @@ import { usePreferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores'; import { useUserStore } from '@vben/stores';
import { import {
Badge,
Button, Button,
Dropdown, Dropdown,
Image, Image,
@@ -29,10 +32,10 @@ import {
Spin, Spin,
Tag, Tag,
Textarea, Textarea,
Tooltip,
message, message,
} from 'ant-design-vue'; } from 'ant-design-vue';
import FloatFab from '#/components/float-fab/FloatFab.vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue'; import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { import {
DEFAULT_SUB_WINDOW_Z_INDEX, DEFAULT_SUB_WINDOW_Z_INDEX,
@@ -355,12 +358,20 @@ defineExpose({ loadList });
</script> </script>
<template> <template>
<FloatFab <div class="doctor-transfer-header-entry">
storage-key="pc_doctor_transfer_fab" <Tooltip title="门店传方">
label="传方" <Badge :count="pendingCount" :overflow-count="99" size="small">
:badge="pendingCount" <Button
@click="openWindow" type="text"
/> class="transfer-header-btn"
@click="openWindow"
>
<FileTextOutlined />
<span class="transfer-header-btn__text">传方</span>
</Button>
</Badge>
</Tooltip>
</div>
<SubWindow <SubWindow
v-model:open="windowOpen" v-model:open="windowOpen"
title="门店传方" title="门店传方"
@@ -848,4 +859,25 @@ defineExpose({ loadList });
margin: 0 auto; margin: 0 auto;
user-select: none; user-select: none;
} }
.doctor-transfer-header-entry {
display: flex;
align-items: center;
margin-right: 4px;
}
.transfer-header-btn {
display: inline-flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 8px;
color: hsl(var(--foreground));
}
.transfer-header-btn__text {
font-size: 13px;
}
@media (max-width: 640px) {
.transfer-header-btn__text {
display: none;
}
}
</style> </style>

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
/**
* 通用文件预览:图片用 Ant ImagePDF fetch→blob→iframe支持多文件切换
* 开票中心与素材库复用,避免两套预览逻辑
*/
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { Button, Image, Modal, Spin, message } from 'ant-design-vue';
const props = withDefaults(
defineProps<{
/** 是否显示 */
open: boolean;
/** 文件 URL 列表 */
files?: string[];
/** 弹窗标题前缀 */
title?: string;
}>(),
{
files: () => [],
title: '文件预览',
},
);
const emit = defineEmits<{
(e: 'update:open', v: boolean): void;
}>();
const currentIndex = ref(0);
const previewIsImage = ref(false);
const previewImageUrl = ref('');
const previewPdfUrl = ref('');
const previewLoading = ref(false);
const visible = computed({
get: () => props.open,
set: (v: boolean) => emit('update:open', v),
});
const currentUrl = computed(() => props.files?.[currentIndex.value] || '');
const displayTitle = computed(() => {
const name = (currentUrl.value || '').split('/').pop() || '';
const total = props.files?.length || 0;
if (total > 1) {
return `${props.title}${currentIndex.value + 1}/${total}${name ? ' - ' + name : ''}`;
}
return name ? `${props.title} - ${name}` : props.title;
});
function isImageUrl(url: string) {
return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(url || '');
}
function revokePdfBlob() {
if (previewPdfUrl.value && previewPdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewPdfUrl.value);
}
previewPdfUrl.value = '';
}
/**
* 加载当前文件到预览区
*/
async function loadCurrent() {
const url = currentUrl.value;
revokePdfBlob();
if (!url) {
message.info('暂无文件');
visible.value = false;
return;
}
if (isImageUrl(url)) {
previewIsImage.value = true;
previewImageUrl.value = url;
previewLoading.value = false;
return;
}
previewIsImage.value = false;
previewLoading.value = true;
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error('文件加载失败');
const blob = await resp.blob();
const pdfBlob =
blob.type && blob.type !== 'application/octet-stream'
? blob
: new Blob([blob], { type: 'application/pdf' });
previewPdfUrl.value = URL.createObjectURL(pdfBlob);
} catch {
message.error('预览失败,请尝试下载后查看');
visible.value = false;
} finally {
previewLoading.value = false;
}
}
function prevFile() {
if (currentIndex.value > 0) {
currentIndex.value -= 1;
}
}
function nextFile() {
if (props.files && currentIndex.value < props.files.length - 1) {
currentIndex.value += 1;
}
}
function handleClose() {
visible.value = false;
}
watch(
() => [props.open, props.files] as const,
([open]) => {
if (open) {
currentIndex.value = 0;
loadCurrent();
} else {
revokePdfBlob();
}
},
);
watch(currentIndex, () => {
if (props.open) {
loadCurrent();
}
});
onBeforeUnmount(() => {
revokePdfBlob();
});
</script>
<template>
<Modal
v-model:open="visible"
:title="displayTitle"
:footer="null"
width="860px"
destroy-on-close
@cancel="handleClose"
>
<div class="preview-wrap">
<div v-if="(files?.length || 0) > 1" class="nav">
<Button size="small" :disabled="currentIndex <= 0" @click="prevFile">上一份</Button>
<Button
size="small"
:disabled="currentIndex >= (files?.length || 0) - 1"
@click="nextFile"
>
下一份
</Button>
</div>
<Spin :spinning="previewLoading">
<div v-if="previewIsImage" class="img-box">
<Image :src="previewImageUrl" :preview="true" style="max-width: 100%" />
</div>
<iframe
v-else-if="previewPdfUrl"
:src="previewPdfUrl"
class="pdf-frame"
title="pdf-preview"
/>
</Spin>
</div>
</Modal>
</template>
<style scoped>
.preview-wrap {
min-height: 240px;
color: hsl(var(--foreground));
}
.nav {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.img-box {
text-align: center;
padding: 8px;
border-radius: 8px;
background: hsl(var(--muted) / 0.25);
border: 1px solid hsl(var(--border));
}
.pdf-frame {
width: 100%;
height: 70vh;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--background));
}
</style>

View File

@@ -1,7 +1,7 @@
import type { CustomComponentType } from './types';
import type { Component } from 'vue'; import type { Component } from 'vue';
import type { CustomComponentType } from './types';
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool'; import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
const componentMap = new Map<CustomComponentType | string, Component>(); const componentMap = new Map<CustomComponentType | string, Component>();

View File

@@ -1,346 +1,346 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { computed, nextTick, ref, watch } from 'vue';
import { useDebounceFn, useVModel } from '@vueuse/core';
import { Avatar, Empty, Input, Popover, Spin, Tabs, Tag } from 'ant-design-vue';
import { useDebounceFn, useVModel } from '@vueuse/core'; import { searchApiLogUsers } from '#/views/log/api-log/api';
import { Avatar, Empty, Input, Popover, Spin, Tabs, Tag } from 'ant-design-vue'; defineOptions({
name: 'ApiLogUserPicker',
inheritAttrs: false,
});
import { searchApiLogUsers } from '#/views/log/api-log/api';
export type ApiLogUserFilter = {
id: number;
user_type: 'admin' | 'patient' | 'doctor';
defineOptions({ platform_type: 0 | 1 | 2;
name: string;
name: 'ApiLogUserPicker', role_label: string;
avatar?: string;
inheritAttrs: false, };
}); type ApiLogUserOption = ApiLogUserFilter;
type UserTab = 'admin' | 'patient' | 'doctor';
export type ApiLogUserFilter = { const props = defineProps<{
value?: ApiLogUserFilter;
id: number; disabled?: boolean;
onPlatformTypeChange?: (platformType: 0 | 1 | 2) => void;
user_type: 'admin' | 'patient' | 'doctor'; }>();
platform_type: 0 | 1 | 2; const emits = defineEmits<{
'update:value': [value: ApiLogUserFilter | undefined];
name: string; }>();
role_label: string; const mValue = useVModel(props, 'value', emits, { passive: true });
avatar?: string; const open = ref(false);
const loading = ref(false);
}; const searchKeyword = ref('');
const activeTab = ref<UserTab>('admin');
const options = ref<ApiLogUserOption[]>([]);
const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
type ApiLogUserOption = ApiLogUserFilter;
const TAB_ITEMS = [
{ key: 'admin', label: '后台用户' },
{ key: 'patient', label: '患者端用户' },
type UserTab = 'admin' | 'patient' | 'doctor'; { key: 'doctor', label: '医生端用户' },
] as const;
const searchPlaceholder = computed(() => {
const props = defineProps<{ if (activeTab.value === 'admin') return '搜索手机号或昵称';
if (activeTab.value === 'patient') return '搜索用户名字';
value?: ApiLogUserFilter; return '搜索名字或手机号';
});
disabled?: boolean;
async function fetchOptions() {
onPlatformTypeChange?: (platformType: 0 | 1 | 2) => void; const keyword = searchKeyword.value.trim();
if (!keyword) {
}>(); options.value = [];
return;
}
const emits = defineEmits<{ loading.value = true;
try {
'update:value': [value: ApiLogUserFilter | undefined]; const result = await searchApiLogUsers({
user_type: activeTab.value,
}>(); keyword,
pageSize: 24,
});
options.value = result?.items ?? [];
const mValue = useVModel(props, 'value', emits, { passive: true }); } finally {
loading.value = false;
}
}
const open = ref(false);
const debouncedFetch = useDebounceFn(fetchOptions, 300);
const loading = ref(false);
watch([searchKeyword, activeTab], () => {
const searchKeyword = ref(''); debouncedFetch();
});
const activeTab = ref<UserTab>('admin');
function selectOption(item: ApiLogUserOption) {
const options = ref<ApiLogUserOption[]>([]); mValue.value = {
id: item.id,
const searchInputRef = ref<InstanceType<typeof Input> | null>(null); user_type: item.user_type,
platform_type: item.platform_type,
name: item.name,
role_label: item.role_label,
const TAB_ITEMS = [ avatar: item.avatar,
};
{ key: 'admin', label: '后台用户' }, props.onPlatformTypeChange?.(item.platform_type);
open.value = false;
{ key: 'patient', label: '患者端用户' }, searchKeyword.value = '';
options.value = [];
{ key: 'doctor', label: '医生端用户' }, }
] as const; function clearSelection(e: Event) {
e.stopPropagation();
mValue.value = undefined;
}
const searchPlaceholder = computed(() => {
function onOpenChange(next: boolean) {
if (activeTab.value === 'admin') return '搜索手机号或昵称'; if (props.disabled) return;
open.value = next;
if (activeTab.value === 'patient') return '搜索用户名字'; if (next) {
searchKeyword.value = '';
return '搜索名字或手机号'; options.value = [];
nextTick(() => searchInputRef.value?.focus?.());
}); }
}
watch(
async function fetchOptions() { () => props.disabled,
(disabled) => {
const keyword = searchKeyword.value.trim(); if (disabled) open.value = false;
},
if (!keyword) { );
</script>
options.value = [];
<template>
return; <Popover
:open="open"
} trigger="click"
placement="bottomLeft"
overlay-class-name="api-log-user-picker-popover"
@open-change="onOpenChange"
loading.value = true; >
<template #content>
try { <div class="api-log-user-panel">
<Tabs v-model:active-key="activeTab" size="small" class="api-log-user-tabs">
const result = await searchApiLogUsers({ <Tabs.TabPane
v-for="tab in TAB_ITEMS"
user_type: activeTab.value, :key="tab.key"
:tab="tab.label"
keyword, />
</Tabs>
pageSize: 24, <Input
ref="searchInputRef"
}); v-model:value="searchKeyword"
allow-clear
options.value = result?.items ?? []; :placeholder="searchPlaceholder"
class="api-log-user-search"
} finally { />
<Spin :spinning="loading">
loading.value = false; <div v-if="options.length" class="api-log-user-grid">
<button
} v-for="item in options"
:key="`${item.user_type}-${item.id}`"
} type="button"
class="api-log-user-card"
:class="{
active:
const debouncedFetch = useDebounceFn(fetchOptions, 300); mValue?.id === item.id && mValue?.user_type === item.user_type,
}"
@click="selectOption(item)"
>
watch([searchKeyword, activeTab], () => { <Avatar :src="item.avatar" :size="30">
{{ (item.name || '?').charAt(0) }}
debouncedFetch(); </Avatar>
<div class="api-log-user-card-id">#{{ item.id }}</div>
}); <div class="api-log-user-card-name" :title="item.name">
{{ item.name }}
</div>
<Tag class="api-log-user-card-role" :bordered="false">
function selectOption(item: ApiLogUserOption) { {{ item.role_label }}
</Tag>
mValue.value = { </button>
</div>
id: item.id, <Empty
v-else
user_type: item.user_type, :description="searchKeyword.trim() ? '无匹配用户' : '请输入关键词搜索'"
class="api-log-user-empty"
platform_type: item.platform_type, />
</Spin>
name: item.name, </div>
</template>
role_label: item.role_label, <div class="api-log-user-trigger" :class="{ disabled: disabled }">
<div v-if="mValue" class="api-log-selected-card">
avatar: item.avatar, <Avatar :src="mValue.avatar" :size="36">
{{ (mValue.name || '?').charAt(0) }}
}; </Avatar>
<div class="api-log-selected-meta">
props.onPlatformTypeChange?.(item.platform_type); <div class="api-log-selected-name">{{ mValue.name }}</div>
<div class="api-log-selected-sub">
open.value = false; #{{ mValue.id }} · {{ mValue.role_label }}
</div>
searchKeyword.value = ''; </div>
<span
options.value = []; v-if="!disabled"
class="api-log-clear-btn"
} @click="clearSelection"
>
×
</span>
function clearSelection(e: Event) { </div>
<div v-else class="api-log-trigger-placeholder">请选择操作用户</div>
e.stopPropagation(); </div>
</Popover>
mValue.value = undefined; </template>
} <style scoped lang="scss">
@use './picker-card-theme.scss' as theme;
.api-log-user-trigger {
function onOpenChange(next: boolean) { width: 100%;
}
if (props.disabled) return;
.api-log-user-trigger.disabled {
open.value = next; cursor: not-allowed;
opacity: 0.65;
if (next) { }
searchKeyword.value = ''; .api-log-user-trigger:not(.disabled) {
cursor: pointer;
options.value = []; }
nextTick(() => searchInputRef.value?.focus?.()); .api-log-selected-card {
@include theme.picker-selected-card;
} }
} .api-log-selected-meta {
min-width: 0;
flex: 1;
}
watch(
.api-log-selected-name {
() => props.disabled, font-size: 14px;
font-weight: 500;
(disabled) => { color: #1d2129;
line-height: 1.4;
if (disabled) open.value = false; overflow: hidden;
text-overflow: ellipsis;
}, white-space: nowrap;
}
);
.api-log-selected-sub {
</script> font-size: 12px;
color: #86909c;
line-height: 1.4;
}
<template>
.api-log-trigger-placeholder {
<Popover @include theme.picker-trigger-placeholder;
}
:open="open"
.api-log-clear-btn {
trigger="click" @include theme.picker-clear-btn;
}
placement="bottomLeft"
.dark {
overlay-class-name="api-log-user-picker-popover" .api-log-selected-card {
@include theme.picker-selected-card-dark-props;
@open-change="onOpenChange" }
> .api-log-selected-name {
@include theme.picker-text-primary-dark;
<template #content> }
<div class="api-log-user-panel"> .api-log-selected-sub {
@include theme.picker-text-secondary-dark;
<Tabs v-model:active-key="activeTab" size="small" class="api-log-user-tabs"> }
<Tabs.TabPane .api-log-trigger-placeholder {
@include theme.picker-placeholder-dark-props;
v-for="tab in TAB_ITEMS" }
:key="tab.key" .api-log-clear-btn {
@include theme.picker-clear-dark-props;
:tab="tab.label" }
}
/> </style>
</Tabs> <style lang="scss">
@use './picker-card-theme.scss' as theme;
<Input
.api-log-user-picker-popover {
ref="searchInputRef" .ant-popover-inner {
padding: 12px;
v-model:value="searchKeyword" }
allow-clear .api-log-user-panel {
width: 540px;
:placeholder="searchPlaceholder" max-width: 86vw;
}
class="api-log-user-search"
.api-log-user-tabs {
/> margin-bottom: 8px;
}
<Spin :spinning="loading">
.api-log-user-search {
<div v-if="options.length" class="api-log-user-grid"> margin-bottom: 10px;
}
<button
.api-log-user-grid {
v-for="item in options" display: grid;
grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
:key="`${item.user_type}-${item.id}`" gap: 8px;
max-height: 300px;
type="button" overflow-y: auto;
padding: 2px;
class="api-log-user-card" }
:class="{ .api-log-user-card {
@include theme.picker-card-base;
active: }
mValue?.id === item.id && mValue?.user_type === item.user_type, .api-log-user-card-id {
@include theme.picker-card-sub;
}" }
@click="selectOption(item)" .api-log-user-card-name {
@include theme.picker-card-name;
> }
<Avatar :src="item.avatar" :size="30"> .api-log-user-card-role {
margin: 0;
{{ (item.name || '?').charAt(0) }} font-size: 11px;
line-height: 18px;
</Avatar> padding: 0 6px;
max-width: 100%;
<div class="api-log-user-card-id">#{{ item.id }}</div> overflow: hidden;
text-overflow: ellipsis;
<div class="api-log-user-card-name" :title="item.name"> }
{{ item.name }} .api-log-user-empty {
margin: 16px 0;
</div> }
}
<Tag class="api-log-user-card-role" :bordered="false">
.dark .api-log-user-picker-popover {
{{ item.role_label }} .api-log-user-card {
@include theme.picker-card-dark-props;
</Tag> }
</button> .api-log-user-card-name {
@include theme.picker-text-primary-dark;
</div> }
<Empty .api-log-user-card-id {
@include theme.picker-text-secondary-dark;
v-else }
}
:description="searchKeyword.trim() ? '无匹配用户' : '请输入关键词搜索'" </style>

View File

@@ -79,7 +79,7 @@ const handleRemove = (e: Event) => {
</div> </div>
<Icon v-else icon="ant-design:plus-outlined" /> <Icon v-else icon="ant-design:plus-outlined" />
</Upload> </Upload>
<GalleryPickLink @select="onGallerySelect" /> <GalleryPickLink :accept-types="[0]" @select="onGallerySelect" />
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { preferences } from '@vben/preferences';
import { QuillEditor } from '@vueup/vue-quill'; import { QuillEditor } from '@vueup/vue-quill';
// eslint-disable-next-line n/no-extraneous-import
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@@ -11,7 +11,9 @@ import { message } from 'ant-design-vue';
import { uploadFile } from '#/api/core/upload'; import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload'; import { uploadToOss } from '#/utils/oss-upload';
import { prepareImageForUpload } from '#/utils/prepare-image-upload'; import { prepareImageForUpload } from '#/utils/prepare-image-upload';
import { preferences } from '@vben/preferences';
// eslint-disable-next-line n/no-extraneous-import
import '@vueup/vue-quill/dist/vue-quill.snow.css';
const props = defineProps({ const props = defineProps({
value: { value: {
@@ -140,26 +142,26 @@ const insertImageToEditor = (quill: any, imageUrl: string): void => {
}; };
/** /**
* 图片上传函数 * 图片上传函数
* *
* 该函数根据preferences配置的上传方式选择使用OSS直传或后端上传 * 该函数根据preferences配置的上传方式选择使用OSS直传或后端上传
* - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS * - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS
* - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS * - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS
* *
* 上传方式说明: * 上传方式说明:
* 1. OSS直传direct * 1. OSS直传direct
* - 调用uploadToOss函数直接上传到OSS * - 调用uploadToOss函数直接上传到OSS
* - 减少服务器负载,上传速度更快 * - 减少服务器负载,上传速度更快
* - 支持上传进度回调(如果需要可以添加) * - 支持上传进度回调(如果需要可以添加)
* *
* 2. 后端上传backend * 2. 后端上传backend
* - 调用uploadFile API通过后端服务器上传 * - 调用uploadFile API通过后端服务器上传
* - 兼容现有功能,所有上传逻辑由后端统一处理 * - 兼容现有功能,所有上传逻辑由后端统一处理
* *
* 兼容性处理: * 兼容性处理:
* - 两种上传方式返回的数据结构保持一致:{ url: string } * - 两种上传方式返回的数据结构保持一致:{ url: string }
* - 上传成功或失败都会显示相应的提示信息 * - 上传成功或失败都会显示相应的提示信息
* - 上传完成后将isUpload标记设置为false允许下次上传 * - 上传完成后将isUpload标记设置为false允许下次上传
* *
* @param {File} file - 要上传的图片文件 * @param {File} file - 要上传的图片文件
* @returns {Promise<string>} - 返回上传后的图片URL如果上传失败返回空字符串 * @returns {Promise<string>} - 返回上传后的图片URL如果上传失败返回空字符串
*/ */
@@ -205,22 +207,22 @@ const uploadImage = async (file: File): Promise<string> => {
key: 'imageUpload', key: 'imageUpload',
duration: 2, duration: 2,
}); });
// 重置上传标记,允许下次上传 // 重置上传标记,允许下次上传
isUpload.value = false; isUpload.value = false;
// 返回上传后的图片URL // 返回上传后的图片URL
return data.url; return data.url;
} catch (error) { } catch (error) {
// 处理上传错误 // 处理上传错误
console.error('图片上传错误:', error); console.error('图片上传错误:', error);
// 显示错误提示 // 显示错误提示
message.error({ content: '图片上传失败', key: 'imageUpload', duration: 2 }); message.error({ content: '图片上传失败', key: 'imageUpload', duration: 2 });
// 重置上传标记,允许重试 // 重置上传标记,允许重试
isUpload.value = false; isUpload.value = false;
// 返回空字符串,表示上传失败 // 返回空字符串,表示上传失败
return ''; return '';
} }

View File

@@ -8,11 +8,17 @@ const props = withDefaults(
multiple?: boolean; multiple?: boolean;
maxCount?: number; maxCount?: number;
disabled?: boolean; disabled?: boolean;
/**
* 限制可选择的文件类型(透传给 image-gallery-picker按 xk_file.type / xk_file_type.value 过滤)
* 例如图片:[0];视频:[1];文件:[6];语音:[3]
*/
acceptTypes?: number[];
}>(), }>(),
{ {
multiple: false, multiple: false,
maxCount: 1, maxCount: 1,
disabled: false, disabled: false,
acceptTypes: () => [] as number[],
}, },
); );
@@ -41,6 +47,7 @@ function onSelect(urls: string[]) {
v-model:open="galleryOpen" v-model:open="galleryOpen"
:multiple="multiple" :multiple="multiple"
:max-count="maxCount" :max-count="maxCount"
:accept-types="acceptTypes"
@select="onSelect" @select="onSelect"
/> />
</div> </div>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Empty, Input, Modal, Pagination, Spin, Tabs } from 'ant-design-vue'; import { Empty, Input, Modal, Pagination, Spin, Tabs } from 'ant-design-vue';
@@ -14,10 +14,18 @@ const props = withDefaults(
open: boolean; open: boolean;
multiple?: boolean; multiple?: boolean;
maxCount?: number; maxCount?: number;
/**
* 限制可选择的文件类型(按 xk_file_type.value 过滤)
* - 不传:显示全部类型(默认行为,保持向后兼容)
* - 传数组:只展示指定类型 tab且自动锁定到第一个匹配的类型
* 例如图片:[0];图片+视频:[0, 1];文件:[6]
*/
acceptTypes?: number[];
}>(), }>(),
{ {
multiple: false, multiple: false,
maxCount: 1, maxCount: 1,
acceptTypes: () => [] as number[],
}, },
); );
@@ -34,6 +42,14 @@ const pageSize = ref(24);
const pageSizeOptions = ['12', '24', '48']; const pageSizeOptions = ['12', '24', '48'];
const selected = ref<string[]>([]); const selected = ref<string[]>([]);
/** 按 acceptTypes 隔离 localStorage避免图片/视频选择器互相污染 */
const storageKey = computed(
() =>
`file-picker-type-${
props.acceptTypes?.length ? props.acceptTypes.join('-') : 'all'
}`,
);
const { const {
activeType, activeType,
keyword, keyword,
@@ -42,14 +58,55 @@ const {
buildListParams, buildListParams,
fileTypes, fileTypes,
typesLoading, typesLoading,
} = useFileGalleryFilter('file-picker-active-type'); } = useFileGalleryFilter(() => storageKey.value);
let searchTimer: ReturnType<typeof setTimeout> | null = null; /**
* 实际渲染的 tabs按 acceptTypes 过滤)
*/
const visibleTabs = computed(() => {
if (!props.acceptTypes || props.acceptTypes.length === 0) {
return tabs.value;
}
return tabs.value.filter(
(tab) => tab.value !== -1 && props.acceptTypes.includes(Number(tab.value)),
);
});
/**
* 强制锁定到 acceptTypes 首项(打开弹窗 / 类型字典就绪时调用)
*/
function lockAcceptType() {
const arr = props.acceptTypes;
if (!arr || arr.length === 0) {
return;
}
if (activeType.value !== arr[0]) {
setActiveType(arr[0]!);
}
}
/**
* 是否允许拉列表:有 acceptTypes 时需等类型字典加载完且 visibleTabs 非空
*/
function canLoadList() {
if (typesLoading.value) {
return false;
}
if (props.acceptTypes && props.acceptTypes.length > 0) {
return visibleTabs.value.length > 0;
}
return true;
}
async function load() { async function load() {
if (!canLoadList()) {
return;
}
loading.value = true; loading.value = true;
try { try {
const res = await getFileGalleryList(buildListParams(page.value, pageSize.value)); const res = await getFileGalleryList(
buildListParams(page.value, pageSize.value),
);
const data = (res as any)?.data ?? res; const data = (res as any)?.data ?? res;
items.value = data?.items ?? []; items.value = data?.items ?? [];
total.value = data?.total ?? 0; total.value = data?.total ?? 0;
@@ -58,21 +115,37 @@ async function load() {
} }
} }
watch(
() => props.acceptTypes,
(arr) => {
if (!arr || arr.length === 0) {
return;
}
lockAcceptType();
page.value = 1;
void load();
},
{ immediate: true },
);
let searchTimer: ReturnType<typeof setTimeout> | null = null;
watch( watch(
() => props.open, () => props.open,
(val) => { (val) => {
if (val) { if (val) {
selected.value = []; selected.value = [];
page.value = 1; page.value = 1;
if (!typesLoading.value) { // 打开时再次按 acceptTypes 重锁,避免沿用其它选择器的视频 tab
void load(); lockAcceptType();
} void load();
} }
}, },
); );
watch(typesLoading, (val) => { watch(typesLoading, (val) => {
if (!val && props.open) { if (!val && props.open) {
lockAcceptType();
void load(); void load();
} }
}); });
@@ -143,6 +216,11 @@ function onSearch(value: string) {
function itemIcon(item: FileGalleryItem) { function itemIcon(item: FileGalleryItem) {
return item.type_icon || resolveTypeIcon(item.type, fileTypes.value); return item.type_icon || resolveTypeIcon(item.type, fileTypes.value);
} }
/** 图片类型展示缩略图xk_file.type0=图片;兼容历史错误写入的 1 */
function isImageItem(item: FileGalleryItem) {
return Number(item.type) === 0 || Number(item.type) === 1;
}
</script> </script>
<template> <template>
@@ -155,7 +233,7 @@ function itemIcon(item: FileGalleryItem) {
@cancel="handleCancel" @cancel="handleCancel"
> >
<Tabs :active-key="String(activeType)" size="small" @change="onTabChange"> <Tabs :active-key="String(activeType)" size="small" @change="onTabChange">
<Tabs.TabPane v-for="tab in tabs" :key="String(tab.value)"> <Tabs.TabPane v-for="tab in visibleTabs" :key="String(tab.value)">
<template #tab> <template #tab>
<span class="tab-label"> <span class="tab-label">
<MIcon v-if="tab.icon" :icon="tab.icon" size="12" /> <MIcon v-if="tab.icon" :icon="tab.icon" size="12" />
@@ -176,7 +254,7 @@ function itemIcon(item: FileGalleryItem) {
/> />
</div> </div>
<Spin :spinning="loading"> <Spin :spinning="loading || typesLoading">
<div v-if="items.length" class="gallery-grid"> <div v-if="items.length" class="gallery-grid">
<div <div
v-for="item in items" v-for="item in items"
@@ -185,10 +263,17 @@ function itemIcon(item: FileGalleryItem) {
:class="{ active: isSelected(item.url) }" :class="{ active: isSelected(item.url) }"
@click="toggleSelect(item.url)" @click="toggleSelect(item.url)"
> >
<img v-if="item.type === 0" :src="item.url" alt="" class="gallery-thumb" /> <img
v-if="isImageItem(item)"
:src="item.url"
alt=""
class="gallery-thumb"
/>
<div v-else class="gallery-file-icon"> <div v-else class="gallery-file-icon">
<MIcon :icon="itemIcon(item)" size="28" /> <MIcon :icon="itemIcon(item)" size="28" />
<span class="file-name">{{ item.file_name || item.original_name || '未命名' }}</span> <span class="file-name">{{
item.file_name || item.original_name || '未命名'
}}</span>
</div> </div>
<div class="gallery-meta"> <div class="gallery-meta">
<div class="file-title"> <div class="file-title">

View File

@@ -1,12 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* 门店多选搜索组件 * 门店多选搜索(对齐开方关键字气泡交互)
* - 普通输入框输入,下方气泡卡片展示匹配结果(名称 / 拼音首拼) * - Teleport + fixed 锚定,避免搜索栏 overflow 裁切导致「气泡不见」
* - 选中后在输入框下方用可关闭 Tag 展示「名称【id】」 * - 多选:点选后不关闭面板,已选项从列表剔除并以 Tag 展示
* - 搜索结果默认高亮第一项Enter 可直接选中 * - Enter / ↑↓ 键盘选用Esc 关闭
* - 样式使用主题 CSS 变量,自动适配暗色
*/ */
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import {
computed,
nextTick,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue';
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { useDebounceFn, useVModel } from '@vueuse/core'; import { useDebounceFn, useVModel } from '@vueuse/core';
@@ -28,16 +34,11 @@ export type StoreSearchItem = {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
/** 已选门店 ID 列表 */
value?: number[]; value?: number[];
/** 限定类型:0诊所 1药店不传则诊所+药店都可搜 */ /** 0诊所 1药店不传则诊所+药店 */
storeType?: 0 | 1; storeType?: 0 | 1;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
/**
* 外部已选门店详情(打开绑定弹窗时回填名称用)
* 仅用于 Tag 展示,不改变 value
*/
initialItems?: StoreSearchItem[]; initialItems?: StoreSearchItem[];
}>(), }>(),
{ {
@@ -59,13 +60,15 @@ const mValue = useVModel(props, 'value', emits, {
}); });
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const searchKeyword = ref(''); const searchKeyword = ref('');
const loading = ref(false); const loading = ref(false);
const showDropdown = ref(false); const showDropdown = ref(false);
const highlightIndex = ref(-1); const highlightIndex = ref(-1);
const options = ref<StoreSearchItem[]>([]); const options = ref<StoreSearchItem[]>([]);
/** 已选门店详情(用于 Tag 展示名称) */
const selectedMap = ref<Record<number, StoreSearchItem>>({}); const selectedMap = ref<Record<number, StoreSearchItem>>({});
const panelStyle = ref<Record<string, string>>({});
const selectedIds = computed(() => const selectedIds = computed(() =>
Array.isArray(mValue.value) ? mValue.value.map(Number) : [], Array.isArray(mValue.value) ? mValue.value.map(Number) : [],
@@ -78,28 +81,68 @@ const selectedItems = computed(() =>
}), }),
); );
/** 下拉中过滤掉已选 */ /** 下拉中过滤掉已选,方便连续多选 */
const visibleOptions = computed(() => const visibleOptions = computed(() =>
options.value.filter((item) => !selectedIds.value.includes(item.id)), options.value.filter((item) => !selectedIds.value.includes(item.id)),
); );
/**
* 将初始/回填门店写入 selectedMap便于 Tag 显示真实名称
*/
function mergeInitialItems(items: StoreSearchItem[]) { function mergeInitialItems(items: StoreSearchItem[]) {
if (!items?.length) return; if (!items?.length) return;
const next = { ...selectedMap.value }; const next = { ...selectedMap.value };
for (const item of items) { for (const item of items) {
if (item?.id) { if (item?.id) next[item.id] = item;
next[item.id] = item;
}
} }
selectedMap.value = next; selectedMap.value = next;
} }
/** /**
* 请求后端门店搜索;有结果时默认高亮第一项 * 相对输入框视口坐标放置面板(与 EntryKeywordBubble 同思路)
*/ */
function updatePanelPlacement() {
const el = containerRef.value;
if (!el) return;
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const gap = 4;
const pad = 8;
const spaceBelow = vh - rect.bottom - pad;
const spaceAbove = rect.top - pad;
const placeTop = spaceBelow < 200 && spaceAbove > spaceBelow;
const avail = Math.max(160, placeTop ? spaceAbove : spaceBelow);
const maxH = Math.min(320, avail);
const width = Math.min(Math.max(rect.width, 360), vw - pad * 2);
let left = rect.left;
if (left + width > vw - pad) left = vw - pad - width;
if (left < pad) left = pad;
const base: Record<string, string> = {
position: 'fixed',
zIndex: '4000',
width: `${width}px`,
maxHeight: `${maxH}px`,
left: `${left}px`,
right: 'auto',
};
panelStyle.value = placeTop
? {
...base,
top: 'auto',
bottom: `${Math.max(pad, vh - rect.top + gap)}px`,
}
: {
...base,
top: `${rect.bottom + gap}px`,
bottom: 'auto',
};
}
async function openPanel() {
showDropdown.value = true;
await nextTick();
updatePanelPlacement();
}
async function fetchOptions() { async function fetchOptions() {
const keyword = searchKeyword.value.trim(); const keyword = searchKeyword.value.trim();
if (!keyword) { if (!keyword) {
@@ -109,7 +152,7 @@ async function fetchOptions() {
return; return;
} }
loading.value = true; loading.value = true;
showDropdown.value = true; await openPanel();
try { try {
const res = await searchStoreOption({ const res = await searchStoreOption({
keyword, keyword,
@@ -119,8 +162,9 @@ async function fetchOptions() {
limit: 20, limit: 20,
}); });
options.value = res?.items ?? []; options.value = res?.items ?? [];
// 默认高亮第一项Enter 可直接选中 highlightIndex.value = visibleOptions.value.length > 0 ? 0 : -1;
highlightIndex.value = options.value.length > 0 ? 0 : -1; await nextTick();
updatePanelPlacement();
} catch { } catch {
options.value = []; options.value = [];
highlightIndex.value = -1; highlightIndex.value = -1;
@@ -136,30 +180,36 @@ function handleInput() {
} }
function handleFocus() { function handleFocus() {
if (visibleOptions.value.length > 0) { if (searchKeyword.value.trim() && options.value.length > 0) {
showDropdown.value = true; openPanel();
if (highlightIndex.value < 0) { if (highlightIndex.value < 0 && visibleOptions.value.length > 0) {
highlightIndex.value = 0; highlightIndex.value = 0;
} }
} }
} }
/** /**
* 选中一项:追加到 value缓存名称清空输入继续搜 * 多选:追加 ID保留关键词与面板不关闭气泡
*/ */
function selectOption(item: StoreSearchItem) { function selectOption(item: StoreSearchItem) {
if (selectedIds.value.includes(item.id)) return; if (selectedIds.value.includes(item.id)) return;
selectedMap.value = { ...selectedMap.value, [item.id]: item }; selectedMap.value = { ...selectedMap.value, [item.id]: item };
mValue.value = [...selectedIds.value, item.id]; mValue.value = [...selectedIds.value, item.id];
searchKeyword.value = ''; // 高亮下一项(列表已剔除刚选的)
options.value = []; nextTick(() => {
showDropdown.value = false; if (visibleOptions.value.length === 0) {
highlightIndex.value = -1; highlightIndex.value = -1;
return;
}
highlightIndex.value = Math.min(
Math.max(highlightIndex.value, 0),
visibleOptions.value.length - 1,
);
updatePanelPlacement();
scrollHighlightIntoView();
});
} }
/**
* 取消某个已选门店
*/
function removeSelected(id: number) { function removeSelected(id: number) {
mValue.value = selectedIds.value.filter((x) => x !== id); mValue.value = selectedIds.value.filter((x) => x !== id);
const next = { ...selectedMap.value }; const next = { ...selectedMap.value };
@@ -167,19 +217,52 @@ function removeSelected(id: number) {
selectedMap.value = next; selectedMap.value = next;
} }
function closePanel() {
showDropdown.value = false;
highlightIndex.value = -1;
}
/** 仅改 list 的 scrollTop表头固定不动 */
function scrollHighlightIntoView() {
nextTick(() => {
const listEl = listRef.value;
if (!listEl || highlightIndex.value < 0) return;
const el = listEl.querySelector(
`[data-store-opt-index="${highlightIndex.value}"]`,
) as HTMLElement | null;
if (!el) return;
const listRect = listEl.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const deltaTop = elRect.top - listRect.top;
const deltaBottom = elRect.bottom - listRect.bottom;
if (deltaTop < 0) {
listEl.scrollTop += deltaTop;
} else if (deltaBottom > 0) {
listEl.scrollTop += deltaBottom;
}
});
}
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (!showDropdown.value || visibleOptions.value.length === 0) return; if (!showDropdown.value || visibleOptions.value.length === 0) {
if (e.key === 'Escape') closePanel();
return;
}
const len = visibleOptions.value.length;
switch (e.key) { switch (e.key) {
case 'ArrowDown': case 'ArrowDown':
e.preventDefault(); e.preventDefault();
highlightIndex.value = Math.min( // 头尾循环:最后一项再按下 → 回到第一项
Math.max(highlightIndex.value, 0) + 1, highlightIndex.value =
visibleOptions.value.length - 1, highlightIndex.value < 0 ? 0 : (highlightIndex.value + 1) % len;
); scrollHighlightIntoView();
break; break;
case 'ArrowUp': case 'ArrowUp':
e.preventDefault(); e.preventDefault();
highlightIndex.value = Math.max(highlightIndex.value - 1, 0); // 头尾循环:第一项再按上 → 跳到最后一项
highlightIndex.value =
highlightIndex.value <= 0 ? len - 1 : highlightIndex.value - 1;
scrollHighlightIntoView();
break; break;
case 'Enter': case 'Enter':
e.preventDefault(); e.preventDefault();
@@ -190,35 +273,42 @@ function handleKeydown(e: KeyboardEvent) {
} }
break; break;
case 'Escape': case 'Escape':
showDropdown.value = false; e.preventDefault();
highlightIndex.value = -1; closePanel();
break; break;
} }
} }
function handleClickOutside(e: MouseEvent) { function handleClickOutside(e: MouseEvent) {
if (containerRef.value && !containerRef.value.contains(e.target as Node)) { if (!showDropdown.value) return;
showDropdown.value = false; const t = e.target as Node;
highlightIndex.value = -1; if (containerRef.value?.contains(t) || panelRef.value?.contains(t)) return;
} closePanel();
}
function onWinChange() {
if (showDropdown.value) updatePanelPlacement();
} }
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('resize', onWinChange);
window.addEventListener('scroll', onWinChange, true);
mergeInitialItems(props.initialItems || []); mergeInitialItems(props.initialItems || []);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('resize', onWinChange);
window.removeEventListener('scroll', onWinChange, true);
}); });
watch( watch(
() => props.storeType, () => props.storeType,
() => { () => {
options.value = []; options.value = [];
showDropdown.value = false; closePanel();
searchKeyword.value = ''; searchKeyword.value = '';
highlightIndex.value = -1;
}, },
); );
@@ -227,6 +317,17 @@ watch(
(items) => mergeInitialItems(items || []), (items) => mergeInitialItems(items || []),
{ deep: true }, { deep: true },
); );
watch(visibleOptions, (list) => {
if (!showDropdown.value) return;
if (list.length === 0) {
highlightIndex.value = -1;
return;
}
if (highlightIndex.value >= list.length) {
highlightIndex.value = list.length - 1;
}
});
</script> </script>
<template> <template>
@@ -246,33 +347,50 @@ watch(
</template> </template>
</Input> </Input>
<!-- 气泡建议列表 --> <Teleport to="body">
<div v-if="showDropdown" class="store-multi-search__dropdown"> <div
<Spin :spinning="loading"> v-if="showDropdown"
<Empty ref="panelRef"
v-if="!loading && visibleOptions.length === 0" class="store-multi-search__panel"
:image="Empty.PRESENTED_IMAGE_SIMPLE" :style="panelStyle"
description="暂无匹配门店" >
class="py-3" <div class="store-multi-search__hint">
/> <span> 循环切换 · Enter 选用 · Esc 关闭 · 可连续多选</span>
<div <span class="store-multi-search__count">
v-for="(item, index) in visibleOptions" {{ visibleOptions.length }} 条可选
:key="item.id" </span>
class="store-multi-search__option"
:class="{ 'is-active': index === highlightIndex }"
@mousedown.prevent="selectOption(item)"
>
<div class="store-multi-search__option-name">
{{ item.name }}{{ item.id }}
</div>
<div v-if="item.shouzimu" class="store-multi-search__option-sub">
首拼{{ item.shouzimu }}
</div>
</div> </div>
</Spin> <div class="store-multi-search__body">
</div> <Spin :spinning="loading">
<Empty
v-if="!loading && visibleOptions.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无匹配门店(或已全部选中)"
class="py-3"
/>
<div ref="listRef" class="store-multi-search__list">
<button
v-for="(item, index) in visibleOptions"
:key="item.id"
type="button"
class="store-multi-search__option"
:class="{ 'is-active': index === highlightIndex }"
:data-store-opt-index="index"
@mousedown.prevent="selectOption(item)"
>
<div class="store-multi-search__option-name">
{{ item.name }}{{ item.id }}
</div>
<div v-if="item.shouzimu" class="store-multi-search__option-sub">
首拼{{ item.shouzimu }}
</div>
</button>
</div>
</Spin>
</div>
</div>
</Teleport>
<!-- 已选 Tag -->
<div v-if="selectedItems.length > 0" class="store-multi-search__tags"> <div v-if="selectedItems.length > 0" class="store-multi-search__tags">
<Tag <Tag
v-for="item in selectedItems" v-for="item in selectedItems"
@@ -288,40 +406,93 @@ watch(
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/* 只用框架主题变量,随亮暗/主题色自动适配 */
.store-multi-search { .store-multi-search {
position: relative; position: relative;
width: 100%; width: 100%;
} }
.store-multi-search__dropdown { .store-multi-search__tags {
position: absolute; display: flex;
z-index: 1050; flex-wrap: wrap;
top: calc(100% + 4px); gap: 6px;
left: 0; margin-top: 8px;
right: 0; }
max-height: 280px; </style>
overflow-y: auto;
padding: 6px; <style lang="scss">
border-radius: 8px; /* Teleport 到 body非 scoped避免气泡被裁切/丢样式 */
background: hsl(var(--card)); .store-multi-search__panel {
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: 10px;
background: hsl(var(--card, var(--background)));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
box-shadow: box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 8px 24px hsl(0 0% 0% / 0.12),
0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 2px 8px hsl(0 0% 0% / 0.06);
0 9px 28px 8px rgba(0, 0, 0, 0.05); }
.store-multi-search__hint {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-shrink: 0;
padding: 8px 12px;
font-size: 12px;
color: hsl(var(--muted-foreground));
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.35);
}
.store-multi-search__count {
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
/* 表头固定,仅 body/list 滚动 */
.store-multi-search__body {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.store-multi-search__body .ant-spin-nested-loading,
.store-multi-search__body .ant-spin-container {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.store-multi-search__list {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding: 6px;
} }
.store-multi-search__option { .store-multi-search__option {
display: block;
width: 100%;
text-align: left;
padding: 8px 10px; padding: 8px 10px;
border: 0;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
background: transparent;
transition: background 0.15s; transition: background 0.15s;
}
&:hover, .store-multi-search__option:hover,
&.is-active { .store-multi-search__option.is-active {
background: hsl(var(--accent-hover)); background: hsl(var(--accent) / 0.85);
}
} }
.store-multi-search__option-name { .store-multi-search__option-name {
@@ -336,11 +507,4 @@ watch(
font-size: 12px; font-size: 12px;
color: hsl(var(--muted-foreground)); color: hsl(var(--muted-foreground));
} }
.store-multi-search__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
</style> </style>

View File

@@ -0,0 +1,371 @@
<script setup lang="ts">
/**
* 通用拖拽 + 粘贴上传Dragger 点击/拖拽,下方外侧「开始监听粘贴」
* v-model 为已上传 URL 字符串数组;列表支持图片/PDF 预览blob避免直接下载
*/
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { preferences } from '@vben/preferences';
import { Button, Image, Modal, Upload, message } from 'ant-design-vue';
import type { UploadFile } from 'ant-design-vue';
import { Icon } from '#/components/icon';
import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload';
import {
matchAccept,
usePasteUploadListen,
} from '#/utils/use-paste-upload-listen';
defineOptions({ name: 'UploadDraggerPaste' });
interface FileItem {
uid: string;
name: string;
status: 'uploading' | 'done' | 'error';
url: string;
}
interface Props {
modelValue?: string[];
accept?: string;
multiple?: boolean;
maxCount?: number;
/** 单文件最大 MB */
maxSizeMb?: number;
tip?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
accept: '.jpg,.jpeg,.png,.gif,.webp,.pdf,image/*,application/pdf',
multiple: true,
maxCount: 9,
maxSizeMb: 20,
tip: '点击或拖拽文件到此处上传',
disabled: false,
});
const emit = defineEmits<{
'update:modelValue': [value: string[]];
}>();
const UploadDragger = Upload.Dragger;
const fileList = ref<FileItem[]>([]);
const uploading = ref(false);
/** 文件预览弹窗状态 */
const previewVisible = ref(false);
const previewTitle = ref('');
const previewIsImage = ref(false);
const previewImageUrl = ref('');
const previewPdfUrl = ref('');
const previewLoading = ref(false);
const maxBytes = computed(() => props.maxSizeMb * 1024 * 1024);
function syncFromValue(urls: string[]) {
fileList.value = (urls || []).map((url) => ({
uid: url,
name: url.split('/').pop() || 'file',
status: 'done' as const,
url,
}));
}
syncFromValue(props.modelValue);
watch(
() => props.modelValue,
(val) => {
const current = fileList.value
.filter((f) => f.status === 'done')
.map((f) => f.url);
if (JSON.stringify(val || []) !== JSON.stringify(current)) {
syncFromValue(val || []);
}
},
{ deep: true },
);
function emitUrls() {
const urls = fileList.value
.filter((f) => f.status === 'done' && f.url)
.map((f) => f.url);
emit('update:modelValue', urls);
}
function isImageUrl(url: string) {
return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(url || '');
}
function revokePdfBlob() {
if (previewPdfUrl.value && previewPdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewPdfUrl.value);
}
previewPdfUrl.value = '';
}
/**
* 预览已上传文件:图片用 ImagePDF fetch 成 blob 后 iframe避免 OSS 直接下载
*/
async function previewItem(item: FileItem) {
if (!item.url) return;
revokePdfBlob();
previewTitle.value = item.name || '预览';
if (isImageUrl(item.url)) {
previewIsImage.value = true;
previewImageUrl.value = item.url;
previewVisible.value = true;
return;
}
previewIsImage.value = false;
previewLoading.value = true;
previewVisible.value = true;
try {
const resp = await fetch(item.url);
if (!resp.ok) throw new Error('加载失败');
const blob = await resp.blob();
const pdfBlob =
blob.type && blob.type !== 'application/octet-stream'
? blob
: new Blob([blob], { type: 'application/pdf' });
previewPdfUrl.value = URL.createObjectURL(pdfBlob);
} catch {
message.error('预览失败');
previewVisible.value = false;
} finally {
previewLoading.value = false;
}
}
function onPreviewClose() {
previewVisible.value = false;
revokePdfBlob();
previewImageUrl.value = '';
}
/**
* 上传单个文件并写入列表
*/
async function uploadOneFile(file: File) {
if (props.disabled) return;
const doneCount = fileList.value.filter((f) => f.status === 'done').length;
if (doneCount >= props.maxCount) {
message.warning(`最多上传 ${props.maxCount} 个文件`);
return;
}
if (!matchAccept(file, props.accept)) {
message.warning('文件类型不符合要求');
return;
}
if (file.size > maxBytes.value) {
message.warning(`文件不能超过 ${props.maxSizeMb}MB`);
return;
}
const uid = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
fileList.value = [
...fileList.value,
{ uid, name: file.name, status: 'uploading', url: '' },
];
uploading.value = true;
try {
const uploadMethod = preferences.app.uploadMethod || 'direct';
const res =
uploadMethod === 'direct'
? await uploadToOss({ file })
: await uploadFile({ file });
const url = res?.url || '';
if (!url) throw new Error('上传失败');
fileList.value = fileList.value.map((item) =>
item.uid === uid ? { ...item, status: 'done' as const, url } : item,
);
emitUrls();
} catch (e: any) {
fileList.value = fileList.value.filter((item) => item.uid !== uid);
message.error(e?.message || '上传失败');
} finally {
uploading.value = false;
}
}
/** Ant Design Upload customRequest */
const customRequest = async (options: any) => {
const { file, onSuccess, onError } = options;
try {
await uploadOneFile(file as File);
onSuccess?.({});
} catch (e) {
onError?.(e);
}
};
function beforeUpload(file: File) {
if (props.disabled) return Upload.LIST_IGNORE;
const doneCount = fileList.value.filter((f) => f.status === 'done').length;
if (doneCount >= props.maxCount) {
message.warning(`最多上传 ${props.maxCount} 个文件`);
return Upload.LIST_IGNORE;
}
if (!matchAccept(file, props.accept)) {
message.warning('文件类型不符合要求');
return Upload.LIST_IGNORE;
}
if (file.size > maxBytes.value) {
message.warning(`文件不能超过 ${props.maxSizeMb}MB`);
return Upload.LIST_IGNORE;
}
return true;
}
function handleRemove(file: UploadFile) {
fileList.value = fileList.value.filter((item) => item.uid !== file.uid);
emitUrls();
return true;
}
const {
listening: pasteListening,
toggle: togglePaste,
} = usePasteUploadListen({
get accept() {
return props.accept;
},
get maxSize() {
return maxBytes.value;
},
onFiles: async (files) => {
for (const file of files) {
const doneCount = fileList.value.filter((f) => f.status === 'done').length;
if (doneCount >= props.maxCount) {
message.warning(`最多上传 ${props.maxCount} 个文件`);
break;
}
await uploadOneFile(file);
}
},
});
function onTogglePaste() {
if (props.disabled) return;
togglePaste();
if (pasteListening.value) {
message.success('已开启粘贴监听,可 Ctrl+V 粘贴文件/截图');
} else {
message.info('已停止粘贴监听');
}
}
const showDragger = computed(() => {
if (!props.multiple) {
return fileList.value.filter((f) => f.status === 'done').length === 0;
}
return fileList.value.filter((f) => f.status === 'done').length < props.maxCount;
});
onBeforeUnmount(() => {
revokePdfBlob();
});
</script>
<template>
<div class="upload-dragger-paste">
<UploadDragger
v-if="showDragger"
:file-list="[]"
:accept="accept"
:multiple="multiple"
:disabled="disabled || uploading"
:before-upload="beforeUpload"
:custom-request="customRequest"
:show-upload-list="false"
:max-count="maxCount"
>
<p class="ant-upload-drag-icon">
<Icon icon="ant-design:inbox-outlined" class="text-4xl text-blue-500" />
</p>
<p class="ant-upload-text">{{ tip }}</p>
<p class="ant-upload-hint text-muted-foreground">
支持拖拽或点击选择单文件不超过 {{ maxSizeMb }}MB
</p>
</UploadDragger>
<div v-if="fileList.length" class="mt-3 space-y-2">
<div
v-for="item in fileList"
:key="item.uid"
class="flex items-center gap-2 rounded border border-border bg-muted/20 px-3 py-2 text-sm text-foreground"
>
<Icon
:icon="
item.status === 'uploading'
? 'ant-design:loading-outlined'
: item.status === 'error'
? 'ant-design:close-circle-outlined'
: 'ant-design:paper-clip-outlined'
"
class="shrink-0"
/>
<button
v-if="item.url"
type="button"
class="min-w-0 flex-1 truncate text-left text-primary"
@click="previewItem(item)"
>
{{ item.name }}
</button>
<span v-else class="min-w-0 flex-1 truncate text-muted-foreground">{{ item.name }}</span>
<Button
type="link"
size="small"
danger
:disabled="disabled"
@click="handleRemove(item as any)"
>
删除
</Button>
</div>
</div>
<!-- 粘贴监听按钮放在拖拽区域下方外侧 -->
<div class="mt-3">
<Button
:type="pasteListening ? 'primary' : 'default'"
:danger="pasteListening"
:disabled="disabled"
@click="onTogglePaste"
>
<Icon
:icon="pasteListening ? 'ant-design:stop-outlined' : 'ant-design:snippets-outlined'"
class="mr-1"
/>
{{ pasteListening ? '停止监听粘贴' : '开始监听粘贴' }}
</Button>
<span v-if="pasteListening" class="ml-2 text-xs text-orange-500 dark:text-orange-400">
监听中请将文件或截图 Ctrl+V 粘贴
</span>
</div>
<Modal
:open="previewVisible"
:title="previewTitle"
:footer="null"
width="80%"
destroy-on-close
@cancel="onPreviewClose"
>
<div v-if="previewLoading" class="py-16 text-center text-muted-foreground">加载中</div>
<div v-else-if="previewIsImage" class="flex justify-center rounded border border-border bg-muted/25 p-2">
<Image :src="previewImageUrl" :preview="true" style="max-height: 70vh" />
</div>
<iframe
v-else-if="previewPdfUrl"
:src="previewPdfUrl"
class="h-[70vh] w-full rounded border border-border bg-background"
title="文件预览"
/>
</Modal>
</div>
</template>

View File

@@ -24,12 +24,18 @@ interface Props {
modelValue: string[]; modelValue: string[];
multiple?: boolean; multiple?: boolean;
maxCount?: number; maxCount?: number;
/**
* 素材库可选类型(透传 GalleryPickLink
* xk_file.type0=图片 1=视频;默认仅图片。不传会展示全部类型
*/
acceptTypes?: number[];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: () => [], modelValue: () => [],
multiple: true, multiple: true,
maxCount: 9, maxCount: 9,
acceptTypes: () => [0],
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@@ -345,6 +351,7 @@ onMounted(() => {
v-if="showUploadButton" v-if="showUploadButton"
:multiple="multiple" :multiple="multiple"
:max-count="galleryRemainCount" :max-count="galleryRemainCount"
:accept-types="acceptTypes"
@select="onGallerySelect" @select="onGallerySelect"
/> />

View File

@@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
/**
* 多图上传picture-card+ 下方「开始监听粘贴」支持截图/图片粘贴
*/
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Modal, Upload } from 'ant-design-vue'; import { Button, Modal, Upload, message } from 'ant-design-vue';
import type { UploadFile } from 'ant-design-vue'; import type { UploadFile } from 'ant-design-vue';
import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue'; import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue';
@@ -10,9 +13,12 @@ import {
beforeImageUpload, beforeImageUpload,
resolveUploadFile, resolveUploadFile,
} from '#/utils/use-image-upload-pending'; } from '#/utils/use-image-upload-pending';
import { usePasteUploadListen } from '#/utils/use-paste-upload-listen';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { Icon } from '#/components/icon'; import { Icon } from '#/components/icon';
defineOptions({ name: 'UploadImage' });
interface FileItem { interface FileItem {
uid: string; uid: string;
name: string; name: string;
@@ -24,18 +30,26 @@ interface Props {
modelValue: string[]; modelValue: string[];
multiple?: boolean; multiple?: boolean;
maxCount?: number; maxCount?: number;
/**
* 素材库可选类型(透传 GalleryPickLink
* xk_file.type0=图片 1=视频,默认仅图片
*/
acceptTypes?: number[];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: () => [], modelValue: () => [],
multiple: true, multiple: true,
maxCount: 9, maxCount: 9,
acceptTypes: () => [0],
}); });
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: string[]]; 'update:modelValue': [value: string[]];
}>(); }>();
const IMAGE_ACCEPT = 'image/*,.jpg,.jpeg,.png,.gif,.webp,.bmp';
const fileList = ref<FileItem[]>([]); const fileList = ref<FileItem[]>([]);
const initFileList = () => { const initFileList = () => {
@@ -74,6 +88,41 @@ const updateModelValue = () => {
emit('update:modelValue', urls); emit('update:modelValue', urls);
}; };
/**
* 上传单个图片文件(点击上传与粘贴共用)
*/
async function uploadOneImage(file: File) {
const remain = props.maxCount - fileList.value.filter((f) => f.status === 'done').length;
if (remain <= 0) {
message.warning(`最多上传 ${props.maxCount} 张图片`);
return;
}
const actualFile = resolveUploadFile(file);
const uid = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uploadingFile: FileItem = {
uid,
name: actualFile.name || file.name || 'paste.png',
status: 'uploading',
url: '',
};
fileList.value = [...fileList.value, uploadingFile];
try {
const uploadMethod = preferences.app.uploadMethod || 'direct';
const res =
uploadMethod === 'direct'
? await uploadToOss({ file: actualFile })
: await uploadFile({ file: actualFile });
fileList.value = fileList.value.map((item) =>
item.uid === uid ? { ...item, status: 'done' as const, url: res.url } : item,
);
updateModelValue();
} catch (error) {
console.error('上传失败', error);
fileList.value = fileList.value.filter((item) => item.uid !== uid);
message.error('上传失败');
}
}
const customRequest = async (options: any) => { const customRequest = async (options: any) => {
const { file, onProgress, onSuccess, onError } = options; const { file, onProgress, onSuccess, onError } = options;
const actualFile = resolveUploadFile(file as File); const actualFile = resolveUploadFile(file as File);
@@ -155,6 +204,30 @@ const showUploadButton = computed(() =>
); );
const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.value.length, 0)); const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.value.length, 0));
const { listening: pasteListening, toggle: togglePaste } = usePasteUploadListen({
accept: IMAGE_ACCEPT,
maxSize: 20 * 1024 * 1024,
onFiles: async (files) => {
for (const file of files) {
const doneCount = fileList.value.filter((f) => f.status === 'done').length;
if (doneCount >= props.maxCount) {
message.warning(`最多上传 ${props.maxCount} 张图片`);
break;
}
await uploadOneImage(file);
}
},
});
function onTogglePaste() {
togglePaste();
if (pasteListening.value) {
message.success('已开启粘贴监听,可 Ctrl+V 粘贴图片/截图');
} else {
message.info('已停止粘贴监听');
}
}
</script> </script>
<template> <template>
@@ -184,9 +257,29 @@ const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.val
v-if="showUploadButton" v-if="showUploadButton"
:multiple="multiple" :multiple="multiple"
:max-count="galleryRemainCount" :max-count="galleryRemainCount"
:accept-types="acceptTypes"
@select="onGallerySelect" @select="onGallerySelect"
/> />
<!-- 粘贴监听放在上传区下方外侧 -->
<div class="mt-2">
<Button
size="small"
:type="pasteListening ? 'primary' : 'default'"
:danger="pasteListening"
@click="onTogglePaste"
>
<Icon
:icon="pasteListening ? 'ant-design:stop-outlined' : 'ant-design:snippets-outlined'"
class="mr-1"
/>
{{ pasteListening ? '停止监听粘贴' : '开始监听粘贴' }}
</Button>
<span v-if="pasteListening" class="ml-2 text-xs text-orange-500">
监听中Ctrl+V 粘贴图片
</span>
</div>
<Modal v-model:visible="previewVisible" :title="previewTitle" footer="" width="60%"> <Modal v-model:visible="previewVisible" :title="previewTitle" footer="" width="60%">
<img alt="预览图片" style="width: 100%" :src="previewImage" /> <img alt="预览图片" style="width: 100%" :src="previewImage" />
</Modal> </Modal>

View File

@@ -1,8 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
/**
* OSS 文件上传(单文件 URL 字符串)
* 可选从素材库选择acceptTypes 过滤,如文件=6、语音=3
*/
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { Button, Upload } from 'ant-design-vue'; import { Button, Upload } from 'ant-design-vue';
import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue';
import { Icon } from '#/components/icon'; import { Icon } from '#/components/icon';
import { uploadFile } from '#/api/core/upload'; import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload'; import { uploadToOss } from '#/utils/oss-upload';
@@ -17,11 +23,17 @@ interface FileItem {
interface Props { interface Props {
modelValue?: string; modelValue?: string;
maxCount?: number; maxCount?: number;
/** 素材库类型过滤,如 [6] 文件、[3] 语音 */
acceptTypes?: number[];
/** input accept如 .pdf,.doc 或 audio/* */
accept?: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
modelValue: '', modelValue: '',
maxCount: 1, maxCount: 1,
acceptTypes: () => [6],
accept: '.pdf,.doc,.docx,.zip,.rar,.txt,application/pdf,audio/*,*/*',
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@@ -131,26 +143,50 @@ function handleRemove() {
return true; return true;
} }
function onGallerySelect(urls: string[]) {
const url = urls[0] || '';
if (!url) return;
syncFileListFromValue(url);
updateModelValue();
}
const showUploadButton = computed( const showUploadButton = computed(
() => fileList.value.filter((item) => item.status !== 'error').length === 0, () => fileList.value.filter((item) => item.status !== 'error').length === 0,
); );
</script> </script>
<template> <template>
<Upload <div class="upload-oss-file-wrap">
:file-list="fileList" <Upload
:before-upload="beforeUpload" :file-list="fileList"
:custom-request="customRequest" :before-upload="beforeUpload"
:max-count="maxCount" :custom-request="customRequest"
list-type="text" :max-count="maxCount"
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,image/*,application/pdf" list-type="text"
@remove="handleRemove" :accept="accept"
> @remove="handleRemove"
<Button v-if="showUploadButton"> >
上传文件 <Button v-if="showUploadButton">
<template #icon> 上传文件
<Icon icon="ant-design:cloud-upload-outlined" /> <template #icon>
</template> <Icon icon="ant-design:cloud-upload-outlined" />
</Button> </template>
</Upload> </Button>
</Upload>
<GalleryPickLink
v-if="showUploadButton"
:max-count="1"
:accept-types="acceptTypes"
@select="onGallerySelect"
/>
</div>
</template> </template>
<style scoped>
.upload-oss-file-wrap {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
</style>

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
/**
* 通用 Tag 选择器(带头像)
*
* - 基于 antd Selectmultiple 模式默认开启,单选传 multiple=false
* - 已选项以 Tag 形式展示头像+名字:
* - 有 avatar 用图片头像
* - 无 avatar 用文本头像(取 label 首字符)
* - 下拉选项同样展示头像+名字
* - 选项数据options: { value, label, avatar? }[]
*/
import { computed } from 'vue';
import { useVModel } from '@vueuse/core';
import { Avatar, Select, Tag } from 'ant-design-vue';
defineOptions({
name: 'UserTagSelect',
inheritAttrs: false,
});
/** 单个选项结构 */
export interface UserTagOption {
value: string | number;
label: string;
avatar?: string;
}
const props = withDefaults(
defineProps<{
/** 已选值数组(多选)或单值(单选时仍用数组包装) */
modelValue?: (string | number)[];
/** 选项列表 */
options: UserTagOption[];
/** 是否多选,默认 true */
multiple?: boolean;
/** 最大选择数,超出截断 */
maxCount?: number;
placeholder?: string;
allowClear?: boolean;
showSearch?: boolean;
disabled?: boolean;
}>(),
{
modelValue: () => [],
multiple: true,
maxCount: 99,
placeholder: '请选择',
allowClear: true,
showSearch: true,
disabled: false,
},
);
const emit = defineEmits<{
'update:modelValue': [v: (string | number)[]];
}>();
// 双向绑定useVModel 自动处理 modelValue 的读写
const innerValue = useVModel(props, 'modelValue', emit, {
defaultValue: [] as (string | number)[],
passive: true,
});
/** 用于 Select 的 mode单选时不传 mode */
const selectMode = computed(() => (props.multiple ? 'multiple' : undefined));
/** 按 value 索引选项,方便 tagRender/option 插槽快速查头像与名字 */
const optionMap = computed(() => {
const map = new Map<string | number, UserTagOption>();
for (const opt of props.options) {
map.set(opt.value, opt);
}
return map;
});
/** 取某 value 对应的展示名 */
function labelOf(value: string | number) {
return optionMap.value.get(value)?.label || String(value);
}
/** 取某 value 对应的头像 */
function avatarOf(value: string | number) {
return optionMap.value.get(value)?.avatar;
}
/** 取首字符(文本头像) */
function firstChar(label?: string) {
return (label || '?').trim().charAt(0) || '?';
}
/** 选项过滤:按 label 包含输入关键字 */
function filterOption(input: string, option: any) {
const v = String(option?.label || '').toLowerCase();
return v.includes((input || '').toLowerCase());
}
/**
* change 处理:
* - 单选:把单值包成数组
* - 多选:超 maxCount 截断
*/
function onChange(v: any) {
if (!props.multiple) {
innerValue.value = v == null ? [] : [v];
return;
}
if (Array.isArray(v) && v.length > props.maxCount) {
v = v.slice(0, props.maxCount);
}
innerValue.value = v;
}
</script>
<template>
<Select
:value="innerValue as any"
:mode="selectMode"
:options="options"
:placeholder="placeholder"
:allow-clear="allowClear"
:show-search="showSearch"
:filter-option="filterOption"
:disabled="disabled"
:max-tag-count="10"
option-filter-prop="label"
style="width: 100%"
@change="onChange"
>
<!-- 多选已选项展示头像+名字 -->
<template v-if="multiple" #tagRender="{ value, closable, onClose }">
<Tag :closable="closable" @close="onClose" class="user-tag-chip">
<Avatar :size="16" :src="avatarOf(value)">
{{ firstChar(labelOf(value)) }}
</Avatar>
<span class="user-tag-name">{{ labelOf(value) }}</span>
</Tag>
</template>
<!-- 下拉选项展示头像+名字 -->
<template #option="{ label, avatar }">
<div class="user-tag-option">
<Avatar :size="20" :src="avatar">{{ firstChar(label) }}</Avatar>
<span>{{ label }}</span>
</div>
</template>
</Select>
</template>
<style scoped>
.user-tag-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
}
.user-tag-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-tag-option {
display: flex;
align-items: center;
gap: 8px;
}
</style>

View File

@@ -0,0 +1,714 @@
<template>
<!--
AI 加载动画处方=药丸病历=病历本
type + text 由调用方传入嵌入抽屉时默认不显示主题切换
-->
<div
ref="containerRef"
class="loading-wrapper"
:class="{ 'loading-wrapper--dark': isDark }"
:style="{ width: '100%', height: '100%' }"
>
<canvas ref="canvasRef" class="loading-canvas"></canvas>
<button
v-if="showToggle"
class="theme-toggle-btn"
:title="isDark ? '切换到亮色模式' : '切换到暗色模式'"
@click="toggleTheme"
>
<span class="theme-icon">
<svg
v-if="!isDark"
class="sun"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
</svg>
<svg
v-else
class="moon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
</span>
<!-- <span class="theme-label">{{ isDark ? '亮色' : '暗色' }}</span>-->
</button>
</div>
</template>
<script setup lang="ts">
/**
* AI 生成加载动画
* - type=prescription中心画胶囊药丸
* - type=medical_record中心画病历本
* - text底部文案可动态传
*/
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
export type AiLoadingType = 'prescription' | 'medical_record';
const props = withDefaults(
defineProps<{
/** 动画类型:处方 / 病历 */
type?: AiLoadingType;
/** 底部文案 */
text?: string;
/** 是否显示主题切换(嵌入业务抽屉建议关闭) */
showToggle?: boolean;
}>(),
{
type: 'prescription',
text: '',
showToggle: false,
},
);
/** 未传文案时按 type 给默认值 */
const displayText = computed(() => {
const t = String(props.text || '').trim();
if (t) return t;
return props.type === 'medical_record'
? 'AI正在生成病历'
: 'AI正在生成处方';
});
const containerRef = ref<HTMLDivElement | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const isDark = ref(false);
/** 画布循环里读取最新文案/类型,避免闭包过期 */
const liveText = ref(displayText.value);
const liveType = ref<AiLoadingType>(props.type);
watch(
displayText,
(v) => {
liveText.value = v;
},
{ immediate: true },
);
watch(
() => props.type,
(v) => {
liveType.value = v || 'prescription';
},
{ immediate: true },
);
function detectAppDark(): boolean {
return (
document.documentElement.classList.contains('dark') ||
document.documentElement.getAttribute('data-theme') === 'dark' ||
document.documentElement.getAttribute('class')?.includes('dark') === true
);
}
function syncThemeFromApp() {
if (localStorage.getItem('prescription-loading-theme')) {
isDark.value = localStorage.getItem('prescription-loading-theme') === 'dark';
return;
}
isDark.value = detectAppDark();
}
function toggleTheme() {
isDark.value = !isDark.value;
localStorage.setItem(
'prescription-loading-theme',
isDark.value ? 'dark' : 'light',
);
resetParticles?.();
}
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
life: number;
maxLife: number;
size: number;
color: { r: number; g: number; b: number };
type: 'circle' | 'cross';
}
interface ThemeColors {
textColor: string;
ringColor: string;
glowInner: string;
glowMiddle: string;
glowOuter: string;
particleBright: number;
iconShadow: string;
}
let animationId = 0;
let resetParticles: (() => void) | null = null;
let themeObserver: MutationObserver | null = null;
let resizeObserver: ResizeObserver | null = null;
let removeWindowResize: (() => void) | null = null;
onMounted(async () => {
await nextTick();
syncThemeFromApp();
if (!containerRef.value || !canvasRef.value) return;
const canvas = canvasRef.value;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const container = containerRef.value;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let cssWidth = 0;
let cssHeight = 0;
let prevW = 0;
let prevH = 0;
let startTime: number | null = null;
let lastTime: number | null = null;
let particles: Particle[] = [];
const MAX_PARTICLES = 28;
resetParticles = () => {
particles = [];
};
function updateSize() {
const rect = container.getBoundingClientRect();
cssWidth = rect.width || 300;
cssHeight = rect.height || 280;
const pw = Math.round(cssWidth * dpr);
const ph = Math.round(cssHeight * dpr);
if (canvas.width !== pw || canvas.height !== ph) {
canvas.width = pw;
canvas.height = ph;
if (
prevW &&
(Math.abs(cssWidth - prevW) > 2 || Math.abs(cssHeight - prevH) > 2)
) {
particles = [];
}
prevW = cssWidth;
prevH = cssHeight;
}
}
function createParticle(cx: number, cy: number, s: number): Particle {
const angle = Math.random() * Math.PI * 2;
const distance = s * (0.15 + Math.random() * 0.45);
const speed = s * (0.03 + Math.random() * 0.08);
const colors = [
{ r: 61, g: 214, b: 208 },
{ r: 74, g: 195, b: 190 },
{ r: 93, g: 173, b: 226 },
{ r: 46, g: 180, b: 170 },
{ r: 100, g: 200, b: 215 },
];
const color = colors[Math.floor(Math.random() * colors.length)]!;
return {
x: cx + Math.cos(angle) * distance,
y: cy + Math.sin(angle) * distance,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1,
maxLife: 1.8 + Math.random() * 2.5,
size: 1.2 + Math.random() * 2.8,
color,
type: Math.random() < 0.25 ? 'cross' : 'circle',
};
}
function updateParticles(dt: number, cx: number, cy: number, s: number) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]!;
p.life -= dt / p.maxLife;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.997;
p.vy *= 0.997;
if (p.life <= 0) particles.splice(i, 1);
}
while (particles.length < MAX_PARTICLES) {
const p = createParticle(cx, cy, s);
const ang = Math.random() * Math.PI * 2;
p.x = cx + Math.cos(ang) * s * 0.12 * Math.random();
p.y = cy + Math.sin(ang) * s * 0.12 * Math.random();
particles.push(p);
}
}
function drawParticle(
c: CanvasRenderingContext2D,
p: Particle,
bright: number,
) {
const alpha = p.life;
const { r, g, b } = p.color;
const size = p.size * (0.7 + 0.3 * p.life);
const br = Math.min(255, Math.round(r * bright));
const bg = Math.min(255, Math.round(g * bright));
const bb = Math.min(255, Math.round(b * bright));
c.save();
c.globalAlpha = alpha;
if (p.type === 'cross') {
const len = size * 1.8;
const w = size * 0.55;
c.strokeStyle = `rgba(${br},${bg},${bb},${alpha})`;
c.lineWidth = w;
c.lineCap = 'round';
c.beginPath();
c.moveTo(p.x - len, p.y);
c.lineTo(p.x + len, p.y);
c.stroke();
c.beginPath();
c.moveTo(p.x, p.y - len);
c.lineTo(p.x, p.y + len);
c.stroke();
c.fillStyle = `rgba(255,255,255,${alpha * 0.9})`;
c.beginPath();
c.arc(p.x, p.y, size * 0.35, 0, Math.PI * 2);
c.fill();
} else {
c.shadowColor = `rgba(${br},${bg},${bb},${alpha * 0.7})`;
c.shadowBlur = size * 2.5;
c.fillStyle = `rgba(${br},${bg},${bb},${alpha})`;
c.beginPath();
c.arc(p.x, p.y, size, 0, Math.PI * 2);
c.fill();
c.shadowBlur = 0;
c.fillStyle = `rgba(255,255,255,${alpha * 0.6})`;
c.beginPath();
c.arc(p.x, p.y, size * 0.4, 0, Math.PI * 2);
c.fill();
}
c.restore();
}
/** 处方药丸(处方) */
function drawPill(
c: CanvasRenderingContext2D,
px: number,
py: number,
pw: number,
ph: number,
floatOff: number,
pulse: number,
shadowCol: string,
) {
c.save();
const cx = px + pw / 2;
const cy = py + ph / 2;
const hw = (pw / 2) * pulse;
const hh = (ph / 2) * pulse;
const adjY = cy + floatOff;
const dx = cx - hw;
const dy = adjY - hh;
const dw = hw * 2;
const dh = hh * 2;
const rad = hh;
const path = new Path2D();
path.roundRect(dx, dy, dw, dh, rad);
c.shadowColor = shadowCol;
c.shadowBlur = hh * 0.7;
c.shadowOffsetY = hh * 0.25;
c.fillStyle = '#ffffff';
c.fill(path);
c.shadowColor = 'transparent';
c.shadowBlur = 0;
c.shadowOffsetY = 0;
c.save();
c.clip(path);
const leftGrad = c.createLinearGradient(dx, 0, dx + dw * 0.5, 0);
leftGrad.addColorStop(0, '#FDFBF7');
leftGrad.addColorStop(0.7, '#F5F1EA');
leftGrad.addColorStop(1, '#EDE8DF');
c.fillStyle = leftGrad;
c.fillRect(dx, dy, dw * 0.5, dh);
const rightGrad = c.createLinearGradient(dx + dw * 0.5, 0, dx + dw, 0);
rightGrad.addColorStop(0, '#5DD9D1');
rightGrad.addColorStop(0.5, '#3DD6D0');
rightGrad.addColorStop(1, '#2CB8B0');
c.fillStyle = rightGrad;
c.fillRect(dx + dw * 0.5, dy, dw * 0.5, dh);
c.strokeStyle = 'rgba(180,180,180,0.5)';
c.lineWidth = Math.max(0.6, hh * 0.06);
c.beginPath();
c.moveTo(dx + dw * 0.5, dy + 1);
c.lineTo(dx + dw * 0.5, dy + dh - 1);
c.stroke();
const hy = dy + dh * 0.15;
const hh2 = dh * 0.38;
const hp = new Path2D();
hp.roundRect(dx + dw * 0.06, hy, dw * 0.88, hh2, hh2 / 2);
c.fillStyle = 'rgba(255,255,255,0.45)';
c.fill(hp);
c.restore();
c.strokeStyle = 'rgba(200,200,200,0.5)';
c.lineWidth = Math.max(0.8, hh * 0.08);
c.stroke(path);
c.restore();
}
/** 病历本(病历) */
function drawMedicalRecord(
c: CanvasRenderingContext2D,
cx: number,
cy: number,
scale: number,
floatOff: number,
pulse: number,
shadowCol: string,
) {
c.save();
const w = scale * 0.28 * pulse;
const h = scale * 0.34 * pulse;
const x = cx - w / 2;
const y = cy - h / 2 + floatOff;
const r = Math.max(4, scale * 0.018);
const path = new Path2D();
path.roundRect(x, y, w, h, r);
c.shadowColor = shadowCol;
c.shadowBlur = scale * 0.035;
c.shadowOffsetY = scale * 0.012;
c.fillStyle = '#ffffff';
c.fill(path);
c.shadowColor = 'transparent';
c.shadowBlur = 0;
c.shadowOffsetY = 0;
// 左侧装订条
c.fillStyle = '#3DD6D0';
c.fillRect(x, y + r * 0.2, w * 0.12, h - r * 0.4);
// 横线(像病历行)
c.strokeStyle = 'rgba(61,214,208,0.35)';
c.lineWidth = Math.max(1, scale * 0.004);
for (let i = 0; i < 4; i++) {
const ly = y + h * (0.32 + i * 0.14);
c.beginPath();
c.moveTo(x + w * 0.22, ly);
c.lineTo(x + w * 0.88, ly);
c.stroke();
}
// 顶部标题条
c.fillStyle = 'rgba(61,214,208,0.2)';
c.beginPath();
c.roundRect(x + w * 0.22, y + h * 0.12, w * 0.5, h * 0.08, 2);
c.fill();
c.strokeStyle = 'rgba(180,200,200,0.55)';
c.lineWidth = Math.max(0.8, scale * 0.005);
c.stroke(path);
c.restore();
}
function drawCross(
c: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
alpha: number,
rot: number,
) {
c.save();
c.translate(x, y);
c.rotate(rot);
c.globalAlpha = alpha;
const len = size;
const w = size * 0.3;
c.strokeStyle = '#3DD6D0';
c.lineWidth = w;
c.lineCap = 'round';
c.beginPath();
c.moveTo(-len, 0);
c.lineTo(len, 0);
c.stroke();
c.beginPath();
c.moveTo(0, -len);
c.lineTo(0, len);
c.stroke();
c.fillStyle = '#ffffff';
c.beginPath();
c.arc(0, 0, size * 0.25, 0, Math.PI * 2);
c.fill();
c.restore();
}
function getTheme(): ThemeColors {
const dark = isDark.value;
return {
textColor: dark ? '#d1d5db' : '#4a5568',
ringColor: dark ? 'rgba(255,255,255,0.25)' : 'rgba(180,195,200,0.45)',
glowInner: dark ? 'rgba(61,214,208,0.15)' : 'rgba(61,214,208,0.13)',
glowMiddle: dark ? 'rgba(61,214,208,0.05)' : 'rgba(61,214,208,0.04)',
glowOuter: 'rgba(61,214,208,0)',
particleBright: dark ? 1.4 : 1.0,
iconShadow: dark ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.12)',
};
}
function draw(timestamp: number) {
updateSize();
if (cssWidth <= 0 || cssHeight <= 0) {
animationId = requestAnimationFrame(draw);
return;
}
if (startTime === null) {
startTime = timestamp;
lastTime = timestamp;
}
const elapsed = (timestamp - startTime) / 1000;
let dt = (timestamp - (lastTime ?? timestamp)) / 1000;
if (dt <= 0) dt = 0.016;
if (dt > 0.2) dt = 0.2;
lastTime = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(dpr, dpr);
const cx = cssWidth / 2;
const cy = cssHeight / 2 - cssHeight * 0.04;
const scale = Math.min(cssWidth, cssHeight);
const orbitR = scale * 0.26;
const fontSize = Math.max(12, scale * 0.055);
const textY = cy + orbitR + fontSize * 2.4;
const text = liveText.value;
const animType = liveType.value;
const dashOff = elapsed * scale * 0.025;
const arcRot = elapsed * 3.5;
const arcLen = (100 * Math.PI) / 180;
const floatOff = Math.sin(elapsed * 2.5) * scale * 0.025;
const pulseS = 1 + Math.sin(elapsed * 3.7) * 0.04;
const textAlpha = 0.55 + 0.45 * Math.sin(elapsed * 1.6);
const theme = getTheme();
const grad = ctx.createRadialGradient(
cx,
cy,
orbitR * 0.15,
cx,
cy,
orbitR * 1.6,
);
grad.addColorStop(0, theme.glowInner);
grad.addColorStop(0.5, theme.glowMiddle);
grad.addColorStop(1, theme.glowOuter);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, cssWidth, cssHeight);
ctx.save();
ctx.setLineDash([scale * 0.018, scale * 0.04]);
ctx.lineDashOffset = -dashOff;
ctx.strokeStyle = theme.ringColor;
ctx.lineWidth = Math.max(1, scale * 0.006);
ctx.beginPath();
ctx.arc(cx, cy, orbitR, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
const arcS = arcRot;
const arcE = arcRot + arcLen;
const fx = cx + orbitR * Math.cos(arcE);
const fy = cy + orbitR * Math.sin(arcE);
ctx.save();
ctx.shadowColor = 'rgba(61,214,208,0.8)';
ctx.shadowBlur = scale * 0.04;
ctx.strokeStyle = '#3DD6D0';
ctx.lineWidth = Math.max(2, scale * 0.014);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(cx, cy, orbitR, arcS, arcE);
ctx.stroke();
ctx.restore();
ctx.save();
ctx.shadowColor = 'rgba(255,255,255,0.9)';
ctx.shadowBlur = scale * 0.03;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(fx, fy, Math.max(2.5, scale * 0.014), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 中心图标:按 type 切换
if (animType === 'medical_record') {
drawMedicalRecord(ctx, cx, cy, scale, floatOff, pulseS, theme.iconShadow);
} else {
const pillW = scale * 0.34;
const pillH = scale * 0.1;
drawPill(
ctx,
cx - pillW / 2,
cy - pillH / 2,
pillW,
pillH,
floatOff,
pulseS,
theme.iconShadow,
);
}
for (let i = 0; i < 3; i++) {
const ang = elapsed * 1.8 + (i * Math.PI * 2) / 3;
const crossOrbitR = orbitR * 1.02;
const rx = cx + crossOrbitR * Math.cos(ang);
const ry = cy + crossOrbitR * Math.sin(ang);
const rs = Math.max(4, scale * 0.022);
const ra = 0.4 + 0.35 * Math.sin(elapsed * 3.2 + i * 1.7);
drawCross(ctx, rx, ry, rs, ra, elapsed * 0.9 + i);
}
updateParticles(dt, cx, cy, scale);
for (const p of particles) {
drawParticle(ctx, p, theme.particleBright);
}
ctx.save();
ctx.globalAlpha = textAlpha;
ctx.fillStyle = theme.textColor;
ctx.font = `${fontSize}px "PingFang SC","Microsoft YaHei","Noto Sans SC","Helvetica Neue",sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(61,214,208,0.25)';
ctx.shadowBlur = fontSize * 0.6;
ctx.fillText(text, cx, textY);
ctx.restore();
const charW = fontSize * 0.55;
const dotsStartX = cx + (text.length / 2) * charW + fontSize * 0.5;
const dotR = Math.max(2, fontSize * 0.16);
const dotSpacing = fontSize * 0.7;
for (let i = 0; i < 3; i++) {
const phase = (elapsed * 2.8 + i * 1.0) % 3;
let brightness = phase < 1 ? phase : phase < 2 ? 2 - phase : 0;
brightness = brightness * brightness * (3 - 2 * brightness);
const alpha = 0.2 + brightness * 0.75;
const dx = dotsStartX + i * dotSpacing * 2.2;
ctx.save();
ctx.shadowColor = `rgba(61,214,208,${alpha * 0.8})`;
ctx.shadowBlur = dotR * 3;
ctx.fillStyle = `rgba(61,214,208,${alpha})`;
ctx.beginPath();
ctx.arc(dx, textY, dotR, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.restore();
animationId = requestAnimationFrame(draw);
}
updateSize();
animationId = requestAnimationFrame(draw);
if (window.ResizeObserver) {
resizeObserver = new ResizeObserver(() => updateSize());
resizeObserver.observe(container);
} else {
const onResize = () => updateSize();
window.addEventListener('resize', onResize);
removeWindowResize = () => window.removeEventListener('resize', onResize);
}
// 跟随应用 dark class未手动锁定主题时
themeObserver = new MutationObserver(() => {
if (!localStorage.getItem('prescription-loading-theme')) {
syncThemeFromApp();
}
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'data-theme'],
});
});
onUnmounted(() => {
cancelAnimationFrame(animationId);
resizeObserver?.disconnect();
resizeObserver = null;
removeWindowResize?.();
removeWindowResize = null;
themeObserver?.disconnect();
themeObserver = null;
resetParticles = null;
});
</script>
<style scoped>
.loading-wrapper {
position: relative;
width: 100%;
height: 100%;
min-height: 220px;
//background: #ffffff;
border-radius: 12px;
overflow: hidden;
}
.loading-wrapper--dark {
//background: #1e2227;
}
.loading-canvas {
display: block;
width: 100%;
height: 100%;
}
.theme-toggle-btn {
position: absolute;
top: 12px;
right: 12px;
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 20px;
cursor: pointer;
font-size: 14px;
color: #4a5568;
backdrop-filter: blur(8px);
transition: all 0.25s;
}
.loading-wrapper--dark .theme-toggle-btn {
background: rgba(30, 34, 39, 0.9);
color: #d1d5db;
}
.theme-toggle-btn:hover {
border-color: #3dd6d0;
transform: scale(1.03);
}
.theme-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
}
</style>

View File

@@ -105,7 +105,9 @@ const downloadQRCode = async (name: string, isQr = false) => {
{{ doctorInfo.doctor.name }} {{ doctorInfo.doctor.title.name }} {{ doctorInfo.doctor.name }} {{ doctorInfo.doctor.title.name }}
</div> </div>
<div class="qrAddress"> <div class="qrAddress">
<span class="addressTitle">诊所地址</span>{{ previewAddress }} <span class="addressTitle">{{
Number(doctorInfo?.store?.type) === 1 ? '药店地址:' : '诊所地址:'
}}</span>{{ previewAddress }}
</div> </div>
<Image :preview="false" :src="doctorInfo.qr_code" class="mt-3" height="30" width="30" /> <Image :preview="false" :src="doctorInfo.qr_code" class="mt-3" height="30" width="30" />
<h3 class="border-l-4 border-blue-500 pl-3 text-lg font-semibold"> <h3 class="border-l-4 border-blue-500 pl-3 text-lg font-semibold">

View File

@@ -1,5 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; /**
* 诊所/药店二维码预览弹窗
* 按门店 type0 诊所 / 1 药店)切换「诊所|药店」文案,列表页与门店卡片共用
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
@@ -11,6 +15,13 @@ const htmlToImage = ref('');
const url = ref(''); const url = ref('');
const previewTitle = ref('二维码'); const previewTitle = ref('二维码');
const previewAddress = ref(''); const previewAddress = ref('');
/** 门店类型0=诊所 1=药店,未传时按诊所示 */
const storeType = ref(0);
const isPharmacy = computed(() => Number(storeType.value) === 1);
const kindLabel = computed(() => (isPharmacy.value ? '药店' : '诊所'));
const addressLabel = computed(() => `${kindLabel.value}地址:`);
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
fullscreenButton: false, fullscreenButton: false,
footer: false, footer: false,
@@ -28,21 +39,28 @@ const [Modal, modalApi] = useVbenModal({
}, },
onOpenChange(isOpen: boolean) { onOpenChange(isOpen: boolean) {
if (isOpen) { if (isOpen) {
const { values, title, address } = const { values, title, address, type, store_type } =
modalApi.getData<Record<string, any>>(); modalApi.getData<Record<string, any>>() || {};
if (values) { if (values) {
url.value = values; url.value = values;
qrCodeUrl.value = values; qrCodeUrl.value = values;
previewTitle.value = title; previewTitle.value = title;
previewAddress.value = address; previewAddress.value = address;
// 兼容 type / store_type 两种传参
storeType.value = Number(type ?? store_type ?? 0);
modalApi.setState({
confirmText: `保存${kindLabel.value}卡片图片`,
cancelText: `保存${kindLabel.value}二维码图片`,
});
} }
} }
}, },
}); });
/** /**
* 下载二维码 * 下载二维码卡片或纯二维码图片
* @param name * @param name 文件名后缀
* @param isQr * @param isQr true=只截二维码区域false=截整张卡片
*/ */
const downloadQRCode = async (name: string, isQr = false) => { const downloadQRCode = async (name: string, isQr = false) => {
const element = isQr ? qrCodeUrl.value : htmlToImage.value; const element = isQr ? qrCodeUrl.value : htmlToImage.value;
@@ -51,25 +69,24 @@ const downloadQRCode = async (name: string, isQr = false) => {
allowTaint: true, allowTaint: true,
logging: false, logging: false,
}).then((canvas) => { }).then((canvas) => {
// 创建a标签下载 const link = document.createElement('a');
const link = document.createElement('a'); // 创建a标签 link.href = canvas.toDataURL();
link.href = canvas.toDataURL(); // 是canvas对象的一种方法用于将canvas对象转换为base64位编码 link.setAttribute('download', `${previewTitle.value}${name}.png`);
link.setAttribute('download', `${previewTitle.value}${name}.png`); // 利用了a标签的download 来下载 canvas图片 link.style.display = 'none';
link.style.display = 'none'; // 将图片隐藏起来 document.body.append(link);
document.body.append(link); // 插入到其中
link.click(); link.click();
}); });
}; };
</script> </script>
<template> <template>
<Modal :title="`${previewTitle}二维码`" class="w-[50%]"> <Modal :title="`${previewTitle}二维码`">
<div class="qrBox" style="text-align: center"> <div class="qrBox" style="text-align: center">
<div ref="htmlToImage" class="qrModal"> <div ref="htmlToImage" class="qrModal">
<div id="qrcode" ref="qrCodeUrl" class="qrcode"> <div id="qrcode" ref="qrCodeUrl" class="qrcode">
<div class="qrTitle">{{ previewTitle }}</div> <div class="qrTitle">{{ previewTitle }}</div>
<Image :preview="false" :src="url" height="30" width="30" /> <Image :preview="false" :src="url" height="30" width="30" />
<div class="qrAddress"> <div class="qrAddress">
<span class="addressTitle">诊所地址</span>{{ previewAddress }} <span class="addressTitle">{{ addressLabel }}</span>{{ previewAddress }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import type { BlockSchema } from './types';
import MIcon from '#/components/icon/icon.vue';
/**
* 左侧组件库:渲染所有可拖拽块类型
* 用户从这里拖动到中间画布SortableJS 用 pull:clone 模式,左侧库不会减少)
*/
defineProps<{
/** 所有可拖拽块定义 */
blocks: BlockSchema[];
}>();
</script>
<template>
<div class="block-library">
<div class="panel-header">
<span class="title">组件库</span>
<span class="hint">拖入画布</span>
</div>
<div class="block-list">
<div
v-for="block in blocks"
:key="block.type"
class="block-item"
:data-block-type="block.type"
>
<MIcon v-if="block.icon" :icon="block.icon" size="16" />
<span class="block-label">{{ block.label }}</span>
</div>
<div v-if="blocks.length === 0" class="empty-hint">暂无可拖入组件</div>
</div>
</div>
</template>
<style scoped lang="scss">
.block-library {
display: flex;
flex-direction: column;
width: 200px;
height: 100%;
border-right: 1px solid var(--ant-color-border-secondary, #e5e6eb);
background: var(--ant-color-fill-quaternary, #fafbfc);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
font-size: 13px;
.title {
font-weight: 600;
color: var(--ant-color-text, #1d2129);
}
.hint {
color: var(--ant-color-text-tertiary, #86909c);
font-size: 11px;
}
}
.block-list {
flex: 1;
overflow-y: auto;
padding: 12px 8px;
}
.block-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
margin-bottom: 6px;
border: 1px dashed var(--ant-color-border, #c9cdd4);
border-radius: 4px;
background: var(--ant-color-bg-container, #fff);
font-size: 13px;
color: var(--ant-color-text-secondary, #4e5969);
cursor: grab;
user-select: none;
transition: all 0.15s;
&:hover {
border-color: var(--ant-color-primary, #165dff);
color: var(--ant-color-primary, #165dff);
background: var(--ant-color-primary-bg, #f2f7ff);
}
&:active {
cursor: grabbing;
}
.block-label {
flex: 1;
}
}
.empty-hint {
padding: 24px 12px;
text-align: center;
color: var(--ant-color-text-quaternary, #c9cdd4);
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,300 @@
<script setup lang="ts">
import type { BlockInstance, BlockSchema } from './types';
import { computed } from 'vue';
import MIcon from '#/components/icon/icon.vue';
/**
* 中间画布:按 targetArrayPath 分组渲染块实例
* 每个分组是一个 SortableJS 接收区,可拖入、排序、删除
*/
const props = defineProps<{
/** 所有块定义(按 type 查找对应 schema */
blockSchemas: BlockSchema[];
/** 所有块实例(已拖入画布的) */
instances: BlockInstance[];
/** 当前选中的块实例 ID高亮显示 */
selectedInstanceId?: string;
}>();
const emit = defineEmits<{
/** 选中块实例(点击时触发) */
select: [instanceId: string];
/** 删除块实例 */
remove: [instanceId: string];
}>();
/**
* 按 targetArrayPath 分组的渲染结构
* 每个分组对应一个 SortableJS 接收区
*/
const groupedRender = computed(() => {
// 收集所有出现的 targetArrayPath按 blockSchemas 顺序排序
const orderedPaths: string[] = [];
const labelMap: Record<string, { label: string; maxCount?: number }> = {};
for (const schema of props.blockSchemas) {
if (!orderedPaths.includes(schema.targetArrayPath)) {
orderedPaths.push(schema.targetArrayPath);
labelMap[schema.targetArrayPath] = {
label: schema.targetArrayPath,
maxCount: schema.maxCount,
};
}
}
// 把 instances 按目标路径分组
const grouped: Record<string, BlockInstance[]> = {};
for (const inst of props.instances) {
const schema = props.blockSchemas.find((s) => s.type === inst.type);
if (!schema) continue;
const path = schema.targetArrayPath;
if (!grouped[path]) grouped[path] = [];
grouped[path].push(inst);
}
return orderedPaths.map((path) => ({
path,
label: labelMap[path]?.label ?? path,
maxCount: labelMap[path]?.maxCount,
items: grouped[path] ?? [],
}));
});
/**
* 获取块实例的 schema 定义
*/
function getSchema(type: string): BlockSchema | undefined {
return props.blockSchemas.find((s) => s.type === type);
}
/**
* 获取块实例的简要描述(属性面板编辑的字段值预览)
*/
function getInstanceSummary(inst: BlockInstance): string {
const schema = getSchema(inst.type);
if (!schema) return '';
const firstField = schema.fields[0];
if (!firstField) return '';
// 嵌套路径取最末级
const path = firstField.name.split('.');
let value: any = inst.data;
for (const key of path) {
value = value?.[key];
}
return value ? String(value) : '(未填写)';
}
function handleClick(instanceId: string) {
emit('select', instanceId);
}
function handleRemove(instanceId: string) {
emit('remove', instanceId);
}
</script>
<template>
<div class="canvas">
<div class="panel-header">
<span class="title">画布</span>
<span class="hint">拖入 / 排序 / 点击编辑属性</span>
</div>
<div class="canvas-body">
<div
v-for="group in groupedRender"
:key="group.path"
class="canvas-group"
>
<div class="group-header">
<span class="group-label">{{ group.label }}</span>
<span class="group-count">
{{ group.items.length }}{{ group.maxCount ? `/${group.maxCount}` : '' }}
</span>
</div>
<div
class="group-dropzone"
:data-target-array-path="group.path"
>
<div
v-for="inst in group.items"
:key="inst.id"
class="instance-item"
:class="{ selected: inst.id === selectedInstanceId }"
:data-instance-id="inst.id"
@click.stop="handleClick(inst.id)"
>
<div class="instance-info">
<MIcon
v-if="getSchema(inst.type)?.icon"
:icon="getSchema(inst.type)!.icon"
size="14"
/>
<div class="instance-text">
<div class="instance-label">{{ getSchema(inst.type)?.label }}</div>
<div class="instance-summary">{{ getInstanceSummary(inst) }}</div>
</div>
</div>
<button
type="button"
class="instance-remove"
title="删除"
@click.stop="handleRemove(inst.id)"
>
×
</button>
</div>
<div v-if="group.items.length === 0" class="empty-dropzone">
拖入组件
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.canvas {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background: var(--ant-color-bg-container, #fff);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
font-size: 13px;
.title {
font-weight: 600;
color: var(--ant-color-text, #1d2129);
}
.hint {
color: var(--ant-color-text-tertiary, #86909c);
font-size: 11px;
}
}
.canvas-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.canvas-group {
margin-bottom: 20px;
}
.group-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
.group-label {
color: var(--ant-color-text-secondary, #4e5969);
font-weight: 500;
font-family: 'JetBrains Mono', monospace;
}
.group-count {
color: var(--ant-color-text-tertiary, #86909c);
}
}
.group-dropzone {
min-height: 60px;
padding: 8px;
border: 2px dashed var(--ant-color-border, #d9d9d9);
border-radius: 6px;
background: var(--ant-color-fill-quaternary, #fafbfc);
}
.empty-dropzone {
display: flex;
align-items: center;
justify-content: center;
min-height: 44px;
color: var(--ant-color-text-quaternary, #c9cdd4);
font-size: 12px;
}
.instance-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
margin-bottom: 6px;
border: 1px solid var(--ant-color-border-secondary, #e5e6eb);
border-radius: 4px;
background: var(--ant-color-bg-container, #fff);
cursor: pointer;
transition: all 0.15s;
&:last-child {
margin-bottom: 0;
}
&:hover {
border-color: var(--ant-color-primary, #165dff);
}
&.selected {
border-color: var(--ant-color-primary, #165dff);
background: var(--ant-color-primary-bg, #f2f7ff);
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.15);
}
}
.instance-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.instance-text {
flex: 1;
min-width: 0;
}
.instance-label {
font-size: 13px;
color: var(--ant-color-text, #1d2129);
font-weight: 500;
}
.instance-summary {
font-size: 11px;
color: var(--ant-color-text-tertiary, #86909c);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.instance-remove {
width: 18px;
height: 18px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--ant-color-text-tertiary, #86909c);
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: all 0.15s;
&:hover {
background: var(--ant-color-error, #f53f3f);
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Button, message } from 'ant-design-vue';
/**
* JSON 实时预览面板
* - 显示最终生成的 JSON 字符串(语法高亮,简单的 key/string 高亮)
* - 提供「复制 JSON」按钮一键拷贝到剪贴板
*/
const props = defineProps<{
/** 最终生成的 JSON 数据 */
value: Record<string, any>;
}>();
const formattedJson = computed(() => {
try {
return JSON.stringify(props.value, null, 2);
} catch {
return '// JSON 序列化失败';
}
});
/**
* 简单的 JSON 语法高亮
* 匹配 key / string / number / boolean 并套用不同颜色 span
*/
const highlightedHtml = computed(() => {
const escape = (s: string) =>
s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const json = escape(formattedJson.value);
return json
.replace(
/("(?:\\.|[^"\\])*")(\s*:)/g,
'<span class="json-key">$1</span>$2',
)
.replace(
/:\s*("(?:\\.|[^"\\])*")/g,
': <span class="json-string">$1</span>',
)
.replace(/:\s*(-?\d+(?:\.\d+)?)/g, ': <span class="json-number">$1</span>')
.replace(/:\s*(true|false|null)/g, ': <span class="json-bool">$1</span>');
});
async function handleCopy() {
try {
await navigator.clipboard.writeText(formattedJson.value);
message.success('JSON 已复制');
} catch {
message.error('复制失败,请手动选择');
}
}
</script>
<template>
<div class="json-preview">
<div class="panel-header">
<span class="title">JSON 预览</span>
<Button size="small" type="link" @click="handleCopy">复制 JSON</Button>
</div>
<pre class="json-body" v-html="highlightedHtml" />
</div>
</template>
<style scoped lang="scss">
.json-preview {
display: flex;
flex-direction: column;
width: 280px;
height: 100%;
border-left: 1px solid var(--ant-color-border-secondary, #e5e6eb);
/* JSON 代码面板保留 VS Code Dark+ 风格暗底(无论浅色/暗色主题) */
background: #1e1e1e;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid #374151;
font-size: 13px;
.title {
font-weight: 600;
color: #e5e7eb;
}
}
.json-body {
flex: 1;
margin: 0;
padding: 12px;
overflow: auto;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
white-space: pre-wrap;
word-break: break-all;
}
/* JSON 语法高亮颜色 */
:deep(.json-key) {
color: #9cdcfe;
}
:deep(.json-string) {
color: #ce9178;
}
:deep(.json-number) {
color: #b5cea8;
}
:deep(.json-bool) {
color: #569cd6;
}
</style>

View File

@@ -0,0 +1,348 @@
<script setup lang="ts">
import type { BlockInstance, BlockSchema, FixedField, FormFieldSchema } from './types';
import { computed, watch } from 'vue';
import { Empty, Input, InputNumber, Radio, RadioGroup, Select, Textarea } from 'ant-design-vue';
import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue';
/**
* 右侧属性面板:根据选中块的字段 schema 渲染表单
* - 顶层固定字段fixedFields渲染在顶部不可删除
* - 选中块的 fields渲染在下方
* - 图片类型字段用 GalleryPickLink透传 acceptTypes
*/
const props = defineProps<{
/** 顶层固定字段定义 */
fixedFields?: FixedField[];
/** 顶层固定字段当前值 */
fixedValues?: Record<string, any>;
/** 当前选中的块定义(可能为空) */
selectedBlockSchema?: BlockSchema;
/** 当前选中的块实例(可能为空) */
selectedInstance?: BlockInstance;
}>();
const emit = defineEmits<{
/** 顶层固定字段值变化 */
'update:fixedValues': [value: Record<string, any>];
/** 选中块实例数据变化 */
'update:instance': [instance: BlockInstance];
}>();
/**
* 设置顶层固定字段的值
* 支持 a.b.c 嵌套路径
*/
function setFixedValue(path: string, value: any) {
const next = { ...(props.fixedValues || {}) };
setByPath(next, path, value);
emit('update:fixedValues', next);
}
/**
* 设置选中块实例的字段值
*/
function setInstanceValue(path: string, value: any) {
if (!props.selectedInstance) return;
const nextData = { ...props.selectedInstance.data };
setByPath(nextData, path, value);
emit('update:instance', {
...props.selectedInstance,
data: nextData,
});
}
/**
* 按 a.b.c 路径设置对象嵌套属性(不可变更新)
*/
function setByPath(obj: Record<string, any>, path: string, value: any) {
const keys = path.split('.');
let cursor: any = obj;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]!;
if (!cursor[k] || typeof cursor[k] !== 'object') {
cursor[k] = {};
} else {
cursor[k] = { ...cursor[k] };
}
cursor = cursor[k];
}
cursor[keys[keys.length - 1]!] = value;
}
/**
* 按 a.b.c 路径读取对象嵌套属性
*/
function getByPath(obj: Record<string, any> | undefined, path: string): any {
if (!obj) return undefined;
const keys = path.split('.');
let cursor: any = obj;
for (const k of keys) {
if (cursor == null) return undefined;
cursor = cursor[k];
}
return cursor;
}
const hasAnyField = computed(() => {
return (
(props.fixedFields && props.fixedFields.length > 0) ||
(props.selectedBlockSchema && props.selectedBlockSchema.fields.length > 0)
);
});
// 当选中的块变化时,触发 watch 让父级感知(用于属性面板切换的动画/数据初始化)
watch(
() => props.selectedInstance?.id,
() => {
// 仅做依赖追踪,实际更新在用户交互时通过 emit 触发
},
);
</script>
<template>
<div class="property-panel">
<div class="panel-header">
<span class="title">属性</span>
<span v-if="selectedBlockSchema" class="hint">{{ selectedBlockSchema.label }}</span>
</div>
<div class="panel-body">
<!-- 顶层固定字段 -->
<div v-if="fixedFields && fixedFields.length > 0" class="field-section">
<div class="section-title">基础信息</div>
<div
v-for="field in fixedFields"
:key="field.name"
class="form-row"
>
<label class="form-label">
{{ field.label }}
<span v-if="field.required" class="required">*</span>
</label>
<!-- 文本 -->
<Input
v-if="!field.type || field.type === 'string' || field.type === 'url'"
:value="getByPath(fixedValues, field.name)"
:placeholder="field.placeholder"
allow-clear
size="small"
@update:value="setFixedValue(field.name, $event)"
/>
<!-- 数字 -->
<InputNumber
v-else-if="field.type === 'number'"
:value="getByPath(fixedValues, field.name)"
:placeholder="field.placeholder"
size="small"
style="width: 100%"
@update:value="setFixedValue(field.name, $event)"
/>
<!-- 多行文本 -->
<Textarea
v-else-if="field.type === 'textarea'"
:value="getByPath(fixedValues, field.name)"
:placeholder="field.placeholder"
:rows="3"
size="small"
@update:value="setFixedValue(field.name, $event)"
/>
<!-- 下拉选择 -->
<Select
v-else-if="field.type === 'select'"
:value="getByPath(fixedValues, field.name)"
:options="field.options"
:placeholder="field.placeholder"
size="small"
@update:value="setFixedValue(field.name, $event)"
/>
<!-- 单选 -->
<RadioGroup
v-else-if="field.type === 'radio'"
:value="getByPath(fixedValues, field.name)"
size="small"
@update:value="setFixedValue(field.name, $event)"
>
<Radio
v-for="opt in field.options"
:key="String(opt.value)"
:value="opt.value"
>
{{ opt.label }}
</Radio>
</RadioGroup>
<!-- 图片素材 -->
<div v-else-if="field.type === 'image'" class="image-picker-row">
<Input
:value="getByPath(fixedValues, field.name)"
:placeholder="field.placeholder || '从素材库选择图片'"
size="small"
read-only
/>
<GalleryPickLink
:accept-types="field.acceptTypes || [0]"
@select="(urls) => setFixedValue(field.name, urls[0] || '')"
/>
</div>
</div>
</div>
<!-- 选中块的属性字段 -->
<div v-if="selectedBlockSchema && selectedInstance" class="field-section">
<div class="section-title">{{ selectedBlockSchema.label }}属性</div>
<div
v-for="field in selectedBlockSchema.fields"
:key="field.name"
class="form-row"
>
<label class="form-label">
{{ field.label }}
<span v-if="field.required" class="required">*</span>
</label>
<Input
v-if="!field.type || field.type === 'string' || field.type === 'url'"
:value="getByPath(selectedInstance.data, field.name)"
:placeholder="field.placeholder"
allow-clear
size="small"
@update:value="setInstanceValue(field.name, $event)"
/>
<InputNumber
v-else-if="field.type === 'number'"
:value="getByPath(selectedInstance.data, field.name)"
:placeholder="field.placeholder"
size="small"
style="width: 100%"
@update:value="setInstanceValue(field.name, $event)"
/>
<Textarea
v-else-if="field.type === 'textarea'"
:value="getByPath(selectedInstance.data, field.name)"
:placeholder="field.placeholder"
:rows="3"
size="small"
@update:value="setInstanceValue(field.name, $event)"
/>
<Select
v-else-if="field.type === 'select'"
:value="getByPath(selectedInstance.data, field.name)"
:options="field.options"
:placeholder="field.placeholder"
size="small"
@update:value="setInstanceValue(field.name, $event)"
/>
<RadioGroup
v-else-if="field.type === 'radio'"
:value="getByPath(selectedInstance.data, field.name)"
size="small"
@update:value="setInstanceValue(field.name, $event)"
>
<Radio
v-for="opt in field.options"
:key="String(opt.value)"
:value="opt.value"
>
{{ opt.label }}
</Radio>
</RadioGroup>
<div v-else-if="field.type === 'image'" class="image-picker-row">
<Input
:value="getByPath(selectedInstance.data, field.name)"
:placeholder="field.placeholder || '从素材库选择图片'"
size="small"
read-only
/>
<GalleryPickLink
:accept-types="field.acceptTypes || [0]"
@select="(urls) => setInstanceValue(field.name, urls[0] || '')"
/>
</div>
</div>
</div>
<!-- 未选中任何块时的提示 -->
<Empty
v-if="!hasAnyField"
description="点击画布中的块以编辑属性"
:image-style="{ height: '60px' }"
style="margin-top: 60px"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.property-panel {
display: flex;
flex-direction: column;
width: 280px;
height: 100%;
border-left: 1px solid var(--ant-color-border-secondary, #e5e6eb);
background: var(--ant-color-bg-container, #fff);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
font-size: 13px;
.title {
font-weight: 600;
color: var(--ant-color-text, #1d2129);
}
.hint {
color: var(--ant-color-text-tertiary, #86909c);
font-size: 11px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.field-section {
margin-bottom: 24px;
}
.section-title {
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--ant-color-split, #f2f3f5);
font-size: 12px;
font-weight: 600;
color: var(--ant-color-text-secondary, #4e5969);
}
.form-row {
margin-bottom: 12px;
}
.form-label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: var(--ant-color-text-secondary, #4e5969);
.required {
margin-left: 2px;
color: var(--ant-color-error, #f53f3f);
}
}
.image-picker-row {
display: flex;
flex-direction: column;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,522 @@
<script setup lang="ts">
import type { BlockInstance, BlockSchema, FixedField } from './types';
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import type Sortable from 'sortablejs';
import type { SortableOptions } from 'sortablejs';
import { Button, message } from 'ant-design-vue';
import BlockLibrary from './BlockLibrary.vue';
import Canvas from './Canvas.vue';
import JsonPreview from './JsonPreview.vue';
import PropertyPanel from './PropertyPanel.vue';
/**
* 通用拖拽式可视化编辑器
*
* 用途:把「左侧组件库 → 中间画布拖拽组装 → 右侧属性面板 → 自动生成 JSON」
* 做成 schema 驱动的通用组件,未来 OA / 工单 / 通知等任何场景需要可视化拼装 JSON 都能复用。
*
* 核心机制:
* - 左侧库 + 画布各分组共用同一个 SortableJS grouppull:clone 模式:左侧库克隆到画布)
* - 拖入 / 排序 / 删除时同步内部 instances 状态
* - 通过 computed 把「fixedFields 值 + 各 targetArrayPath 数组」合并成最终 JSONemit 给父级
*
* SortableJS 用法参照项目内 views/system/role/components/quick-nav-transfer.vue
*/
const props = withDefaults(
defineProps<{
/** 双向绑定的 JSON 数据 */
modelValue: Record<string, any>;
/** 块定义 schema决定左侧能拖入哪些块、各自有哪些字段 */
schema: BlockSchema[];
/** 是否显示 JSON 预览面板 */
showJsonPreview?: boolean;
/** 顶层固定字段(不可拖拽,固定显示在属性面板顶部) */
fixedFields?: FixedField[];
}>(),
{
showJsonPreview: true,
fixedFields: () => [] as FixedField[],
},
);
const emit = defineEmits<{
'update:modelValue': [value: Record<string, any>];
}>();
// ====== 内部状态 ======
/** 所有块实例(按拖入顺序) */
const instances = ref<BlockInstance[]>([]);
/** 当前选中的块实例 ID */
const selectedInstanceId = ref<string>('');
/** 顶层固定字段值 */
const fixedValues = ref<Record<string, any>>({});
/** 顶层固定字段 + 块数组组装后的最终 JSON双向同步给父级 */
const assembledJson = computed<Record<string, any>>(() => {
const result: Record<string, any> = { ...fixedValues.value };
// 按 targetArrayPath 收集块实例数据
for (const schema of props.schema) {
const path = schema.targetArrayPath;
const items = instances.value
.filter((inst) => inst.type === schema.type)
.map((inst) => {
// 深拷贝避免引用问题
const data = JSON.parse(JSON.stringify(inst.data));
// 移除内部使用的 id 字段(如果有)
delete data._blockType;
return data;
});
if (items.length > 0) {
// 设置嵌套路径
setByPath(result, path, items);
}
}
return result;
});
/** 当前选中的块实例对象 */
const selectedInstance = computed<BlockInstance | undefined>(() => {
return instances.value.find((i) => i.id === selectedInstanceId.value);
});
/** 当前选中的块 schema */
const selectedBlockSchema = computed<BlockSchema | undefined>(() => {
if (!selectedInstance.value) return undefined;
return props.schema.find((s) => s.type === selectedInstance.value!.type);
});
// ====== SortableJS 实例管理 ======
const editorRootRef = ref<HTMLElement>();
const libraryRef = ref<HTMLElement>();
/** 画布中各分组的 dropzone DOM 引用key = targetArrayPath */
const dropzoneRefs = ref<Record<string, HTMLElement>>({});
const librarySortable = ref<Sortable | null>(null);
const dropzoneSortables = ref<Record<string, Sortable>>({});
/**
* 动态导入 SortableJS参照 quick-nav-transfer.vue 模式)
*/
async function createSortable(
el: HTMLElement,
options: SortableOptions = {},
): Promise<Sortable> {
const mod = await import(
// @ts-expect-error sortablejs modular esm path
'sortablejs/modular/sortable.complete.esm.js'
);
return mod.default.create(el, {
animation: 200,
...options,
}) as Sortable;
}
/**
* 初始化 SortableJS 实例
* 1. 左侧组件库pull:clone 模式(拖出后元素保留)
* 2. 画布各分组pull:clone/put跨组可移动
*/
async function initSortables() {
destroySortables();
await nextTick();
if (libraryRef.value) {
librarySortable.value = await createSortable(libraryRef.value, {
group: {
name: 'template-editor',
pull: 'clone',
put: false, // 左侧库不接受拖入
},
sort: false, // 左侧库不排序
onEnd: handleLibraryDragEnd,
});
}
// 通过 querySelector 直接查询画布中的所有 dropzone DOM避免父子组件 DOM 引用传递)
const editorRoot = editorRootRef.value;
const dropzoneEls = editorRoot?.querySelectorAll('.group-dropzone[data-target-array-path]') ?? [];
dropzoneEls.forEach((el) => {
const path = el.getAttribute('data-target-array-path');
if (!path) return;
dropzoneRefs.value[path] = el as HTMLElement;
void createSortable(el as HTMLElement, {
group: {
name: 'template-editor',
pull: true,
put: true,
},
onAdd: (evt) => handleDropzoneAdd(evt, path),
onRemove: (evt) => handleDropzoneRemove(evt, path),
onUpdate: (evt) => handleDropzoneUpdate(evt, path),
}).then((s) => {
dropzoneSortables.value[path] = s;
});
});
}
function destroySortables() {
librarySortable.value?.destroy();
librarySortable.value = null;
for (const sortable of Object.values(dropzoneSortables.value)) {
sortable.destroy();
}
dropzoneRefs.value = {};
dropzoneSortables.value = {};
}
/**
* 设置 dropzone DOM 引用Canvas 子组件通过回调注入)
*/
function setDropzoneRef(path: string, el: HTMLElement | null) {
if (el) {
dropzoneRefs.value[path] = el;
} else {
delete dropzoneRefs.value[path];
}
}
// ====== 拖拽事件处理(在 SortableJS 修改 DOM 后,同步到 Vue 状态) ======
/**
* 左侧库 → 画布的拖拽结束回调
* SortableJS 会把左侧库的元素 clone 一份移到画布 DOM我们要
* 1. 移除 DOM 中被克隆过去的元素(由 Vue 重新渲染)
* 2. 在 instances 中追加新块
*/
function handleLibraryDragEnd(evt: any) {
// 如果只是库内拖动(没拖到画布),不做处理
if (!evt.to || !evt.to.classList.contains('group-dropzone')) {
return;
}
const blockType = evt.item?.dataset?.blockType;
if (!blockType) return;
// 检查 maxCount 限制
const schema = props.schema.find((s) => s.type === blockType);
if (!schema) return;
const targetPath = schema.targetArrayPath;
const currentCount = instances.value.filter(
(i) => props.schema.find((s) => s.type === i.type)?.targetArrayPath === targetPath,
).length;
if (schema.maxCount && currentCount >= schema.maxCount) {
// 移除 SortableJS 已经克隆到画布 DOM 的元素
evt.item?.remove();
message.warning(`${schema.label}」最多 ${schema.maxCount}`);
return;
}
// 移除 SortableJS 已经克隆到画布 DOM 的元素Vue 会重新渲染)
evt.item?.remove();
// 根据 SortableJS 在画布 DOM 中的位置,计算插入位置
// 简化处理:直接追加到末尾(实现严格的位置同步太复杂,先保留追加行为)
const newInstance: BlockInstance = {
id: generateId(),
type: blockType,
data: schema.defaultValue ? JSON.parse(JSON.stringify(schema.defaultValue)) : {},
};
instances.value = [...instances.value, newInstance];
selectedInstanceId.value = newInstance.id;
}
/**
* 画布分组间或分组内的添加事件(块从其他分组拖入)
*/
function handleDropzoneAdd(_evt: any, _path: string) {
// SortableJS 自动处理 DOM 顺序,但因为 Vue 控制渲染,需要同步状态
syncInstancesFromDom();
}
function handleDropzoneRemove(_evt: any, _path: string) {
syncInstancesFromDom();
}
function handleDropzoneUpdate(_evt: any, _path: string) {
syncInstancesFromDom();
}
/**
* 从画布 DOM 同步块实例状态
* SortableJS 修改了 DOM 后,从 DOM 重新读取 instanceId 顺序
*/
function syncInstancesFromDom() {
const newInstances: BlockInstance[] = [];
for (const path of Object.keys(dropzoneRefs.value)) {
const el = dropzoneRefs.value[path];
if (!el) continue;
const items = el.querySelectorAll('[data-instance-id]');
items.forEach((item) => {
const id = item.getAttribute('data-instance-id');
if (!id) return;
const existing = instances.value.find((i) => i.id === id);
if (existing) {
newInstances.push(existing);
}
});
}
instances.value = newInstances;
}
/**
* 删除块实例
*/
function removeInstance(id: string) {
instances.value = instances.value.filter((i) => i.id !== id);
if (selectedInstanceId.value === id) {
selectedInstanceId.value = '';
}
}
/**
* 选中块实例
*/
function selectInstance(id: string) {
selectedInstanceId.value = id;
}
/**
* 更新顶层固定字段值PropertyPanel 触发)
*/
function updateFixedValues(values: Record<string, any>) {
fixedValues.value = values;
}
/**
* 更新块实例数据PropertyPanel 触发)
*/
function updateInstance(updated: BlockInstance) {
instances.value = instances.value.map((i) =>
i.id === updated.id ? updated : i,
);
}
// ====== 工具函数 ======
function generateId(): string {
return `inst_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
/**
* 按 a.b.c 路径设置对象嵌套属性(不可变更新)
*/
function setByPath(obj: Record<string, any>, path: string, value: any) {
const keys = path.split('.');
let cursor: any = obj;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]!;
if (!cursor[k]) cursor[k] = {};
cursor = cursor[k];
}
cursor[keys[keys.length - 1]!] = value;
}
/**
* 按 a.b.c 路径读取对象嵌套属性
*/
function getByPath(obj: Record<string, any> | undefined, path: string): any {
if (!obj) return undefined;
const keys = path.split('.');
let cursor: any = obj;
for (const k of keys) {
if (cursor == null) return undefined;
cursor = cursor[k];
}
return cursor;
}
// ====== modelValue 双向同步 ======
/**
* 监听 modelValue 变化,反向初始化内部状态(用于编辑场景回显)
*/
watch(
() => props.modelValue,
(val) => {
if (!val || Object.keys(val).length === 0) {
// 重置
instances.value = [];
fixedValues.value = {};
selectedInstanceId.value = '';
return;
}
// 简化处理:只在 modelValue 引用变化时才反解析(避免无限循环)
// 从 modelValue 还原 fixedValues 和 instances
const newFixed: Record<string, any> = {};
for (const field of props.fixedFields || []) {
const v = getByPath(val, field.name);
if (v !== undefined) {
setByPath(newFixed, field.name, v);
}
}
fixedValues.value = newFixed;
// 从各 targetArrayPath 还原 instances
const newInstances: BlockInstance[] = [];
for (const schema of props.schema) {
const arr = getByPath(val, schema.targetArrayPath);
if (Array.isArray(arr)) {
for (const item of arr) {
newInstances.push({
id: generateId(),
type: schema.type,
data: JSON.parse(JSON.stringify(item)),
});
}
}
}
instances.value = newInstances;
},
{ immediate: false, deep: false },
);
/**
* 监听 assembledJson 变化emit 给父级
*/
watch(
assembledJson,
(val) => {
// 浅比较避免不必要的 emit
const current = JSON.stringify(props.modelValue || {});
const next = JSON.stringify(val);
if (current !== next) {
emit('update:modelValue', val);
}
},
{ deep: true },
);
// 监听 schema 变化时重置 SortableJS画布 DOM 可能重建)
watch(
() => props.schema,
() => {
void nextTick(() => initSortables());
},
);
onMounted(async () => {
// 初始化 fixedValues从 props.fixedFields 的 defaultValue
for (const field of props.fixedFields || []) {
if (field.defaultValue !== undefined) {
setByPath(fixedValues.value, field.name, field.defaultValue);
}
}
// 从 modelValue 回显数据
if (props.modelValue && Object.keys(props.modelValue).length > 0) {
for (const field of props.fixedFields || []) {
const v = getByPath(props.modelValue, field.name);
if (v !== undefined) {
setByPath(fixedValues.value, field.name, v);
}
}
for (const schema of props.schema) {
const arr = getByPath(props.modelValue, schema.targetArrayPath);
if (Array.isArray(arr)) {
for (const item of arr) {
instances.value.push({
id: generateId(),
type: schema.type,
data: JSON.parse(JSON.stringify(item)),
});
}
}
}
}
await nextTick();
await initSortables();
});
onUnmounted(() => {
destroySortables();
});
// 暴露给父组件
defineExpose({
initSortables,
});
</script>
<template>
<div ref="editorRootRef" class="template-editor">
<div class="editor-toolbar">
<span class="title">模板编辑器</span>
<Button
size="small"
type="link"
@click="initSortables"
>
重置拖拽
</Button>
</div>
<div class="editor-main">
<!-- 左侧组件库 ref 暴露给 SortableJS 初始化 -->
<div ref="libraryRef">
<BlockLibrary :blocks="schema" />
</div>
<!-- 中间画布 -->
<Canvas
:block-schemas="schema"
:instances="instances"
:selected-instance-id="selectedInstanceId"
@select="selectInstance"
@remove="removeInstance"
/>
<!-- 右侧属性面板 -->
<PropertyPanel
:fixed-fields="fixedFields"
:fixed-values="fixedValues"
:selected-block-schema="selectedBlockSchema"
:selected-instance="selectedInstance"
@update:fixed-values="updateFixedValues"
@update:instance="updateInstance"
/>
<!-- 最右侧JSON 预览可选 -->
<JsonPreview
v-if="showJsonPreview"
:value="assembledJson"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.template-editor {
display: flex;
flex-direction: column;
width: 100%;
height: 480px;
border: 1px solid var(--ant-color-border-secondary, #e5e6eb);
border-radius: 6px;
overflow: hidden;
background: var(--ant-color-bg-container, #fff);
}
.editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
background: var(--ant-color-fill-quaternary, #fafbfc);
font-size: 13px;
.title {
font-weight: 600;
color: var(--ant-color-text, #1d2129);
}
}
.editor-main {
display: flex;
flex: 1;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,99 @@
/**
* 通用拖拽式可视化编辑器类型定义
*
* 设计目标:把「左侧组件库 → 中间画布拖拽 → 右侧属性面板 → 自动生成 JSON」
* 做成 schema 驱动的通用编辑器,未来任何需要拼装 JSON 结构的场景都能复用。
*
* 核心概念:
* - BlockSchema可拖拽块定义左侧组件库中展示决定属性面板渲染什么字段
* - FormFieldSchema单个字段定义驱动右侧属性面板表单渲染
* - 块的 instances 按 targetArrayPath 分组存到画布,每组数量不超过 maxCount
*/
/**
* 字段类型枚举
* - string单行文本
* - number数字
* - textarea多行文本
* - select下拉选择
* - radio单选按钮组
* - image图片素材选择用 GalleryPickLinkacceptTypes 限定图片)
* - urlURL 链接
*/
export type FormFieldType =
| 'image'
| 'number'
| 'radio'
| 'select'
| 'string'
| 'textarea'
| 'url';
/**
* 单个字段定义(驱动属性面板表单渲染)
*/
export interface FormFieldSchema {
/** 字段名(在 payload 对象中的 key支持 a.b.c 嵌套路径) */
name: string;
/** 中文标签 */
label: string;
/** 字段类型 */
type: FormFieldType;
/** 占位提示 */
placeholder?: string;
/** 是否必填 */
required?: boolean;
/** select / radio 类型的选项列表 */
options?: { label: string; value: any }[];
/** 图片字段:限定素材库文件类型(如 [0] 只显示图片) */
acceptTypes?: number[];
/** 最大长度 */
maxLength?: number;
/** 默认值 */
defaultValue?: any;
}
/**
* 可拖拽块定义(左侧组件库中展示,决定画布上能放什么块、各块有哪些字段)
*/
export interface BlockSchema {
/** 块类型标识(唯一) */
type: string;
/** 块显示名(如「二级标题+文本项」「跳转链接项」) */
label: string;
/** 块图标Iconify */
icon: string;
/** 该块的字段定义(驱动右侧属性面板表单渲染) */
fields: FormFieldSchema[];
/**
* 该块在最终 JSON 中放到的数组路径
* 例如 'horizontal_content_list' 表示拖入的块实例会被收集到 result.horizontal_content_list 数组
* 多个块共用同一个 targetArrayPath 即被收集到同一数组
*/
targetArrayPath: string;
/** 该数组路径下最多允许的块数量(如企微 jump_list 最多 3 项) */
maxCount?: number;
/** 块实例的默认值(拖入时初始化) */
defaultValue?: Record<string, any>;
}
/**
* 画布中的块实例(拖入后生成的实际数据)
*/
export interface BlockInstance {
/** 块实例唯一 ID用于 SortableJS 拖拽排序追踪) */
id: string;
/** 关联的块类型(对应 BlockSchema.type */
type: string;
/** 块实例的当前数据(属性面板编辑后会更新这里) */
data: Record<string, any>;
}
/**
* 顶层固定字段(不可拖拽删除,固定显示在属性面板顶部)
* 用于 card_type / main_title / source 等必填顶层字段
*/
export interface FixedField extends FormFieldSchema {
/** 字段在最终 JSON 中的路径(支持 a.b.c 嵌套,如 'main_title.title' */
name: string;
}

View File

@@ -8,6 +8,7 @@ import type { Dayjs } from 'dayjs';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import {
@@ -35,9 +36,15 @@ import {
} from '#/views/system/store/api'; } from '#/views/system/store/api';
import { getSalespersonList } from '#/views/system/store/api/salesperson'; import { getSalespersonList } from '#/views/system/store/api/salesperson';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser'; import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
import AddDoctorFromStoreModal from '#/views/system/store/components/AddDoctorFromStoreModal.vue';
import { canAddStoreDoctor } from '#/views/system/admin/_shared/platform-admin-role';
const RangePicker = DatePicker.RangePicker; const RangePicker = DatePicker.RangePicker;
const userStore = useUserStore();
// 是否允许「添加医生」:超管/管理员/省经理/市经理/业务员
const canAddDoctor = computed(() => canAddStoreDoctor(userStore.userInfo));
const storeId = ref(0); const storeId = ref(0);
const loading = ref(false); const loading = ref(false);
const activeTab = ref('basic'); const activeTab = ref('basic');
@@ -64,10 +71,13 @@ const stats = computed(() => cardData.value?.stats_summary ?? {});
const doctorList = computed(() => cardData.value?.doctor_list ?? []); const doctorList = computed(() => cardData.value?.doctor_list ?? []);
const isClinic = computed(() => Number(store.value?.type) === 0); const isClinic = computed(() => Number(store.value?.type) === 0);
const isPharmacy = computed(() => Number(store.value?.type) === 1); const isPharmacy = computed(() => Number(store.value?.type) === 1);
/** 按门店类型区分诊所/药店文案,避免药店仍显示「诊所」 */
const storeKindLabel = computed(() => (isPharmacy.value ? '药店' : '诊所'));
const qrTabLabel = computed(() => `${storeKindLabel.value}二维码`);
const modalTitle = computed(() => { const modalTitle = computed(() => {
const name = store.value?.name; const name = store.value?.name;
if (!name) return '门店详情'; if (!name) return '门店详情';
return isPharmacy.value ? `药店详情 · ${name}` : `诊所详情 · ${name}`; return `${storeKindLabel.value}详情 · ${name}`;
}); });
const drugTypeOptions = [ const drugTypeOptions = [
@@ -121,6 +131,25 @@ const [DoctorModal, DoctorModalApi] = useVbenModal({
connectedComponent: DoctorCardModal, connectedComponent: DoctorCardModal,
}); });
/** 添加医生弹窗:诊所详情医生团队 Tab 内入口 */
const [AddDoctorModal, AddDoctorModalApi] = useVbenModal({
connectedComponent: AddDoctorFromStoreModal,
});
/**
* 打开「添加医生」弹窗:默认绑定当前门店,提交成功后刷新医生团队列表
*/
const openAddDoctorModal = () => {
AddDoctorModalApi.setData({
storeId: storeId.value,
storeName: (store.value as any)?.name || '',
onSuccess: () => {
loadCard();
},
});
AddDoctorModalApi.open();
};
function searchTimeParam(): [string, string] { function searchTimeParam(): [string, string] {
return [ return [
searchTime.value[0].format('YYYY-MM-DD 00:00:00'), searchTime.value[0].format('YYYY-MM-DD 00:00:00'),
@@ -256,6 +285,7 @@ function clinicTypeText(v: number) {
<template> <template>
<Modal :title="modalTitle"> <Modal :title="modalTitle">
<AddDoctorModal />
<Spin :spinning="loading"> <Spin :spinning="loading">
<div v-if="cardData" class="mb-4 flex flex-wrap items-center justify-between gap-3 px-1"> <div v-if="cardData" class="mb-4 flex flex-wrap items-center justify-between gap-3 px-1">
<div class="text-sm text-muted-foreground">统计时间范围</div> <div class="text-sm text-muted-foreground">统计时间范围</div>
@@ -399,6 +429,15 @@ function clinicTypeText(v: number) {
<Tabs.TabPane v-if="isClinic" key="doctors" tab="诊所医生团队"> <Tabs.TabPane v-if="isClinic" key="doctors" tab="诊所医生团队">
<div class="pl-4"> <div class="pl-4">
<div v-if="canAddDoctor" class="mb-3">
<Button
type="primary"
size="small"
@click="openAddDoctorModal"
>
添加医生
</Button>
</div>
<Empty v-if="doctorList.length === 0" description="暂无医生" /> <Empty v-if="doctorList.length === 0" description="暂无医生" />
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2"> <div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div <div
@@ -443,7 +482,7 @@ function clinicTypeText(v: number) {
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane key="qr" tab="门店二维码"> <Tabs.TabPane key="qr" :tab="qrTabLabel">
<div class="flex flex-col items-center gap-4 py-6 pl-4"> <div class="flex flex-col items-center gap-4 py-6 pl-4">
<template v-if="cardData?.qr_code || store.qr_code"> <template v-if="cardData?.qr_code || store.qr_code">
<Image <Image
@@ -452,13 +491,13 @@ function clinicTypeText(v: number) {
:preview="true" :preview="true"
/> />
<div class="text-sm text-muted-foreground"> <div class="text-sm text-muted-foreground">
{{ store.name }} 门店二维码 {{ store.name }} {{ qrTabLabel }}
</div> </div>
</template> </template>
<template v-else> <template v-else>
<Empty description="尚未生成二维码" /> <Empty :description="`尚未生成${storeKindLabel}二维码`" />
<Button type="primary" :loading="generatingQr" @click="ensureQrCode"> <Button type="primary" :loading="generatingQr" @click="ensureQrCode">
生成二维码 生成{{ storeKindLabel }}二维码
</Button> </Button>
</template> </template>
</div> </div>

View File

@@ -0,0 +1,349 @@
<script lang="ts" setup>
/**
* 顶栏 VIP 徽标:点击弹出诊所端风格会员气泡卡
* 展示等级名(不展示 CUSTOM/V1 等编码)、开通信息、已开功能,并可查看开通记录
*/
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { Button, Popover } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import VipBadgeCombo from './VipBadgeCombo.vue';
import VipStoreRecordsModal from './VipStoreRecordsModal.vue';
const userStore = useUserStore();
const authStore = useAuthStore();
const popoverOpen = ref(false);
const refreshing = ref(false);
const vip = computed(() => (userStore.userInfo as any)?.vip || null);
const storeId = computed(() => Number((userStore.userInfo as any)?.store_id || 0));
const storeName = computed(
() =>
String(
(userStore.userInfo as any)?.store_name ||
(userStore.userInfo as any)?.store?.name ||
'',
),
);
const show = computed(() => !!vip.value);
/** 只展示等级名称,不展示 CUSTOM/V1 等编码 */
const levelName = computed(
() => String(vip.value?.level_name || '').trim() || '普通会员',
);
const durationLabel = computed(
() => String(vip.value?.duration_label || '').trim() || '普通会员',
);
/** 当前等级已开功能列表 */
const features = computed(() => {
const list = vip.value?.features;
return Array.isArray(list) ? list : [];
});
/** 到期文案 */
const expireText = computed(() => {
const v = vip.value;
if (!v) return '默认会员';
if (v.is_lifetime) return '永久有效';
const expireAt = Number(v.expire_at || 0);
if (!expireAt) {
if ((v.level_code || 'V0') === 'V0') return '默认会员';
return '永久有效';
}
if (typeof v.expire_at === 'string' && String(v.expire_at).includes('-')) {
return `有效期至 ${v.expire_at}`;
}
const d = new Date(expireAt * 1000);
const pad = (n: number) => String(n).padStart(2, '0');
return `有效期至 ${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
});
/** 开通时间文案opened_at 为时间戳);无数据返回空串 */
const openedAtText = computed(() => {
const ts = Number(vip.value?.opened_at || 0);
if (!ts) return '';
const d = new Date(ts * 1000);
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
});
const openTypeText = computed(() =>
String(vip.value?.open_type_txt || '').trim(),
);
const operatorText = computed(() => {
const name = String(vip.value?.operator_name || '').trim();
if (name) return name;
const id = Number(vip.value?.operator_id || 0);
if (id > 0) return `管理员#${id}`;
return '';
});
/** 是否有开通流水信息:无则整块隐藏,避免展示「—」占位 */
const hasOpenInfo = computed(() => {
const v = vip.value;
if (!v) return false;
return !!(
openTypeText.value ||
operatorText.value ||
Number(v.opened_at || 0) > 0 ||
Number(v.open_record_id || 0) > 0
);
});
const hasVipVisual = computed(
() => !!(vip.value?.badge_url || vip.value?.duration_badge_url),
);
const [RecordsModalComp, recordsModalApi] = useVbenModal({
connectedComponent: VipStoreRecordsModal,
});
/**
* 打开气泡时刷新 my-info
* 为什么:登录态里的 vip 可能是旧缓存,缺少开通人/开通方式
*/
watch(popoverOpen, async (open) => {
if (!open || refreshing.value) return;
refreshing.value = true;
try {
await authStore.fetchUserInfo();
} catch {
// 刷新失败仍展示本地缓存
} finally {
refreshing.value = false;
}
});
/** 打开本门店开通记录弹窗 */
function openRecords() {
if (!storeId.value) return;
popoverOpen.value = false;
recordsModalApi.setData({
store_id: storeId.value,
store_name: storeName.value,
});
recordsModalApi.open();
}
</script>
<template>
<div v-if="show" class="mr-2 flex items-center">
<Popover
v-model:open="popoverOpen"
trigger="click"
placement="bottomRight"
overlay-class-name="header-vip-popover"
:overlay-inner-style="{ padding: '0', background: 'transparent', boxShadow: 'none' }"
>
<template #content>
<div class="header-vip-bubble">
<!-- 诊所端风格会员卡头图 -->
<div class="header-vip-hero">
<VipBadgeCombo
v-if="hasVipVisual"
size="lg"
:badge-url="vip?.badge_url"
:duration-badge-url="vip?.duration_badge_url"
:level-name="levelName"
:duration-label="durationLabel"
/>
<div v-else class="header-vip-hero__placeholder">V</div>
<div class="header-vip-hero__info">
<div class="header-vip-hero__title">{{ levelName }}</div>
<div class="header-vip-hero__sub">{{ durationLabel }}</div>
<div class="header-vip-hero__expire">{{ expireText }}</div>
</div>
</div>
<div class="header-vip-body">
<div v-if="hasOpenInfo" class="header-vip-rows">
<div v-if="openTypeText" class="header-vip-row">
<span class="header-vip-row__label">开通方式</span>
<span class="header-vip-row__value">{{ openTypeText }}</span>
</div>
<div v-if="operatorText" class="header-vip-row">
<span class="header-vip-row__label">开通人</span>
<span class="header-vip-row__value">{{ operatorText }}</span>
</div>
<div v-if="openedAtText" class="header-vip-row">
<span class="header-vip-row__label">开通时间</span>
<span class="header-vip-row__value">{{ openedAtText }}</span>
</div>
</div>
<div
class="header-vip-features"
:class="{ 'header-vip-features--alone': !hasOpenInfo }"
>
<div class="header-vip-features__title">已开功能</div>
<div v-if="features.length" class="header-vip-features__list">
<span
v-for="item in features"
:key="item.code"
class="header-vip-feature-tag"
:title="item.description || item.name"
>
{{ item.name }}
</span>
</div>
<div v-else class="header-vip-features__empty">暂无开通功能</div>
</div>
<Button
v-if="storeId > 0 && hasOpenInfo"
type="primary"
block
size="small"
class="mt-3"
@click="openRecords"
>
查看开通记录
</Button>
</div>
</div>
</template>
<div
class="header-vip-trigger"
role="button"
tabindex="0"
@keydown.enter.prevent="popoverOpen = !popoverOpen"
>
<VipBadgeCombo
size="sm"
:badge-url="vip.badge_url"
:duration-badge-url="vip.duration_badge_url"
:level-name="levelName"
:duration-label="durationLabel"
/>
</div>
</Popover>
<RecordsModalComp />
</div>
</template>
<style scoped>
.header-vip-trigger {
cursor: pointer;
border-radius: 8px;
padding: 2px;
transition: background 0.15s ease;
}
.header-vip-trigger:hover {
background: hsl(var(--primary) / 0.1);
}
.header-vip-bubble {
width: 320px;
border-radius: 14px;
overflow: hidden;
border: 1px solid hsl(var(--border));
background: hsl(var(--card, var(--background)));
box-shadow: 0 8px 24px hsl(var(--foreground) / 0.12);
}
.header-vip-hero {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 18px;
background: linear-gradient(135deg, #2c3e50 0%, #3d5a40 50%, #6acdbb 100%);
}
.header-vip-hero__placeholder {
width: 72px;
height: 72px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
color: #fff;
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.header-vip-hero__info {
min-width: 0;
color: #fff;
}
.header-vip-hero__title {
font-size: 18px;
font-weight: 700;
line-height: 1.3;
}
.header-vip-hero__sub {
margin-top: 4px;
font-size: 13px;
opacity: 0.9;
}
.header-vip-hero__expire {
margin-top: 4px;
font-size: 12px;
opacity: 0.78;
}
.header-vip-body {
padding: 12px 14px 14px;
}
.header-vip-rows {
display: flex;
flex-direction: column;
gap: 8px;
}
.header-vip-row {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 12px;
line-height: 1.4;
}
.header-vip-row__label {
flex-shrink: 0;
color: hsl(var(--muted-foreground));
}
.header-vip-row__value {
text-align: right;
color: hsl(var(--foreground));
word-break: break-all;
}
.header-vip-features {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid hsl(var(--border));
}
.header-vip-features--alone {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.header-vip-features__title {
font-size: 12px;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 8px;
}
.header-vip-features__list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.header-vip-feature-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.12);
border: 1px solid hsl(var(--primary) / 0.25);
}
.header-vip-features__empty {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
html.dark .header-vip-hero,
.dark .header-vip-hero {
background: linear-gradient(135deg, #1a2332 0%, #243528 50%, #2d6b5f 100%);
}
</style>

View File

@@ -0,0 +1,79 @@
<script lang="ts" setup>
/**
* 诊所列表 VIP 单元格:徽标+会员名+到期时间,点击打开 VIP 入口弹窗
*/
import { computed } from 'vue';
import VipBadgeCombo from '#/components/vip/VipBadgeCombo.vue';
import { formatVipDurationExpireLine } from '#/utils/vip';
const props = defineProps<{
vip?: Record<string, any> | null;
}>();
const emit = defineEmits<{
click: [];
}>();
/** 副行展示时长类型与到期,方便列表一眼看到过期时间 */
const subText = computed(() => formatVipDurationExpireLine(props.vip));
</script>
<template>
<div
class="store-vip-cell"
role="button"
tabindex="0"
@click.stop="emit('click')"
@keydown.enter.prevent="emit('click')"
>
<VipBadgeCombo
size="sm"
:badge-url="vip?.badge_url"
:duration-badge-url="vip?.duration_badge_url"
:level-name="vip?.level_name"
:duration-label="vip?.duration_label"
/>
<div class="store-vip-cell__text">
<div class="store-vip-cell__name">{{ vip?.level_name || '普通会员' }}</div>
<div class="store-vip-cell__sub">{{ subText }}</div>
</div>
</div>
</template>
<style scoped>
.store-vip-cell {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 8px 4px 4px;
border-radius: 10px;
transition: background 0.15s ease;
max-width: 100%;
}
.store-vip-cell:hover {
background: rgba(106, 205, 187, 0.12);
}
.store-vip-cell__text {
min-width: 0;
line-height: 1.25;
}
.store-vip-cell__name {
font-size: 13px;
font-weight: 600;
color: hsl(var(--foreground));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.store-vip-cell__sub {
font-size: 11px;
color: hsl(var(--muted-foreground));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
html.dark .store-vip-cell:hover,
.dark .store-vip-cell:hover {
background: rgba(106, 205, 187, 0.2);
}
</style>

View File

@@ -0,0 +1,106 @@
<script lang="ts" setup>
/**
* 诊所设置「基础资料」会员卡片
* 大尺寸等级徽标 + 期限丝带叠加,展示等级名与到期时间
*/
import { computed } from 'vue';
import VipBadgeCombo from './VipBadgeCombo.vue';
const props = defineProps<{
vip?: Record<string, any> | null;
}>();
const hasVipVisual = computed(() => {
const v = props.vip;
return !!(v && (v.badge_url || v.duration_badge_url));
});
const expireText = computed(() => {
const v = props.vip;
if (!v) return '默认会员';
if (v.is_lifetime) return '永久有效';
const expireAt = Number(v.expire_at || 0);
if (!expireAt) {
if ((v.level_code || 'V0') === 'V0') return '默认会员';
return '永久有效';
}
if (typeof v.expire_at === 'string' && String(v.expire_at).includes('-')) {
return `有效期至 ${v.expire_at}`;
}
const d = new Date(expireAt * 1000);
const pad = (n: number) => String(n).padStart(2, '0');
return `有效期至 ${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
});
</script>
<template>
<div class="store-vip-card">
<VipBadgeCombo
v-if="hasVipVisual"
size="xl"
:badge-url="vip?.badge_url"
:duration-badge-url="vip?.duration_badge_url"
:level-name="vip?.level_name"
:duration-label="vip?.duration_label"
/>
<div v-else class="store-vip-card__placeholder">V</div>
<div class="store-vip-card__info">
<div class="store-vip-card__title">
{{ vip?.level_name || '普通会员' }}
</div>
<div class="store-vip-card__sub">
{{ vip?.duration_label || '普通会员' }}
</div>
<div class="store-vip-card__expire">{{ expireText }}</div>
</div>
</div>
</template>
<style scoped>
.store-vip-card {
display: flex;
align-items: center;
gap: 24px;
padding: 20px 28px;
border-radius: 16px;
background: linear-gradient(135deg, #2c3e50 0%, #3d5a40 50%, #6acdbb 100%);
box-shadow: 0 8px 24px rgba(44, 62, 80, 0.16);
margin-bottom: 24px;
}
.store-vip-card__placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
color: #fff;
font-size: 48px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.store-vip-card__info {
min-width: 0;
color: #fff;
}
.store-vip-card__title {
font-size: 22px;
font-weight: 700;
line-height: 1.3;
margin-bottom: 6px;
}
.store-vip-card__sub {
font-size: 14px;
opacity: 0.9;
margin-bottom: 6px;
}
.store-vip-card__expire {
font-size: 13px;
opacity: 0.78;
}
html.dark .store-vip-card,
.dark .store-vip-card {
background: linear-gradient(135deg, #1a2332 0%, #243528 50%, #2d6b5f 100%);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
</style>

View File

@@ -0,0 +1,117 @@
<script lang="ts" setup>
/**
* VIP 组合徽标:等级徽标在下,期限丝带(透明底)叠在上方绑定
* size: sm 顶栏 / md 表格 / lg 详情 / xl 诊所设置会员卡
*/
withDefaults(
defineProps<{
badgeUrl?: string;
durationBadgeUrl?: string;
levelName?: string;
durationLabel?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
}>(),
{
badgeUrl: '',
durationBadgeUrl: '',
levelName: '',
durationLabel: '',
size: 'md',
},
);
</script>
<template>
<div
v-if="badgeUrl || durationBadgeUrl"
class="vip-badge-combo"
:class="[`vip-badge-combo--${size}`]"
:title="[durationLabel, levelName].filter(Boolean).join(' · ')"
>
<img
v-if="badgeUrl"
class="vip-badge-combo__level"
:src="badgeUrl"
:alt="levelName || '会员等级'"
/>
<img
v-if="durationBadgeUrl"
class="vip-badge-combo__ribbon"
:src="durationBadgeUrl"
:alt="durationLabel || '会员类型'"
/>
</div>
<span v-else>-</span>
</template>
<style scoped>
.vip-badge-combo {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
vertical-align: middle;
}
.vip-badge-combo__level {
object-fit: contain;
display: block;
position: relative;
z-index: 1;
}
.vip-badge-combo__ribbon {
position: absolute;
left: 50%;
bottom: 8%;
transform: translateX(-50%);
z-index: 2;
object-fit: contain;
pointer-events: none;
}
.vip-badge-combo--sm {
width: 52px;
height: 52px;
}
.vip-badge-combo--sm .vip-badge-combo__level {
width: 48px;
height: 48px;
}
.vip-badge-combo--sm .vip-badge-combo__ribbon {
width: 72px;
height: 28px;
}
.vip-badge-combo--md {
width: 64px;
height: 64px;
}
.vip-badge-combo--md .vip-badge-combo__level {
width: 56px;
height: 56px;
}
.vip-badge-combo--md .vip-badge-combo__ribbon {
width: 84px;
height: 30px;
}
.vip-badge-combo--lg {
width: 96px;
height: 96px;
}
.vip-badge-combo--lg .vip-badge-combo__level {
width: 84px;
height: 84px;
}
.vip-badge-combo--lg .vip-badge-combo__ribbon {
width: 120px;
height: 42px;
}
.vip-badge-combo--xl {
width: 140px;
height: 140px;
}
.vip-badge-combo--xl .vip-badge-combo__level {
width: 120px;
height: 120px;
}
.vip-badge-combo--xl .vip-badge-combo__ribbon {
width: 168px;
height: 58px;
}
</style>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
/**
* VIP 权限显隐全局组件
* 用法:
* <VipGate code="medical_record">有权限才渲染</VipGate>
* <VipGate :code="VIP_FEATURE.AI_PRESCRIPTION" :vip="receptionVip">...</VipGate>
* 接诊/复诊务必传 :vip="履约诊所vip",不要只靠登录态
*/
import { computed } from 'vue';
import {
hasVipPermission,
levelAtLeast,
type StoreVipInfo,
} from '#/utils/vip';
const props = withDefaults(
defineProps<{
/** 权限码(与 xk_vip_feature.code / 等级 permissions 对齐) */
code?: string;
/** 最低等级编码 */
minLevel?: string;
/**
* 指定 VIP 来源(挂号履约诊所);不传则用登录态 userInfo.vip
*/
vip?: StoreVipInfo | string[] | null;
/** 无权限时是否渲染 fallback 插槽(默认不渲染) */
fallback?: boolean;
}>(),
{
code: '',
minLevel: '',
fallback: false,
},
);
const allowed = computed(() => {
if (props.code) {
// 传了 :vip 则按履约诊所判;未传则回落登录态
if (props.vip !== undefined) {
return hasVipPermission(props.code, props.vip);
}
return hasVipPermission(props.code);
}
if (props.minLevel) {
const vipObj = Array.isArray(props.vip) ? null : (props.vip ?? null);
return levelAtLeast(props.minLevel, vipObj);
}
return true;
});
</script>
<template>
<slot v-if="allowed" />
<slot v-else-if="fallback" name="fallback" />
</template>

View File

@@ -0,0 +1,233 @@
<script lang="ts" setup>
/**
* 门店 VIP 入口弹窗
* 先展示两张操作卡片:开通/升级、开通记录;再分别打开对应子弹窗
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import VipBadgeCombo from '#/components/vip/VipBadgeCombo.vue';
import VipStoreRecordsModal from '#/components/vip/VipStoreRecordsModal.vue';
import VipUpgradeModal from '#/components/vip/VipUpgradeModal.vue';
import { formatVipExpireAt } from '#/utils/vip';
const gridApi = ref<any>();
const onSuccess = ref<null | (() => void)>(null);
const storeId = ref(0);
const storeName = ref('');
const vip = ref<Record<string, any> | null>(null);
const title = computed(
() => `VIP管理 - ${storeName.value || storeId.value || ''}`,
);
/** 当前等级 + 时长类型 */
const currentLevelText = computed(() => {
const v = vip.value;
if (!v) return '当前:普通会员';
const name = String(v.level_name || '').trim();
const code = String(v.level_code || '').trim();
const duration = String(v.duration_label || '').trim();
const level =
name && code ? `${name}${code}` : name || code || '普通会员';
return duration ? `当前:${level} · ${duration}` : `当前:${level}`;
});
/** 到期时间独立一行,打开弹窗时一眼可见 */
const currentExpireText = computed(() => {
const v = vip.value;
if (!v) return '';
const code = String(v.level_code || 'V0').trim() || 'V0';
if (code === 'V0') return '';
const expire = formatVipExpireAt(v, {
lifetimeText: '终身',
withPrefix: false,
});
if (!expire) return '';
return expire === '终身' ? '到期时间:终身' : `到期时间:${expire}`;
});
const [UpgradeModalComp, upgradeModalApi] = useVbenModal({
connectedComponent: VipUpgradeModal,
});
const [RecordsModalComp, recordsModalApi] = useVbenModal({
connectedComponent: VipStoreRecordsModal,
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
class: 'w-[640px]',
showConfirmButton: false,
cancelText: '关闭',
onCancel() {
modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
storeId.value = 0;
storeName.value = '';
vip.value = null;
gridApi.value = null;
onSuccess.value = null;
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
storeId.value = Number(data.store_id || 0);
storeName.value = String(data.store_name || '');
vip.value = data.vip || null;
gridApi.value = data.gridApi || null;
onSuccess.value = data.onSuccess || null;
modalApi.setState({ title: title.value });
},
});
/**
* 打开开通/升级弹窗
* 为什么不直接嵌在本弹窗:升级表单已有独立组件,复用可保持交互一致
*/
function openUpgrade() {
if (!storeId.value) return;
upgradeModalApi.setData({
store_id: storeId.value,
store_name: storeName.value,
vip: vip.value,
gridApi: gridApi.value,
onSuccess: () => {
onSuccess.value?.();
gridApi.value?.query?.();
},
});
upgradeModalApi.open();
}
/** 打开当前门店开通记录(含开通人) */
function openRecords() {
if (!storeId.value) return;
recordsModalApi.setData({
store_id: storeId.value,
store_name: storeName.value,
});
recordsModalApi.open();
}
</script>
<template>
<Modal>
<div class="vip-hub-current mb-4">
<VipBadgeCombo
size="md"
:badge-url="vip?.badge_url"
:duration-badge-url="vip?.duration_badge_url"
:level-name="vip?.level_name"
:duration-label="vip?.duration_label"
/>
<div class="vip-hub-current__meta">
<div class="vip-hub-current__text">{{ currentLevelText }}</div>
<div v-if="currentExpireText" class="vip-hub-current__expire">
{{ currentExpireText }}
</div>
</div>
</div>
<div class="vip-hub-grid">
<button type="button" class="vip-hub-card" @click="openUpgrade">
<div class="vip-hub-card__icon"></div>
<div class="vip-hub-card__title">开通 / 升级</div>
<div class="vip-hub-card__desc">
为该门店开通或调整会员等级与时长
</div>
</button>
<button type="button" class="vip-hub-card" @click="openRecords">
<div class="vip-hub-card__icon"></div>
<div class="vip-hub-card__title">开通记录</div>
<div class="vip-hub-card__desc">
查看历史开通流水与开通人
</div>
</button>
</div>
</Modal>
<UpgradeModalComp />
<RecordsModalComp />
</template>
<style scoped>
.vip-hub-current {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid hsl(var(--border));
border-radius: 10px;
background: hsl(var(--muted) / 0.25);
}
.vip-hub-current__meta {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.vip-hub-current__text {
font-size: 13px;
font-weight: 500;
color: hsl(var(--foreground));
}
.vip-hub-current__expire {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.vip-hub-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.vip-hub-card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
min-height: 148px;
padding: 18px 16px;
border: 1px solid hsl(var(--border));
border-radius: 12px;
background: hsl(var(--card, var(--background)));
text-align: left;
cursor: pointer;
transition:
border-color 0.15s,
background 0.15s,
box-shadow 0.15s;
}
.vip-hub-card:hover {
border-color: hsl(var(--primary));
background: hsl(var(--primary) / 0.08);
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.2);
}
.vip-hub-card__icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.12);
}
.vip-hub-card__title {
font-size: 16px;
font-weight: 650;
color: hsl(var(--foreground));
}
.vip-hub-card__desc {
font-size: 12px;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
@media (max-width: 560px) {
.vip-hub-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
/**
* 门店 VIP 开通记录弹窗
* 按 store_id 拉取开通流水,重点展示开通人、等级、时效与开通时间
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Empty, Pagination, Spin, Tag } from 'ant-design-vue';
import VipBadgeCombo from '#/components/vip/VipBadgeCombo.vue';
import { getVipRecordList } from '#/views/system/vip/record/api';
const storeId = ref(0);
const storeName = ref('');
const loading = ref(false);
const items = ref<any[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const title = computed(
() => `开通记录 - ${storeName.value || storeId.value || ''}`,
);
/**
* 拉取当前门店开通记录
* 为什么单独封装:弹窗打开、翻页都要复用同一套查询参数
*/
async function loadList() {
if (!storeId.value) {
items.value = [];
total.value = 0;
return;
}
loading.value = true;
try {
const res = await getVipRecordList({
page: page.value,
pageSize: pageSize.value,
store_id: storeId.value,
});
items.value = Array.isArray(res?.items) ? res.items : [];
total.value = Number(res?.total || 0);
} finally {
loading.value = false;
}
}
/** 等级展示:只展示名称,不展示 CUSTOM/V1 等编码 */
function levelLabel(row: Record<string, any>) {
return String(row?.level_name || '').trim() || '—';
}
/** 开通人兜底:无操作人时标明系统/未知 */
function operatorLabel(row: Record<string, any>) {
const name = String(row?.operator_name || '').trim();
if (name) return name;
const id = Number(row?.operator_id || 0);
if (id > 0) return `管理员#${id}`;
return '—';
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
class: 'w-[820px]',
showConfirmButton: false,
cancelText: '关闭',
onCancel() {
modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
storeId.value = 0;
storeName.value = '';
items.value = [];
total.value = 0;
page.value = 1;
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
storeId.value = Number(data.store_id || 0);
storeName.value = String(data.store_name || '');
page.value = 1;
modalApi.setState({ title: title.value });
await loadList();
},
});
/** 分页切换 */
function onPageChange(current: number, size: number) {
page.value = current;
pageSize.value = size;
loadList();
}
</script>
<template>
<Modal>
<Spin :spinning="loading">
<Empty
v-if="!loading && !items.length"
description="暂无开通记录"
/>
<div v-else class="vip-record-list">
<div
v-for="row in items"
:key="row.id"
class="vip-record-card"
>
<div class="vip-record-card__badge">
<VipBadgeCombo
size="md"
:badge-url="row.badge_url"
:duration-badge-url="row.duration_badge_url"
:level-name="row.level_name"
:duration-label="row.duration_label"
/>
</div>
<div class="vip-record-card__body">
<div class="vip-record-card__title">
{{ levelLabel(row) }}
<Tag class="ml-2">{{ row.duration_label || '—' }}</Tag>
<Tag color="blue">{{ row.open_type_txt || '—' }}</Tag>
</div>
<div class="vip-record-card__meta">
<span>开通人{{ operatorLabel(row) }}</span>
<span>开通时间{{ row.created_at || '—' }}</span>
<span>到期{{ row.expire_at || '—' }}</span>
</div>
<div class="vip-record-card__meta">
<span>原价 ¥{{ row.original_price ?? '0.00' }}</span>
<span>实付 ¥{{ row.pay_amount ?? '0.00' }}</span>
<span v-if="row.remark">备注{{ row.remark }}</span>
</div>
</div>
</div>
</div>
<div v-if="total > 0" class="mt-4 flex justify-end">
<Pagination
v-model:current="page"
v-model:page-size="pageSize"
:total="total"
size="small"
show-size-changer
@change="onPageChange"
/>
</div>
</Spin>
</Modal>
</template>
<style scoped>
.vip-record-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 520px;
overflow-y: auto;
padding-right: 4px;
}
.vip-record-card {
display: flex;
gap: 14px;
align-items: flex-start;
padding: 12px 14px;
border: 1px solid hsl(var(--border));
border-radius: 10px;
background: hsl(var(--card, var(--background)));
}
.vip-record-card__badge {
flex-shrink: 0;
}
.vip-record-card__body {
min-width: 0;
flex: 1;
}
.vip-record-card__title {
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: 14px;
font-weight: 600;
color: hsl(var(--foreground));
}
.vip-record-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
margin-top: 6px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
</style>

View File

@@ -0,0 +1,496 @@
<script lang="ts" setup>
/**
* VIP 升级/开通弹窗
* 等级/时长用两行横向滑动单选卡片,预览丝带随所选时长自动切换
*/
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Input, InputNumber, message } from 'ant-design-vue';
import VipBadgeCombo from '#/components/vip/VipBadgeCombo.vue';
import { getVipDurationOption } from '#/views/system/vip/duration/api';
import { getVipLevelOption } from '#/views/system/vip/level/api';
import {
getVipDurationPresets,
openVipStore,
} from '#/views/system/vip/store/api';
/** 时长预设 → 时效类型 code与后端 VipDurationHelper 对齐keep 走当前会员 duration_type */
const PRESET_TO_DURATION: Record<string, string> = {
'7d': 'trial',
'1w': 'week',
'1m': 'month',
'3m': 'month',
'6m': 'month',
'1y': 'year',
xy: 'year',
lifetime: 'lifetime',
};
const gridApi = ref<any>();
const onSuccess = ref<null | (() => void)>(null);
const storeId = ref(0);
const storeName = ref('');
/** 打开弹窗时带入的当前门店 VIP用于判断能否「继承剩余时间」 */
const currentVip = ref<Record<string, any> | null>(null);
const levelId = ref<number | undefined>();
const durationPreset = ref<string | undefined>();
const customYears = ref<number | undefined>();
const remark = ref('');
const levels = ref<any[]>([]);
const presets = ref<{ label: string; value: string }[]>([]);
const durationTypes = ref<any[]>([]);
const selectedLevel = computed(() =>
levels.value.find((i) => i.id === levelId.value),
);
/**
* 是否可继承剩余时间:非普通会员(非 V0且未过期
* 普通会员没有可继承的到期时间,不展示/不默认 keep
*/
const canKeepDuration = computed(() => {
const v = currentVip.value;
if (!v) return false;
const code = String(v.level_code || 'V0').trim() || 'V0';
if (code === 'V0') return false;
const expireAt = Number(v.expire_at || 0);
// expire_at=0 表示终身,视为可继承;>0 须未过期
if (expireAt > 0 && expireAt * 1000 <= Date.now()) return false;
return true;
});
/** 按时长预设解析对应丝带资源 */
function ribbonByPreset(preset?: string) {
if (preset === 'keep') {
const code = String(currentVip.value?.duration_type || '').trim();
if (!code) return null;
return durationTypes.value.find((i) => i.code === code) || null;
}
const code = PRESET_TO_DURATION[preset || ''] || '';
if (!code) return null;
return durationTypes.value.find((i) => i.code === code) || null;
}
const selectedDurationType = computed(() =>
ribbonByPreset(durationPreset.value),
);
/** 时长卡片:普通会员隐藏 keep其它预设照常 */
const durationCards = computed(() =>
presets.value
.filter((p) => p.value !== 'keep' || canKeepDuration.value)
.map((p) => {
const ribbon = ribbonByPreset(p.value);
return {
...p,
ribbonUrl: ribbon?.badge_url || '',
ribbonName: ribbon?.name || '',
};
}),
);
const previewBadgeUrl = computed(() => selectedLevel.value?.badge_url || '');
const previewRibbonUrl = computed(
() => selectedDurationType.value?.badge_url || '',
);
const previewLevelName = computed(
() => selectedLevel.value?.name || '请选择等级',
);
const previewDurationLabel = computed(() => {
if (!durationPreset.value) return '请选择时长';
if (durationPreset.value === 'keep') {
const label = String(currentVip.value?.duration_label || '').trim();
const expireHint = formatVipExpireHint(currentVip.value);
if (label && expireHint) return `继承剩余:${label}${expireHint}`;
if (label) return `继承剩余:${label}`;
return '继承剩余时间(不改动)';
}
const preset = presets.value.find((p) => p.value === durationPreset.value);
return preset?.label || selectedDurationType.value?.name || '请选择时长';
});
/**
* 格式化当前会员到期提示(终身 / 已格式化字符串 / 时间戳)
*/
function formatVipExpireHint(v: Record<string, any> | null | undefined): string {
if (!v) return '';
if (v.is_lifetime) return '终身';
const raw = v.expire_at;
if (raw == null || raw === '' || raw === 0) return '';
if (typeof raw === 'string' && Number.isNaN(Number(raw))) {
return `${raw}`;
}
const n = Number(raw);
if (!(n > 0)) return '';
const d = new Date(n * 1000);
if (Number.isNaN(d.getTime())) return '';
const pad = (x: number) => String(x).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
const title = computed(() => `升级VIP - ${storeName.value || storeId.value}`);
/** 选择会员等级 */
function selectLevel(id: number) {
levelId.value = id;
}
/** 选择开通时长(同步驱动预览丝带) */
function selectDuration(value: string) {
durationPreset.value = value;
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (!levelId.value) {
message.warning('请选择会员等级');
return;
}
if (!durationPreset.value) {
message.warning('请选择开通时长');
return;
}
if (
durationPreset.value === 'xy' &&
!(Number(customYears.value) > 0)
) {
message.warning('请填写有效年数');
return;
}
modalApi.setState({ confirmLoading: true });
try {
await openVipStore({
store_id: storeId.value,
level_id: levelId.value,
duration_preset: durationPreset.value,
custom_years: customYears.value || 0,
remark: remark.value || '',
});
message.success('开通成功');
gridApi.value?.query?.();
onSuccess.value?.();
modalApi.close();
} finally {
modalApi.setState({ confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
levelId.value = undefined;
durationPreset.value = undefined;
customYears.value = undefined;
remark.value = '';
currentVip.value = null;
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
gridApi.value = data.gridApi;
onSuccess.value = data.onSuccess || null;
storeId.value = Number(data.store_id || 0);
storeName.value = data.store_name || '';
currentVip.value = data.vip || null;
const [levelList, presetList, durationList] = await Promise.all([
getVipLevelOption(),
getVipDurationPresets(),
getVipDurationOption(),
]);
levels.value = levelList || [];
presets.value = (presetList || []).map((i: any) => ({
label: i.label,
value: i.value,
}));
durationTypes.value = durationList || [];
// 回填当前 VIP 等级(便于续期/改档)
if (data.vip?.level_id) {
levelId.value = Number(data.vip.level_id);
}
// 非普通会员:默认选「继承剩余时间」,到期日不改动
if (canKeepDuration.value) {
durationPreset.value = 'keep';
}
},
});
watch(durationPreset, (v) => {
if (v !== 'xy') customYears.value = undefined;
});
</script>
<template>
<Modal :title="title" class="w-[50%]">
<!-- 预览徽标卡片 + 丝带随时长自动适配 -->
<div class="vip-upgrade-preview mb-5">
<VipBadgeCombo
size="xl"
:badge-url="previewBadgeUrl"
:duration-badge-url="previewRibbonUrl"
:level-name="previewLevelName"
:duration-label="previewDurationLabel"
/>
<div class="vip-upgrade-preview__text">
<div class="vip-upgrade-preview__title">{{ previewLevelName }}</div>
<div class="vip-upgrade-preview__sub">{{ previewDurationLabel }}</div>
<div v-if="selectedLevel" class="vip-upgrade-preview__price">
原价 ¥{{ selectedLevel.price }} · 管理员开通实付 ¥0
</div>
</div>
</div>
<div class="space-y-4">
<div>
<div class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">
会员等级
</div>
<div class="vip-pick-scroll" role="listbox" aria-label="会员等级">
<button
v-for="item in levels"
:key="item.id"
type="button"
class="vip-pick-card"
:class="{ 'vip-pick-card--active': levelId === item.id }"
role="option"
:aria-selected="levelId === item.id"
@click="selectLevel(item.id)"
>
<img
v-if="item.badge_url"
:src="item.badge_url"
alt=""
class="vip-pick-card__badge"
/>
<div
v-else
class="vip-pick-card__badge vip-pick-card__badge--empty"
>
{{ item.code }}
</div>
<div class="vip-pick-card__name">{{ item.name }}</div>
<div class="vip-pick-card__meta">
{{ item.code }} · ¥{{ item.price }}
</div>
</button>
</div>
</div>
<div>
<div class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">
开通时长
</div>
<div class="vip-pick-scroll" role="listbox" aria-label="开通时长">
<button
v-for="item in durationCards"
:key="item.value"
type="button"
class="vip-pick-card vip-pick-card--duration"
:class="{ 'vip-pick-card--active': durationPreset === item.value }"
role="option"
:aria-selected="durationPreset === item.value"
@click="selectDuration(item.value)"
>
<img
v-if="item.ribbonUrl"
:src="item.ribbonUrl"
alt=""
class="vip-pick-card__ribbon"
/>
<div
v-else
class="vip-pick-card__ribbon vip-pick-card__ribbon--empty"
>
丝带
</div>
<div class="vip-pick-card__name">{{ item.label }}</div>
</button>
</div>
</div>
<div v-if="durationPreset === 'xy'">
<div class="mb-1 text-sm text-gray-600 dark:text-gray-300">
自定义年数
</div>
<InputNumber
v-model:value="customYears"
class="w-full"
:min="1"
:max="99"
placeholder="请输入年数"
/>
</div>
<div>
<div class="mb-1 text-sm text-gray-600 dark:text-gray-300">备注</div>
<Input.TextArea v-model:value="remark" :rows="2" placeholder="选填" />
</div>
</div>
</Modal>
</template>
<style scoped>
.vip-upgrade-preview {
display: flex;
align-items: center;
gap: 20px;
padding: 20px 24px;
border-radius: 14px;
background: linear-gradient(135deg, #2c3e50 0%, #3d5a40 50%, #6acdbb 100%);
color: #fff;
}
.vip-upgrade-preview__title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.vip-upgrade-preview__sub {
font-size: 13px;
opacity: 0.9;
margin-bottom: 6px;
}
.vip-upgrade-preview__price {
font-size: 12px;
opacity: 0.8;
}
/* 两行 + 横向滑动的单选卡片轨道 */
.vip-pick-scroll {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(2, minmax(0, auto));
grid-auto-columns: 112px;
gap: 10px;
overflow-x: auto;
overflow-y: hidden;
padding: 4px 2px 10px;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
}
.vip-pick-scroll::-webkit-scrollbar {
height: 6px;
}
.vip-pick-scroll::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 999px;
}
.vip-pick-card {
scroll-snap-align: start;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
min-height: 108px;
padding: 10px 8px;
border: 1.5px solid #e8e8e8;
border-radius: 12px;
background: #fff;
cursor: pointer;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease,
background 0.15s ease;
text-align: center;
}
.vip-pick-card:hover {
border-color: #6acdbb;
}
.vip-pick-card--active {
border-color: #6acdbb;
background: rgba(106, 205, 187, 0.1);
box-shadow: 0 0 0 2px rgba(106, 205, 187, 0.25);
}
.vip-pick-card__badge {
width: 48px;
height: 48px;
object-fit: contain;
}
.vip-pick-card__badge--empty {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f5f5;
color: #999;
font-size: 12px;
font-weight: 600;
}
.vip-pick-card__ribbon {
width: 88px;
height: 28px;
object-fit: contain;
}
.vip-pick-card__ribbon--empty {
display: flex;
align-items: center;
justify-content: center;
width: 88px;
height: 28px;
border-radius: 6px;
background: #f5f5f5;
color: #bbb;
font-size: 11px;
}
.vip-pick-card--duration {
min-height: 88px;
}
.vip-pick-card__name {
font-size: 13px;
font-weight: 600;
color: #1f1f1f;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vip-pick-card__meta {
font-size: 11px;
color: #8c8c8c;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ========== 暗色适配html.dark / .dark========== */
html.dark .vip-upgrade-preview,
.dark .vip-upgrade-preview {
background: linear-gradient(135deg, #1a2332 0%, #243528 50%, #2d6b5f 100%);
box-shadow: inset 0 0 0 1px rgba(106, 205, 187, 0.18);
}
html.dark .vip-pick-scroll::-webkit-scrollbar-thumb,
.dark .vip-pick-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.22);
}
html.dark .vip-pick-card,
.dark .vip-pick-card {
border-color: #3f3f46;
background: #1f1f23;
}
html.dark .vip-pick-card:hover,
.dark .vip-pick-card:hover {
border-color: #6acdbb;
background: #27272a;
}
html.dark .vip-pick-card--active,
.dark .vip-pick-card--active {
border-color: #6acdbb;
background: rgba(106, 205, 187, 0.16);
box-shadow: 0 0 0 2px rgba(106, 205, 187, 0.28);
}
html.dark .vip-pick-card__badge--empty,
.dark .vip-pick-card__badge--empty,
html.dark .vip-pick-card__ribbon--empty,
.dark .vip-pick-card__ribbon--empty {
background: #2a2a2e;
color: #a1a1aa;
}
html.dark .vip-pick-card__name,
.dark .vip-pick-card__name {
color: #f4f4f5;
}
html.dark .vip-pick-card__meta,
.dark .vip-pick-card__meta {
color: #a1a1aa;
}
</style>

View File

@@ -43,6 +43,10 @@ export interface WarehouseDrugItem {
id: number; id: number;
drug_name: string; drug_name: string;
pinyin_simple?: string; pinyin_simple?: string;
/** 关键词命中的别名(后端 matched_alias */
matched_alias?: string;
/** 命中别名对应拼音展示 */
matched_alias_pinyin?: string;
specification?: string; specification?: string;
image?: string; image?: string;
type?: number; type?: number;
@@ -212,8 +216,27 @@ watch(
<div v-else class="warehouse-drug-item__img-placeholder">无图</div> <div v-else class="warehouse-drug-item__img-placeholder">无图</div>
</div> </div>
<div class="warehouse-drug-item__info"> <div class="warehouse-drug-item__info">
<div class="warehouse-drug-item__name">{{ item.drug_name }}</div> <div
class="warehouse-drug-item__name"
:title="
item.matched_alias
? `${item.drug_name}(别名:${item.matched_alias}`
: item.drug_name
"
>
{{ item.drug_name }}
</div>
<!-- 与开方气泡一致命中别名时展示别名及拼音 -->
<div v-if="item.matched_alias" class="warehouse-drug-item__alias">
别名{{ item.matched_alias }}
<span v-if="item.matched_alias_pinyin">
{{ item.matched_alias_pinyin }}
</span>
</div>
<div class="warehouse-drug-item__meta"> <div class="warehouse-drug-item__meta">
<span v-if="item.pinyin_simple && !item.matched_alias" class="warehouse-drug-item__py">
拼音{{ item.pinyin_simple }}
</span>
<span v-if="item.specification" class="warehouse-drug-item__spec"> <span v-if="item.specification" class="warehouse-drug-item__spec">
规格{{ item.specification }} 规格{{ item.specification }}
</span> </span>
@@ -321,7 +344,17 @@ watch(
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-bottom: 2px;
}
&__alias {
font-size: 12px;
color: #8c8c8c;
line-height: 1.3;
margin-bottom: 4px; margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
&__meta { &__meta {
@@ -332,6 +365,10 @@ watch(
color: #999; color: #999;
} }
&__py {
color: #8c8c8c;
}
&__spec { &__spec {
color: #666; color: #666;
} }

View File

@@ -336,6 +336,14 @@ const orderColumns = [
bordered bordered
size="small" size="small"
> >
<Descriptions.Item label="现病史">
{{
historyText(
profile.health_inquiry.present_status,
profile.health_inquiry.present_history,
)
}}
</Descriptions.Item>
<Descriptions.Item label="既往史"> <Descriptions.Item label="既往史">
{{ {{
historyText( historyText(

View File

@@ -1,16 +1,15 @@
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref, toValue, type MaybeRefOrGetter } from 'vue';
import type { FileTypeItem } from '#/api/core/file-gallery'; import type { FileTypeItem } from '#/api/core/file-gallery';
import { getFileTypes } from '#/api/core/file-gallery'; import { getFileTypes } from '#/api/core/file-gallery';
import type { FileGroupOptionItem } from '#/api/core/file-group'; import type { FileGroupOptionItem } from '#/api/core/file-group';
import { useFileGroupCache } from '#/composables/use-file-group-cache'; import { useFileGroupCache } from '#/composables/use-file-group-cache';
import { ALL_TYPE_ICON } from '#/composables/use-file-type-icon';
export const ALL_TYPE_VALUE = -1; export const ALL_TYPE_VALUE = -1;
export const ALL_GROUP_VALUE = 0; export const ALL_GROUP_VALUE = 0;
import { ALL_TYPE_ICON } from '#/composables/use-file-type-icon';
export interface FileGalleryTab { export interface FileGalleryTab {
key: string; key: string;
value: number; value: number;
@@ -18,7 +17,13 @@ export interface FileGalleryTab {
icon?: string; icon?: string;
} }
export function useFileGalleryFilter(storageKey: string) { /**
* 文件库筛选(类型 tab + 分组 + 关键字)
* @param storageKeyInput localStorage 键;支持动态 getter便于按 acceptTypes 隔离记忆
*/
export function useFileGalleryFilter(
storageKeyInput: MaybeRefOrGetter<string> = 'file-picker-active-type',
) {
const fileTypes = ref<FileTypeItem[]>([]); const fileTypes = ref<FileTypeItem[]>([]);
const activeType = ref<number>(ALL_TYPE_VALUE); const activeType = ref<number>(ALL_TYPE_VALUE);
const activeGroupId = ref<number>(ALL_GROUP_VALUE); const activeGroupId = ref<number>(ALL_GROUP_VALUE);
@@ -27,6 +32,10 @@ export function useFileGalleryFilter(storageKey: string) {
const { groupOptions, loadGroupOptions } = useFileGroupCache(); const { groupOptions, loadGroupOptions } = useFileGroupCache();
function resolveStorageKey() {
return toValue(storageKeyInput);
}
const tabs = computed<FileGalleryTab[]>(() => [ const tabs = computed<FileGalleryTab[]>(() => [
{ key: 'all', value: ALL_TYPE_VALUE, label: '全部', icon: ALL_TYPE_ICON }, { key: 'all', value: ALL_TYPE_VALUE, label: '全部', icon: ALL_TYPE_ICON },
...fileTypes.value.map((item) => ({ ...fileTypes.value.map((item) => ({
@@ -48,7 +57,7 @@ export function useFileGalleryFilter(storageKey: string) {
]); ]);
function restoreActiveType() { function restoreActiveType() {
const saved = localStorage.getItem(storageKey); const saved = localStorage.getItem(resolveStorageKey());
if (saved === null) { if (saved === null) {
activeType.value = ALL_TYPE_VALUE; activeType.value = ALL_TYPE_VALUE;
return; return;
@@ -64,7 +73,7 @@ export function useFileGalleryFilter(storageKey: string) {
function setActiveType(value: number) { function setActiveType(value: number) {
activeType.value = value; activeType.value = value;
localStorage.setItem(storageKey, String(value)); localStorage.setItem(resolveStorageKey(), String(value));
} }
function setActiveGroupId(value: number) { function setActiveGroupId(value: number) {

View File

@@ -17,6 +17,8 @@ export const ENCRYPT_FIELDS = new Set([
'password', 'password',
'patient_mobile', 'patient_mobile',
'phone', 'phone',
'open_api_key',
'api_key',
]); ]);
/** 需要生成 _tm_text 并默认脱敏展示的字段encrypt_only */ /** 需要生成 _tm_text 并默认脱敏展示的字段encrypt_only */
@@ -96,7 +98,7 @@ export function maskSensitiveField(field: string, plainText: unknown): string {
return ''; return '';
} }
const str = String(plainText); const str = String(plainText);
if (field === 'password') { if (field === 'password' || field === 'api_key' || field === 'open_api_key') {
return '*'.repeat(Math.max(str.length, 6)); return '*'.repeat(Math.max(str.length, 6));
} }
if (PHONE_FIELDS.has(field)) { if (PHONE_FIELDS.has(field)) {

View File

@@ -8,6 +8,7 @@ import { $t } from '#/locales';
const appName = computed(() => preferences.app.name); const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
// const logo = 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/logo.png?Expires=1734595037&OSSAccessKeyId=TMP.3KhpnCAjxkdTGWSj1ZCdYe3e9c6MBfEVZwN1CiHdUKUAT2D1mPEpACYgYMkUiQDVqy9Zqb7VqgUoh21W37FC3CztogHJPN&Signature=KmymeelO2OZxaAmvm2ACDK5UfF8%3D'; // const logo = 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/logo.png?Expires=1734595037&OSSAccessKeyId=TMP.3KhpnCAjxkdTGWSj1ZCdYe3e9c6MBfEVZwN1CiHdUKUAT2D1mPEpACYgYMkUiQDVqy9Zqb7VqgUoh21W37FC3CztogHJPN&Signature=KmymeelO2OZxaAmvm2ACDK5UfF8%3D';
</script> </script>
@@ -15,6 +16,7 @@ const logo = computed(() => preferences.logo.source);
<AuthPageLayout <AuthPageLayout
:app-name="appName" :app-name="appName"
:logo="logo" :logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')"
:version="preferences.app.version" :version="preferences.app.version"

View File

@@ -1,185 +1,197 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 管理端基础布局:铃铛站内信轮询 + 动作宿主挂载
*/
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import {computed, onUnmounted, ref, watch} from 'vue'; import { computed, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal, VbenIcon } from '@vben/common-ui';
// import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks'; import { useWatermark } from '@vben/hooks';
// import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons'; import { $t } from '@vben/locales';
// import { CircleHelp } from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
LockScreen, LockScreen,
Notification, Notification,
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
// import { openWindow } from '@vben/utils';
import { import { notification } from 'ant-design-vue';
Bell, import { Bell, Users } from 'lucide-vue-next';
CalendarClock,
Megaphone,
Users,
} from 'lucide-vue-next';
// import { $t } from '#/locales'; import DoctorTransferFloat from '#/components/doctor-transfer-float/DoctorTransferFloat.vue';
import { useAuthStore } from '#/store'; import HeaderVipBadge from '#/components/vip/HeaderVipBadge.vue';
import LoginForm from '#/views/_core/authentication/login.vue';
import SwitchAccountModal from '#/layouts/components/SwitchAccountModal.vue'; import SwitchAccountModal from '#/layouts/components/SwitchAccountModal.vue';
import { useRouter } from 'vue-router'; import { useAuthStore } from '#/store';
import {getNoticeListApi, readAllApi} from "#/views/notice/api"; import { formatTimeToRelative } from '#/util/tool';
import {formatTimeToRelative} from "#/util/tool"; import LoginForm from '#/views/_core/authentication/login.vue';
import {notification} from "ant-design-vue"; import NoticeActionHost from '#/views/notice/action/NoticeActionHost.vue';
import {
emitNoticeRefresh,
onNoticeRefresh,
openNoticeAction,
} from '#/views/notice/action/bus';
import { resolveNoticeColor } from '#/views/notice/action/types';
import {
getNoticeListApi,
getNoticeUnreadCountApi,
readAllApi,
readApi,
} from '#/views/notice/api';
const router = useRouter(); const router = useRouter();
const switchAccountModalRef = ref<InstanceType<typeof SwitchAccountModal>>(); const switchAccountModalRef = ref<InstanceType<typeof SwitchAccountModal>>();
// 样式相关
const typeIcons = {
0: Megaphone,
1: Bell,
2: CalendarClock,
};
const typeColors = {
0: 'text-sky-500',
1: 'text-amber-500',
2: 'text-emerald-500',
};
const typeBgColors = {
0: 'bg-sky-50',
1: 'bg-amber-50',
2: 'bg-emerald-50',
};
const typeBorderColors = {
0: 'border-sky-200',
1: 'border-amber-200',
2: 'border-emerald-200',
};
const notifications = ref<NotificationItem[]>([]); const notifications = ref<NotificationItem[]>([]);
const unreadCount = ref(0);
// 获取消息列表数据 /** 拉取未读列表 + 角标数字 */
const getNoticeList = async () => { const getNoticeList = async () => {
try { try {
const res = await getNoticeListApi({ const [res, countRes] = await Promise.all([
status: 0, getNoticeListApi({ status: 0, page: 1, pageSize: 20 }),
}); getNoticeUnreadCountApi().catch(() => null),
let message = '未读消息'; ]);
if (res.items.length > 0) { const items = res?.items || [];
if (res.items.length > notifications.value.length) { const prevLen = notifications.value.length;
if (notifications.value.length === 0) { if (items.length > prevLen && prevLen > 0) {
message = '新消息'; notification.info({
} message: '您有新消息',
notification.info({ description: `您有 ${items.length} 条未读消息请注意查收`,
message: `您有${message}`, duration: 5,
description: `您有 ${res.items.length}${message}请注意查收`, });
duration: 5,
});
}
notifications.value = res.items.map(item => ({
id: item.id,
icon: typeIcons[item.type],
// 将API返回的数据映射到组件需要的格式
message: item.title,
title: getMessageTypeName(item.type),
date: formatTimeToRelative(item.created_at),
color: typeColors[item.type],
isRead: item.status
}));
} else {
notifications.value = [];
} }
notifications.value = items.map((item: any) => {
const color = resolveNoticeColor(item.type_color);
return {
id: item.id,
// Notification 默认用组件当 icon我们走自定义 content 槽,这里占位即可
icon: Bell as any,
message: item.detail || item.type_name || '',
title: item.title || '通知',
date: formatTimeToRelative(item.created_at),
color: color.text,
colorBg: color.bg,
isRead: Number(item.status) === 1,
status: item.status,
type_code: item.type_code,
type_name: item.type_name,
type_icon: item.type_icon,
type_color: item.type_color,
action_type: item.action_type,
action_payload: item.action_payload,
detail: item.detail,
content: item.content,
edit_type: item.edit_type,
priority: item.priority,
raw: item,
};
});
unreadCount.value = Number(
countRes?.count ?? res?.unread_total ?? items.length,
);
} catch (error) { } catch (error) {
console.error('获取消息列表失败:', error); console.error('获取消息列表失败:', error);
} }
}; };
getNoticeList(); getNoticeList();
// 每隔二十秒刷新一次
let intervalId = setInterval(getNoticeList, 20_000); let intervalId = setInterval(getNoticeList, 20_000);
// 封装定时器重启逻辑
const restartInterval = () => { const restartInterval = () => {
clearInterval(intervalId); clearInterval(intervalId);
getNoticeList(); // 立即获取最新数据 getNoticeList();
intervalId = setInterval(getNoticeList, 20_000); intervalId = setInterval(getNoticeList, 20_000);
}; };
// 处理跨标签页修改 const handleStorageChange = (event: StorageEvent) => {
const handleStorageChange = (event) => {
if (event.key === 'readStatus') { if (event.key === 'readStatus') {
restartInterval(); restartInterval();
} }
}; };
// 处理当前标签页修改(核心新增部分)
const handleLocalChange = () => {
restartInterval();
};
// 添加双重监听
window.addEventListener('storage', handleStorageChange); // 跨标签页监听
const originalSetItem = localStorage.setItem.bind(localStorage); const originalSetItem = localStorage.setItem.bind(localStorage);
// 拦截当前页的 localStorage.setItem 操作
localStorage.setItem = (key, value) => { localStorage.setItem = (key, value) => {
originalSetItem(key, value); originalSetItem(key, value);
if (key === 'readStatus') { if (key === 'readStatus') {
handleLocalChange(); // 手动触发当前页回调 restartInterval();
} }
}; };
// 组件卸载时清理 window.addEventListener('storage', handleStorageChange);
const stopRefreshListen = onNoticeRefresh(() => restartInterval());
onUnmounted(() => { onUnmounted(() => {
clearInterval(intervalId); clearInterval(intervalId);
window.removeEventListener('storage', handleStorageChange); window.removeEventListener('storage', handleStorageChange);
localStorage.setItem = originalSetItem; // 恢复原始方法 localStorage.setItem = originalSetItem;
stopRefreshListen();
}); });
// 消息类型定义
const messageTypes = [
{ id: -1, name: '全部消息' },
{ id: 0, name: '系统公告', icon: Megaphone },
{ id: 1, name: '系统通知', icon: Bell },
{ id: 2, name: '周报提醒', icon: CalendarClock },
];
// 根据类型ID获取类型名称
const getMessageTypeName = (typeId) => {
const type = messageTypes.find(t => t.id === typeId);
return type ? type.name : '未知类型';
};
const goNoticeList = () => { const goNoticeList = () => {
// TODO 跳转通知列表
router.push('/notice/list'); router.push('/notice/list');
}; };
// 处理消息点击 /** 从铃铛列表移除并扣减角标 */
const handleItemClick = (item) => { function removeFromBell(item: NotificationItem) {
notifications.value.forEach((value) => { notifications.value = notifications.value.filter((n) => n.id !== item.id);
if (value.id === item.id) { unreadCount.value = Math.max(0, unreadCount.value - 1);
value.status = 1; }
value.isRead = 1;
} /** 行点击:立刻取消未读并打开场景 */
const handleItemClick = async (item: NotificationItem) => {
const raw = (item as any).raw || item;
const wasUnread = !item.isRead;
item.isRead = true;
if (wasUnread) {
removeFromBell(item);
}
await openNoticeAction({
...raw,
id: item.id,
status: wasUnread ? 0 : 1,
}); });
router.push(`/notice/detail/${item.id}`);
}; };
const handleClick = async (item: NotificationItem) => {
await handleItemClick(item);
};
/** 勾选已读:只标已读,不打开详情 */
async function handleMarkRead(item: NotificationItem) {
if (item.isRead) return;
item.isRead = true;
removeFromBell(item);
try {
await readApi(Number(item.id));
emitNoticeRefresh();
} catch {
/* ignore */
}
}
const userStore = useUserStore(); const userStore = useUserStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark(); const { destroyWatermark, updateWatermark } = useWatermark();
const showDot = computed(() => const { isDark } = usePreferences();
notifications.value.some((item) => !item.isRead), const showDot = computed(() => unreadCount.value > 0);
);
const showDoctorTransfer = computed(() => {
const info = userStore.userInfo as Record<string, any> | null | undefined;
if (!info) return false;
return !!(info.doctor_id && info.store_id);
});
const menus = computed(() => [ const menus = computed(() => [
{
handler: () => {
router.push({ name: 'Profile' });
},
icon: 'lucide:user',
text: $t('page.auth.profile'),
},
{ {
handler: () => { handler: () => {
switchAccountModalRef.value?.openModal(); switchAccountModalRef.value?.openModal();
@@ -201,7 +213,20 @@ async function handleLogout() {
function handleNoticeClear() { function handleNoticeClear() {
readAllApi().then(() => { readAllApi().then(() => {
notifications.value = []; notifications.value = [];
// 成功后可以重新获取数据或者直接更新本地状态 unreadCount.value = 0;
notification.success({
message: '标记成功',
duration: 2,
});
emitNoticeRefresh();
});
}
/** 全部已读:必须走后端 API */
function handleMakeAll() {
readAllApi().then(() => {
notifications.value = [];
unreadCount.value = 0;
notification.success({ notification.success({
message: '标记成功', message: '标记成功',
duration: 2, duration: 2,
@@ -209,49 +234,100 @@ function handleNoticeClear() {
}); });
} }
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
watch( watch(
() => preferences.app.watermark, () => ({
async (enable) => { enable: preferences.app.watermark,
isDark: isDark.value,
}),
async ({ enable }) => {
if (enable) { if (enable) {
await updateWatermark({ await updateWatermark({
// 这里更改水印内容
// content: `${userStore.userInfo?.nick_name || import.meta.env.VITE_APP_TITLE}`,
content: `${userStore.userInfo?.nick_name}\r\n${userStore.userInfo?.phone || import.meta.env.VITE_APP_TITLE}`, content: `${userStore.userInfo?.nick_name}\r\n${userStore.userInfo?.phone || import.meta.env.VITE_APP_TITLE}`,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();
} }
}, },
{ { immediate: true },
immediate: true,
},
); );
</script> </script>
<template> <template>
<BasicLayout @clear-preferences-and-logout="handleLogout"> <BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #header-right-55>
<DoctorTransferFloat v-if="showDoctorTransfer" />
</template>
<template #user-dropdown> <template #user-dropdown>
<UserDropdown <!-- VIP 徽标放在头像左侧与用户下拉并排 -->
:avatar <div class="flex items-center">
:description="userStore.userInfo?.email" <HeaderVipBadge />
:menus <UserDropdown
:tag-text="userStore.userInfo?.roles?.name" :avatar
:text="userStore.userInfo?.nick_name" :menus
@logout="handleLogout" :text="userStore.userInfo?.nick_name"
/> :description="userStore.userInfo?.email"
:tag-text="userStore.userInfo?.roles?.name"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
/>
</div>
</template> </template>
<template #notification> <template #notification>
<Notification <Notification
:dot="showDot" :dot="showDot"
:count="unreadCount"
:notifications="notifications" :notifications="notifications"
@clear="handleNoticeClear" @clear="handleNoticeClear"
@make-all="handleMakeAll" @make-all="handleMakeAll"
@read="handleItemClick" @read="handleMarkRead"
@view-all="goNoticeList" @view-all="goNoticeList"
/> @on-click="handleClick"
>
<template #content="{ item }">
<span
v-if="!item.isRead"
class="absolute top-2 right-2 size-2 rounded-sm bg-primary"
></span>
<span
class="relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-xl"
:class="[(item as any).colorBg || 'bg-muted', (item as any).color]"
>
<VbenIcon
v-if="(item as any).type_icon"
:icon="(item as any).type_icon"
class="size-5"
/>
<Bell v-else class="size-5" />
</span>
<div class="min-w-0 flex-1 pr-8 leading-none">
<div class="mb-1 flex items-center gap-2">
<span
v-if="(item as any).type_name"
class="truncate text-[11px] font-semibold"
:class="(item as any).color"
>
{{ (item as any).type_name }}
</span>
</div>
<p class="truncate text-sm font-semibold text-foreground">
{{ item.title }}
</p>
<p
v-if="item.message"
class="mt-1 line-clamp-2 text-xs text-muted-foreground"
>
{{ item.message }}
</p>
<p class="mt-1 text-xs text-muted-foreground">{{ item.date }}</p>
</div>
</template>
</Notification>
<NoticeActionHost />
</template> </template>
<template #extra> <template #extra>
<AuthenticationLoginExpiredModal <AuthenticationLoginExpiredModal

View File

@@ -1,7 +1,9 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { Locale } from 'ant-design-vue/es/locale'; import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue'; import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue'; import { ref } from 'vue';
import { import {

View File

@@ -6,7 +6,9 @@
"about": "About", "about": "About",
"document": "Document", "document": "Document",
"antdv": "Ant Design Vue Version", "antdv": "Ant Design Vue Version",
"antdv-next": "Antdv Next Version",
"naive-ui": "Naive UI Version", "naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version" "element-plus": "Element Plus Version",
"tdesign": "TDesign Vue Version"
} }
} }

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