From 879efb8f292bcff2e91d68ecd03f21b1b0316b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Fri, 21 Aug 2026 12:51:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=A1=E7=90=86=E7=AB=AF=E5=85=AC?= =?UTF-8?q?=E8=B4=A6=E6=94=AF=E4=BB=98=E4=B8=8E=E7=A1=AE=E8=AE=A4=E3=80=81?= =?UTF-8?q?=E9=94=81=E5=BA=97=E9=81=AE=E7=BD=A9=E5=8F=8A=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增公账账单/到账确认页与全局锁店组件;系统配置补公账参数面板; 统一弹窗 onConfirm 校验提交,并完善订单详情与门店相关交互。 --- .cursor/rules/Code-Standards.mdc | 55 +- README.md | 10 + apps/desktop/README.md | 3 + apps/web-antd/index.html | 62 +- apps/web-antd/src/adapter/component/index.ts | 33 +- apps/web-antd/src/adapter/form.ts | 40 +- apps/web-antd/src/api/request.ts | 14 +- .../form/components/promoter-picker.vue | 1 + .../form/components/store-multi-search.vue | 25 +- .../form/components/upload-dragger-paste.vue | 1 + .../public-account-lock/LockOrdersPanel.vue | 440 +++++++++++ .../public-account-lock/LockPayPanel.vue | 152 ++++ .../public-account-lock/LockWaitPanel.vue | 237 ++++++ .../public-account-lock/LockWarningCanvas.vue | 306 ++++++++ .../PublicAccountLockBanner.vue | 270 +++++++ .../PublicAccountLockHeaderChip.vue | 112 +++ .../PublicAccountLockOverlay.vue | 720 ++++++++++++++++++ apps/web-antd/src/layouts/basic.vue | 6 + .../components/UpdatePasswordModal.vue | 27 +- .../src/router/routes/modules/dashboard.ts | 76 +- apps/web-antd/src/store/auth.ts | 5 + apps/web-antd/src/utils/publicAccountLock.ts | 179 +++++ .../src/views/_core/profile/base-setting.vue | 33 +- .../views/_core/profile/password-setting.vue | 25 +- .../express-company/components/modal.vue | 8 +- .../OrderPricePercentAdjustDrawer.vue | 1 + .../order/components/order-trace-drawer.vue | 32 +- .../business/order/prescription/api/index.ts | 5 +- .../business/order/prescription/index.vue | 21 +- .../business/order/product-order/api/index.ts | 16 +- .../components/ProductOrderCardList.vue | 5 +- .../components/cells/OrderUserInfoCell.vue | 70 +- .../order/product-order/components/detail.vue | 150 +++- .../components/logistics-modal.vue | 1 + .../order/product-order/components/modal.vue | 61 +- .../order/product-order/config/constants.ts | 7 + .../order/product-order/config/search.ts | 2 + .../business/order/product-order/index.vue | 162 +++- .../business/order/register/api/index.ts | 5 +- .../components/RegisterOrderCardList.vue | 13 +- .../order/register/components/refund.vue | 30 +- .../views/business/order/register/index.vue | 45 +- .../business/order/utils/popup-z-index.ts | 32 + .../components/modal.vue | 8 +- .../product/health-food/components/modal.vue | 45 +- .../medical-device/components/modal.vue | 8 +- .../product/non-drug/components/modal.vue | 8 +- .../product/service-pack/components/modal.vue | 8 +- .../western-medicine/components/modal.vue | 8 +- .../components/modal.vue | 8 +- .../store/settings/components/uploadModal.vue | 8 +- .../admin/components/modal.vue | 7 +- .../shop/components/modal.vue | 8 +- .../store/components/modal.vue | 8 +- .../doctor-article/components/modal.vue | 8 +- .../components/AiPrescriptionDrawer.vue | 3 + .../components/PrescriptionDetail.vue | 106 +-- .../views/doctor/doctor-reception/index.vue | 51 +- .../views/doctor/doctor/components/modal.vue | 8 +- .../src/views/doctor/doctor/config/form.ts | 10 +- .../doctor/pharmacist/components/modal.vue | 8 +- .../views/doctor/pharmacist/config/form.ts | 10 +- .../finance/charge-cash-pay-record/index.vue | 3 +- .../components/MonthlyPayment.vue | 72 +- .../monthly-payment-settlement/config/form.ts | 44 +- .../monthly-payment-settlement/index.vue | 6 +- .../components/MonthlyPayment.vue | 8 +- .../public-account-confirm/api/index.ts | 13 + .../config/daily-search.ts | 41 + .../config/daily-table.ts | 87 +++ .../public-account-confirm/config/search.ts | 65 ++ .../public-account-confirm/config/table.ts | 55 ++ .../finance/public-account-confirm/index.vue | 279 +++++++ .../finance/public-account-pay/api/index.ts | 101 +++ .../components/apply-modal.vue | 125 +++ .../components/confirm-modal.vue | 236 ++++++ .../components/daily-confirm-modal.vue | 211 +++++ .../components/daily-upload-form.vue | 182 +++++ .../components/daily-upload-modal.vue | 52 ++ .../components/generate-daily-modal.vue | 116 +++ .../components/merge-orders-modal.vue | 62 ++ .../components/voucher-thumbs.vue | 72 ++ .../public-account-pay/config/constants.ts | 35 + .../public-account-pay/config/daily-search.ts | 39 + .../public-account-pay/config/daily-table.ts | 91 +++ .../public-account-pay/config/search.ts | 63 ++ .../public-account-pay/config/table.ts | 58 ++ .../finance/public-account-pay/index.vue | 547 +++++++++++++ .../views/finance/withdrawal-audit/index.vue | 3 +- .../components/account-info-card.vue | 127 ++- .../withdrawal/components/card-modal.vue | 8 +- .../withdrawal/components/statistics.vue | 3 + .../src/views/finance/withdrawal/index.vue | 3 +- .../utils/use-withdraw-order-detail.ts | 27 +- .../views/notice/action/NoticeActionHost.vue | 18 + .../web-antd/src/views/notice/action/types.ts | 2 + .../audit-prescription/components/modal.vue | 40 +- .../views/system/admin/components/modal.vue | 8 +- .../system/base-config/components/modal.vue | 8 +- .../components/modal.vue | 8 +- .../delivery-warehouse-order/api/index.ts | 9 + .../components/modal.vue | 50 +- .../system/delivery-warehouse-order/index.vue | 39 +- .../components/audit-modal.vue | 8 +- .../components/modal.vue | 8 +- .../delivery-warehouse/components/modal.vue | 37 +- .../system/department/components/modal.vue | 31 +- .../system/doctor-input/components/modal.vue | 8 +- .../system/home-zones/components/modal.vue | 8 +- .../views/system/menu/components/modal.vue | 8 +- .../views/system/notice/components/modal.vue | 8 +- .../views/system/oa-chat/components/modal.vue | 8 +- .../system/oa-robot/components/modal.vue | 8 +- .../system/oa-scene/components/modal.vue | 8 +- .../oa-scene/components/msg-preview.vue | 6 +- .../system/pharmacy/components/modal.vue | 8 +- .../src/views/system/pharmacy/config/form.ts | 12 +- .../system/platform-info/components/modal.vue | 8 +- .../components/qualifications-modal.vue | 8 +- .../views/system/platform-info/config/form.ts | 10 +- .../components/modal.vue | 8 +- .../system/platform/components/modal.vue | 8 +- .../role-quick-nav/components/modal.vue | 8 +- .../views/system/role/components/modal.vue | 32 +- .../system/store-input/components/modal.vue | 85 ++- .../views/system/store-input/config/form.ts | 9 +- .../src/views/system/store/api/index.ts | 5 + .../components/AddDoctorFromStoreModal.vue | 7 +- .../cells/StoreConfigTogglesCell.vue | 37 + .../views/system/store/components/modal.vue | 69 +- .../src/views/system/store/config/form.ts | 21 +- .../web-antd/src/views/system/store/index.vue | 10 + .../system/supplier/components/modal.vue | 8 +- .../public-account-pay-config-panel.vue | 442 +++++++++++ .../src/views/system/system-config/index.vue | 7 + .../components/AssistantConfigModal.vue | 30 +- .../transfer-question/components/modal.vue | 8 +- build-antd-desktop.mjs | 155 ++++ package.json | 1 + .../__tests__/form-integration.test.ts | 35 +- packages/@core/ui-kit/form-ui/src/config.ts | 10 + .../form-ui/src/form-render/form-field.vue | 49 +- .../ui-kit/form-ui/src/vben-use-form.vue | 7 + .../access/src/__tests__/accessible.test.ts | 73 ++ packages/effects/access/src/accessible.ts | 52 +- 145 files changed, 7326 insertions(+), 884 deletions(-) create mode 100644 apps/web-antd/src/components/public-account-lock/LockOrdersPanel.vue create mode 100644 apps/web-antd/src/components/public-account-lock/LockPayPanel.vue create mode 100644 apps/web-antd/src/components/public-account-lock/LockWaitPanel.vue create mode 100644 apps/web-antd/src/components/public-account-lock/LockWarningCanvas.vue create mode 100644 apps/web-antd/src/components/public-account-lock/PublicAccountLockBanner.vue create mode 100644 apps/web-antd/src/components/public-account-lock/PublicAccountLockHeaderChip.vue create mode 100644 apps/web-antd/src/components/public-account-lock/PublicAccountLockOverlay.vue create mode 100644 apps/web-antd/src/utils/publicAccountLock.ts create mode 100644 apps/web-antd/src/views/business/order/utils/popup-z-index.ts create mode 100644 apps/web-antd/src/views/finance/public-account-confirm/api/index.ts create mode 100644 apps/web-antd/src/views/finance/public-account-confirm/config/daily-search.ts create mode 100644 apps/web-antd/src/views/finance/public-account-confirm/config/daily-table.ts create mode 100644 apps/web-antd/src/views/finance/public-account-confirm/config/search.ts create mode 100644 apps/web-antd/src/views/finance/public-account-confirm/config/table.ts create mode 100644 apps/web-antd/src/views/finance/public-account-confirm/index.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/api/index.ts create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/apply-modal.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/confirm-modal.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/daily-confirm-modal.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/daily-upload-form.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/daily-upload-modal.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/generate-daily-modal.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/merge-orders-modal.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/components/voucher-thumbs.vue create mode 100644 apps/web-antd/src/views/finance/public-account-pay/config/constants.ts create mode 100644 apps/web-antd/src/views/finance/public-account-pay/config/daily-search.ts create mode 100644 apps/web-antd/src/views/finance/public-account-pay/config/daily-table.ts create mode 100644 apps/web-antd/src/views/finance/public-account-pay/config/search.ts create mode 100644 apps/web-antd/src/views/finance/public-account-pay/config/table.ts create mode 100644 apps/web-antd/src/views/finance/public-account-pay/index.vue create mode 100644 apps/web-antd/src/views/system/system-config/components/public-account-pay-config-panel.vue create mode 100644 build-antd-desktop.mjs diff --git a/.cursor/rules/Code-Standards.mdc b/.cursor/rules/Code-Standards.mdc index 5400dfa9..ced6a183 100644 --- a/.cursor/rules/Code-Standards.mdc +++ b/.cursor/rules/Code-Standards.mdc @@ -68,9 +68,54 @@ alwaysApply: true ## 表单校验 -- 必填用字符串规则 `rules: 'required'`(选择项用 `'selectRequired'`),规则定义在 `#/adapter/form.ts` 的 `defineRules` -- 校验调用统一用 `formApi.validate().then(e => { if (e.valid) {...} })` +- 必填用字符串规则 `rules: 'required'`(选择项用 `'selectRequired'`),规则定义在 `#/adapter/form.ts` 的 `rules` - 必填字段在 schema 中加 `rules: 'required'`,禁止散落 `validator` 写法 +- 校验调用统一用 **单次 await**(禁止 `.then` 与 `validateAndSubmitForm` 并用): + +```ts +const e = await formApi.validate(); +if (!e.valid) return; +const values = await formApi.getValues(); +``` + +## 弹窗 onConfirm(强制,防「点确定无反应」) + +弹窗内提交必须按下列写法,**禁止**复制旧模板: + +```ts +onConfirm: async () => { + const e = await formApi.validate(); + if (!e.valid) return; + const values = await formApi.getValues(); + modalApi.setState({ loading: true, confirmLoading: true }); + try { + await submitApi(values); + message.success('保存成功'); + gridApi.value?.reload?.(); + modalApi.close(); + } catch (err) { + console.error(err); + } finally { + modalApi.setState({ loading: false, confirmLoading: false }); + } +}; +``` + +- **禁止** `formApi.validate().then(...)` 后再 `await formApi.validateAndSubmitForm()`(并发二次校验,表现为点确定无提交) +- **禁止**无 `handleSubmit` 时调用 `validateAndSubmitForm` / `validateAndSubmit` +- 参考:`product-order/components/refund.vue`、`audit-prescription/components/modal.vue`、`delivery-warehouse-order/components/modal.vue` + +## 表单 Divider / 分区标题 + +- `component: 'Divider'` 仅作分区展示,**禁止**加 `rules: 'required'`(无业务值,必失败) +- **禁止**用占位 `label: 'x'`(会渲染成校验文案「请输入x」) +- `fieldName` 用唯一占位如 `_section_basic`,不要空字符串 + +## 敏感字段展示(SensitiveText) + +- 模板使用 `` 时,**必须**显式:`import SensitiveText from '#/components/sensitive-text/SensitiveText.vue'` +- 该组件**未**全局注册;表单选择器(`form/components/*-picker.vue`)同样要 import,对齐 `doctor-picker` / `store-picker` +- **禁止**只写模板标签不写 import(会报 `Failed to resolve component: SensitiveText`) ## 字典/枚举管理 @@ -177,10 +222,10 @@ alwaysApply: true - 表单必须用 `useVbenForm`,禁止直接 `
` 或 antd `Form` - 动态修改 schema 必须用 `formApi.updateSchema([...])`,**vben form 没有 `setComponentProps` 这个方法**(这是已踩过的坑) -- 重置表单用 `formApi.resetForm()`,**严禁 `formApi.resetFields()`**(那是 antd Form 的 API,Vben Form 没有,会报 `resetFields is not a function`;已踩过坑) +- 重置表单用 `formApi.reset()`(`resetForm` 已弃用);**严禁 `formApi.resetFields()`**(那是 antd Form 的 API,Vben Form 没有,会报 `resetFields is not a function`;已踩过坑) - 取值/赋值用 `formApi.getValues()` / `formApi.setValues(obj)`,禁止自己 `v-model` 收集 -- 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()` -- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册 +- 校验用 `const e = await formApi.validate(); if (!e.valid) return;`,**禁止** `validate().then` 与 `validateAndSubmitForm` 并用(见上文「弹窗 onConfirm」) +- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `rules` 注册 - schema 中字段名(`fieldName`)必须与后端字段 snake_case 对齐,禁止前端 camelCase - 密码/敏感字段用 `VbenInputPassword` 组件,禁止用 `VbenInput` 配 `type="password"` - 单选/多选选项来自接口时,必须在弹窗/页面 `onMounted` 或 `onOpenChange` 里拉取后通过 `updateSchema` 注入,禁止写死常量 diff --git a/README.md b/README.md index cc951b47..08c16855 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,13 @@ ```vue v-access:code="['Admin'] ``` + + +# Web + 桌面一起打 +pnpm build:antd-desktop + +# 只打 Web +node build-antd-desktop.mjs --only=web + +# 只打桌面(需已有 apps/web-antd/dist) +node build-antd-desktop.mjs --only=desktop diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 294d9789..710bef69 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -10,6 +10,9 @@ pnpm dev:desktop # 打包:turbo 先构建 web-antd dist,再构建主进程并出安装包(产物在 apps/desktop/release/) pnpm build:desktop + +# 一键同时打 Web + 桌面(根目录 build-antd-desktop.mjs,先 web 再桌面) +pnpm build:antd-desktop ``` - Windows 机器出 NSIS 安装包;mac 包必须在 macOS 机器上执行同一命令(electron-builder 不支持交叉编译 mac 包)。 diff --git a/apps/web-antd/index.html b/apps/web-antd/index.html index e3b9ae5c..73f2ce29 100644 --- a/apps/web-antd/index.html +++ b/apps/web-antd/index.html @@ -1,19 +1,71 @@ - + - - - + + + + + + + %VITE_APP_TITLE% - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-antd/src/components/public-account-lock/LockPayPanel.vue b/apps/web-antd/src/components/public-account-lock/LockPayPanel.vue new file mode 100644 index 00000000..7b0b7677 --- /dev/null +++ b/apps/web-antd/src/components/public-account-lock/LockPayPanel.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/apps/web-antd/src/components/public-account-lock/LockWaitPanel.vue b/apps/web-antd/src/components/public-account-lock/LockWaitPanel.vue new file mode 100644 index 00000000..ad4967ce --- /dev/null +++ b/apps/web-antd/src/components/public-account-lock/LockWaitPanel.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/apps/web-antd/src/components/public-account-lock/LockWarningCanvas.vue b/apps/web-antd/src/components/public-account-lock/LockWarningCanvas.vue new file mode 100644 index 00000000..bd4a3301 --- /dev/null +++ b/apps/web-antd/src/components/public-account-lock/LockWarningCanvas.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/apps/web-antd/src/components/public-account-lock/PublicAccountLockBanner.vue b/apps/web-antd/src/components/public-account-lock/PublicAccountLockBanner.vue new file mode 100644 index 00000000..0e0a25f8 --- /dev/null +++ b/apps/web-antd/src/components/public-account-lock/PublicAccountLockBanner.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/apps/web-antd/src/components/public-account-lock/PublicAccountLockHeaderChip.vue b/apps/web-antd/src/components/public-account-lock/PublicAccountLockHeaderChip.vue new file mode 100644 index 00000000..daf581cf --- /dev/null +++ b/apps/web-antd/src/components/public-account-lock/PublicAccountLockHeaderChip.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/apps/web-antd/src/components/public-account-lock/PublicAccountLockOverlay.vue b/apps/web-antd/src/components/public-account-lock/PublicAccountLockOverlay.vue new file mode 100644 index 00000000..f8e979d4 --- /dev/null +++ b/apps/web-antd/src/components/public-account-lock/PublicAccountLockOverlay.vue @@ -0,0 +1,720 @@ + + + + + + + diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 835d7065..f7f02926 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -23,6 +23,9 @@ import { notification } from 'ant-design-vue'; import { Bell, Users } from 'lucide-vue-next'; import DoctorTransferFloat from '#/components/doctor-transfer-float/DoctorTransferFloat.vue'; +import PublicAccountLockBanner from '#/components/public-account-lock/PublicAccountLockBanner.vue'; +import PublicAccountLockHeaderChip from '#/components/public-account-lock/PublicAccountLockHeaderChip.vue'; +import PublicAccountLockOverlay from '#/components/public-account-lock/PublicAccountLockOverlay.vue'; import HeaderVipBadge from '#/components/vip/HeaderVipBadge.vue'; import SwitchAccountModal from '#/layouts/components/SwitchAccountModal.vue'; import { useAuthStore } from '#/store'; @@ -294,6 +297,7 @@ watch( @logout="handleLogout" > + + diff --git a/apps/web-antd/src/layouts/components/UpdatePasswordModal.vue b/apps/web-antd/src/layouts/components/UpdatePasswordModal.vue index 0e2f4f41..0dc00baa 100644 --- a/apps/web-antd/src/layouts/components/UpdatePasswordModal.vue +++ b/apps/web-antd/src/layouts/components/UpdatePasswordModal.vue @@ -15,20 +15,19 @@ const [Modal, modalApi] = useVbenModal({ modalApi.close(); }, onConfirm: async () => { - formApi.validate().then(async (e: any) => { - if (e.valid) { - const values = await formApi.getValues(); - modalApi.setState({loading: true, confirmLoading: true}); - updatePassword(values).then(() => { - modalApi.close(); - message.success('修改成功,请重新登录'); - }) - .finally(() => { - modalApi.setState({loading: false, confirmLoading: false}); - }); - } - }); - await formApi.validateAndSubmitForm(); + const e = await formApi.validate(); + if (!e.valid) return; + const values = await formApi.getValues(); + modalApi.setState({ loading: true, confirmLoading: true }); + try { + await updatePassword(values); + modalApi.close(); + message.success('修改成功,请重新登录'); + } catch (err) { + console.error(err); + } finally { + modalApi.setState({ loading: false, confirmLoading: false }); + } }, }); diff --git a/apps/web-antd/src/router/routes/modules/dashboard.ts b/apps/web-antd/src/router/routes/modules/dashboard.ts index 44f15ef9..28a392dd 100644 --- a/apps/web-antd/src/router/routes/modules/dashboard.ts +++ b/apps/web-antd/src/router/routes/modules/dashboard.ts @@ -1,38 +1,38 @@ -import type { RouteRecordRaw } from 'vue-router'; - -import { $t } from '#/locales'; - -const routes: RouteRecordRaw[] = [ - { - meta: { - icon: 'lucide:layout-dashboard', - order: -1, - title: $t('page.dashboard.title'), - }, - name: 'Dashboard', - path: '/dashboard', - children: [ - { - name: 'Analytics', - path: 'analytics', - component: () => import('#/views/dashboard/analytics/index.vue'), - meta: { - affixTab: true, - icon: 'lucide:area-chart', - title: $t('page.dashboard.analytics'), - }, - }, - { - name: 'Workspace', - path: 'workspace', - component: () => import('#/views/dashboard/workspace/index.vue'), - meta: { - icon: 'carbon:workspace', - title: $t('page.dashboard.workspace'), - }, - }, - ], - }, -]; - -export default routes; +// import type { RouteRecordRaw } from 'vue-router'; +// +// import { $t } from '#/locales'; +// +// const routes: RouteRecordRaw[] = [ +// { +// meta: { +// icon: 'lucide:layout-dashboard', +// order: -1, +// title: $t('page.dashboard.title'), +// }, +// name: 'Dashboard', +// path: '/dashboard', +// children: [ +// { +// name: 'Analytics', +// path: 'analytics', +// component: () => import('#/views/dashboard/analytics/index.vue'), +// meta: { +// affixTab: true, +// icon: 'lucide:area-chart', +// title: $t('page.dashboard.analytics'), +// }, +// }, +// { +// name: 'Workspace', +// path: 'workspace', +// component: () => import('#/views/dashboard/workspace/index.vue'), +// meta: { +// icon: 'carbon:workspace', +// title: $t('page.dashboard.workspace'), +// }, +// }, +// ], +// }, +// ]; +// +// export default routes; diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index 01822513..82ff13d7 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -13,6 +13,7 @@ import { defineStore } from 'pinia'; import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { $t } from '#/locales'; +import { applyPublicAccountLockSession } from '#/utils/publicAccountLock'; const { updateWatermark } = useWatermark(); @@ -143,6 +144,10 @@ export const useAuthStore = defineStore('auth', () => { let userInfo: null | UserInfo = null; userInfo = await getUserInfoApi(); userStore.setUserInfo(userInfo); + const lock = (userInfo as any)?.public_account_lock; + if (lock) { + applyPublicAccountLockSession(lock); + } return userInfo; } diff --git a/apps/web-antd/src/utils/publicAccountLock.ts b/apps/web-antd/src/utils/publicAccountLock.ts new file mode 100644 index 00000000..c776cb04 --- /dev/null +++ b/apps/web-antd/src/utils/publicAccountLock.ts @@ -0,0 +1,179 @@ +/** + * 公账逾期锁店弹窗 + 工作台条幅状态 + * locked/visible=身份硬锁全屏;bannerVisible=门店逾期且未硬锁时的顶部条幅 + */ +import { reactive } from 'vue'; + +export type PublicAccountLockAlertColor = 'blue' | 'yellow' | 'red'; + +export interface PublicAccountLockContact { + name?: string; + phone: string; + /** 明文拨号号码(拦截器/后端写出,不走 phone 加密脱敏) */ + phone_txt?: string; + /** 兼容旧字段,同 phone_txt */ + tel?: string; +} + +export interface PublicAccountLockPayload { + action?: string; + daily_id?: number; + store_id?: number; + /** 诊所名称,锁框问候用 */ + store_name?: string; + platform_amount?: string; + /** 多天逾期合计应付款 */ + platform_amount_total?: string; + /** 逾期日账单张数 */ + bill_count?: number; + bill_date?: number; + status?: number; + bill_status?: number; + alert_color?: PublicAccountLockAlertColor | string; + unlock_mode?: 'voucher' | 'confirm' | string; + lock_time?: string; + /** 账单日起算宽限天数,默认 1 */ + lock_days?: number; + bill_date_from?: number; + bill_date_to?: number; + bill_date_range_txt?: string; + overdue_daily_ids?: number[]; + /** 合并批次 id(预览=最早日账单 id) */ + merge_id?: number; + /** 当前身份是否硬锁(全屏) */ + locked?: boolean; + /** 门店是否逾期(条幅) */ + store_locked?: boolean; + contact_phones?: PublicAccountLockContact[]; +} + +/** 后台配置的提醒框颜色,非法值回落黄色 */ +export function normalizeLockAlertColor( + raw: unknown, +): PublicAccountLockAlertColor { + const color = String(raw || 'yellow'); + if (color === 'blue' || color === 'red') { + return color; + } + return 'yellow'; +} + +const state = reactive({ + /** 身份硬锁全屏 */ + visible: false, + /** 门店逾期条幅(未硬锁时) */ + bannerVisible: false, + /** 条幅已收起到顶栏小胶囊 */ + bannerDocked: false, + /** 条幅「去处理」打开的软处理层(可返回,非硬锁) */ + handleVisible: false, + payload: {} as PublicAccountLockPayload, +}); + +/** 锁层是 2100,antd message 默认 1010,锁屏后提示会被挡住 */ +export const PUBLIC_ACCOUNT_LOCK_PROMPT_Z_INDEX = 5000; +const LOCK_BODY_CLASS = 'pap-lock-open'; + +export function getPublicAccountLockState() { + return state; +} + +/** 锁屏时抬高 message/notification,不改锁层自己的 z-index */ +function syncLockPromptLayer(visible: boolean) { + if (typeof document === 'undefined') { + return; + } + document.body.classList.toggle(LOCK_BODY_CLASS, visible); +} + +function isTruthyFlag(v: unknown): boolean { + return v === true || v === 1 || v === '1'; +} + +/** 是否门店逾期(条幅) */ +export function isStoreLockedPayload(payload?: PublicAccountLockPayload | null) { + if (!payload || typeof payload !== 'object') { + return false; + } + if (isTruthyFlag(payload.store_locked) || isTruthyFlag(payload.locked)) { + return true; + } + return !!(payload.daily_id || payload.bill_date); +} + +/** + * 登录 my-info / lock-status:按 locked / store_locked 分别驱动全屏与条幅 + */ +export function applyPublicAccountLockSession(payload?: PublicAccountLockPayload | null) { + const data = (payload && typeof payload === 'object' ? payload : {}) as PublicAccountLockPayload; + state.payload = data; + const hard = isTruthyFlag(data.locked); + const storeLocked = isStoreLockedPayload(data); + state.visible = hard; + const showBanner = storeLocked && !hard; + // 新出现条幅时重新展开几秒,避免一直贴顶无人注意 + if (showBanner && !state.bannerVisible) { + state.bannerDocked = false; + } + state.bannerVisible = showBanner; + if (!showBanner) { + state.bannerDocked = false; + } + if (!hard) { + // 硬锁解除时,处理层由业务自行关 + } + syncLockPromptLayer(hard || state.handleVisible); +} + +/** 4101:强制全屏硬锁 */ +export function showPublicAccountLock(payload: PublicAccountLockPayload) { + const data = { + ...(payload || {}), + locked: true, + store_locked: true, + } as PublicAccountLockPayload; + state.payload = data; + state.visible = true; + state.bannerVisible = false; + state.bannerDocked = false; + state.handleVisible = false; + syncLockPromptLayer(true); +} + +/** 条幅收起到顶栏 */ +export function dockPublicAccountBanner() { + if (state.bannerVisible) { + state.bannerDocked = true; + } +} + +/** 顶栏胶囊再展开大条幅 */ +export function expandPublicAccountBanner() { + state.bannerDocked = false; +} + +/** + * 条幅「去处理」:打开软处理层(上传/看进度),避免已在账单页时 router.push 无感 + */ +export function openPublicAccountLockHandle() { + if (!isStoreLockedPayload(state.payload) && !state.bannerVisible && !state.visible) { + return; + } + state.handleVisible = true; + syncLockPromptLayer(true); +} + +/** 关闭软处理层(硬锁时不可用) */ +export function closePublicAccountLockHandle() { + state.handleVisible = false; + syncLockPromptLayer(state.visible); +} + +export function clearPublicAccountLock() { + state.visible = false; + state.bannerVisible = false; + state.bannerDocked = false; + state.handleVisible = false; + state.payload = {}; + syncLockPromptLayer(false); +} diff --git a/apps/web-antd/src/views/_core/profile/base-setting.vue b/apps/web-antd/src/views/_core/profile/base-setting.vue index aae04904..5c1225f6 100644 --- a/apps/web-antd/src/views/_core/profile/base-setting.vue +++ b/apps/web-antd/src/views/_core/profile/base-setting.vue @@ -97,23 +97,22 @@ const saving = ref(false); * 保存资料:校验 → 提交白名单字段 → 重拉全局登录态 → 通知父组件刷新 * 为什么要 fetchUserInfo:后端已刷新 Redis 会话,前端重拉 my-info 才能让顶栏/工作台立即显示新头像昵称 */ -function handleSave() { - formApi.validate().then(async (e: any) => { - if (!e.valid) return; - const values = await formApi.getValues(); - saving.value = true; - try { - await updateMyProfile({ - nick_name: values.nick_name, - avatar: values.avatar || '', - }); - message.success('保存成功'); - await authStore.fetchUserInfo(); - emits('saved'); - } finally { - saving.value = false; - } - }); +async function handleSave() { + const e = await formApi.validate(); + if (!e.valid) return; + const values = await formApi.getValues(); + saving.value = true; + try { + await updateMyProfile({ + nick_name: values.nick_name, + avatar: values.avatar || '', + }); + message.success('保存成功'); + await authStore.fetchUserInfo(); + emits('saved'); + } finally { + saving.value = false; + } }