1190 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
李琦
b1df6fb4e8 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
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-23 16:55:26 +08:00
李琦
5fcb65aa0d feat:优化了门店搜索组件、优化了模态框默认的内容 2026-07-23 16:37:55 +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
李琦
bbb133476a feat:优化了门店搜索组件、优化了模态框默认的内容 2026-07-23 14:23:32 +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
李琦
700db76f16 feat:优化了订单、诊所搜索组件、药品这些交互 2026-07-23 09:24:06 +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
李琦
ee4490a91c feat:优化了订单、药品这些交互 2026-07-21 07:36:52 +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
陆伯言
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
李琦
7656c3ac49 feat:优化在线问诊的患者选药显示信息,并且优化了交互,患者挂号卡片可以直接把药品导入至处方 2026-07-19 16:10:55 +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
李琦
c1b71142df fix:
1. 优化了审方pc的效果
2. 就诊人信息优化
2026-07-18 17:08:37 +08:00
李琦
92149f43a6 fix:
1. 优化了审方pc的效果
2. 就诊人信息优化
2026-07-18 14:16:25 +08:00
李琦
47a829c1b1 1. 配送仓库 2026-07-18 10:22:36 +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
李琦
5ff6f0d837 1. 修复了接诊页面的剂量、天数输入框过窄 2026-07-15 08:46:37 +08:00
李琦
8eca0bd84d 1. 修复了接诊页面的剂量、天数输入框过窄 2026-07-15 08:46:31 +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
1858 changed files with 128774 additions and 24303 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

@@ -107,6 +107,37 @@ alwaysApply: true
- 上一部分代码和下一部分代码中间空行不超过 2 行 - 上一部分代码和下一部分代码中间空行不超过 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 组件使用规范 ## Vben 组件使用规范
### VbenModal来自 @vben/common-ui ### VbenModal来自 @vben/common-ui
@@ -145,6 +176,7 @@ alwaysApply: true
- 表单必须用 `useVbenForm`,禁止直接 `<form>` 或 antd `Form` - 表单必须用 `useVbenForm`,禁止直接 `<form>` 或 antd `Form`
- 动态修改 schema 必须用 `formApi.updateSchema([...])`**vben form 没有 `setComponentProps` 这个方法**(这是已踩过的坑) - 动态修改 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.getValues()` / `formApi.setValues(obj)`,禁止自己 `v-model` 收集
- 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()` - 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()`
- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册 - 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册
@@ -152,82 +184,3 @@ alwaysApply: true
- 密码/敏感字段用 `VbenInputPassword` 组件,禁止用 `VbenInput` 配 `type="password"` - 密码/敏感字段用 `VbenInputPassword` 组件,禁止用 `VbenInput` 配 `type="password"`
- 单选/多选选项来自接口时,必须在弹窗/页面 `onMounted` 或 `onOpenChange` 里拉取后通过 `updateSchema` 注入,禁止写死常量 - 单选/多选选项来自接口时,必须在弹窗/页面 `onMounted` 或 `onOpenChange` 里拉取后通过 `updateSchema` 注入,禁止写死常量
# 专项模块规范
## OA 通知模块前端规则
### 组件与封装复用
- 所有 OA 配置入口(系统配置 tab、CRUD 页面、员工 OA 账号弹窗)必须复用 `Tabs.TabPane`、`useVbenVxeGrid`、`useVbenModal`、`useVbenForm`,禁止用原生 antd 表格/表单
### 平台动态化(禁止写死)
- **平台列表必须从接口 `oa-platform/list` 动态拉取**,禁止在前端写死 `['work_wechat', 'dingtalk', 'feishu']` 常量数组
- 系统配置 tab 中的平台开关行、机器人表单中的平台下拉、场景表单中的机器人分组,都要走接口动态渲染
- 平台编码统一为字符串 snake_case如 `work_wechat`、`dingtalk`、`feishu`),与后端 `xk_oa_platform.platform_code` 字段对齐
### 系统配置 tab 交互
- 系统配置 tab 中的「总开关关闭」必须禁用下方所有平台子开关(视觉上置灰),禁止总开关关闭时仍能切换子开关
- 平台子开关切换必须立即调 `oa-platform/update-enabled` 接口,**禁止依赖保存按钮批量提交**(避免总开关与子开关状态不一致)
- 系统配置页的总开关走现有 `getSystemConfigList` / `saveSystemConfig` 接口config_key = `oa_notify_enabled`**禁止新建专用接口**
### 表单字段约定
- @ 人配置必须按平台分组(用 Tabs 或 Collapse 分组),员工必须从 `xk_admin` 表选择(复用员工 picker 组件)
- OA 机器人表单中的 webhook、secret 字段必须用密码型输入框(`InputPassword`),禁止明文展示
- OA 场景表单中的机器人多选必须按平台分组展示(用 `Tabs` 或 `Collapse` 分组)
### 消息类型全量支持(数据库驱动 schema
- 场景弹窗每个平台 Tab 内的消息类型选项**必须**通过 `GET /oa-scene/message-types` 接口动态拉取,
按 `platform_code` 过滤当前平台支持的所有 `message_type`
- 每个消息类型对应的 payload 表单**必须**按后端返回的 `payload_schema`JSON 字符串)动态渲染,
禁止前端写死 schema数据库可启停类型前端硬编码会不同步
- payload 字段渲染规则(与后端 `payload_schema` 约定):
- `Textarea`:多行文本
- `Input` / `InputNumber`:单行文本/数字
- `RadioGroup` / `Select`:选项组
- `GalleryPickLink`:素材库选择器,透传 `props.acceptTypes: number[]` 过滤文件类型
- `TemplateCardEditor`:企微模板卡片专用编辑器(用 `views/system/oa-scene/components/template-card-form.vue`
- 提交结构:`message_config: { platform_code: { message_type, payload } }`
### image-gallery-picker 文件类型过滤
- `image-gallery-picker.vue` 和 `gallery-pick-link.vue` 都支持 `acceptTypes?: number[]` prop
- 取值对应 `xk_file_type.value`1=图片、2=视频、3=音频、6=文件 等
- 不传 `acceptTypes` 时显示全部类型(默认行为,保持向后兼容)
- 传数组时只展示匹配类型 tab且自动锁定到第一个匹配类型
- OA 场景中图片字段必须传 `[1]`、语音字段传 `[3]`、文件字段传 `[6]`,避免用户选错类型
### 通用拖拽可视化编辑器(`components/oa-template-card-editor/`
- 任何「左侧组件库 → 右侧画布拖拽组装 → 自动生成 JSON」的需求**必须复用此通用组件**,禁止重复造轮子
- schema 驱动:新增块类型只需扩展 `BlockSchema[]` 配置,禁止改组件内部
- SortableJS 用法参照 `views/system/role/components/quick-nav-transfer.vue`(动态 import `sortablejs/modular/sortable.complete.esm.js`
- 双向绑定走 `v-model``modelValue` + `update:modelValue`),禁止内部直接修改 props
- OA 场景中的企微模板卡片必须用 `views/system/oa-scene/components/template-card-form.vue`(在通用编辑器之上做 OA 封装)
### 测试发送(全类型循环)
- OA 机器人 modal 的「测试发送」必须循环跑该平台支持的所有消息类型(单平台最多 8 种)
- 后端返回 `results` 数组,前端必须遍历分类型展示成功/失败(不要只展示第一条)
- 测试按钮 loading 用独立状态(不要复用 modal 的 `confirmLoading`
- 测试发送结果以列表形式展示在表单下方,每种类型显示状态/耗时/错误信息
- 测试发送支持两套入口(共用 `testSendOaRobot` API
- **表单弹窗内测试**modal.vue用于「保存前验证密钥」直接传 webhook_url/secret不带 id
- **行级测试**(独立组件 `test-send-modal.vue`):用于列表中已有机器人,仅传 `id`,后端自动取已加密密钥
- 行级测试弹窗入参:`test_content` + `test_at_all` 开关 + `test_mobiles` 手机号多选
- 飞书 webhook 不支持手机号 @,前端必须在飞书平台下隐藏手机号字段并提示用户(仅 @all 有效)
### 群聊绑定(按平台过滤的 N:N 多选)
- 「OA群聊管理」是独立菜单pid=232 OA 通知父级下,菜单 id=233路由 `/system/oa-chat`
- 群聊管理页结构必须与 `oa-robot` 一致:`index.vue + api/index.ts + config/{table,search,form}.ts + components/modal.vue`
- 机器人表单中的「绑定群聊」字段必须用 `Checkbox.Group`
- 群聊列表通过 `getOaChatListByPlatform()` 一次性拉取全量(按平台分组的对象)
- 前端按当前 `platform_code` 过滤展示(平台切换时自动过滤掉不属于当前平台的勾选)
- 平台切换通过 `componentProps.onChange` 监听vben form 字段值变化无现成 watch必须通过 schema 注入 onChange 回调)
- 编辑场景下,机器人详情接口返回 `chat_ids: number[]`,前端 `selectedChatIds` 直接回显
- 提交时把 `chat_ids` 数组合并到 payload后端 `OaRobotController::notRequest` 已包含该字段)
- 列表展示用后端拼接好的 `chat_names` 字符串(按「、」拼接),详情接口才返回 `chat_ids` 数组

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

@@ -21,3 +21,4 @@ VITE_ARCHIVER=true
# WebSocket 连接地址 # WebSocket 连接地址
VITE_WS_URL=wss://api.ws.g.xiaokang88.com/ws VITE_WS_URL=wss://api.ws.g.xiaokang88.com/ws
# VITE_WS_URL=wss://xk.ws.nailaoyun.cn/ws

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,7 +622,8 @@ export type ComponentType =
| 'DatePicker' | 'DatePicker'
| 'DefaultButton' | 'DefaultButton'
| 'Divider' | 'Divider'
| 'GalleryPickLink' // OA 场景表单的图片/文件素材选择(透传 acceptTypes 过滤类型) /** OA 场景表单的图片/文件素材选择(透传 acceptTypes 过滤类型) */
| 'GalleryPickLink'
| 'IconPicker' | 'IconPicker'
| 'Input' | 'Input'
| 'InputNumber' | 'InputNumber'
@@ -82,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,
@@ -156,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

@@ -6,19 +6,20 @@
* - 支持中药/西药类型切换 * - 支持中药/西药类型切换
* - 下拉选项展示:药品图片、名称、供应商、规格、价格 * - 下拉选项展示:药品图片、名称、供应商、规格、价格
* - 选择时返回完整药品数据(包含默认用法字段) * - 选择时返回完整药品数据(包含默认用法字段)
* - isCard单选确认场景如仓绑药选中后以内嵌卡片展示已选药品
* @author 系统 * @author 系统
* @date 2024 * @date 2024
*/ */
import { ref, watch, onMounted, onUnmounted } from 'vue'; import { ref, watch, onMounted, onUnmounted } from 'vue';
import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue'; import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { Input, Spin } from 'ant-design-vue'; import { Button, Input, Spin } from 'ant-design-vue';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api'; import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
/** 中药无图时与仓库列表一致的占位图 */ /** 无图时占位(中药/西药等统一兜底,避免仓绑药下拉只剩文字) */
const TCM_PLACEHOLDER_IMAGE = const DRUG_PLACEHOLDER_IMAGE =
'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg'; 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg';
// ==================== Props 定义 ==================== // ==================== Props 定义 ====================
@@ -29,12 +30,17 @@ interface Props {
* 1-中药2-西药 * 1-中药2-西药
*/ */
type?: number; type?: number;
/**
* 多药品类型(优先于 type有值时支持空关键词拉全量
* 例如 [2,4] 表示西药+中成药
*/
types?: number[];
/** /**
* 占位提示文本 * 占位提示文本
*/ */
placeholder?: string; placeholder?: string;
/** /**
* 诊所ID * 诊所ID(接诊/开方必传;仓药绑定等平台场景可省略,默认 0 走主库 types 检索)
*/ */
storeId?: number; storeId?: number;
/** /**
@@ -45,14 +51,21 @@ interface Props {
* 是否禁用 * 是否禁用
*/ */
disabled?: boolean; disabled?: boolean;
/**
* 是否以卡片展示已选药品(仓绑药等单选确认场景;开方默认 false
*/
isCard?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
type: 1, type: 1,
types: () => [],
placeholder: '输入药品名称搜索', placeholder: '输入药品名称搜索',
storeId: 2, // 默认 0平台无门店诊所场景由调用方传 :store-id
storeId: 0,
registerId: undefined, registerId: undefined,
disabled: false, disabled: false,
isCard: false,
}); });
// ==================== Emits 定义 ==================== // ==================== Emits 定义 ====================
@@ -97,10 +110,21 @@ const containerRef = ref<HTMLElement | null>(null);
*/ */
const highlightIndex = ref(-1); const highlightIndex = ref(-1);
/**
* isCard 模式下已选药品(用于输入框下方卡片)
*/
const selectedDrug = ref<any | null>(null);
/**
* 搜索框引用,便于「重新选择」时聚焦
*/
const inputRef = ref<{ focus?: () => void } | null>(null);
/**
* 解析下拉展示图:优先真图,无图用统一占位
*/
function resolveDropdownImage(item: any): string { function resolveDropdownImage(item: any): string {
if (item._image) return item._image; return item._image || DRUG_PLACEHOLDER_IMAGE;
if (props.type === 1) return TCM_PLACEHOLDER_IMAGE;
return '';
} }
// ==================== 方法定义 ==================== // ==================== 方法定义 ====================
@@ -108,10 +132,12 @@ function resolveDropdownImage(item: any): string {
/** /**
* 搜索药品 * 搜索药品
* @param keyword 搜索关键词 * @param keyword 搜索关键词
* @description 调用后端API搜索药品支持按名称和拼音搜索 * @description 调用后端API搜索药品支持按名称和拼音搜索
* 传入 types 时允许空关键词拉取该类型全量列表(用于下拉展开)
*/ */
const searchDrugs = debounce(async (keyword: string) => { const searchDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) { // 单类型模式:空关键词清空;多类型模式:空关键词仍请求后端拉全量
if ((!keyword || keyword.length < 1) && props.types.length === 0) {
searchResults.value = []; searchResults.value = [];
showDropdown.value = false; showDropdown.value = false;
return; return;
@@ -121,37 +147,43 @@ const searchDrugs = debounce(async (keyword: string) => {
showDropdown.value = true; showDropdown.value = true;
try { try {
// types 优先:有多类型时只传 types不传 type避免后端被单 type 覆盖
const typeParams =
props.types.length > 0
? { types: props.types }
: { type: props.type };
const res = await getProductListDoctorReception({ const res = await getProductListDoctorReception({
name: keyword, name: keyword || '',
type: props.type, ...typeParams,
store_id: props.storeId, store_id: props.storeId,
...(props.registerId ? { register_id: props.registerId } : {}), ...(props.registerId ? { register_id: props.registerId } : {}),
}); });
// 处理返回数据 // 处理返回数据(兼容门店 relation 与平台主库打平结构)
if (Array.isArray(res)) { if (Array.isArray(res)) {
searchResults.value = res.map((item: any) => ({ searchResults.value = res.map((item: any) => {
// 保留原始数据 const drug = item.drug || {};
...item, return {
// 提取常用字段便于访问 ...item,
_id: item.id, _id: item.id,
_drugId: item.drug_id || item.drug?.id, _drugId: item.drug_id || drug.id || item.id,
_drugName: item.drug?.drug_name || item.drug_name || '', _drugName: drug.drug_name || item.drug_name || '',
_image: item.drug?.image || '', _image: drug.image || item.image || '',
_specification: item.drug?.specification || '', _specification: drug.specification || item.specification || '',
_supplier: item.drug?.supplier?.name || '', _supplier: drug.supplier?.name || item.supplier?.name || '',
_price: item.price || 0, _price: Number(item.price ?? 0),
// 默认用法字段 // 已绑定配送仓:列表展示「多仓」标识
_timeId: item.drug?.time_id || 0, _hasDeliveryWarehouse: Number(item.has_delivery_warehouse ?? 0) === 1,
_typeId: item.drug?.type_id || 0, _timeId: drug.time_id || 0,
_frequencyId: item.drug?.frequency_id || 0, _typeId: drug.type_id || 0,
_unitId: item.drug?.unit_id || 0, _frequencyId: drug.frequency_id || 0,
_number: item.drug?.number || 1, _unitId: drug.unit_id || 0,
// 用法名称 _number: drug.number || 1,
_useNum: item.drug?.useNum, _useNum: drug.useNum,
_useType: item.drug?.useType, _useType: drug.useType,
_useFrequency: item.drug?.useFrequency, _useFrequency: drug.useFrequency,
})); };
});
} else { } else {
searchResults.value = []; searchResults.value = [];
} }
@@ -176,8 +208,14 @@ function handleInput(e: Event) {
/** /**
* 处理输入框获得焦点 * 处理输入框获得焦点
* @description 多类型模式聚焦即拉取列表并展开;单类型模式仅在已有结果时展示下拉
*/ */
function handleFocus() { function handleFocus() {
if (props.types.length > 0) {
searchDrugs('');
showDropdown.value = true;
return;
}
if (searchResults.value.length > 0) { if (searchResults.value.length > 0) {
showDropdown.value = true; showDropdown.value = true;
} }
@@ -186,15 +224,36 @@ function handleFocus() {
/** /**
* 处理选中药品 * 处理选中药品
* @param drug 选中的药品数据 * @param drug 选中的药品数据
* @description isCard 时保留 selectedDrug 用于卡片展示;否则清空选中态
*/ */
function handleSelectDrug(drug: any) { function handleSelectDrug(drug: any) {
emit('select', drug); emit('select', drug);
// 清空搜索状态
searchKeyword.value = ''; searchKeyword.value = '';
searchResults.value = []; searchResults.value = [];
showDropdown.value = false; showDropdown.value = false;
highlightIndex.value = -1; highlightIndex.value = -1;
if (props.isCard) {
selectedDrug.value = drug;
} else {
selectedDrug.value = null;
}
}
/**
* 清空已选卡片并重新搜索(仅 isCard
* 同步 emit null便于父级清空 drug_id
*/
function handleReselect() {
selectedDrug.value = null;
searchKeyword.value = '';
searchResults.value = [];
showDropdown.value = false;
highlightIndex.value = -1;
emit('select', null);
// 下一帧聚焦搜索框,方便重新选药
setTimeout(() => {
inputRef.value?.focus?.();
}, 0);
} }
/** /**
@@ -261,6 +320,7 @@ watch(
searchResults.value = []; searchResults.value = [];
showDropdown.value = false; showDropdown.value = false;
searchKeyword.value = ''; searchKeyword.value = '';
selectedDrug.value = null;
}, },
); );
</script> </script>
@@ -269,6 +329,7 @@ watch(
<div ref="containerRef" class="drug-search-select"> <div ref="containerRef" class="drug-search-select">
<!-- 搜索输入框 --> <!-- 搜索输入框 -->
<Input <Input
ref="inputRef"
v-model:value="searchKeyword" v-model:value="searchKeyword"
:placeholder="placeholder" :placeholder="placeholder"
:disabled="disabled" :disabled="disabled"
@@ -283,6 +344,49 @@ watch(
</template> </template>
</Input> </Input>
<!-- isCard已选药品卡片与下拉项同结构带边框区分 -->
<div
v-if="isCard && selectedDrug"
class="drug-item drug-item--selected-card"
>
<div class="drug-item__image">
<img
:src="resolveDropdownImage(selectedDrug)"
alt=""
class="drug-item__img"
@error="
(e) => ((e.target as HTMLImageElement).src = DRUG_PLACEHOLDER_IMAGE)
"
/>
</div>
<div class="drug-item__info">
<div class="drug-item__name">{{ selectedDrug._drugName }}</div>
<div class="drug-item__meta">
<span class="drug-item__id">ID{{ selectedDrug._drugId }}</span>
<span v-if="selectedDrug._specification" class="drug-item__spec">
规格{{ selectedDrug._specification }}
</span>
<span v-if="selectedDrug._supplier" class="drug-item__supplier">
供应商{{ selectedDrug._supplier }}
</span>
</div>
</div>
<div class="drug-item__price">
<template v-if="Number(selectedDrug._price) > 0">
¥{{ Number(selectedDrug._price).toFixed(2) }}
</template>
<span v-else class="drug-item__price--empty">暂无</span>
</div>
<Button
type="link"
size="small"
class="drug-item__reselect"
@click="handleReselect"
>
重新选择
</Button>
</div>
<!-- 下拉列表 --> <!-- 下拉列表 -->
<div v-if="showDropdown" class="drug-dropdown"> <div v-if="showDropdown" class="drug-dropdown">
<!-- 加载中 --> <!-- 加载中 -->
@@ -321,12 +425,14 @@ watch(
<div v-else class="drug-item__img-placeholder">无图</div> <div v-else class="drug-item__img-placeholder">无图</div>
</div> </div>
<!-- 药品信息 --> <!-- 药品信息药名 / 多仓 / ID / 规格 / 供应商 -->
<div class="drug-item__info"> <div class="drug-item__info">
<!-- 药品名称 --> <div class="drug-item__name-row">
<div class="drug-item__name">{{ item._drugName }}</div> <div class="drug-item__name">{{ item._drugName }}</div>
<!-- 规格和供应商 --> <span v-if="item._hasDeliveryWarehouse" class="drug-item__multi-wh">多仓</span>
</div>
<div class="drug-item__meta"> <div class="drug-item__meta">
<span class="drug-item__id">ID{{ item._drugId }}</span>
<span v-if="item._specification" class="drug-item__spec"> <span v-if="item._specification" class="drug-item__spec">
规格{{ item._specification }} 规格{{ item._specification }}
</span> </span>
@@ -336,9 +442,12 @@ watch(
</div> </div>
</div> </div>
<!-- 价格 --> <!-- 价格无价平台主库常见展示暂无 -->
<div class="drug-item__price"> <div class="drug-item__price">
¥{{ Number(item._price).toFixed(2) }} <template v-if="Number(item._price) > 0">
¥{{ Number(item._price).toFixed(2) }}
</template>
<span v-else class="drug-item__price--empty">暂无</span>
</div> </div>
</div> </div>
</div> </div>
@@ -347,6 +456,7 @@ watch(
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 只用框架主题变量,随亮暗/主题色自动适配,不写 .dark 硬编码覆盖 */
.drug-search-select { .drug-search-select {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -359,8 +469,8 @@ watch(
right: 0; right: 0;
z-index: 1050; z-index: 1050;
margin-top: 4px; margin-top: 4px;
background: #fff; background: hsl(var(--card));
border: 1px solid #e5e7eb; border: 1px solid hsl(var(--border));
border-radius: 8px; border-radius: 8px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05); box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
max-height: 400px; max-height: 400px;
@@ -373,7 +483,7 @@ watch(
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
padding: 24px; padding: 24px;
color: #999; color: hsl(var(--muted-foreground));
font-size: 14px; font-size: 14px;
} }
@@ -387,7 +497,7 @@ watch(
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #d9d9d9; background: hsl(var(--border));
border-radius: 3px; border-radius: 3px;
} }
@@ -407,7 +517,25 @@ watch(
&:hover, &:hover,
&--active { &--active {
background-color: #f5f7fa; background-color: hsl(var(--accent-hover));
}
/* 已选确认卡片:固定展示,非下拉浮层 */
&--selected-card {
margin-top: 8px;
cursor: default;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 8px;
&:hover {
background: hsl(var(--muted) / 0.3);
}
}
&__reselect {
flex-shrink: 0;
padding: 0 4px;
} }
&__image { &__image {
@@ -421,7 +549,7 @@ watch(
height: 48px; height: 48px;
object-fit: cover; object-fit: cover;
border-radius: 6px; border-radius: 6px;
border: 1px solid #f0f0f0; border: 1px solid hsl(var(--border));
} }
&__img-placeholder { &__img-placeholder {
@@ -430,10 +558,10 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #f5f5f5; background: hsl(var(--accent));
border-radius: 6px; border-radius: 6px;
font-size: 12px; font-size: 12px;
color: #bbb; color: hsl(var(--muted-foreground));
} }
&__info { &__info {
@@ -442,14 +570,35 @@ watch(
overflow: hidden; overflow: hidden;
} }
&__name-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
min-width: 0;
}
&__name { &__name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #333; color: hsl(var(--foreground));
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-bottom: 4px; min-width: 0;
}
/* 配送仓绑定药品标识:小号胶囊,不抢主信息 */
&__multi-wh {
flex-shrink: 0;
padding: 0 6px;
height: 18px;
line-height: 16px;
font-size: 11px;
color: #2b6de5;
background: rgba(43, 109, 229, 0.08);
border: 1px solid rgba(43, 109, 229, 0.45);
border-radius: 999px;
} }
&__meta { &__meta {
@@ -457,57 +606,31 @@ watch(
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
font-size: 12px; font-size: 12px;
color: #999; color: hsl(var(--muted-foreground));
}
&__id {
color: hsl(var(--muted-foreground));
} }
&__spec { &__spec {
color: #666; color: hsl(var(--muted-foreground));
} }
&__supplier { &__supplier {
color: #1890ff; color: hsl(var(--primary));
} }
&__price { &__price {
flex-shrink: 0; flex-shrink: 0;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #ff4d4f; color: hsl(var(--destructive));
}
}
/* 暗色模式适配 */ &--empty {
.dark { font-size: 13px;
.drug-dropdown { font-weight: 400;
background: #1f2937; color: hsl(var(--muted-foreground));
border-color: #374151;
}
.drug-item {
&:hover,
&--active {
background-color: #374151;
}
&__img-placeholder {
background: #374151;
color: #6b7280;
}
&__name {
color: #f3f4f6;
}
&__meta {
color: #9ca3af;
}
&__spec {
color: #d1d5db;
}
&__supplier {
color: #60a5fa;
} }
} }
} }

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

@@ -4,6 +4,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { Empty, Input, Popover, Spin } from 'ant-design-vue'; import { Empty, Input, Popover, Spin } from 'ant-design-vue';
import { matchExpressByTrackingNo } from '#/utils/matchExpressByTrackingNo';
import { getExpressCompaniesOption } from '#/views/business/express/express-company/api'; import { getExpressCompaniesOption } from '#/views/business/express/express-company/api';
defineOptions({ defineOptions({
@@ -12,9 +13,11 @@ defineOptions({
}); });
const props = defineProps<{ const props = defineProps<{
value?: string;
disabled?: boolean; disabled?: boolean;
placeholder?: string; placeholder?: string;
/** 运单号:变化时按前缀自动匹配快递公司(用户手动改选后不再覆盖) */
trackingNo?: string;
value?: string;
}>(); }>();
const emits = defineEmits<{ const emits = defineEmits<{
@@ -24,9 +27,9 @@ const emits = defineEmits<{
const mValue = useVModel(props, 'value', emits, { passive: true }); const mValue = useVModel(props, 'value', emits, { passive: true });
type ExpressCompanyOption = { type ExpressCompanyOption = {
code: string;
id: number; id: number;
name: string; name: string;
code: string;
}; };
const open = ref(false); const open = ref(false);
@@ -34,8 +37,13 @@ const loading = ref(false);
const searchKeyword = ref(''); const searchKeyword = ref('');
const options = ref<ExpressCompanyOption[]>([]); const options = ref<ExpressCompanyOption[]>([]);
const searchInputRef = ref<InstanceType<typeof Input> | null>(null); const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
/** 用户是否已手动选择/清空;为 true 时不再按单号自动覆盖 */
const userPicked = ref(false);
function filterExpressCompanyOptions(keyword: string, list: ExpressCompanyOption[]) { function filterExpressCompanyOptions(
keyword: string,
list: ExpressCompanyOption[],
) {
const q = keyword.trim().toLowerCase(); const q = keyword.trim().toLowerCase();
if (!q) return list; if (!q) return list;
return list.filter((item) => { return list.filter((item) => {
@@ -56,20 +64,39 @@ const selectedOption = computed(() =>
const displayText = computed(() => { const displayText = computed(() => {
const item = selectedOption.value; const item = selectedOption.value;
if (!item) return ''; if (!item) return '';
return item.code ? `${item.name || '-'}${item.code}` : (item.name || '-'); return item.code ? `${item.name || '-'}${item.code}` : item.name || '-';
}); });
/**
* 根据 trackingNo 自动选中快递公司
* 规则:尚未手动改选,或当前选中仍与规则结果一致时才覆盖
*/
function tryAutoMatchByTrackingNo() {
if (options.value.length === 0) return;
const matched = matchExpressByTrackingNo(props.trackingNo, options.value);
if (!matched?.code) return;
const matchedCode = matched.code;
if (userPicked.value && mValue.value && mValue.value !== matchedCode) {
// 用户已选其他公司,不覆盖
return;
}
if (mValue.value === matchedCode) return;
mValue.value = matchedCode;
}
async function loadOptions() { async function loadOptions() {
loading.value = true; loading.value = true;
try { try {
const res = await getExpressCompaniesOption({}); const res = await getExpressCompaniesOption({});
options.value = Array.isArray(res) ? res : []; options.value = Array.isArray(res) ? res : [];
tryAutoMatchByTrackingNo();
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
function selectOption(item: ExpressCompanyOption) { function selectOption(item: ExpressCompanyOption) {
userPicked.value = true;
mValue.value = item.code; mValue.value = item.code;
open.value = false; open.value = false;
searchKeyword.value = ''; searchKeyword.value = '';
@@ -77,6 +104,7 @@ function selectOption(item: ExpressCompanyOption) {
function clearSelection(e: Event) { function clearSelection(e: Event) {
e.stopPropagation(); e.stopPropagation();
userPicked.value = true;
mValue.value = undefined; mValue.value = undefined;
} }
@@ -85,7 +113,7 @@ function onOpenChange(next: boolean) {
open.value = next; open.value = next;
if (next) { if (next) {
searchKeyword.value = ''; searchKeyword.value = '';
if (!options.value.length) { if (options.value.length === 0) {
loadOptions(); loadOptions();
} }
nextTick(() => searchInputRef.value?.focus?.()); nextTick(() => searchInputRef.value?.focus?.());
@@ -99,6 +127,14 @@ watch(
}, },
); );
watch(
() => props.trackingNo,
() => {
// 单号变化时允许再自动匹配(除非用户已改选为不一致公司)
tryAutoMatchByTrackingNo();
},
);
onMounted(() => { onMounted(() => {
loadOptions(); loadOptions();
}); });
@@ -107,9 +143,9 @@ onMounted(() => {
<template> <template>
<Popover <Popover
:open="open" :open="open"
trigger="click"
placement="bottomLeft"
overlay-class-name="express-company-select-popover" overlay-class-name="express-company-select-popover"
placement="bottomLeft"
trigger="click"
@open-change="onOpenChange" @open-change="onOpenChange"
> >
<template #content> <template #content>
@@ -118,17 +154,17 @@ onMounted(() => {
ref="searchInputRef" ref="searchInputRef"
v-model:value="searchKeyword" v-model:value="searchKeyword"
allow-clear allow-clear
placeholder="搜索公司名称或编码"
class="express-search" class="express-search"
placeholder="搜索公司名称或编码"
/> />
<Spin :spinning="loading"> <Spin :spinning="loading">
<div v-if="filteredOptions.length" class="express-list"> <div v-if="filteredOptions.length > 0" class="express-list">
<button <button
v-for="item in filteredOptions" v-for="item in filteredOptions"
:key="item.id" :key="item.id"
type="button"
class="express-item"
:class="{ active: item.code === mValue }" :class="{ active: item.code === mValue }"
class="express-item"
type="button"
@click="selectOption(item)" @click="selectOption(item)"
> >
<div class="express-meta"> <div class="express-meta">
@@ -137,20 +173,20 @@ onMounted(() => {
</div> </div>
</button> </button>
</div> </div>
<Empty v-else description="无匹配快递公司" class="express-empty" /> <Empty v-else class="express-empty" description="无匹配快递公司" />
</Spin> </Spin>
</div> </div>
</template> </template>
<div <div
:class="{ disabled, placeholder: !displayText }"
class="express-trigger" class="express-trigger"
:class="{ disabled: disabled, placeholder: !displayText }"
> >
<Input <Input
:value="displayText"
readonly
:disabled="disabled" :disabled="disabled"
:placeholder="placeholder || '请选择快递公司'" :placeholder="placeholder || '请选择快递公司'"
:value="displayText"
class="express-trigger-input" class="express-trigger-input"
readonly
> >
<template v-if="displayText && !disabled" #suffix> <template v-if="displayText && !disabled" #suffix>
<span class="clear-btn" @click="clearSelection">×</span> <span class="clear-btn" @click="clearSelection">×</span>
@@ -163,6 +199,8 @@ onMounted(() => {
<style scoped> <style scoped>
.express-trigger { .express-trigger {
width: 100%; width: 100%;
/* min-width: fit-content; */
min-width: 250px;
cursor: pointer; cursor: pointer;
} }
.express-trigger.disabled { .express-trigger.disabled {
@@ -177,7 +215,7 @@ onMounted(() => {
.clear-btn { .clear-btn {
pointer-events: auto; pointer-events: auto;
cursor: pointer; cursor: pointer;
color: #86909c; color: hsl(var(--muted-foreground));
font-size: 16px; font-size: 16px;
line-height: 1; line-height: 1;
padding: 0 4px; padding: 0 4px;
@@ -203,10 +241,11 @@ onMounted(() => {
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
border-radius: 6px; border-radius: 6px;
color: hsl(var(--foreground));
} }
.express-item:hover, .express-item:hover,
.express-item.active { .express-item.active {
background: #f2f3f5; background: hsl(var(--accent));
} }
.express-meta { .express-meta {
min-width: 0; min-width: 0;
@@ -214,11 +253,11 @@ onMounted(() => {
} }
.express-name { .express-name {
font-size: 14px; font-size: 14px;
color: #1d2129; color: hsl(var(--foreground));
} }
.express-sub { .express-sub {
font-size: 12px; font-size: 12px;
color: #86909c; color: hsl(var(--muted-foreground));
} }
.express-empty { .express-empty {
margin: 12px 0; margin: 12px 0;
@@ -228,5 +267,8 @@ onMounted(() => {
<style> <style>
.express-company-select-popover .ant-popover-inner { .express-company-select-popover .ant-popover-inner {
padding: 12px; padding: 12px;
background: hsl(var(--card));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
} }
</style> </style>

View File

@@ -9,8 +9,8 @@ const props = withDefaults(
maxCount?: number; maxCount?: number;
disabled?: boolean; disabled?: boolean;
/** /**
* 限制可选择的文件类型(透传给 image-gallery-picker按 xk_file_type.value 过滤) * 限制可选择的文件类型(透传给 image-gallery-picker xk_file.type / xk_file_type.value 过滤)
* 例如图片:[1];文件:[6];语音:[3] * 例如图片:[0];视频:[1];文件:[6];语音:[3]
*/ */
acceptTypes?: number[]; acceptTypes?: number[];
}>(), }>(),

View File

@@ -18,7 +18,7 @@ const props = withDefaults(
* 限制可选择的文件类型(按 xk_file_type.value 过滤) * 限制可选择的文件类型(按 xk_file_type.value 过滤)
* - 不传:显示全部类型(默认行为,保持向后兼容) * - 不传:显示全部类型(默认行为,保持向后兼容)
* - 传数组:只展示指定类型 tab且自动锁定到第一个匹配的类型 * - 传数组:只展示指定类型 tab且自动锁定到第一个匹配的类型
* 例如图片:[1];图片+视频:[1, 2];文件:[6] * 例如图片:[0];图片+视频:[0, 1];文件:[6]
*/ */
acceptTypes?: number[]; acceptTypes?: number[];
}>(), }>(),
@@ -217,9 +217,9 @@ function itemIcon(item: FileGalleryItem) {
return item.type_icon || resolveTypeIcon(item.type, fileTypes.value); return item.type_icon || resolveTypeIcon(item.type, fileTypes.value);
} }
/** 图片类型展示缩略图xk_file_type1=图片) */ /** 图片类型展示缩略图xk_file.type0=图片;兼容历史错误写入的 1 */
function isImageItem(item: FileGalleryItem) { function isImageItem(item: FileGalleryItem) {
return Number(item.type) === 1 || Number(item.type) === 0; return Number(item.type) === 0 || Number(item.type) === 1;
} }
</script> </script>

View File

@@ -0,0 +1,510 @@
<script setup lang="ts">
/**
* 门店多选搜索(对齐开方关键字气泡交互)
* - Teleport + fixed 锚定,避免搜索栏 overflow 裁切导致「气泡不见」
* - 多选:点选后不关闭面板,已选项从列表剔除并以 Tag 展示
* - Enter / ↑↓ 键盘选用Esc 关闭
*/
import {
computed,
nextTick,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue';
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { useDebounceFn, useVModel } from '@vueuse/core';
import { Empty, Input, Spin, Tag } from 'ant-design-vue';
import { searchStoreOption } from '#/views/system/store/api';
defineOptions({
name: 'StoreMultiSearch',
inheritAttrs: false,
});
export type StoreSearchItem = {
id: number;
name: string;
type?: number;
shouzimu?: string;
};
const props = withDefaults(
defineProps<{
value?: number[];
/** 0诊所 1药店不传则诊所+药店 */
storeType?: 0 | 1;
placeholder?: string;
disabled?: boolean;
initialItems?: StoreSearchItem[];
}>(),
{
value: () => [],
storeType: undefined,
placeholder: '输入名称或拼音首拼搜索',
disabled: false,
initialItems: () => [],
},
);
const emits = defineEmits<{
'update:value': [value: number[]];
}>();
const mValue = useVModel(props, 'value', emits, {
passive: true,
defaultValue: [],
});
const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const searchKeyword = ref('');
const loading = ref(false);
const showDropdown = ref(false);
const highlightIndex = ref(-1);
const options = ref<StoreSearchItem[]>([]);
const selectedMap = ref<Record<number, StoreSearchItem>>({});
const panelStyle = ref<Record<string, string>>({});
const selectedIds = computed(() =>
Array.isArray(mValue.value) ? mValue.value.map(Number) : [],
);
const selectedItems = computed(() =>
selectedIds.value.map((id) => {
const cached = selectedMap.value[id];
return cached || { id, name: `门店${id}` };
}),
);
/** 下拉中过滤掉已选,方便连续多选 */
const visibleOptions = computed(() =>
options.value.filter((item) => !selectedIds.value.includes(item.id)),
);
function mergeInitialItems(items: StoreSearchItem[]) {
if (!items?.length) return;
const next = { ...selectedMap.value };
for (const item of items) {
if (item?.id) next[item.id] = item;
}
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() {
const keyword = searchKeyword.value.trim();
if (!keyword) {
options.value = [];
showDropdown.value = false;
highlightIndex.value = -1;
return;
}
loading.value = true;
await openPanel();
try {
const res = await searchStoreOption({
keyword,
...(props.storeType === 0 || props.storeType === 1
? { type: props.storeType }
: {}),
limit: 20,
});
options.value = res?.items ?? [];
highlightIndex.value = visibleOptions.value.length > 0 ? 0 : -1;
await nextTick();
updatePanelPlacement();
} catch {
options.value = [];
highlightIndex.value = -1;
} finally {
loading.value = false;
}
}
const debouncedFetch = useDebounceFn(fetchOptions, 300);
function handleInput() {
debouncedFetch();
}
function handleFocus() {
if (searchKeyword.value.trim() && options.value.length > 0) {
openPanel();
if (highlightIndex.value < 0 && visibleOptions.value.length > 0) {
highlightIndex.value = 0;
}
}
}
/**
* 多选:追加 ID保留关键词与面板不关闭气泡
*/
function selectOption(item: StoreSearchItem) {
if (selectedIds.value.includes(item.id)) return;
selectedMap.value = { ...selectedMap.value, [item.id]: item };
mValue.value = [...selectedIds.value, item.id];
// 高亮下一项(列表已剔除刚选的)
nextTick(() => {
if (visibleOptions.value.length === 0) {
highlightIndex.value = -1;
return;
}
highlightIndex.value = Math.min(
Math.max(highlightIndex.value, 0),
visibleOptions.value.length - 1,
);
updatePanelPlacement();
scrollHighlightIntoView();
});
}
function removeSelected(id: number) {
mValue.value = selectedIds.value.filter((x) => x !== id);
const next = { ...selectedMap.value };
delete next[id];
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) {
if (!showDropdown.value || visibleOptions.value.length === 0) {
if (e.key === 'Escape') closePanel();
return;
}
const len = visibleOptions.value.length;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
// 头尾循环:最后一项再按下 → 回到第一项
highlightIndex.value =
highlightIndex.value < 0 ? 0 : (highlightIndex.value + 1) % len;
scrollHighlightIntoView();
break;
case 'ArrowUp':
e.preventDefault();
// 头尾循环:第一项再按上 → 跳到最后一项
highlightIndex.value =
highlightIndex.value <= 0 ? len - 1 : highlightIndex.value - 1;
scrollHighlightIntoView();
break;
case 'Enter':
e.preventDefault();
{
const idx = highlightIndex.value >= 0 ? highlightIndex.value : 0;
const item = visibleOptions.value[idx];
if (item) selectOption(item);
}
break;
case 'Escape':
e.preventDefault();
closePanel();
break;
}
}
function handleClickOutside(e: MouseEvent) {
if (!showDropdown.value) return;
const t = e.target as Node;
if (containerRef.value?.contains(t) || panelRef.value?.contains(t)) return;
closePanel();
}
function onWinChange() {
if (showDropdown.value) updatePanelPlacement();
}
onMounted(() => {
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('resize', onWinChange);
window.addEventListener('scroll', onWinChange, true);
mergeInitialItems(props.initialItems || []);
});
onUnmounted(() => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('resize', onWinChange);
window.removeEventListener('scroll', onWinChange, true);
});
watch(
() => props.storeType,
() => {
options.value = [];
closePanel();
searchKeyword.value = '';
},
);
watch(
() => props.initialItems,
(items) => mergeInitialItems(items || []),
{ 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>
<template>
<div ref="containerRef" class="store-multi-search">
<Input
v-model:value="searchKeyword"
:placeholder="placeholder"
:disabled="disabled"
allow-clear
@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="showDropdown"
ref="panelRef"
class="store-multi-search__panel"
:style="panelStyle"
>
<div class="store-multi-search__hint">
<span> 循环切换 · Enter 选用 · Esc 关闭 · 可连续多选</span>
<span class="store-multi-search__count">
{{ visibleOptions.length }} 条可选
</span>
</div>
<div class="store-multi-search__body">
<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>
<div v-if="selectedItems.length > 0" class="store-multi-search__tags">
<Tag
v-for="item in selectedItems"
:key="item.id"
closable
color="blue"
@close="() => removeSelected(item.id)"
>
{{ item.name }}{{ item.id }}
</Tag>
</div>
</div>
</template>
<style scoped lang="scss">
.store-multi-search {
position: relative;
width: 100%;
}
.store-multi-search__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
</style>
<style lang="scss">
/* Teleport 到 body非 scoped避免气泡被裁切/丢样式 */
.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));
box-shadow:
0 8px 24px hsl(0 0% 0% / 0.12),
0 2px 8px hsl(0 0% 0% / 0.06);
}
.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 {
display: block;
width: 100%;
text-align: left;
padding: 8px 10px;
border: 0;
border-radius: 6px;
cursor: pointer;
background: transparent;
transition: background 0.15s;
}
.store-multi-search__option:hover,
.store-multi-search__option.is-active {
background: hsl(var(--accent) / 0.85);
}
.store-multi-search__option-name {
font-size: 13px;
font-weight: 500;
color: hsl(var(--foreground));
line-height: 1.4;
}
.store-multi-search__option-sub {
margin-top: 2px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
</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;
@@ -25,7 +31,8 @@ interface Props {
multiple?: boolean; multiple?: boolean;
maxCount?: number; maxCount?: number;
/** /**
* 素材库可选类型(透传 GalleryPickLink,默认仅图片 * 素材库可选类型(透传 GalleryPickLink
* xk_file.type0=图片 1=视频,默认仅图片
*/ */
acceptTypes?: number[]; acceptTypes?: number[];
} }
@@ -34,13 +41,15 @@ const props = withDefaults(defineProps<Props>(), {
modelValue: () => [], modelValue: () => [],
multiple: true, multiple: true,
maxCount: 9, maxCount: 9,
acceptTypes: () => [1], 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 = () => {
@@ -79,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);
@@ -160,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>
@@ -193,6 +261,25 @@ const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.val
@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

@@ -5,4 +5,5 @@ export type CustomComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect' | 'ApiTreeSelect'
| 'IconPicker' | 'IconPicker'
| 'StoreMultiSearch'
| 'WarehouseAdminDrugSearch'; | 'WarehouseAdminDrugSearch';

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

@@ -45,7 +45,7 @@ export interface FormFieldSchema {
required?: boolean; required?: boolean;
/** select / radio 类型的选项列表 */ /** select / radio 类型的选项列表 */
options?: { label: string; value: any }[]; options?: { label: string; value: any }[];
/** 图片字段:限定素材库文件类型(如 [1] 只显示图片) */ /** 图片字段:限定素材库文件类型(如 [0] 只显示图片) */
acceptTypes?: number[]; acceptTypes?: number[];
/** 最大长度 */ /** 最大长度 */
maxLength?: number; maxLength?: number;

View File

@@ -0,0 +1,276 @@
<script lang="ts" setup>
/**
* 处方溯源展示内容(对齐小程序药师溯源:就诊→开具→审核)
* 由审方/业务处方两个 Modal 共用,避免双份漂移
*/
import { computed } from 'vue';
import { Timeline, TimelineItem } from 'ant-design-vue';
import {
FileTextOutlined,
MedicineBoxOutlined,
ShopOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
const props = defineProps<{
data: Record<string, any> | null;
}>();
const formatTime = (timestamp: unknown) => {
if (!timestamp) return '--';
if (typeof timestamp === 'string' && timestamp.includes('-')) return timestamp;
const n = Number(timestamp);
if (!n) return '--';
return new Date(n * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
const prescriptionTypeMap: Record<number, string> = {
1: '中药处方',
2: '西药处方',
3: '保健食品',
5: '产品服务包',
6: '非药品',
7: '医疗器械',
};
const statusMap: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '已拒绝',
3: '已过期',
};
const prescriptionTypeText = computed(() => {
if (!props.data) return '--';
return prescriptionTypeMap[props.data.prescription_type] || '未知';
});
const statusText = computed(() => {
if (!props.data) return '--';
return statusMap[props.data.status] || '未知';
});
const statusColor = computed(() => {
if (!props.data) return 'gray';
const map: Record<number, string> = {
0: 'orange',
1: 'green',
2: 'red',
3: 'gray',
};
return map[props.data.status] || 'gray';
});
const expireTime = computed(() =>
props.data ? formatTime(props.data.auto_expire_time) : '--',
);
const patientSexText = computed(() => {
const sex = props.data?.user_patient?.sex;
if (sex === 1) return '男';
if (sex === 2) return '女';
return '--';
});
const doctorInfo = computed(
() => props.data?.doctor_info || props.data?.doctorInfo || null,
);
const pharmacistInfo = computed(
() => props.data?.pharmacist_info || props.data?.pharmacistInfo || null,
);
const userPatient = computed(
() => props.data?.user_patient || props.data?.userPatient || null,
);
const registerInfo = computed(
() => props.data?.register || null,
);
</script>
<template>
<div v-if="data" class="prescription-source-container">
<div class="mb-6 rounded-lg p-6 shadow-md">
<div class="mb-4 flex items-center">
<ShopOutlined class="mr-2 text-xl text-purple-500" />
<h2 class="text-xl font-bold">开具机构</h2>
</div>
<div v-if="data.store" class="grid grid-cols-1 gap-4">
<div class="info-item">
<span class="label">机构名称:</span>
<span class="value">{{ data.store.name }}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无机构信息</div>
</div>
<div class="mb-6 rounded-lg p-6 shadow-md">
<div class="mb-4 flex items-center">
<FileTextOutlined class="mr-2 text-xl text-blue-500" />
<h2 class="text-xl font-bold">时间线</h2>
</div>
<Timeline>
<TimelineItem>
<div class="font-medium">就诊</div>
<div class="text-sm text-gray-500">
{{ formatTime(registerInfo?.created_at) }}
· {{ userPatient?.name || '--' }}
</div>
<div class="mt-1 text-sm">
性别{{ patientSexText }} · 年龄{{ userPatient?.age ?? '--' }}
</div>
<div v-if="data.clinical_diagnose" class="mt-1 text-sm">
诊断{{ data.clinical_diagnose }}
</div>
</TimelineItem>
<TimelineItem>
<div class="font-medium">开具</div>
<div class="text-sm text-gray-500">{{ data.created_at || '--' }}</div>
<div class="mt-1 text-sm">
医生{{ doctorInfo?.name || '--' }} · 科室{{
doctorInfo?.depart?.name || '--'
}}
</div>
<div class="mt-1 text-sm">
处方编号{{ data.prescription_no }} · 类型{{ prescriptionTypeText }}
</div>
</TimelineItem>
<TimelineItem>
<div class="font-medium">审核</div>
<div class="text-sm text-gray-500">
{{ data.pharmacist_view_time || '--' }}
</div>
<div class="mt-1 text-sm">
状态
<span :class="`text-${statusColor}-500`">{{ statusText }}</span>
· 药师{{ pharmacistInfo?.name || '--' }}
</div>
<div v-if="data.reject_reason" class="mt-1 text-sm text-red-500">
驳回原因{{ data.reject_reason }}
</div>
<div
v-else-if="data.cancel_remark && Number(data.status) === 2"
class="mt-1 text-sm text-red-500"
>
驳回原因{{ data.cancel_remark }}
</div>
</TimelineItem>
</Timeline>
</div>
<div class="mb-6 rounded-lg p-6 shadow-md">
<div class="mb-4 flex items-center">
<FileTextOutlined class="mr-2 text-xl text-blue-500" />
<h2 class="text-xl font-bold">处方基本信息</h2>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="info-item">
<span class="label">处方编号:</span>
<span class="value">{{ data.prescription_no }}</span>
</div>
<div class="info-item">
<span class="label">处方类型:</span>
<span class="value">{{ prescriptionTypeText }}</span>
</div>
<div class="info-item">
<span class="label">处方状态:</span>
<span :class="`text-${statusColor}-500`" class="value">{{
statusText
}}</span>
</div>
<div class="info-item">
<span class="label">总金额:</span>
<span class="value font-bold text-red-500"
>¥{{ data.total_pay_price }}</span
>
</div>
<div class="info-item">
<span class="label">过期时间:</span>
<span class="value">{{ expireTime }}</span>
</div>
<div v-if="data.reject_reason" class="info-item">
<span class="label">驳回原因:</span>
<span class="value text-red-500">{{ data.reject_reason }}</span>
</div>
</div>
</div>
<div class="mb-6 rounded-lg p-6 shadow-md">
<div class="mb-4 flex items-center">
<MedicineBoxOutlined class="mr-2 text-xl text-green-500" />
<h2 class="text-xl font-bold">医生信息</h2>
</div>
<div v-if="doctorInfo" class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="info-item">
<span class="label">开具医生:</span>
<span class="value">{{ doctorInfo.name }}</span>
</div>
<div class="info-item">
<span class="label">开具科室:</span>
<span class="value">{{ doctorInfo.depart?.name || '--' }}</span>
</div>
<div class="info-item">
<span class="label">开具机构:</span>
<span class="value">{{ data.store?.name || '--' }}</span>
</div>
<div class="info-item">
<span class="label">开具时间:</span>
<span class="value">{{ data.created_at || '--' }}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无医生信息</div>
</div>
<div class="rounded-lg p-6 shadow-md">
<div class="mb-4 flex items-center">
<UserOutlined class="mr-2 text-xl text-amber-500" />
<h2 class="text-xl font-bold">患者信息</h2>
</div>
<div
v-if="userPatient"
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
>
<div class="info-item">
<span class="label">患者姓名:</span>
<span class="value">{{ userPatient.name }}</span>
</div>
<div class="info-item">
<span class="label">年龄:</span>
<span class="value">{{ userPatient.age }}</span>
</div>
<div class="info-item">
<span class="label">性别:</span>
<span class="value">{{ patientSexText }}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无患者信息</div>
</div>
</div>
<div v-else class="flex h-64 items-center justify-center">
<div
class="h-12 w-12 animate-spin rounded-full border-b-2 border-t-2 border-blue-500"
></div>
</div>
</template>
<style lang="scss" scoped>
.prescription-source-container {
@apply max-h-[70vh] overflow-auto p-4;
}
.info-item {
@apply flex flex-col rounded-md p-3;
}
.label {
@apply mb-1 text-sm text-gray-500;
}
.value {
@apply font-medium;
}
</style>

View File

@@ -0,0 +1,550 @@
<script lang="ts" setup>
/**
* 门店详情弹窗(诊所/药店)
* 布局对齐医生档案:顶部统计 + 左侧竖向 Tabs
*/
import type { Dayjs } from 'dayjs';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import dayjs from 'dayjs';
import {
Avatar,
Button,
DatePicker,
Descriptions,
Empty,
Image,
Select,
Spin,
Table,
Tabs,
Tag,
message,
} from 'ant-design-vue';
import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vue';
import { getStoreBankCardReportDetail } from '#/views/system/store-bank-card/api';
import {
getStoreCardApi,
getStoreCardStatsApi,
getStoreDrugsByTypeApi,
openQrCodeApi,
} from '#/views/system/store/api';
import { getSalespersonList } from '#/views/system/store/api/salesperson';
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 userStore = useUserStore();
// 是否允许「添加医生」:超管/管理员/省经理/市经理/业务员
const canAddDoctor = computed(() => canAddStoreDoctor(userStore.userInfo));
const storeId = ref(0);
const loading = ref(false);
const activeTab = ref('basic');
const cardData = ref<any>(null);
const searchTime = ref<[Dayjs, Dayjs]>([
dayjs().startOf('month'),
dayjs(),
]);
const bankLoading = ref(false);
const bankDetail = ref<any>(null);
const drugType = ref(2);
const drugLoading = ref(false);
const drugList = ref<any[]>([]);
const promoterLoading = ref(false);
const promoterList = ref<any[]>([]);
const generatingQr = ref(false);
const store = computed(() => cardData.value?.store ?? {});
const stats = computed(() => cardData.value?.stats_summary ?? {});
const doctorList = computed(() => cardData.value?.doctor_list ?? []);
const isClinic = computed(() => Number(store.value?.type) === 0);
const isPharmacy = computed(() => Number(store.value?.type) === 1);
/** 按门店类型区分诊所/药店文案,避免药店仍显示「诊所」 */
const storeKindLabel = computed(() => (isPharmacy.value ? '药店' : '诊所'));
const qrTabLabel = computed(() => `${storeKindLabel.value}二维码`);
const modalTitle = computed(() => {
const name = store.value?.name;
if (!name) return '门店详情';
return `${storeKindLabel.value}详情 · ${name}`;
});
const drugTypeOptions = [
{ label: '中药', value: 1 },
{ label: '西药', value: 2 },
{ label: '保健食品', value: 3 },
{ label: '产品服务包', value: 5 },
{ label: '非药品', value: 6 },
{ label: '医疗器械', value: 7 },
];
const drugColumns = [
{ title: '药品名称', dataIndex: 'drug_name', key: 'drug_name', ellipsis: true },
{ title: '规格', dataIndex: 'specification', key: 'specification', width: 140 },
{ title: '售价', dataIndex: 'price', key: 'price', width: 100 },
{ title: '进价', dataIndex: 'buy_price', key: 'buy_price', width: 100 },
];
const promoterColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
];
const [Modal, modalApi] = useVbenModal({
class: 'w-[900px] xl:w-[1100px]',
fullscreenButton: true,
footer: false,
draggable: true,
closeOnClickModal: false,
onCancel() {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const data = modalApi.getData<Record<string, any>>() || {};
storeId.value = Number(data.storeId || data.id || 0);
activeTab.value = 'basic';
searchTime.value = [dayjs().startOf('month'), dayjs()];
bankDetail.value = null;
drugList.value = [];
promoterList.value = [];
if (storeId.value) {
loadCard();
}
},
});
const [DoctorModal, DoctorModalApi] = useVbenModal({
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] {
return [
searchTime.value[0].format('YYYY-MM-DD 00:00:00'),
searchTime.value[1].format('YYYY-MM-DD 23:59:59'),
];
}
/** 加载门店卡片概览 */
async function loadCard() {
if (!storeId.value) return;
loading.value = true;
try {
const res = await getStoreCardApi({
id: storeId.value,
search_time: searchTimeParam(),
});
cardData.value = res;
if (isPharmacy.value) {
drugType.value = 2;
}
} catch (e: any) {
message.error(e?.message || '加载门店详情失败');
} finally {
loading.value = false;
}
}
/** 仅刷新统计 */
async function refreshStats() {
if (!storeId.value) return;
try {
const res = await getStoreCardStatsApi({
id: storeId.value,
search_time: searchTimeParam(),
});
if (cardData.value) {
cardData.value = {
...cardData.value,
stats_summary: res.stats_summary,
search_time: res.search_time,
};
}
} catch (e: any) {
message.error(e?.message || '刷新统计失败');
}
}
async function loadBank() {
if (!storeId.value || bankDetail.value) return;
bankLoading.value = true;
try {
bankDetail.value = await getStoreBankCardReportDetail(storeId.value);
} catch {
bankDetail.value = null;
} finally {
bankLoading.value = false;
}
}
async function loadDrugs() {
if (!storeId.value) return;
drugLoading.value = true;
try {
const res = await getStoreDrugsByTypeApi(storeId.value, drugType.value);
const list = Array.isArray(res) ? res : res?.items || res?.list || [];
drugList.value = (list || []).map((item: any) => {
const drug = item.drug || {};
return {
id: item.id,
drug_name: drug.drug_name || item.drug_name || '-',
specification: drug.specification || item.specification || '-',
price: item.price ?? '-',
buy_price: item.buy_price ?? '-',
};
});
} catch {
drugList.value = [];
} finally {
drugLoading.value = false;
}
}
async function loadPromoters() {
if (!storeId.value || promoterList.value.length > 0) return;
promoterLoading.value = true;
try {
const res = await getSalespersonList({
store_id: storeId.value,
page: 1,
pageSize: 100,
});
const list = res?.items || res?.list?.items || res || [];
promoterList.value = Array.isArray(list) ? list : [];
} catch {
promoterList.value = [];
} finally {
promoterLoading.value = false;
}
}
function onTabChange(key: string | number) {
activeTab.value = String(key);
if (key === 'bank') loadBank();
if (key === 'drugs') loadDrugs();
if (key === 'promoters') loadPromoters();
}
function openDoctor(suId: number) {
DoctorModalApi.setData({ su_id: suId, hideStoresTab: true, readonly: true });
DoctorModalApi.open();
}
async function ensureQrCode() {
if (cardData.value?.qr_code || store.value?.qr_code) return;
generatingQr.value = true;
try {
await openQrCodeApi(storeId.value);
await loadCard();
message.success('二维码已生成');
} catch (e: any) {
message.error(e?.message || '生成二维码失败');
} finally {
generatingQr.value = false;
}
}
function clinicTypeText(v: number) {
if (v === 1) return '西医诊所';
if (v === 2) return '中医诊所';
return '未设置';
}
</script>
<template>
<Modal :title="modalTitle">
<AddDoctorModal />
<Spin :spinning="loading">
<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>
<RangePicker
v-model:value="searchTime"
format="YYYY-MM-DD"
@change="refreshStats"
/>
</div>
<!-- 顶部统计 -->
<div
v-if="cardData"
class="mb-6 grid grid-cols-1 gap-4 px-1 md:grid-cols-3"
>
<div
class="relative flex flex-col overflow-hidden rounded-xl border border-blue-100 bg-blue-50 p-5 shadow-sm transition-all group hover:shadow-md dark:border-blue-800/50 dark:bg-blue-900/20"
>
<span class="text-sm font-semibold text-blue-600/80 dark:text-blue-300/80">
销售额
</span>
<div class="mt-2 text-2xl font-bold text-blue-900 dark:text-blue-100">
<span class="text-lg">¥</span>
{{ Number(stats.sales_amount ?? 0).toFixed(2) }}
</div>
</div>
<div
class="relative flex flex-col overflow-hidden rounded-xl border border-emerald-100 bg-emerald-50 p-5 shadow-sm transition-all group hover:shadow-md dark:border-emerald-800/50 dark:bg-emerald-900/20"
>
<span class="text-sm font-semibold text-emerald-600/80 dark:text-emerald-300/80">
利润
</span>
<div class="mt-2 text-2xl font-bold text-emerald-900 dark:text-emerald-100">
<span class="text-lg">¥</span>
{{ Number(stats.profit ?? 0).toFixed(2) }}
</div>
</div>
<div
class="relative flex flex-col overflow-hidden rounded-xl border border-purple-100 bg-purple-50 p-5 shadow-sm transition-all group hover:shadow-md dark:border-purple-800/50 dark:bg-purple-900/20"
>
<span class="text-sm font-semibold text-purple-600/80 dark:text-purple-300/80">
处方量
</span>
<div class="mt-2 text-2xl font-bold text-purple-900 dark:text-purple-100">
{{ stats.prescription_count ?? 0 }}
<span class="text-xs font-normal text-purple-700/60 dark:text-purple-300/60">
</span>
</div>
</div>
</div>
<Tabs
:active-key="activeTab"
tab-position="left"
class="custom-vertical-tabs min-h-[480px]"
@change="onTabChange"
>
<Tabs.TabPane key="basic" tab="门店基本信息">
<div class="pl-4">
<Descriptions bordered :column="2" size="small">
<Descriptions.Item label="名称">
{{ store.name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="ID">{{ store.id }}</Descriptions.Item>
<Descriptions.Item label="类型">
{{ isPharmacy ? '药店' : '诊所' }}
</Descriptions.Item>
<Descriptions.Item v-if="isClinic" label="诊所类型">
{{ clinicTypeText(Number(store.clinic_type)) }}
</Descriptions.Item>
<Descriptions.Item label="联系人">
{{ store.contact || '-' }}
</Descriptions.Item>
<Descriptions.Item label="手机号">
{{ store.mobile || '-' }}
</Descriptions.Item>
<Descriptions.Item label="地址" :span="2">
{{ store.position || store.address || '-' }}
</Descriptions.Item>
<Descriptions.Item label="ERP ID">
{{ store.erp_id || '-' }}
</Descriptions.Item>
<Descriptions.Item label="MES ID">
{{ store.mes_id || '-' }}
</Descriptions.Item>
<Descriptions.Item label="银行卡报备">
{{ store.bank_card_report_status_text || '-' }}
</Descriptions.Item>
<Descriptions.Item label="业务员">
{{ store.new_admin?.nick_name || '-' }}
</Descriptions.Item>
</Descriptions>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="bank" tab="银行卡">
<div class="pl-4">
<Spin :spinning="bankLoading">
<template v-if="bankDetail">
<Descriptions bordered :column="2" size="small">
<Descriptions.Item label="户名">
{{ bankDetail.bank_user_name || bankDetail.account_name || store.bank_user_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="卡号">
{{ bankDetail.bank_card || bankDetail.card_no || store.bank_card || '-' }}
</Descriptions.Item>
<Descriptions.Item label="开户行">
{{ bankDetail.bank_name || store.bank_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="联行号">
{{ bankDetail.bank_no || store.bank_no || '-' }}
</Descriptions.Item>
<Descriptions.Item label="报备状态">
{{ bankDetail.report_status_text || store.bank_card_report_status_text || '-' }}
</Descriptions.Item>
<Descriptions.Item label="账户类型">
{{ bankDetail.bank_account_type ?? store.bank_account_type ?? '-' }}
</Descriptions.Item>
</Descriptions>
</template>
<Empty v-else description="暂无银行卡报备信息,展示门店预留信息">
<Descriptions bordered :column="2" size="small" class="mt-4 text-left">
<Descriptions.Item label="户名">
{{ store.bank_user_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="卡号">
{{ store.bank_card || '-' }}
</Descriptions.Item>
<Descriptions.Item label="开户行">
{{ store.bank_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="联行号">
{{ store.bank_no || '-' }}
</Descriptions.Item>
</Descriptions>
</Empty>
</Spin>
</div>
</Tabs.TabPane>
<Tabs.TabPane v-if="isClinic" key="doctors" tab="诊所医生团队">
<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="暂无医生" />
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div
v-for="item in doctorList"
:key="item.su_id"
class="flex items-center gap-3 rounded-lg border border-gray-100 p-3 dark:border-slate-700"
>
<Avatar :src="resolveAvatarUrl(item.doctor?.avatar)" :size="48" />
<div class="min-w-0 flex-1">
<Button type="link" class="!px-0" @click="openDoctor(item.doctor?.su_id || item.su_id)">
{{ item.doctor?.name || '未知医生' }}
</Button>
<div class="text-xs text-muted-foreground">
{{ item.doctor?.mobile || '-' }}
</div>
</div>
</div>
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane v-if="isPharmacy" key="drugs" tab="药店药品">
<div class="pl-4">
<div class="mb-3 flex items-center gap-2">
<span class="text-sm text-muted-foreground">药品类型</span>
<Select
v-model:value="drugType"
:options="drugTypeOptions"
style="width: 160px"
@change="loadDrugs"
/>
</div>
<Table
:columns="drugColumns"
:data-source="drugList"
:loading="drugLoading"
:pagination="false"
row-key="id"
size="small"
:scroll="{ y: 360 }"
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="qr" :tab="qrTabLabel">
<div class="flex flex-col items-center gap-4 py-6 pl-4">
<template v-if="cardData?.qr_code || store.qr_code">
<Image
:src="cardData?.qr_code || store.qr_code"
:width="200"
:preview="true"
/>
<div class="text-sm text-muted-foreground">
{{ store.name }} {{ qrTabLabel }}
</div>
</template>
<template v-else>
<Empty :description="`尚未生成${storeKindLabel}二维码`" />
<Button type="primary" :loading="generatingQr" @click="ensureQrCode">
生成{{ storeKindLabel }}二维码
</Button>
</template>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="promoters" tab="门店推广员">
<div class="pl-4">
<Table
:columns="promoterColumns"
:data-source="promoterList"
:loading="promoterLoading"
:pagination="false"
row-key="id"
size="small"
:scroll="{ y: 360 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.name || record.nick_name || '-' }}
</template>
<template v-if="column.key === 'status'">
<Tag>{{ record.status ?? '-' }}</Tag>
</template>
</template>
</Table>
</div>
</Tabs.TabPane>
</Tabs>
</Spin>
</Modal>
<DoctorModal />
</template>
<style scoped>
:deep(.custom-vertical-tabs .ant-tabs-nav) {
width: 140px;
}
:deep(.custom-vertical-tabs .ant-tabs-tab) {
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 6px;
transition: all 0.3s;
}
:deep(.custom-vertical-tabs .ant-tabs-tab-active) {
background-color: var(
--ant-primary-color-active-deprecated-f-12,
rgba(22, 119, 255, 0.08)
);
}
</style>

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

@@ -0,0 +1,112 @@
<script lang="ts" setup>
/**
* 处方失效时间展示
* - 剩余 ≤2 小时:标红
* - 剩余 ≤10 分钟:额外显示倒计时(每秒刷新)
*/
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
const props = defineProps<{
/** 失效时间戳(秒)或已格式化的字符串 */
autoExpireTime?: number | string | null;
}>();
const nowTs = ref(Math.floor(Date.now() / 1000));
let timer: ReturnType<typeof setInterval> | null = null;
/** 解析为秒级时间戳 */
function resolveExpireTs(raw: number | string | null | undefined): number {
if (raw == null || raw === '') return 0;
if (typeof raw === 'number') {
return raw > 1e12 ? Math.floor(raw / 1000) : raw;
}
const n = Number(raw);
if (Number.isFinite(n) && n > 0) {
return n > 1e12 ? Math.floor(n / 1000) : n;
}
const parsed = Date.parse(String(raw).replace(/-/g, '/'));
return Number.isFinite(parsed) ? Math.floor(parsed / 1000) : 0;
}
const expireTs = computed(() => resolveExpireTs(props.autoExpireTime));
const remainSec = computed(() => {
if (!expireTs.value) return null;
return expireTs.value - nowTs.value;
});
const isUrgent = computed(() => {
const r = remainSec.value;
return r != null && r > 0 && r <= 2 * 3600;
});
const showCountdown = computed(() => {
const r = remainSec.value;
return r != null && r > 0 && r <= 10 * 60;
});
const displayText = computed(() => {
if (!expireTs.value) return '—';
const r = remainSec.value ?? 0;
if (r <= 0) return '已过期';
const d = new Date(expireTs.value * 1000);
const pad = (n: number) => String(n).padStart(2, '0');
const base = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
if (showCountdown.value) {
const m = Math.floor(r / 60);
const s = r % 60;
return `${base}(剩余 ${m}:${pad(s)}`;
}
return base;
});
function startTimer() {
stopTimer();
timer = setInterval(() => {
nowTs.value = Math.floor(Date.now() / 1000);
}, 1000);
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
watch(
() => props.autoExpireTime,
() => {
nowTs.value = Math.floor(Date.now() / 1000);
},
);
onMounted(startTimer);
onBeforeUnmount(stopTimer);
</script>
<template>
<span
class="expire-time"
:class="{
'expire-time--urgent': isUrgent || (remainSec != null && remainSec <= 0),
'expire-time--countdown': showCountdown,
}"
>
{{ displayText }}
</span>
</template>
<style scoped>
.expire-time {
font-size: 12px;
color: inherit;
}
.expire-time--urgent {
color: #cf1322;
font-weight: 600;
}
.expire-time--countdown {
font-variant-numeric: tabular-nums;
}
</style>

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
/**
* 就诊类型标签:线下就诊 / 在线问诊 / 在线复诊
* 对应处方、订单字段 is_online
*/
import { computed } from 'vue';
import { Tag } from 'ant-design-vue';
const props = defineProps<{
/** 就诊渠道0线下 1在线问诊 2/3在线复诊 */
isOnline?: number | null;
}>();
const label = computed(() => {
const v = Number(props.isOnline ?? 0);
if (v === 1) return { text: '在线问诊', color: 'blue' };
if (v === 2 || v === 3) return { text: '在线复诊', color: 'green' };
return { text: '线下就诊', color: 'purple' };
});
</script>
<template>
<Tag :color="label.color">{{ label.text }}</Tag>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
/**
* 列表/卡片内紧凑展示:小程序用户 + 就诊人
* 点击后由父级打开 WxUserPatientDrawer
*/
import { computed } from 'vue';
import { Avatar, Button } from 'ant-design-vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
const props = defineProps<{
/** 审方/订单行数据,需含 user、user_patient或 patient */
row: Record<string, any>;
}>();
const emit = defineEmits<{
open: [payload: { upId: number; patientName: string }];
}>();
const defaultAvatar = '/img/user-default-avatar.png';
const user = computed(() => props.row?.user ?? {});
const patient = computed(
() => props.row?.user_patient ?? props.row?.userPatient ?? {},
);
const upId = computed(() =>
Number(patient.value?.id ?? props.row?.up_id ?? 0),
);
const patientName = computed(
() => String(patient.value?.name ?? props.row?.patient ?? '') || '—',
);
function userAvatarSrc() {
const raw = String(user.value?.avatarurl ?? '').trim();
if (!raw) return defaultAvatar;
return resolveAvatarUrl(raw) || defaultAvatar;
}
/** 打开就诊人档案抽屉 */
function handleOpen() {
if (!upId.value) return;
emit('open', { upId: upId.value, patientName: patientName.value });
}
</script>
<template>
<div class="wx-user-patient-cell">
<div class="wx-user-patient-cell__row">
<Avatar :size="24" :src="userAvatarSrc()" class="shrink-0" />
<div class="wx-user-patient-cell__text min-w-0">
<span class="text-[11px] text-gray-400">微信用户</span>
<span class="truncate text-xs">{{ user?.nickname || '—' }}</span>
<div class="text-[11px] text-gray-500">ID{{ user?.id ?? '—' }}</div>
</div>
</div>
<div class="wx-user-patient-cell__row">
<Avatar :size="24" :src="defaultAvatar" class="shrink-0" />
<div class="wx-user-patient-cell__text min-w-0">
<span class="text-[11px] text-gray-400">就诊人</span>
<Button
v-if="upId"
class="!h-auto !px-0 !py-0"
type="link"
@click.stop="handleOpen"
>
{{ patientName }}
</Button>
<span v-else class="text-xs">{{ patientName }}</span>
<div
v-if="patient?.sex || patient?.age"
class="text-[11px] text-gray-500"
>
<template v-if="patient?.sex === 1"></template>
<template v-else-if="patient?.sex === 2"></template>
<template v-if="patient?.age"> · {{ patient.age }}</template>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.wx-user-patient-cell {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.wx-user-patient-cell__row {
display: flex;
align-items: flex-start;
gap: 6px;
min-width: 0;
}
.wx-user-patient-cell__text {
display: flex;
flex-direction: column;
line-height: 1.3;
}
</style>

View File

@@ -0,0 +1,459 @@
<script lang="ts" setup>
/**
* 就诊人详情 Modal患者视角
* 与 Drawer 内容一致:资料 + 挂号/处方/订单;会员管理、订单「就诊人」入口使用
*/
import { h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import {
Avatar,
Button,
Descriptions,
Empty,
Spin,
Table,
Tabs,
Tag,
message,
} from 'ant-design-vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import OrderDetail from '#/views/business/order/product-order/components/detail.vue';
import {
getUserPatientOrderListApi,
getUserPatientPrescriptionListApi,
getUserPatientProfileDetailApi,
getUserPatientRegisterListApi,
} from './api';
import VisitTypeTag from './VisitTypeTag.vue';
defineOptions({ name: 'WxUserPatientDetailModal' });
const defaultAvatar = '/img/user-default-avatar.png';
const upId = ref(0);
const loading = ref(false);
const activeTab = ref('info');
const profile = ref<Record<string, any> | null>(null);
const registerList = ref<any[]>([]);
const prescriptionList = ref<any[]>([]);
const orderList = ref<any[]>([]);
const registerLoading = ref(false);
const prescriptionLoading = ref(false);
const orderLoading = ref(false);
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
connectedComponent: OrderDetail,
});
const [Modal, modalApi] = useVbenModal({
footer: false,
class: 'w-[760px]',
onOpenChange(isOpen: boolean) {
if (!isOpen) {
profile.value = null;
registerList.value = [];
prescriptionList.value = [];
orderList.value = [];
return;
}
const data = modalApi.getData<{ upId?: number; patientName?: string }>();
upId.value = Number(data?.upId || 0);
activeTab.value = 'info';
const name = String(data?.patientName || '').trim();
modalApi.setState({ title: name ? `就诊人:${name}` : '就诊人详情' });
if (upId.value > 0) {
void loadDetail();
}
},
});
/** 解析头像 URL */
function avatarSrc(raw?: string) {
const s = String(raw ?? '').trim();
if (!s) return defaultAvatar;
return resolveAvatarUrl(s) || defaultAvatar;
}
/** 既往/过敏/家族0 无,否则展示 history */
function historyText(status?: number, history?: string) {
if (Number(status) === 0) return '无';
const t = String(history || '').trim();
return t || '有';
}
/** 肝/肾功能0 正常,异常时附带指标文案 */
function functionText(flag?: number, indexText?: string) {
if (Number(flag) === 0) return '正常';
const t = String(indexText || '').trim();
return t ? `异常 · ${t}` : '异常';
}
async function loadDetail() {
loading.value = true;
try {
profile.value = await getUserPatientProfileDetailApi(upId.value);
} catch (e) {
console.error(e);
message.error('获取用户信息失败');
} finally {
loading.value = false;
}
}
async function loadRegisterList() {
registerLoading.value = true;
try {
const res = await getUserPatientRegisterListApi(upId.value);
registerList.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('获取挂号记录失败');
} finally {
registerLoading.value = false;
}
}
async function loadPrescriptionList() {
prescriptionLoading.value = true;
try {
const res = await getUserPatientPrescriptionListApi(upId.value);
prescriptionList.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('获取处方记录失败');
} finally {
prescriptionLoading.value = false;
}
}
async function loadOrderList() {
orderLoading.value = true;
try {
const res = await getUserPatientOrderListApi(upId.value);
orderList.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('获取订单记录失败');
} finally {
orderLoading.value = false;
}
}
/** Tab 切换时懒加载对应列表 */
function onTabChange(key: string | number) {
const k = String(key);
activeTab.value = k;
if (k === 'register' && registerList.value.length === 0) {
void loadRegisterList();
} else if (k === 'prescription' && prescriptionList.value.length === 0) {
void loadPrescriptionList();
} else if (k === 'order' && orderList.value.length === 0) {
void loadOrderList();
}
}
function openPrescription(id: number) {
PrescriptionDetailModalApi.setData({ values: id });
PrescriptionDetailModalApi.open();
}
function openOrder(id: number) {
OrderDetailModalApi.setData({ id });
OrderDetailModalApi.open();
}
const registerColumns = [
{ title: '订单编号', dataIndex: 'order_no', key: 'order_no' },
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{
title: '费用',
dataIndex: 'price',
key: 'price',
customRender: ({ text }: { text: number }) =>
`¥${Number(text || 0).toFixed(2)}`,
},
{
title: '支付',
dataIndex: 'is_pay',
key: 'is_pay',
customRender: ({ text }: { text: number }) =>
h(Tag, { color: text === 1 ? 'green' : 'red' }, () =>
text === 1 ? '已支付' : '未支付',
),
},
{ title: '状态', dataIndex: 'status_text', key: 'status_text' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
const prescriptionColumns = [
{
title: '处方编号',
dataIndex: 'prescription_no',
key: 'prescription_no',
customRender: ({ text, record }: { text: string; record: any }) =>
h(
Button,
{ type: 'link', onClick: () => openPrescription(record.id) },
() => text,
),
},
{
title: '就诊类型',
dataIndex: 'is_online',
key: 'is_online',
customRender: ({ text }: { text: number }) =>
h(VisitTypeTag, { isOnline: text }),
},
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{ title: '诊断', dataIndex: 'clinical_diagnose', key: 'clinical_diagnose' },
{
title: '状态',
dataIndex: 'status',
key: 'status',
customRender: ({ text }: { text: number }) => {
const map: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '未通过',
3: '无需审核',
};
return map[text] || '未知';
},
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
const orderColumns = [
{
title: '订单号',
dataIndex: 'order_no',
key: 'order_no',
customRender: ({ text, record }: { text: string; record: any }) =>
h(Button, { type: 'link', onClick: () => openOrder(record.id) }, () => text),
},
{
title: '就诊类型',
dataIndex: 'is_online',
key: 'is_online',
customRender: ({ text }: { text: number }) =>
h(VisitTypeTag, { isOnline: text }),
},
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{
title: '实付',
dataIndex: 'total_pay_price',
key: 'total_pay_price',
customRender: ({ text }: { text: number }) =>
`¥${Number(text || 0).toFixed(2)}`,
},
{
title: '支付',
dataIndex: 'is_pay',
key: 'is_pay',
customRender: ({ text }: { text: number }) =>
h(Tag, { color: text === 1 ? 'green' : 'red' }, () =>
text === 1 ? '已支付' : '未支付',
),
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
</script>
<template>
<Modal>
<PrescriptionDetailModal />
<OrderDetailModal />
<Spin :spinning="loading">
<template v-if="profile">
<div class="mb-4 flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-slate-800">
<Avatar :size="48" :src="avatarSrc(profile.user?.avatarurl)" />
<div class="min-w-0">
<div class="text-sm font-medium">
{{ profile.user?.nickname || '—' }}
</div>
<div class="text-xs text-gray-500">
用户 ID{{ profile.user?.id ?? '—' }}
</div>
</div>
</div>
<Tabs :active-key="activeTab" @change="onTabChange">
<Tabs.TabPane key="info" tab="就诊人信息">
<Descriptions :column="2" bordered size="small">
<Descriptions.Item label="姓名">
{{ profile.patient?.name || '—' }}
</Descriptions.Item>
<Descriptions.Item label="性别">
{{
profile.patient?.sex === 1
? '男'
: profile.patient?.sex === 2
? '女'
: '—'
}}
</Descriptions.Item>
<Descriptions.Item label="年龄">
{{ profile.patient?.age ? `${profile.patient.age}` : '—' }}
</Descriptions.Item>
<Descriptions.Item label="手机号">
<SensitiveText
v-if="profile.patient"
:record="profile.patient"
field="mobile"
/>
<span v-else></span>
</Descriptions.Item>
<Descriptions.Item
v-if="profile.patient?.id_card"
label="身份证"
:span="2"
>
<SensitiveText
:record="profile.patient"
field="id_card"
/>
</Descriptions.Item>
</Descriptions>
<!-- 健康问诊 / 功能异常与小程序资料 Tab 一致 -->
<div class="mt-4">
<div class="mb-2 text-sm font-medium text-gray-700">健康信息</div>
<Descriptions
v-if="profile.health_inquiry"
:column="2"
bordered
size="small"
>
<Descriptions.Item label="现病史">
{{
historyText(
profile.health_inquiry.present_status,
profile.health_inquiry.present_history,
)
}}
</Descriptions.Item>
<Descriptions.Item label="既往史">
{{
historyText(
profile.health_inquiry.person_status,
profile.health_inquiry.person_history,
)
}}
</Descriptions.Item>
<Descriptions.Item label="过敏史">
<span
:class="
Number(profile.health_inquiry.allergic_status) !== 0
? 'text-red-600 font-medium'
: ''
"
>
{{
historyText(
profile.health_inquiry.allergic_status,
profile.health_inquiry.allergic_history,
)
}}
</span>
</Descriptions.Item>
<Descriptions.Item label="家族遗传史">
{{
historyText(
profile.health_inquiry.family_status,
profile.health_inquiry.family_history,
)
}}
</Descriptions.Item>
<Descriptions.Item label="肝功能异常">
<span
:class="
Number(profile.health_inquiry.liver_function) !== 0
? 'text-red-600 font-medium'
: ''
"
>
{{
functionText(
profile.health_inquiry.liver_function,
profile.health_inquiry.liver_index,
)
}}
</span>
</Descriptions.Item>
<Descriptions.Item label="肾功能异常" :span="2">
<span
:class="
Number(profile.health_inquiry.renal_function) !== 0
? 'text-red-600 font-medium'
: ''
"
>
{{
functionText(
profile.health_inquiry.renal_function,
profile.health_inquiry.renal_index,
)
}}
</span>
</Descriptions.Item>
</Descriptions>
<Empty v-else description="暂无健康问诊记录" />
</div>
</Tabs.TabPane>
<Tabs.TabPane key="register" tab="挂号记录">
<Spin :spinning="registerLoading">
<Table
v-if="registerList.length"
:columns="registerColumns"
:data-source="registerList"
:pagination="false"
row-key="id"
size="small"
/>
<Empty v-else description="暂无挂号记录" />
</Spin>
</Tabs.TabPane>
<Tabs.TabPane key="prescription" tab="处方记录">
<Spin :spinning="prescriptionLoading">
<Table
v-if="prescriptionList.length"
:columns="prescriptionColumns"
:data-source="prescriptionList"
:pagination="false"
row-key="id"
size="small"
/>
<Empty v-else description="暂无处方记录" />
</Spin>
</Tabs.TabPane>
<Tabs.TabPane key="order" tab="订单记录">
<Spin :spinning="orderLoading">
<Table
v-if="orderList.length"
:columns="orderColumns"
:data-source="orderList"
:pagination="false"
row-key="id"
size="small"
/>
<Empty v-else description="暂无订单记录" />
</Spin>
</Tabs.TabPane>
</Tabs>
</template>
<Empty v-else-if="!loading" description="暂无数据" />
</Spin>
</Modal>
</template>

View File

@@ -0,0 +1,359 @@
<script lang="ts" setup>
/**
* 微信用户 + 就诊人档案抽屉
* 展示小程序用户基础信息、就诊人信息及挂号/处方/订单记录(跨医生)
*/
import { h, ref } from 'vue';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
import {
Avatar,
Button,
Descriptions,
Empty,
Spin,
Table,
Tabs,
Tag,
message,
} from 'ant-design-vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import OrderDetail from '#/views/business/order/product-order/components/detail.vue';
import {
getUserPatientOrderListApi,
getUserPatientPrescriptionListApi,
getUserPatientProfileDetailApi,
getUserPatientRegisterListApi,
} from './api';
import VisitTypeTag from './VisitTypeTag.vue';
defineOptions({ name: 'WxUserPatientDrawer' });
const defaultAvatar = '/img/user-default-avatar.png';
const upId = ref(0);
const loading = ref(false);
const activeTab = ref('info');
const profile = ref<Record<string, any> | null>(null);
const registerList = ref<any[]>([]);
const prescriptionList = ref<any[]>([]);
const orderList = ref<any[]>([]);
const registerLoading = ref(false);
const prescriptionLoading = ref(false);
const orderLoading = ref(false);
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
connectedComponent: OrderDetail,
});
const [Drawer, drawerApi] = useVbenDrawer({
footer: false,
onOpenChange(isOpen: boolean) {
if (!isOpen) {
profile.value = null;
registerList.value = [];
prescriptionList.value = [];
orderList.value = [];
return;
}
const data = drawerApi.getData<{ upId?: number; patientName?: string }>();
upId.value = Number(data?.upId || 0);
activeTab.value = 'info';
if (upId.value > 0) {
void loadDetail();
}
},
});
/** 解析头像 URL */
function avatarSrc(raw?: string) {
const s = String(raw ?? '').trim();
if (!s) return defaultAvatar;
return resolveAvatarUrl(s) || defaultAvatar;
}
async function loadDetail() {
loading.value = true;
try {
profile.value = await getUserPatientProfileDetailApi(upId.value);
} catch (e) {
console.error(e);
message.error('获取用户信息失败');
} finally {
loading.value = false;
}
}
async function loadRegisterList() {
registerLoading.value = true;
try {
const res = await getUserPatientRegisterListApi(upId.value);
registerList.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('获取挂号记录失败');
} finally {
registerLoading.value = false;
}
}
async function loadPrescriptionList() {
prescriptionLoading.value = true;
try {
const res = await getUserPatientPrescriptionListApi(upId.value);
prescriptionList.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('获取处方记录失败');
} finally {
prescriptionLoading.value = false;
}
}
async function loadOrderList() {
orderLoading.value = true;
try {
const res = await getUserPatientOrderListApi(upId.value);
orderList.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('获取订单记录失败');
} finally {
orderLoading.value = false;
}
}
/** Tab 切换时懒加载对应列表 */
function onTabChange(key: string | number) {
const k = String(key);
activeTab.value = k;
if (k === 'register' && registerList.value.length === 0) {
void loadRegisterList();
} else if (k === 'prescription' && prescriptionList.value.length === 0) {
void loadPrescriptionList();
} else if (k === 'order' && orderList.value.length === 0) {
void loadOrderList();
}
}
function openPrescription(id: number) {
PrescriptionDetailModalApi.setData({ values: id });
PrescriptionDetailModalApi.open();
}
function openOrder(id: number) {
OrderDetailModalApi.setData({ id });
OrderDetailModalApi.open();
}
const registerColumns = [
{ title: '订单编号', dataIndex: 'order_no', key: 'order_no' },
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{
title: '费用',
dataIndex: 'price',
key: 'price',
customRender: ({ text }: { text: number }) =>
`¥${Number(text || 0).toFixed(2)}`,
},
{
title: '支付',
dataIndex: 'is_pay',
key: 'is_pay',
customRender: ({ text }: { text: number }) =>
h(Tag, { color: text === 1 ? 'green' : 'red' }, () =>
text === 1 ? '已支付' : '未支付',
),
},
{ title: '状态', dataIndex: 'status_text', key: 'status_text' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
const prescriptionColumns = [
{
title: '处方编号',
dataIndex: 'prescription_no',
key: 'prescription_no',
customRender: ({ text, record }: { text: string; record: any }) =>
h(
Button,
{ type: 'link', onClick: () => openPrescription(record.id) },
() => text,
),
},
{
title: '就诊类型',
dataIndex: 'is_online',
key: 'is_online',
customRender: ({ text }: { text: number }) =>
h(VisitTypeTag, { isOnline: text }),
},
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{ title: '诊断', dataIndex: 'clinical_diagnose', key: 'clinical_diagnose' },
{
title: '状态',
dataIndex: 'status',
key: 'status',
customRender: ({ text }: { text: number }) => {
const map: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '未通过',
3: '无需审核',
};
return map[text] || '未知';
},
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
const orderColumns = [
{
title: '订单号',
dataIndex: 'order_no',
key: 'order_no',
customRender: ({ text, record }: { text: string; record: any }) =>
h(Button, { type: 'link', onClick: () => openOrder(record.id) }, () => text),
},
{
title: '就诊类型',
dataIndex: 'is_online',
key: 'is_online',
customRender: ({ text }: { text: number }) =>
h(VisitTypeTag, { isOnline: text }),
},
{ title: '医生', dataIndex: 'doctor', key: 'doctor' },
{ title: '诊所', dataIndex: 'store', key: 'store' },
{
title: '实付',
dataIndex: 'total_pay_price',
key: 'total_pay_price',
customRender: ({ text }: { text: number }) =>
`¥${Number(text || 0).toFixed(2)}`,
},
{
title: '支付',
dataIndex: 'is_pay',
key: 'is_pay',
customRender: ({ text }: { text: number }) =>
h(Tag, { color: text === 1 ? 'green' : 'red' }, () =>
text === 1 ? '已支付' : '未支付',
),
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
</script>
<template>
<Drawer class="w-[720px]" title="微信用户 / 就诊人">
<PrescriptionDetailModal />
<OrderDetailModal />
<Spin :spinning="loading">
<template v-if="profile">
<!-- 小程序用户 -->
<div class="mb-4 flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-slate-800">
<Avatar :size="48" :src="avatarSrc(profile.user?.avatarurl)" />
<div class="min-w-0">
<div class="text-sm font-medium">
{{ profile.user?.nickname || '—' }}
</div>
<div class="text-xs text-gray-500">
用户 ID{{ profile.user?.id ?? '—' }}
</div>
</div>
</div>
<Tabs :active-key="activeTab" @change="onTabChange">
<Tabs.TabPane key="info" tab="就诊人信息">
<Descriptions :column="2" bordered size="small">
<Descriptions.Item label="姓名">
{{ profile.patient?.name || '—' }}
</Descriptions.Item>
<Descriptions.Item label="性别">
{{
profile.patient?.sex === 1
? '男'
: profile.patient?.sex === 2
? '女'
: '—'
}}
</Descriptions.Item>
<Descriptions.Item label="年龄">
{{ profile.patient?.age ? `${profile.patient.age}` : '—' }}
</Descriptions.Item>
<Descriptions.Item label="手机号">
<SensitiveText
v-if="profile.patient"
:record="profile.patient"
field="mobile"
/>
<span v-else></span>
</Descriptions.Item>
<Descriptions.Item
v-if="profile.patient?.id_card"
label="身份证"
:span="2"
>
<SensitiveText
:record="profile.patient"
field="id_card"
/>
</Descriptions.Item>
</Descriptions>
</Tabs.TabPane>
<Tabs.TabPane key="register" tab="挂号记录">
<Spin :spinning="registerLoading">
<Table
v-if="registerList.length"
:columns="registerColumns"
:data-source="registerList"
:pagination="false"
row-key="id"
size="small"
/>
<Empty v-else description="暂无挂号记录" />
</Spin>
</Tabs.TabPane>
<Tabs.TabPane key="prescription" tab="处方记录">
<Spin :spinning="prescriptionLoading">
<Table
v-if="prescriptionList.length"
:columns="prescriptionColumns"
:data-source="prescriptionList"
:pagination="false"
row-key="id"
size="small"
/>
<Empty v-else description="暂无处方记录" />
</Spin>
</Tabs.TabPane>
<Tabs.TabPane key="order" tab="订单记录">
<Spin :spinning="orderLoading">
<Table
v-if="orderList.length"
:columns="orderColumns"
:data-source="orderList"
:pagination="false"
row-key="id"
size="small"
/>
<Empty v-else description="暂无订单记录" />
</Spin>
</Tabs.TabPane>
</Tabs>
</template>
<Empty v-else-if="!loading" description="暂无数据" />
</Spin>
</Drawer>
</template>

View File

@@ -0,0 +1,147 @@
<script lang="ts" setup>
/**
* 某微信用户下的就诊人列表 Modal
* 点行再打开就诊人详情 Modal由父级或本组件内嵌 DetailModal 处理)
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Avatar, Button, Empty, Spin, Table, message } from 'ant-design-vue';
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
import { getPatientListByUserApi } from './api';
import WxUserPatientDetailModal from './WxUserPatientDetailModal.vue';
defineOptions({ name: 'WxUserPatientsModal' });
const defaultAvatar = '/img/user-default-avatar.png';
const userId = ref(0);
const userNickname = ref('');
const userAvatar = ref('');
const loading = ref(false);
const list = ref<any[]>([]);
const [DetailModal, DetailModalApi] = useVbenModal({
connectedComponent: WxUserPatientDetailModal,
});
const [Modal, modalApi] = useVbenModal({
footer: false,
class: 'w-[640px]',
onOpenChange(isOpen: boolean) {
if (!isOpen) {
list.value = [];
return;
}
const data = modalApi.getData<{
userId?: number;
nickname?: string;
avatarurl?: string;
}>();
userId.value = Number(data?.userId || 0);
userNickname.value = String(data?.nickname || '');
userAvatar.value = String(data?.avatarurl || '');
modalApi.setState({
title: userNickname.value
? `就诊人列表 · ${userNickname.value}`
: '就诊人列表',
});
if (userId.value > 0) {
void loadList();
}
},
});
function avatarSrc(raw?: string) {
const s = String(raw ?? '').trim();
if (!s) return defaultAvatar;
return resolveAvatarUrl(s) || defaultAvatar;
}
async function loadList() {
loading.value = true;
try {
const res = await getPatientListByUserApi(userId.value);
list.value = res?.items ?? [];
} catch (e) {
console.error(e);
message.error('加载就诊人失败');
} finally {
loading.value = false;
}
}
/** 打开就诊人详情 Modal与微信用户列表分离 */
function openPatientDetail(row: Record<string, any>) {
const upId = Number(row.up_id || 0);
if (!upId) {
message.warning('缺少就诊人信息');
return;
}
DetailModalApi.setData({
upId,
patientName: row.name || '',
});
DetailModalApi.open();
}
const columns = [
{ title: '姓名', key: 'name' },
{ title: '性别', key: 'sex', width: 80 },
{ title: '手机', key: 'mobile' },
{ title: '操作', key: 'action', width: 100 },
];
</script>
<template>
<Modal>
<DetailModal />
<div class="mb-3 flex items-center gap-2">
<Avatar :size="36" :src="avatarSrc(userAvatar)" />
<div class="min-w-0 text-sm">
<div class="font-medium">{{ userNickname || '—' }}</div>
<div class="text-xs text-gray-500">用户 ID{{ userId || '—' }}</div>
</div>
</div>
<Spin :spinning="loading">
<Table
v-if="list.length"
:columns="columns"
:data-source="list"
:pagination="false"
row-key="up_id"
size="small"
:custom-row="
(record) => ({
onClick: () => openPatientDetail(record),
style: { cursor: 'pointer' },
})
"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.name || '—' }}
</template>
<template v-else-if="column.key === 'sex'">
<template v-if="record.sex === 1">男</template>
<template v-else-if="record.sex === 2">女</template>
<template v-else>—</template>
</template>
<template v-else-if="column.key === 'mobile'">
<SensitiveText :record="record" field="mobile" />
</template>
<template v-else-if="column.key === 'action'">
<Button type="link" @click.stop="openPatientDetail(record)">
详情
</Button>
</template>
</template>
</Table>
<Empty v-else-if="!loading" description="暂无就诊人" />
</Spin>
</Modal>
</template>

View File

@@ -0,0 +1,68 @@
/**
* 就诊人档案 API跨医生视角供药师审方等场景
*/
import { requestClient } from '#/api/request';
const prefix = 'user-patient-profile/';
/** 用户管理列表(微信用户 + 就诊人,旧接口) */
export async function getUserPatientProfileListApi(params?: {
page?: number;
pageSize?: number;
keyword?: string;
}) {
return requestClient.get<any>(`${prefix}list`, { params });
}
/** 会员管理:微信用户分页 */
export async function getWxUserListApi(params?: {
page?: number;
pageSize?: number;
keyword?: string;
}) {
return requestClient.get<any>(`${prefix}user-list`, { params });
}
/** 某微信用户下的就诊人列表 */
export async function getPatientListByUserApi(userId: number) {
return requestClient.get<any>(`${prefix}patient-list-by-user`, {
params: { user_id: userId },
});
}
/** 小程序用户 + 就诊人基础信息 */
export async function getUserPatientProfileDetailApi(upId: number) {
return requestClient.get<any>(`${prefix}detail`, {
params: { up_id: upId },
});
}
/** 就诊人挂号记录 */
export async function getUserPatientRegisterListApi(
upId: number,
params?: { page?: number; pageSize?: number },
) {
return requestClient.get<any>(`${prefix}register-list`, {
params: { up_id: upId, ...params },
});
}
/** 就诊人处方记录 */
export async function getUserPatientPrescriptionListApi(
upId: number,
params?: { page?: number; pageSize?: number },
) {
return requestClient.get<any>(`${prefix}prescription-list`, {
params: { up_id: upId, ...params },
});
}
/** 就诊人商品订单记录 */
export async function getUserPatientOrderListApi(
upId: number,
params?: { page?: number; pageSize?: number },
) {
return requestClient.get<any>(`${prefix}order-list`, {
params: { up_id: upId, ...params },
});
}

View File

@@ -0,0 +1,10 @@
/**
* 微信用户/就诊人相关组件导出
*/
export { default as WxUserPatientCell } from './WxUserPatientCell.vue';
export { default as WxUserPatientDrawer } from './WxUserPatientDrawer.vue';
export { default as WxUserPatientDetailModal } from './WxUserPatientDetailModal.vue';
export { default as WxUserPatientsModal } from './WxUserPatientsModal.vue';
export { default as VisitTypeTag } from './VisitTypeTag.vue';
export { default as PrescriptionExpireTime } from './PrescriptionExpireTime.vue';
export * from './api';

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)) {

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