Compare commits

12 Commits
dev ... main

Author SHA1 Message Date
李琦
7fd14152ba feat: 地区选择器改走接口,修复辅料列表 key 重复
静态地区树改为 regionPicker API;新建就诊人异步解析地址;处方辅料列表 :key 改用 index 避免 drug_id 重复。
2026-10-15 10:02:17 +08:00
李琦
e6e51ac805 feat: 地区选择器改走接口,修复辅料列表 key 重复
静态地区树改为 regionPicker API;新建就诊人异步解析地址;处方辅料列表 :key 改用 index 避免 drug_id 重复。
2026-10-09 10:02:59 +08:00
李琦
1dff190e14 feat: 语音开方、修复分账问题、识图成方、识图传方、优化订单卡片展示 2026-09-30 17:02:44 +08:00
李琦
bd080674bf feat: 语音开方、修复分账问题、识图成方、识图传方、优化订单卡片展示 2026-09-23 16:22:27 +08:00
李琦
87f21de8de feat: 语音开方 2026-08-31 16:36:43 +08:00
李琦
8aafb0114b feat: 完善公账锁店流程,静态图改走 OSS
强化公账锁店页与首页横幅入口,支持声明已支付与催办;
代付二维码与 base64 工具抽到公共组件;本地 static 大图改为 OSS 引用。
2026-08-27 15:33:33 +08:00
李琦
158ffd9e6e feat: 完善公账锁店流程,静态图改走 OSS
强化公账锁店页与首页横幅入口,支持声明已支付与催办;
代付二维码与 base64 工具抽到公共组件;本地 static 大图改为 OSS 引用。
2026-08-21 12:52:09 +08:00
李琦
22e93c5dba feat: 锁定诊所、公账支付 2026-08-19 17:33:05 +08:00
李琦
2a1fe884d7 feat: 锁定诊所、公账支付 2026-08-19 16:48:43 +08:00
李琦
62ffb6fd31 feat: 工作台常用入口可批量编辑,简报改为横幅,列表补下拉刷新
各角色工作台支持批量保存常用入口和业务员看板;AI 简报改成单行横幅避免锁死滑动;仓库/提现/对账等列表统一下拉刷新,并补代付码弹窗。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 08:53:16 +08:00
李琦
97bc48c17c feat: 按 API 拆分规则新建 api/workbench.js(xk-api、unwrapXkApi 取值)。
新增 components/ai-brief-card/ai-brief-card.vue:卡片 + 全文底部弹层(page-container + v-if 拦截返回键),每日首弹用 storage 按天记录;模板遵守小程序限制(无 ?./??、无 :class 函数调用)。
      工作台页面挂载卡片并接入 refreshWorkbenchData,组件内部按「门店+日期」去重——页面 onShow 反复触发也只拉一次,切店/跨天自动重拉(跨天首次会重新生成并触发新一天的首弹)。
2026-08-17 09:51:49 +08:00
李琦
1fbc14638a feat: 工作台首页与即将到访预约抽屉
- 工作台:workbench 首页、诊所/平台 admin 首页改造
- 预约:新增 appointment-upcoming-drawer 即将到访预约抽屉
- 接口:reception/clinicAdmin/platformAdmin 模块补充接口
- 规范:新增 Api-Module-Split.mdc 接口模块拆分规则
2026-08-14 13:27:28 +08:00
269 changed files with 29811 additions and 2420 deletions

View File

@@ -0,0 +1,41 @@
---
description: 接口迁移到 xk-api 后必须拆出独立模块文件,禁止再往 all.js 大文件里塞
alwaysApply: true
---
# API 模块拆分规则(xk-api 迁移必读)
`api/all.js`(约 1700+ 行)是历史遗留的 Yii 接口聚合文件。
**任何迁移到 xk-api 的接口、以及所有新增的 xk-api 接口,一律不得写进 `all.js`**,必须提取/新建到 `api/` 下的独立业务模块文件。
## 必须做
1. **按业务域建独立文件**:`api/<module>.js`,小驼峰命名(已有先例:`pharmacistExamine.js`、`reception.js`、`clinicAdmin.js`、`platformAdmin.js`、`chat.js`)
2. **xk-api 接口 URL 必须带 `/newApi/` 前缀**(`common/js/common.js` 据此切到 `newApiBase`),Yii 接口用无前缀相对路径
3. **文件头注释标明后端与取值方式**,每个函数注释标 HTTP 方法与路径,例如:
```js
/**
* 药师审方相关接口(xk-api,取值用 code / result / message,配合 unwrapXkApi)
* 老 Yii service/v1/examine/* 已迁移到 xk-api 的 /pharmacist-examine/*
*/
import { req } from '@/common/js/index.js';
/** 审方列表 xk-api GET /pharmacist-examine/list */
export function getExamineList(data) {
return req.request({
url: '/newApi/pharmacist-examine/list',
method: 'GET',
data
})
}
```
4. **迁移某接口时**(Yii → xk-api):把它从 `all.js` 挪到对应模块文件,`all.js` 里原函数直接删除(调用方改 import),确实来不及改完所有调用方时才允许在 `all.js` 保留一行 re-export 并标 `@deprecated`
5. 页面取值统一 `unwrapXkApi`(见 Api-Response.mdc)
## 禁止做
- 禁止在 `all.js` 中新增任何函数(包括 Yii 接口——新 Yii 接口也应放模块文件)
- 禁止新代码写 `unwrapClinicRes` 这类「双形态自动兼容」取值;按后端固定取值
- 禁止一个模块文件里混装两个后端的接口而不加注释区分;同一文件确需并存时,每个函数注释必须标明 `// xk-api` 或 `// Yii`

View File

@@ -0,0 +1,32 @@
---
description: 后台角色(诊所/平台/业务员)必须复用 sub_business_shared,禁止再复制商品订单等业务页
alwaysApply: true
---
# 后台角色共享页(禁止重复造轮子)
诊所管理员 / 平台管理员 / 业务员(含省/市经理)的业务页优先复用 `subPackages/sub_business_shared`,用 `businessMixin` + `resolveBusinessContext` 按 `loginMode` 切 API,不要再复制一套列表/详情。
## 商品订单
- **必须**走 `sub_business_shared/order`(平台、业务员)
- 诊所管理员继续用历史页 `sub_clinic_admin/order`,**本次不迁、也不要再抄一份**
- API:平台 `platform-admin-order/*`,业务员 `salesperson-order/*`(`api/businessApi.js`)
- **数据范围跟 PC**:业务员/省/市经理看名下门店全部订单(后端 `getMyStoreIds()` / `store.code = admin.code`),**禁止**前端再按 `salesperson_id` 过滤
- **操作跟 PC**:业务员 `canShip/canRefund = false`(`context.js` 的 `bizCap`),详情按钮按能力显隐
```js
// ❌ BAD:再写一套业务员订单页,或按推广员 ID 滤单
// ✅ GOOD:工作台入口指向 /subPackages/sub_business_shared/order/index
```
## 可复用清单(先找再写)
`business-page-layout`、`CollapsibleFilterPanel`、`ListSkeleton`、`InputListPage`、`xk-list-card`、`filterCache`、`pullRefreshMixin`
## 下拉刷新
- 自定义导航 + 内部列表:`scroll-view` 的 `refresher-enabled` + `isRefreshing`(对齐仓库/订单),mixin 用 `pullRefreshMixin`
- 整页滚动列表:`pages.json` 开 `enablePullDownRefresh` + `onPullDownRefresh` + `uni.stopPullDownRefresh`
- **禁止**给聊天页(`sub_online_reception/pages/chat`)加下拉刷新
- 开方编辑器、录入/申请表单、登录、协议、静态成功页不加

View File

@@ -0,0 +1,11 @@
---
description: 不懂先问,明确目标后再动手
alwaysApply: true
---
# 先问再做
1. **不懂就问**:需求、业务口径、技术选型、影响范围等有不确定之处,必须先向你确认,禁止凭猜测直接改代码。
2. **一问一答**:每次只提一个最关键的问题,等你回复后再继续;避免一次抛出多个选项把讨论打散。
3. **说明提问原因**:每个问题都要附带「我为什么要问」——例如缺什么信息、有哪些备选理解、猜错会有什么后果。
4. **目标对齐后再动手**:确认对你的目标(要达成什么、不做什么、验收标准)有明确认知后,再开始查代码、写 SQL、改页面。

View File

@@ -6,7 +6,7 @@ alwaysApply: true
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则 1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行 2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子 3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show) 4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show);**禁止 v-if 与 :show 同绑**,开/关拆分与 350ms 卸挂载对齐切换账号,详见 `Page-Container-Drawer.mdc`
5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化 5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化
6. 微信小程序 `.vue` 模板(会编译成 WXML)禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.` 6. 微信小程序 `.vue` 模板(会编译成 WXML)禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定 7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定

View File

@@ -6,9 +6,11 @@ alwaysApply: true
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则 1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行 2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子 3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show) 4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show);**禁止 v-if 与 :show 同绑**,开/关拆分与 350ms 卸挂载对齐切换账号,详见 `Page-Container-Drawer.mdc`
5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化 5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化
6. 微信小程序 `.vue` 模板(会编译成 WXML)禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.` 6. 微信小程序 `.vue` 模板(会编译成 WXML)禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定 7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定
8. 接口取值:写代码时先确认接口是 xk-api 还是 Yii,再固定用 `code/result/message` 或 `errcode/data/msg`(可用 `unwrapXkApi` / `unwrapYiiApi`);禁止运行时猜测、禁止 `result || data`(详见 Api-Response.mdc) 8. 接口取值:写代码时先确认接口是 xk-api 还是 Yii,再固定用 `code/result/message` 或 `errcode/data/msg`(可用 `unwrapXkApi` / `unwrapYiiApi`);禁止运行时猜测、禁止 `result || data`(详见 Api-Response.mdc)
9. VIP 功能判断:统一用 `@/utils/vip.js` 的 `hasVipPermission` / `fetchStoreVipPermissions` / `VIP_FEATURE`;模板显隐用 `<vip-gate code="medical_record" :permissions="storeVipPermissions">`;禁止各页手写 `permissions.indexOf` 或重复解析 get-current-store-type 9. VIP 功能判断:统一用 `@/utils/vip.js` 的 `hasVipPermission` / `fetchStoreVipPermissions` / `VIP_FEATURE`;模板显隐用 `<vip-gate code="medical_record" :permissions="storeVipPermissions">`;禁止各页手写 `permissions.indexOf` 或重复解析 get-current-store-type
10. 卡片强调样式:禁止用左侧竖色条(`border-left` 色条/左侧色块)做卡片强调或分类标识,统一用「1px 同色系边框 + 同色低透明度外发光(box-shadow 微光)」;历史页面暂不强制回改,新增/改动的卡片必须遵守
11. API 模块拆分:迁移到 xk-api 的接口和所有新增接口必须放 `api/<module>.js` 独立模块文件,禁止再往 `api/all.js` 大文件里新增(详见 Api-Module-Split.mdc)

View File

@@ -0,0 +1,95 @@
---
description: 小程序抽屉 page-container 返回拦截(对齐切换账号 / 工作台切换诊所)
globs: **/*.{vue,js}
alwaysApply: false
---
# 小程序抽屉返回拦截(强制)
标杆实现:
- **切换账号**:`components/account-switch/account-switch-panel.vue`
- **工作台切换诊所**:`pages/workbench/index.vue`(门店选择 `storePageGuard` + `openShow`)
新增/改抽屉、底部弹层、认领码等凡需拦返回的,**必须**按本规则,禁止再 `v-if` 与 `:show` 绑同一个变量。
## 核心原则
1. 微信**同一页只能有一个** `page-container`;多抽屉必须互斥(先卸再挂,或等约 350ms)。
2. **挂载**与**可见**拆开:
- `pageGuard`(或 `storePageGuard`)→ `v-if`(控制是否挂载)
- `visible` / `panelShow` / `openShow` → `:show`(控制是否展示)
3. 必须用 `v-if`,禁止 `v-show` 控制 `page-container`。
4. 关抽屉:先 `:show = false` 播离场,再 **约 350ms**(或 `@afterleave`)后才 `pageGuard = false` 卸节点。
5. 开抽屉:先 `pageGuard = true`,`$nextTick` 后再 `:show = true`。
## 标准写法(对齐切换账号)
```vue
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="pageGuard"
:show="visible"
:overlay="false"
@leave="close"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
```
```js
// 打开
open() {
this.clearReleaseTimer();
this.pageGuard = true;
this.$nextTick(() => {
this.visible = true;
});
},
// 关闭
close() {
this.visible = false;
this.scheduleReleaseGuard(); // 350ms 后再 pageGuard=false
},
releasePageGuard() {
this.clearReleaseTimer();
this.pageGuard = false;
},
scheduleReleaseGuard() {
this.clearReleaseTimer();
this.releaseTimer = setTimeout(() => {
this.pageGuard = false;
this.releaseTimer = null;
}, 350);
},
```
## 禁止
```vue
<!-- ❌ v-if 与 :show 同绑 → 二次侧滑/返回不再拦截 -->
<page-container v-if="show" :show="show" />
<!-- ❌ 关抽屉立刻 v-if=false → 页面滑动锁残留 / 二次打开失败 -->
<page-container v-if="show" :show="true" />
```
## 同页多抽屉互斥(工作台切换诊所已落地)
打开 A 前必须先关掉 B 的 `page-container`:
- 关本层:可先 `show=false` 再 `scheduleReleaseGuard(350)`。
- **立刻要挂另一层**:必须 **同步** `pageGuard = false`(不要等 350ms),或等对方卸完再挂(约 360ms)。
- 参考:`openStoreSelect` / `openApptDrawer` / `onBriefPopupChange`(工作台)。
## 组件内嵌套两层抽屉(如患者详情 + 回访)
同一组件内也只能保留一个 `page-container`:
1. 开内层前:外层 `panelShow=false` 且 **立刻** `pageGuard=false`(勿只靠 afterleave,晚到会冲掉恢复后的 guard)。
2. 关内层后:再 `pageGuard=true` → `$nextTick` → `panelShow=true`。
3. 恢复逻辑加防重入,避免 `update:visible` + `close` 双事件恢复两次。
## 与 Code-Standards 的关系
`Code-Standards.mdc` 第 4 条要求抽屉必须配 `page-container`;**具体拆分与 350ms 卸挂载以本规则 + 切换账号组件为准**。

View File

@@ -0,0 +1,90 @@
# xk-doctor-wx 长期约定
> 只放「下次动手前必须先知道」的规则与契约。踩坑经过、逐轮取舍、验证输出在 `.workbuddy/memory/YYYY-MM-DD.md`;
> 无运行环境下的验证方法论在技能 `uni-app-sfc-change-verification`(改 UI 前先加载,别在这里重复)。
## 设计语言(改 UI 前先对齐,别另起一套)
- **品牌色**:浅 `#6ACDBB`、深 `#008771`;导航渐变 `linear-gradient(90deg,#00BFA6 0%,#6ACDBB 93%)`。
- **页面底** `#f9fafb` / `#f7f8fa`;卡片白 + 圆角 `24~26rpx` + 浅阴影。**数字**(看板/金额)用 `"DIN Alternate", sans-serif`。
- **文本色阶**:`#16302C`(主)/ `#7A8794`(次)/ `#98A2B3`(三级)。
- **识图传方这条业务线的 C 端语言 = 液态玻璃**(基准文件 `subPackages/sub_image_transfer/order-list.vue`):
```
background: linear-gradient(158deg, rgba(255,255,255,.68), rgba(255,255,255,.34));
border: 1rpx solid rgba(255,255,255,.72); backdrop-filter: blur(14~22px); /* 用 px */
box-shadow: 0 12rpx 34rpx rgba(12,48,44,.12), 0 2rpx 8rpx rgba(12,48,44,.05),
0 0 0 1rpx rgba(0,135,113,.12), /* 青绿外环:浅底上唯一能圈边界的东西,别删 */
inset 0 1rpx 0 rgba(255,255,255,.90);
border-radius: 32rpx; 页面底 linear-gradient(180deg,#e8f6f4,#f4f6f8 42%,#eef2f5)
```
**三条铁律**:① 玻璃要成立,底下必须有可折射的底色(`radial-gradient` 色斑做环境色层,**别用 `filter: blur()`**);纯灰底上怎么调 backdrop-filter 都只是白卡。② 浅底上白描边等于没边界,必须补青绿外环。③ 不支持 backdrop-filter 时退化为半透明渐变卡,不会变回白块。
## 实心按钮的白字必须落在足够深的面上(对比度实测)
浅牌青绿 + 白字不达标:`#00BFA6 → #6ACDBB` 实测仅 **2.22:1**;`#5EC8B4 → #6ACDBB` ≈ 2.0:1(连大字 3:1 都不到)。
凡「实心填充 + 白字」统一用**深牌青绿** `linear-gradient(135deg, #00806A 0%, #10A288 100%)`(文字处 4.3:1)——已用于 `g2__cta`、`.recent-empty__btn`。浅牌只配「浅面 + 深字」(`#E4F7F3` 面 + `#07564A` 字 = 7.75:1)。
**教训**:白字按钮底色别凭感觉挑,用渲染截图取像素算 WCAG;手算渐变中间点不可靠。
> 遗留:`ImageOcrDrawer` 的 `ocrPrimaryBtnStyle` 仍是 `#6ACDBB` + 白字(≈2.2:1)——已三次提出,待回复。
## ⚠ WXSS 可移植性(本仓特有;通用 mp-weixin 约束见技能 §5)
- **flex `gap` 在旧版安卓内核可能静默失效** → 一律 `margin-left`/`margin-right` + `:last-child { margin-right: 0 }`。
- **后代选择器 `.a .b` 在 WXSS 不可靠** → 拆成独立类(`.g2__cta-text--ghost`,而不是 `.g2__cta--ghost .g2__cta-text`)。
- **本仓未验证过的 CSS 一律不用**:`clip-path` / `mask-composite` / `filter: blur()` 全仓 grep 为 **0**。要「揭示」用 `overflow:hidden` + `height` 动画;要「未生效」用透明度对比。
- 其余(`<text>` 不吃宽高、`:key`/`:class` 禁字面方法调用、`hover-class` 必须静态、`v-if/v-else` 必须紧邻)见技能 §5。
## 传方订单状态口径(单一来源,禁止各页自写)
`subPackages/sub_image_transfer/common/display.js` 导出 `transferStatusMeta / transferPillStyle / transferStatusText`:
`1 待审核 #FF9900` / `2 已驳回 #FA3534` / `3 已传方 #12B886` / `4 传方失败 #E4572E`,未知兜底 `#8A94A6`。
## 接口与鉴权
- 传方员(role=17)与诊所管理员**共用 clinic token**:走 `api/clinicAdmin.js` 的 `clinic-admin-*`,及 `api/imageTransferOrder.js` 里带 `_clinicAdmin` 头的 `/newApi/clinic-admin-image-transfer/*`。
- 列表返回 `{ code, result: { items: [...] }, message }`,用 `unwrapXkApi` 解包(`code===0` 取 **`result`**,不是 `data`)。
- 传方单字段(已核对):`id / patient_name / patient_mobile / doctor_name / uploader_role_text / status / status_text / reject_reason / drug_count / drug_preview / mes_message / created_at / total_price / can_view_price`;`total_price` 仅 `can_view_price === 1` 的门店可见。
## 平台瓷片风格(card_style):02 是主力
`utils/workbenchCardPref.js` 合法值 `02/03/04/08/17/19`,非法回落 `02`;`components/workbench-tile-board/` 渲染,被 `workbench-user-entries`(四个首页)+ `sub_transfer_clerk/home/index.vue` 消费 → **改它 = 五端一起变**(`02/03/17` 自带分区标题,`04/08/19` 不带)。
**02(定稿第 3 版,`g2*` 类名族)= 液态玻璃栅格**:版式由 `wideIdx = N===1 ? 0 : (N%2===1 ? N-1 : -1)` 一条规则决定(1 条整行 / 2 条双列 / 3 条前两条双列 + 第三条整行,避免「两列 + 半个空槽」破版);主次**不靠明暗,靠材质 + 按钮形态**(主卡 = 染色玻璃 + 实心 CTA,次卡 = 清透玻璃 + 幽灵 CTA),两卡等高。
- 两个易忘的配置契约:`workbench-user-entries` 里 **02 的 `tileBoardTitle` 传空串**(上方 `wue-header` 已有「我的常用」,否则标题出现两遍);图标走 `ICON_TILES` 表(`entry.key || entry.code` → `/static/image/transfer/*.png`),未命中回落原 `g2__chip` + u-icon。
- ⚠ 走错过的路:**彩色磁贴不能放在深色卡面上**(圆角轮廓被同色吃掉,只剩白物体飘着 + 脏影)→ **入口卡一律浅面,颜色全交给磁贴**;主次改用「品牌浅染色面 + 稍重投影 + 实心 CTA」建立(磁贴生成配方见 2026-10-12 日志)。
- 分区标题现行写法:**标题(32rpx/700/字距 1rpx) + 渐隐细线(2rpx 青绿淡出) + 右侧动作**(左侧竖条已废弃两轮)。
## 识图传方 upload 页(`sub_image_transfer/upload.vue`)
真实身份是「识图抽屉的等待屏 + 兜底层」,**不是落地页**(系统导航栏已写标题,别再加大标题)。
- **⚠ gate 不能写 `v-if="!showOcr"`**:`showOcr` 在 `onReady` 就置 true,而抽屉是 **98KB 分包异步组件**、`pages.json` 配了 `componentPlaceholder: view`(加载期只渲染**一个空 view**)→「进页 → 抽屉盖上」这段真实等待期里用户看到的是**一片空白**。**正解:gate 常驻**,由抽屉 `position:fixed; z-index:10090` 的不透明渐变底盖住;抽屉加载失败时它正是「点一下就重挂载」的兜底层。
- **兜底按钮(已修的真 bug)**:`openOcr()` 原来只 `showOcr = true`,抽屉已处于「应显示」态时再点等于把 true 赋成 true → **点了毫无反应**。现改为 `ocrKey += 1` 强制重挂载(模板 `:key="ocrKey"`)。
- 版式(`gate*`):底色与抽屉 `.ocr-host` **同一条渐变**(否则盖上瞬间闪色);流光条用 `@keyframes gate-sweep` 跑 `translateX`(别改 left);三步玻璃卡高亮用 3 个独立类(**绝不用后代选择器**)。
## 识图抽屉 ImageOcrDrawer(`sub_workbench/prescription_v2/components/ImageOcrDrawer.vue`)
**医生端(开方)与诊所端(传方)共用** → 改它先想清楚影响哪几端。传方专属结构一律走 `tx-` 类名族 + `v-if="isTransfer"` 隔离,医生端分支不动。
### ⚠ 两个通用坑
- **u-popup 的 z-index 只在 center 模式够高**:`center` 内置 `99999`;`bottom/top/left/right` 用 `$u.zIndex.popup`(默认 **10075**)。而 `.ocr-host` 是 `z-index:10090` → **bottom 模式的 u-popup 会被整个识图面板盖住,表现为「点了没反应」**。正解 `<u-popup mode="bottom" :z-index="10100">`。凡用非 center 模式,先查父级有没有更高的 z-index 元素。
- **page-container 同页只能有一个**(双实例双双失效)。防误触推出:① 抽屉打开期间**保持 `panelShow = true`**(变 false → afterleave → closed → 父页 navigateBack);② `handlePageLeave` 加分支:关内层浮层 → `$nextTick` 恢复 `panelShow = true` → return,**吞掉这次返回**;③ 分支顺序放在内层浮层(中药匹配 / 克数弹窗 / 医生选择)**之后**(抽屉 10100 < 克数弹窗 99999,更靠上的先关)。这顺序是逻辑用例抓出来的。
### 传方端改造(tx- 类名族)
- **生成记录**:顶部横滑卡列表(固定高、横滑手势与纵向滚动打架)→ 右下 **FAB + 底部抽屉**(FAB 用 `--raised` 躲开结果态 `.ocr-footer`)。**上传区**:140rpx「+」灰方块 → 通栏虚线投放区(240rpx)。**已对照卡**:「重新搜索」文字链 → 与候选同尺寸的「换药」虚线 chip,并入候选行末尾。
- **折叠摘要行(方案 A)**:就诊人 / 处方 / 开方医生改为 `.tx-acc` 折叠行(head 整行可点,`min-height:88rpx`),费用合计并入底栏 `.tx-sum` 常驻,删掉独立的「费用预览」卡。三个 key 必须预置进 `data.txAccOpen`(Vue2 侦听)。
- `initAccOpenOnce()` 只在首次进结果态按「缺什么」推断一次(`txAccInited` 把关),之后尊重手动操作;`isResultView` 变 false 时重置。
- 校验失败走 `openAccFor(key, anchor)`:**先展开再滚**(滚的是 `#ocr-cm-wrap`,不展开它不在 DOM 里);`scrollDrawerTo` 内 `$nextTick + setTimeout(80ms)` 等渲染。
- 展开态 `.tx-acc--open` **必须真的写出来**(只写 `:class` 不写样式 = 静默失效)。底栏要与 `.tx-sum` 共存:`'is-full': !isStandaloneChrome && !txShowFooterSum`。
- **结果态缩略图去重**:曾同时渲染两组(都调 `previewOrigin`)。修复:`img-row` 加 `&& !isResultView`(顶部那组的 `×` 在结果态**有害** —— `removeImage` 只 splice 不重识别,缩略图会和药味对不上);结果态那组改 `v-if="imageUrls.length"`;删重复 `sec-title`。
### 识别中的全屏 Loading(`.ocr-scan` 遮罩 + `ocr-image-loading` 组件)
**视觉基准 = PC 端 `xk-admin/apps/web-antd/src/components/loading/OcrImageLoading.vue`**(识图成方:扫描原图 → 进度条 → 结构化处方逐条揭示)。
⚠ `xk-admin` 里有**两个** loading:`PrescriptionLoading.vue` 对应小程序既有的 `components/ai-generation-loading`,**别认错**。
以下是不看代码就不知道的契约(其余 rationale 已写进两个文件本身的注释):
1. **不用 canvas**(我第一版方向就错了):① PC 那个组件本身就是 DOM + CSS(`requestAnimationFrame` 改 style);② `ai-generation-loading.vue` 注释早已写明「用 CSS 动画贴近 PC 效果,**避免小程序 canvas 生命周期问题**」——本仓刻意避开 canvas,全仓 `type="2d"` 为 0。
2. **组件只负责卡本体**(`components/ocr-image-loading/`,类名族 `oil__`,`easycom.autoscan` 自动解析、**无需** pages.json 条目);**全屏遮罩与「取消识别」出口留在 `ImageOcrDrawer`**(遮罩要盖 FAB,属调用方职责,卡里写 `position:fixed` 就是越界)。新层是 `ocr-drawer` 的**兄弟节点**(`tx-fab` 之后、记录抽屉之前),`absolute; inset:0; z-index:100` 盖住 header/body/footer/FAB(FAB 是 30)。
3. **零 setData**:扫描线 / 揭示 / 进度条 / 骨架行入场全走 WXSS `animation`;只有百分比必须 JS(文本内容改不了),`setInterval(…, 90)` 单字段、到顶自停。三个时长必须同速(= 脚本常量 `OIL_SCAN_MS` 2400 = `.oil__fill`/`.oil__reveal`/`.oil__scan` 的 duration),且「揭示目标高度 = `.oil__doc` 高度 = 扫描线位移 = 300rpx」,**改一个得改四个**;`const OIL_SCAN_MS` 要放在 `export default` **之前**(`startPct` 读它,写在后面 TDZ 报错)。
4. **只播一次不循环**(识别耗时不固定,循环回绕会出现「100% → 0%」的反直觉),三条动画用 `both` 停终态;**右侧只给骨架条不给药名**(PC 里的「炙黄芪 15g…」是演示数据,显示药名会让人误以为已识别完)。
旧 `.ocr-loading*` 只有一根**静态**渐变条(整条规则里没有任何 `animation`)→ 真机根本不动,已连样式整块删除。**删 `<view v-if="loading">` 后紧跟的 `v-else-if` 必须改 `v-if`,否则断链。**
全屏遮罩挡住 header/footer,而微信 `request` **默认超时 60s** → 留了克制的「取消识别」出口(走 `handleClose`,`requestSeq` 会丢弃在途响应)。
> **待确认的取舍**:扫描 2.4s 播完后百分比停在 100%,若请求还要十几秒,用户可能误以为「已完成、结果呢」。可选改为封顶 96%(永不宣称完成);目前按「对齐 PC」保留 100%。
## 本机验证工具(无微信开发者工具 / HBuilderX 时)
工具在 `C:\Users\admin\.workbuddy\binaries\node\workspace\`(隔离工作区,不进仓库),跑法
`"C:/Users/admin/.workbuddy/binaries/node/versions/22.22.2-6/node.exe" <script>`(`verify-sfc.cjs` 需加 `--experimental-vm-modules`):
`verify-sfc.cjs`(模板编译 + 脚本语法 + **类名对账**,按文件配前缀 `g2`/`gate`/`tx`/`oil`)、
`verify-logic.cjs` / `verify-tile-logic.cjs` / `verify-upload-logic.cjs` / `verify-ocr-logic.cjs`(抽屉不动 panelShow、`handlePageLeave` 分支顺序、医生端隔离)/ `verify-oil-logic.cjs`(`ocr-image-loading`)、
`shot-*.cjs`、`gen-loading-preview.cjs`(真实渲染截图与量测)。
**改完 UI 三个层面(SFC/逻辑/渲染)都要跑绿再交付。**
夹具与断言的写法坑(含本项目踩过的 `isTransfer` 覆盖、注释误匹配、永真式负向用例)见技能 §8–§10。

49
_tmp_scan_excipient.py Normal file
View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
import re
from pathlib import Path
files = [
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_workbench\prescription_v2\components\modals\ChineseMedicineModal.vue'),
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_workbench\prescription_v2\index.vue'),
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_clinic_admin\order\components\PrescriptionDetailPopup.vue'),
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_business_shared\order\components\PrescriptionDetailPopup.vue'),
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_my\prescriptionDetail.vue'),
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_workbench\prescriptionDetail.vue'),
Path(r'd:\worker\code\xk-doctor-wx\subPackages\sub_workbench\prescription_v2\components\modals\SimpleProductModal.vue'),
]
for p in files:
if not p.exists():
print('MISSING', p)
continue
text = p.read_text(encoding='utf-8')
lines = text.splitlines()
in_tpl = False
issues = []
for i, line in enumerate(lines, 1):
if re.match(r'\s*<template[\s>]', line) or line.strip() == '<template>':
in_tpl = True
continue
if re.match(r'\s*</template>', line):
in_tpl = False
continue
if not in_tpl:
continue
stripped = line.strip()
if '??' in line or re.search(r'\?\.[A-Za-z_\[]', line):
issues.append((i, 'optional', stripped[:140]))
if re.search(r':class\s*=\s*"[^"]*[a-zA-Z_][\w]*\([^)]*\)', line) or re.search(r":class\s*=\s*'[^']*[a-zA-Z_][\w]*\([^)]*\)", line):
issues.append((i, 'class-fn', stripped[:140]))
if re.search(r':style\s*=\s*"[^"]*[a-zA-Z_][\w]*\([^)]*\)', line) or re.search(r":style\s*=\s*'[^']*[a-zA-Z_][\w]*\([^)]*\)", line):
issues.append((i, 'style-fn', stripped[:140]))
if 'page-container' in line and 'v-show' in line:
issues.append((i, 'pc-vshow', stripped[:140]))
if re.search(r'v-if\s*=\s*"[^"]*\bshow\b[^"]*"', line) and 'page-container' in ''.join(lines[max(0,i-3):i+2]):
# crude: same-var check later
pass
print('===', p.name, '===')
if not issues:
print(' (no template ??/?. / class-fn / style-fn)')
else:
for it in issues:
print(' L%s [%s] %s' % (it[0], it[1], it[2]))

View File

@@ -70,6 +70,13 @@ export function createBusinessApi(prefix, headerFlag) {
submitWithdrawal(data) { submitWithdrawal(data) {
return bizRequest({ url: `${prefix}-withdrawal/withdrawal`, method: 'POST', data }); return bizRequest({ url: `${prefix}-withdrawal/withdrawal`, method: 'POST', data });
}, },
getWithdrawableOrders(params) {
return bizRequest({ url: `${prefix}-withdrawal/withdrawable-orders`, method: 'GET', data: params || {} });
},
/** 提现规则文案 xk-api GET /{prefix}-withdrawal/rules */
getWithdrawalRules() {
return bizRequest({ url: `${prefix}-withdrawal/rules`, method: 'GET', data: {} });
},
passWithdrawal(data) { passWithdrawal(data) {
return bizRequest({ url: `${prefix}-withdrawal/pass-application`, method: 'POST', data }); return bizRequest({ url: `${prefix}-withdrawal/pass-application`, method: 'POST', data });
}, },
@@ -122,6 +129,22 @@ export function createBusinessApi(prefix, headerFlag) {
wareSendOrder(data) { wareSendOrder(data) {
return bizRequest({ url: `${prefix}-order/ware-send`, method: 'POST', data }); return bizRequest({ url: `${prefix}-order/ware-send`, method: 'POST', data });
}, },
/** 发货弹窗打开:当前收件信息 xk-api GET /{prefix}-order/ship-receiver */
getShipReceiver(orderId) {
return bizRequest({
url: `${prefix}-order/ship-receiver`,
method: 'GET',
data: { order_id: orderId },
});
},
/** 发货弹窗「用诊所地址」xk-api GET /{prefix}-order/store-ship-address */
getStoreShipAddress(orderId) {
return bizRequest({
url: `${prefix}-order/store-ship-address`,
method: 'GET',
data: { order_id: orderId },
});
},
getExpressDetailByOrderId(orderId) { getExpressDetailByOrderId(orderId) {
return bizRequest({ return bizRequest({
url: `${prefix}-express-detail/detail-by-order`, url: `${prefix}-express-detail/detail-by-order`,

View File

@@ -78,6 +78,14 @@ export function getClinicAdminDashboardStats() {
return clinicRequest({ url: `${PREFIX}-dashboard/stats`, method: 'GET' }); return clinicRequest({ url: `${PREFIX}-dashboard/stats`, method: 'GET' });
} }
/**
* 未来7天预约明细(工作台预约卡片点击弹层,本店维度)
* xk-api GET /clinic-admin-dashboard/appointment-upcoming-list
*/
export function getClinicAdminAppointmentUpcomingList(data) {
return clinicRequest({ url: `${PREFIX}-dashboard/appointment-upcoming-list`, method: 'GET', data });
}
/** /**
* 工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码) * 工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /clinic-admin-dashboard/entries * xk-api GET /clinic-admin-dashboard/entries
@@ -102,6 +110,14 @@ export function toggleClinicAdminFavoriteEntry(code) {
return clinicRequest({ url: `${PREFIX}-dashboard/toggle-favorite-entry`, method: 'POST', data: { code } }); return clinicRequest({ url: `${PREFIX}-dashboard/toggle-favorite-entry`, method: 'POST', data: { code } });
} }
/**
* 批量保存我的常用(功能页悬浮编辑 / 首页快捷添加)
* xk-api POST /clinic-admin-dashboard/batch-save-favorite-entry
*/
export function batchSaveClinicAdminFavoriteEntry(codes) {
return clinicRequest({ url: `${PREFIX}-dashboard/batch-save-favorite-entry`, method: 'POST', data: { codes } });
}
/** /**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败) * 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /clinic-admin-dashboard/report-entry-usage * xk-api POST /clinic-admin-dashboard/report-entry-usage
@@ -162,6 +178,16 @@ export function submitWithdrawal(data) {
return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawal`, method: 'POST', data }); return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawal`, method: 'POST', data });
} }
/** 可提订单明细 xk-api GET /clinic-admin-withdrawal/withdrawable-orders */
export function getWithdrawableOrders(params) {
return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawable-orders`, method: 'GET', data: params || {} });
}
/** 提现规则文案 xk-api GET /clinic-admin-withdrawal/rules */
export function getWithdrawalRules() {
return clinicRequest({ url: `${PREFIX}-withdrawal/rules`, method: 'GET', data: {} });
}
export function getReconciliationList(params) { export function getReconciliationList(params) {
return clinicRequest({ url: `${PREFIX}-reconciliation/list`, method: 'GET', data: params }); return clinicRequest({ url: `${PREFIX}-reconciliation/list`, method: 'GET', data: params });
} }
@@ -263,3 +289,8 @@ export function updateWarehousePrice(data) {
export function getWarehouseProductTypeOptions() { export function getWarehouseProductTypeOptions() {
return clinicRequest({ url: `${PREFIX}-warehouse/product-type-options`, method: 'GET' }); return clinicRequest({ url: `${PREFIX}-warehouse/product-type-options`, method: 'GET' });
} }
/** 诊所/药店可见仓库 Tab xk-api GET /clinic-admin-warehouse/warehouse-tabs */
export function getWarehouseTabs() {
return clinicRequest({ url: `${PREFIX}-warehouse/warehouse-tabs`, method: 'GET' });
}

View File

@@ -114,6 +114,14 @@ export function toggleClinicSalespersonFavoriteEntry(code) {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } }); return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
} }
/**
* 批量保存我的常用(功能页悬浮编辑 / 首页快捷添加)
* xk-api POST /clinic-salesperson-dashboard/batch-save-favorite-entry
*/
export function batchSaveClinicSalespersonFavoriteEntry(codes) {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/batch-save-favorite-entry', method: 'POST', data: { codes } });
}
/** /**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败) * 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /clinic-salesperson-dashboard/report-entry-usage * xk-api POST /clinic-salesperson-dashboard/report-entry-usage

View File

@@ -89,16 +89,7 @@ export function imageOrderInfo(data) {
}) })
} }
/*咨询列表 挂号列表 // 老 Yii register/list(挂号列表)已迁移到 xk-api doctor-reception-wx/patient-list(见 api/reception.js getPatientList)
order_id int 是 订单id
*/
export function registerList(data) {
return req.request({
url: 'register/list',
method: 'post',
data
})
}
// 咨询列表角标 // 咨询列表角标
/** /**
* @param {Object} data * @param {Object} data

232
api/imageTransferOrder.js Normal file
View File

@@ -0,0 +1,232 @@
/**
* 识图传方订单(xk-api)
* 医生:/image-transfer-order-wx/* 取值 unwrapXkApi
* 诊所管理员:/clinic-admin-image-transfer/* 请求头 _clinicAdmin
*/
import { req } from '@/common/js/index.js';
function clinicReq(options) {
return req.request({
...options,
header: {
...(options.header || {}),
_clinicAdmin: '1',
},
});
}
/** 平台管理员请求:带 _platformAdmin 头走平台传方接口 */
function platformReq(options) {
return req.request({
...options,
header: {
...(options.header || {}),
_platformAdmin: '1',
},
});
}
/** 医生我的传方 xk-api GET /image-transfer-order-wx/list */
export function listMyImageTransferOrders(data) {
return req.request({
url: '/newApi/image-transfer-order-wx/list',
method: 'GET',
data,
});
}
/** 医生传方详情 xk-api GET /image-transfer-order-wx/detail */
export function getMyImageTransferOrder(data) {
return req.request({
url: '/newApi/image-transfer-order-wx/detail',
method: 'GET',
data,
});
}
/** 医生驳回后再交 xk-api POST /image-transfer-order-wx/resubmit */
export function resubmitMyImageTransferOrder(data) {
return req.request({
url: '/newApi/image-transfer-order-wx/resubmit',
method: 'POST',
data,
});
}
/** 诊所识图 OCR xk-api POST /clinic-admin-image-transfer/ocr-prescription-image */
export function clinicOcrPrescriptionImage(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/ocr-prescription-image',
method: 'POST',
data,
});
}
/** 诊所药库搜索 xk-api GET /clinic-admin-image-transfer/product-list */
export function clinicSearchTransferDrugs(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/product-list',
method: 'GET',
data,
});
}
/** 诊所提交识图传方 xk-api POST /clinic-admin-image-transfer/confirm */
export function clinicConfirmImageTransfer(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/confirm',
method: 'POST',
data,
});
}
/** 诊所识图传方匹配就诊人 xk-api POST /clinic-admin-image-transfer/match-transfer-patient */
export function clinicMatchTransferPatient(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/match-transfer-patient',
method: 'POST',
data,
});
}
/**
* 诊所识图传方:按手机号列出名下就诊人 xk-api POST /clinic-admin-image-transfer/transfer-patients-by-mobile
* 入参 patient_mobile(请求拦截器加密);返回 items[].mobile 已被响应拦截器脱敏,明文在 mobile_txt
*/
export function clinicTransferPatientsByMobile(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/transfer-patients-by-mobile',
method: 'POST',
data,
});
}
/** 诊所识图历史列表 xk-api GET /clinic-admin-image-transfer/ai-list-generations */
export function clinicAiListGenerations(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/ai-list-generations',
method: 'GET',
data,
});
}
/** 诊所识图历史详情 xk-api GET /clinic-admin-image-transfer/ai-generation-detail */
export function clinicAiGenerationDetail(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/ai-generation-detail',
method: 'GET',
data,
});
}
/** 诊所传方列表 xk-api GET /clinic-admin-image-transfer/list */
export function listStoreImageTransferOrders(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/list',
method: 'GET',
data,
});
}
/** 诊所传方详情 xk-api GET /clinic-admin-image-transfer/detail */
export function getStoreImageTransferOrder(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/detail',
method: 'GET',
data,
});
}
/** 诊所驳回后再交 xk-api POST /clinic-admin-image-transfer/resubmit */
export function resubmitStoreImageTransferOrder(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/resubmit',
method: 'POST',
data,
});
}
/** 医生传方报价预览 xk-api POST /image-transfer-order-wx/quote */
export function quoteMyImageTransferOrder(data) {
return req.request({
url: '/newApi/image-transfer-order-wx/quote',
method: 'POST',
data,
});
}
/** 诊所传方报价预览 xk-api POST /clinic-admin-image-transfer/quote */
export function quoteStoreImageTransferOrder(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/quote',
method: 'POST',
data,
});
}
/**
* 诊所委托调剂规则级联列表
* xk-api GET /clinic-admin-image-transfer/process-rule-list
* 入参与医生端一致:pid 拉下级,rule_id 拉备注
*/
export function clinicProcessRuleList(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/process-rule-list',
method: 'GET',
data,
});
}
/** 平台传方列表 xk-api GET /platform-admin-image-transfer/list */
export function listPlatformImageTransferOrders(data) {
return platformReq({
url: '/newApi/platform-admin-image-transfer/list',
method: 'GET',
data,
});
}
/** 平台传方详情 xk-api GET /platform-admin-image-transfer/detail */
export function getPlatformImageTransferOrder(data) {
return platformReq({
url: '/newApi/platform-admin-image-transfer/detail',
method: 'GET',
data,
});
}
/** 平台审方通过 xk-api POST /platform-admin-image-transfer/approve */
export function approvePlatformImageTransferOrder(data) {
return platformReq({
url: '/newApi/platform-admin-image-transfer/approve',
method: 'POST',
data,
});
}
/** 平台审方驳回 xk-api POST /platform-admin-image-transfer/reject */
export function rejectPlatformImageTransferOrder(data) {
return platformReq({
url: '/newApi/platform-admin-image-transfer/reject',
method: 'POST',
data,
});
}
/** 平台失败重试推送 xk-api POST /platform-admin-image-transfer/retry */
export function retryPlatformImageTransferOrder(data) {
return platformReq({
url: '/newApi/platform-admin-image-transfer/retry',
method: 'POST',
data,
});
}
/** 诊所本店医生选项 xk-api GET /clinic-admin-image-transfer/doctor-option */
export function clinicTransferDoctorOption(data) {
return clinicReq({
url: '/newApi/clinic-admin-image-transfer/doctor-option',
method: 'GET',
data,
});
}

View File

@@ -28,6 +28,14 @@ export function getPlatformAdminDashboardStats() {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/stats', method: 'GET' }); return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/stats', method: 'GET' });
} }
/**
* 未来7天预约明细(工作台预约卡片点击弹层,平台全量维度)
* xk-api GET /platform-admin-dashboard/appointment-upcoming-list
*/
export function getPlatformAdminAppointmentUpcomingList(data) {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/appointment-upcoming-list', method: 'GET', data });
}
/** /**
* 平台工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码) * 平台工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /platform-admin-dashboard/entries * xk-api GET /platform-admin-dashboard/entries
@@ -52,6 +60,14 @@ export function togglePlatformAdminFavoriteEntry(code) {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/toggle-favorite-entry', method: 'POST', data: { code } }); return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
} }
/**
* 批量保存我的常用(功能页悬浮编辑 / 首页快捷添加)
* xk-api POST /platform-admin-dashboard/batch-save-favorite-entry
*/
export function batchSavePlatformAdminFavoriteEntry(codes) {
return platformDashboardRequest({ url: '/newApi/platform-admin-dashboard/batch-save-favorite-entry', method: 'POST', data: { codes } });
}
/** /**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败) * 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /platform-admin-dashboard/report-entry-usage * xk-api POST /platform-admin-dashboard/report-entry-usage

View File

@@ -0,0 +1,53 @@
/**
* 处方图 OCR / 识图传方(xk-api)
* 取值:unwrapXkApi → code / result / message
*/
import { req } from '@/common/js/index.js';
/** 处方图 OCR xk-api POST /doctor-reception-wx/ocr-prescription-image */
export function ocrPrescriptionImageApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/ocr-prescription-image',
method: 'POST',
data,
});
}
/** 确认识图传方 xk-api POST /doctor-reception-wx/confirm-transfer-image-ocr */
export function confirmTransferImageOcrApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/confirm-transfer-image-ocr',
method: 'POST',
data,
});
}
/** 识图传方匹配就诊人 xk-api POST /doctor-reception-wx/match-transfer-patient */
export function matchTransferPatientApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/match-transfer-patient',
method: 'POST',
data,
});
}
/**
* 识图传方:按手机号列出名下就诊人 xk-api POST /doctor-reception-wx/transfer-patients-by-mobile
* 入参 patient_mobile(请求拦截器加密);返回 items[].mobile 已被响应拦截器脱敏,明文在 mobile_txt
*/
export function transferPatientsByMobileApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/transfer-patients-by-mobile',
method: 'POST',
data,
});
}
/** 医生导入传方准备 xk-api GET /transfer-prescription/prepare-doctor-import */
export function prepareDoctorImportApi(transferId, registerId) {
return req.request({
url: '/newApi/transfer-prescription/prepare-doctor-import',
method: 'GET',
data: { id: transferId, register_id: registerId },
});
}

41
api/processRulePreset.js Normal file
View File

@@ -0,0 +1,41 @@
/**
* 医生煎药预设方案(xk-api)
* 取值:unwrapXkApi → code / result / message
*/
import { req } from '@/common/js/index.js';
/** 方案列表 xk-api GET /doctor-reception-wx/get-process-rule-preset-list */
export function getProcessRulePresetListApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/get-process-rule-preset-list',
method: 'GET',
data
});
}
/** 保存方案 xk-api POST /doctor-reception-wx/save-process-rule-preset */
export function saveProcessRulePresetApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/save-process-rule-preset',
method: 'POST',
data
});
}
/** 设为默认 xk-api POST /doctor-reception-wx/set-process-rule-preset-default */
export function setProcessRulePresetDefaultApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/set-process-rule-preset-default',
method: 'POST',
data
});
}
/** 删除方案 xk-api POST /doctor-reception-wx/delete-process-rule-preset */
export function deleteProcessRulePresetApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/delete-process-rule-preset',
method: 'POST',
data
});
}

88
api/publicAccountPay.js Normal file
View File

@@ -0,0 +1,88 @@
/**
* 公账支付(xk-api,取值 unwrapXkApi → code / result / message)
* 医生走 /public-account-pay;诊所管理员走 /clinic-admin-public-account-pay
*/
import { req } from '@/common/js/index.js'
function isClinicAdmin() {
return uni.getStorageSync('loginMode') === 'clinic_admin'
}
function dailyPrefix() {
return isClinicAdmin()
? '/newApi/clinic-admin-public-account-pay/'
: '/newApi/public-account-pay/'
}
function dailyHeader() {
return isClinicAdmin() ? { _clinicAdmin: '1' } : {}
}
/** 申请公账支付 xk-api POST /public-account-pay/apply */
export function applyPublicAccountPayApi(data) {
return req.request({
url: '/newApi/public-account-pay/apply',
method: 'POST',
data,
})
}
/** 日汇总详情 xk-api GET .../daily-detail */
export function getPublicAccountDailyBillInfoApi(id) {
return req.request({
url: dailyPrefix() + 'daily-detail',
method: 'GET',
data: { id },
header: dailyHeader(),
})
}
/** 锁店只读:日账单 + 商品订单 xk-api GET .../daily-lock-orders */
export function getPublicAccountDailyLockOrdersApi(data = {}) {
return req.request({
url: dailyPrefix() + 'daily-lock-orders',
method: 'GET',
data,
header: dailyHeader(),
})
}
/** 补出日汇总 xk-api POST .../daily-generate */
export function generatePublicAccountDailyBillApi(data) {
return req.request({
url: dailyPrefix() + 'daily-generate',
method: 'POST',
data,
header: dailyHeader(),
})
}
/** 上传日汇总凭证 xk-api POST .../daily-upload */
export function uploadPublicAccountDailyBillApi(data) {
return req.request({
url: dailyPrefix() + 'daily-upload',
method: 'POST',
data,
header: dailyHeader(),
})
}
/** 催办确认到账 xk-api POST .../daily-urge */
export function urgePublicAccountDailyBillApi(data) {
return req.request({
url: dailyPrefix() + 'daily-urge',
method: 'POST',
data,
header: dailyHeader(),
})
}
/** 锁店现算状态(配色/日汇总状态/联系人)xk-api GET .../lock-status */
export function getPublicAccountLockStatusApi() {
return req.request({
url: dailyPrefix() + 'lock-status',
method: 'GET',
data: {},
header: dailyHeader(),
})
}

View File

@@ -43,6 +43,42 @@ export function getWorkbenchStats(data) {
}) })
} }
/** 未来7天预约明细(工作台预约卡片点击弹层);xk-api,业务在 result */
export function getAppointmentUpcomingListApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/appointment-upcoming-list',
method: 'GET',
data
})
}
/** 医生代患者改期 xk-api POST /doctor-reception-wx/reschedule-appointment(register_id/visit_date/slot_id/remark,写改期日志) */
export function rescheduleAppointmentApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/reschedule-appointment',
method: 'POST',
data
})
}
/** 生成患者代付码 xk-api POST /doctor-reception-wx/proxy-pay-qrcode(order_id;返回 base64 小程序码 + 订单号/金额) */
export function getProxyPayQrcodeApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/proxy-pay-qrcode',
method: 'POST',
data
})
}
/** 医生本人号源日历 xk-api GET /doctor-reception-wx/my-schedule-calendar(改期抽屉选新日期/时段) */
export function getMyScheduleCalendarApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/my-schedule-calendar',
method: 'GET',
data: data || {}
})
}
// 获取患者详情(挂号维度的就诊信息,不含处方列表) // 获取患者详情(挂号维度的就诊信息,不含处方列表)
export function getPatientItem(id) { export function getPatientItem(id) {
return req.request({ return req.request({
@@ -153,6 +189,31 @@ export function getProductListDoctorReception(data) {
}) })
} }
/**
* 医生用过的辅料(发方成功云端沉淀;抽屉空搜优先展示)
* xk-api GET /doctor-reception-wx/used-excipient-list
*/
export function getUsedExcipientListDoctorReception(data) {
return req.request({
url: '/newApi/doctor-reception-wx/used-excipient-list',
method: 'GET',
data
})
}
/**
* 复用/常用方填方前对比中药快照价与当前门店价
* 为什么:供货比例变更后关系表已更新,价格不一致时要让医生确认是否按最新价计算
* xk-api POST /doctor-reception-wx/compare-chinese-drug-prices
*/
export function compareChineseDrugPricesApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/compare-chinese-drug-prices',
method: 'POST',
data
})
}
/** 中药全局快捷克数(与药品无关,每次打开中药抽屉拉取) */ /** 中药全局快捷克数(与药品无关,每次打开中药抽屉拉取) */
export function getChineseMedicineQuickGramsApi() { export function getChineseMedicineQuickGramsApi() {
return req.request({ return req.request({
@@ -584,6 +645,34 @@ export function aiGeneratePrescriptionApi(data) {
}) })
} }
/**
* 语音转写(multipart:字段名 audio)
* @param {{ filePath: string, duration_ms?: number, audio_format?: string }} data
*/
export function aiVoiceAsrApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/ai-voice-asr',
method: 'POST',
type: 'upload',
filePath: data.filePath,
name: 'audio',
formData: {
duration_ms: String(data.duration_ms || 0),
// 显式声明:服务端优先采信,避免 MIME/扩展名误杀
audio_format: String(data.audio_format || 'wav')
}
})
}
/** 语音口述落方(只抽药名剂量) */
export function aiGenerateVoicePrescriptionApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/ai-generate-voice-prescription',
method: 'POST',
data
})
}
/** 本挂号 AI 生成历史(轻量列表) */ /** 本挂号 AI 生成历史(轻量列表) */
export function aiListGenerationsApi(data) { export function aiListGenerationsApi(data) {
return req.request({ return req.request({
@@ -602,6 +691,15 @@ export function aiGenerationDetailApi(data) {
}) })
} }
/** 标记 AI 生成记录为已读(熄灭未读微光) */
export function aiMarkGenerationReadApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/ai-mark-generation-read',
method: 'POST',
data
})
}
/** AI 处方药名模糊对照 */ /** AI 处方药名模糊对照 */
export function aiMatchPrescriptionDrugsApi(data) { export function aiMatchPrescriptionDrugsApi(data) {
return req.request({ return req.request({
@@ -664,3 +762,44 @@ export function goldenFormulaSaveMatchApi(data) {
data data
}) })
} }
/**
* 医生接诊相关:新建就诊人 / 创建记录 / 选择接诊 / 认领码
* 取值:xk-api code / result / message(unwrapXkApi)
*/
/** 医生新建就诊人并自动挂号 xk-api POST /doctor-reception-wx/create-user-patient */
export function createUserPatientApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/create-user-patient',
method: 'POST',
data
})
}
/** 医生创建过的全部就诊人 xk-api GET /doctor-reception-wx/doctor-created-user-patient-list */
export function getDoctorCreatedUserPatientListApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/doctor-created-user-patient-list',
method: 'GET',
data: data || {}
})
}
/** 选择医生创建的就诊人开始线下接诊 xk-api POST /doctor-reception-wx/start-offline-reception-by-patient */
export function startOfflineReceptionByPatientApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/start-offline-reception-by-patient',
method: 'POST',
data
})
}
/** 就诊人认领小程序码 xk-api GET /doctor-reception-wx/user-patient-claim-qrcode */
export function getUserPatientClaimQrcodeApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/user-patient-claim-qrcode',
method: 'GET',
data: data || {}
})
}

72
api/regionPicker.js Normal file
View File

@@ -0,0 +1,72 @@
/**
* 省市区选择器接口(xk-api,取值用 code / result / message,配合 unwrapXkApi)
* 按 loginMode 切角色前缀,对齐 upload.js:医生走 region-picker,业务员/平台/诊所管理员走带前缀路由
* 为什么:doctor.php 里同名路由只会命中最先注册的医生鉴权组,业务员 token 会被 401
*/
import { req } from '@/common/js/index.js'
/**
* 按当前登录角色解析 region-picker 前缀与鉴权头标记
* @returns {{ prefix: string, header: Record<string, string> }}
*/
function resolveRegionPickerRoute() {
const mode = uni.getStorageSync('loginMode')
if (mode === 'salesperson') {
return {
prefix: '/newApi/salesperson-region-picker/',
header: { _salesperson: '1' },
}
}
if (mode === 'platform_admin') {
return {
prefix: '/newApi/platform-admin-region-picker/',
header: { _platformAdmin: '1' },
}
}
if (mode === 'clinic_admin') {
return {
prefix: '/newApi/clinic-admin-region-picker/',
header: { _clinicAdmin: '1' },
}
}
// 医生 / 药师等默认
return {
prefix: '/newApi/region-picker/',
header: {},
}
}
function regionPickerRequest(path, data) {
const { prefix, header } = resolveRegionPickerRoute()
return req.request({
url: `${prefix}${path}`,
method: 'GET',
data,
header,
})
}
/** 按 pid 拉一层子节点 */
export function getRegionChildren(pid = 0) {
return regionPickerRequest('children', { pid })
}
/** 地名/首拼/全拼合一搜索 */
export function searchRegions(keyword, limit = 30) {
return regionPickerRequest('search', { keyword, limit })
}
/** 按省+叶回显 */
export function resolveRegionPair(provinceId, cityId) {
return regionPickerRequest('resolve', {
province_id: provinceId,
city_id: cityId,
})
}
/** 批量 ID 解析 */
export function resolveRegionIds(ids) {
return regionPickerRequest('resolve', {
ids: (ids || []).join(','),
})
}

View File

@@ -20,6 +20,14 @@ function salespersonDashboardRequest(options) {
}); });
} }
/**
* 业务员工作台首页看板(录入进度 + 名下诊所营业额/处方/挂号)
* xk-api GET /salesperson-dashboard/stats?range=today|week|month
*/
export function getSalespersonDashboardStats(params = {}) {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/stats', method: 'GET', data: params });
}
/** /**
* 业务员工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码) * 业务员工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /salesperson-dashboard/entries * xk-api GET /salesperson-dashboard/entries
@@ -44,6 +52,14 @@ export function toggleSalespersonFavoriteEntry(code) {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } }); return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
} }
/**
* 批量保存我的常用(功能页悬浮编辑 / 首页快捷添加)
* xk-api POST /salesperson-dashboard/batch-save-favorite-entry
*/
export function batchSaveSalespersonFavoriteEntry(codes) {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/batch-save-favorite-entry', method: 'POST', data: { codes } });
}
/** /**
* 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败) * 上报功能入口使用(点击跳转时静默调用,调用方无需处理失败)
* xk-api POST /salesperson-dashboard/report-entry-usage * xk-api POST /salesperson-dashboard/report-entry-usage

View File

@@ -204,6 +204,7 @@ const LEGACY_ADMIN_UPLOAD_URL = 'https://api.xiaokang88.com/open-api/upload/old-
/** /**
* 录入/管理端旧版图片上传(与 d-upload、select-avatar-image 一致) * 录入/管理端旧版图片上传(与 d-upload、select-avatar-image 一致)
* 注意:只认医生态 token;诊所管理员不要走这条
*/ */
export function uploadLegacyAdminImage(params) { export function uploadLegacyAdminImage(params) {
const { filePath, name = 'file', formData = { store_id: '11001' } } = params || {}; const { filePath, name = 'file', formData = { store_id: '11001' } } = params || {};
@@ -238,3 +239,39 @@ export function uploadLegacyAdminImage(params) {
}); });
}); });
} }
/**
* 识图传方选图上传:管理端与个人中心头像同一条 xk-api `{end}-upload/image`
* 医生端仍走旧 Yii admin-img(医生态 token)
* @param {{ filePath: string }} params
* @returns {Promise<string>} 可访问的图片 URL
*/
export async function uploadOcrImageByLoginMode(params) {
const filePath = params && params.filePath;
if (!filePath) {
throw new Error('缺少文件路径');
}
const mode = uni.getStorageSync('loginMode');
// 诊所管理员等:与头像上传完全同路,避免旧接口/缺 token 导致无响应
if (
mode === 'clinic_admin'
|| mode === 'platform_admin'
|| mode === 'salesperson'
|| mode === 'clinic_salesperson'
) {
const upload = await uploadProfileAvatarApi(filePath);
if (upload && upload.ok && upload.url) {
return String(upload.url);
}
throw new Error((upload && upload.message) || '上传失败');
}
// 兜底:有诊所管理员 token 但 loginMode 丢失时,仍走诊所头像接口
if (uni.getStorageSync('clinic_admin_token')) {
const upload = await uploadProfileAvatarApi(filePath);
if (upload && upload.ok && upload.url) {
return String(upload.url);
}
throw new Error((upload && upload.message) || '上传失败');
}
return uploadLegacyAdminImage({ filePath });
}

16
api/workbench.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* 医生工作台扩展接口(xk-api,取值用 code / result / message,配合 unwrapXkApi)
* 看板统计/预约明细等历史接口在 api/reception.js,本文件放工作台新增能力(AI 简报等)
*/
import {
req
} from '@/common/js/index.js';
/** AI 今日门诊简报 xk-api GET /doctor-reception-wx/ai-daily-brief(store_id 必传;refresh=1 强制重新生成,后端每天首次生成后按天缓存) */
export function getAiDailyBriefApi(data) {
return req.request({
url: '/newApi/doctor-reception-wx/ai-daily-brief',
method: 'GET',
data
})
}

View File

@@ -1,7 +1,4 @@
import $store from '@/store/index.js'; import $store from '@/store/index.js';
import {
registerList,
} from '@/api/consult.js'
class common { class common {
constructor(arg) { constructor(arg) {
this.timer = null this.timer = null

View File

@@ -3,6 +3,7 @@ import errorCode from './errorCode.js';
import {Base64} from "js-base64"; import {Base64} from "js-base64";
import { getDoctorWxEnv } from '@/config/app-env.js'; import { getDoctorWxEnv } from '@/config/app-env.js';
import { resolveDoctorWxAuthToken } from '@/utils/doctorWxAuthToken.js'; import { resolveDoctorWxAuthToken } from '@/utils/doctorWxAuthToken.js';
import { setPublicAccountLock } from '@/utils/publicAccountLock.js'
const { legacyApiBase } = getDoctorWxEnv(); const { legacyApiBase } = getDoctorWxEnv();
@@ -117,9 +118,10 @@ const handleAuthExpired = (tipMsg, response, conf) => {
uni.removeStorageSync('clinic_salesperson_token') uni.removeStorageSync('clinic_salesperson_token')
uni.removeStorageSync('clinic_salesperson_user') uni.removeStorageSync('clinic_salesperson_user')
uni.removeStorageSync('loginMode') uni.removeStorageSync('loginMode')
} else if (loginMode === 'clinic_admin') { } else if (loginMode === 'clinic_admin' || loginMode === 'transfer_clerk') {
uni.removeStorageSync('clinic_admin_token') uni.removeStorageSync('clinic_admin_token')
uni.removeStorageSync('clinic_admin_user') uni.removeStorageSync('clinic_admin_user')
uni.removeStorageSync('transfer_clerk_user')
uni.removeStorageSync('loginMode') uni.removeStorageSync('loginMode')
} else { } else {
uni.removeStorageSync('token') uni.removeStorageSync('token')
@@ -156,6 +158,20 @@ const resInterceptor = (response, conf = {}) => {
if (res == null || typeof res !== 'object') { if (res == null || typeof res !== 'object') {
return res return res
} }
// 4101 锁店:先解密联系人(会写出 phone_txt 再脱敏 phone),再落库给锁店页拨号
const bizCode = Number(res.code != null ? res.code : res.errcode)
if (bizCode === 4101) {
const payload = res.result || {}
if (payload.contact_phones) {
payload.contact_phones = getRes(payload.contact_phones, true)
}
setPublicAccountLock(payload)
return {
wakaryReqToReject: true,
msg: res.message || '公账日账单已逾期',
res: response
}
}
if (res.data != null) { if (res.data != null) {
res.data = getRes(res.data) res.data = getRes(res.data)
} }
@@ -163,7 +179,6 @@ const resInterceptor = (response, conf = {}) => {
res.result = getRes(res.result) res.result = getRes(res.result)
} }
// Laravel notAuth 返回 HTTP 200 + 业务码 401,需与 HTTP 401 走同一套登录失效处理 // Laravel notAuth 返回 HTTP 200 + 业务码 401,需与 HTTP 401 走同一套登录失效处理
const bizCode = Number(res.code != null ? res.code : res.errcode)
if (bizCode === 401) { if (bizCode === 401) {
return handleAuthExpired(res.msg || res.message || '账号需重新登录', response, conf) return handleAuthExpired(res.msg || res.message || '账号需重新登录', response, conf)
} }
@@ -222,19 +237,26 @@ function _responseLog(res, conf = {}, describe = null) {
} }
} }
/** 解密后写出 {字段}_txt 明文,给锁店拨号等场景用;原字段仍脱敏 */
const PHONE_TXT_KEYS = ['phone', 'mobile', 'call_mobile', 'express_mobile', 'patient_mobile', 'accept_tel']
function getRes(obj, isDecode = true) { function getRes(obj, isDecode = true) {
if (obj == null || typeof obj !== 'object') { if (obj == null || typeof obj !== 'object') {
return obj return obj
} }
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
if (obj[i] != null && typeof obj[i] === 'object') {
obj[i] = getRes(obj[i], isDecode)
}
}
return obj
}
try { try {
for (const key in obj) { for (const key in obj) {
if (Array.isArray(obj[key])) { if (obj[key] != null && typeof obj[key] === 'object') {
obj[key] = getRes(obj[key]) obj[key] = getRes(obj[key], isDecode)
} else if (typeof obj[key] === 'object' && obj[key] !== null) { } else if (arr.indexOf(key) !== -1) {
obj[key] = getRes(obj[key])
} else {
// 判断obj[key]是否在arr中
if (arr.indexOf(key) !== -1) {
if (obj[key] == null || obj[key] === '') continue if (obj[key] == null || obj[key] === '') continue
let aseFile = '' let aseFile = ''
if (isDecode === true) { if (isDecode === true) {
@@ -248,38 +270,37 @@ function getRes(obj, isDecode = true) {
if (isGarbled) { if (isGarbled) {
aseFile = obj[key] aseFile = obj[key]
} }
if (isDecode === true) { // 请求必须加密,不能跳过;解密后先留明文 _txt 再脱敏
if (sensitiveData.indexOf(key) !== -1) { if (isDecode === true && PHONE_TXT_KEYS.indexOf(key) !== -1) {
// 数据脱敏,自动匹配姓名、手机号、身份证 const plain = String(aseFile || '')
if (/^1\d{10}$/.test(plain)) {
obj[key + '_txt'] = plain
}
}
if (isDecode === true && sensitiveData.indexOf(key) !== -1) {
switch (key) { switch (key) {
case 'express_name': case 'express_name':
case 'express_region': case 'express_region':
case 'accept_name': case 'accept_name':
case 'patient': case 'patient':
// 保留前两个字符,其余用星号代替
aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*'); aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
break; break;
case 'mobile': case 'mobile':
case 'express_mobile': case 'express_mobile':
case 'phone': case 'phone':
// 保留前三位和后四位,中间用星号代替
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4); aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break; break;
case 'id_card': case 'id_card':
case 'idcard': case 'idcard':
// 保留前六位和后四位,中间用星号代替
aseFile = aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4); aseFile = aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break; break;
default: default:
// 默认情况下,全部用星号代替
break; break;
} }
} }
}
obj[key] = aseFile obj[key] = aseFile
} }
} }
}
return obj return obj
} catch (error) { } catch (error) {
console.error('错误', error) console.error('错误', error)

View File

@@ -1,11 +1,13 @@
<template> <template>
<view> <view>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<!-- pageGuard 与 visible 拆开:关抽屉先离场再卸,避免工作台无法滑动 -->
<page-container <page-container
v-if="mode === 'switch'" v-if="mode === 'switch' && pageGuard"
:show="visible" :show="visible"
:overlay="false" :overlay="false"
@leave="handlePageLeave" @leave="handlePageLeave"
@afterleave="releasePageGuard"
/> />
<!-- #endif --> <!-- #endif -->
<u-popup <u-popup
@@ -63,9 +65,14 @@ export default {
default: '', default: '',
}, },
}, },
beforeDestroy() {
this.clearReleaseTimer();
},
data() { data() {
return { return {
visible: false, visible: false,
pageGuard: false,
releaseTimer: null,
accounts: [], accounts: [],
selected: null, selected: null,
loading: false, loading: false,
@@ -104,10 +111,28 @@ export default {
return list.find((item) => item.password_matched) || list[0]; return list.find((item) => item.password_matched) || list[0];
}, },
showPanel() { showPanel() {
this.pageGuard = true;
this.$nextTick(() => { this.$nextTick(() => {
this.visible = true; this.visible = true;
}); });
}, },
releasePageGuard() {
this.clearReleaseTimer();
this.pageGuard = false;
},
scheduleReleaseGuard() {
this.clearReleaseTimer();
this.releaseTimer = setTimeout(() => {
this.pageGuard = false;
this.releaseTimer = null;
}, 350);
},
clearReleaseTimer() {
if (this.releaseTimer) {
clearTimeout(this.releaseTimer);
this.releaseTimer = null;
}
},
open(accounts = null, options = {}) { open(accounts = null, options = {}) {
this.defaultAccountId = options.defaultAccountId ?? null; this.defaultAccountId = options.defaultAccountId ?? null;
this.defaultAccountType = options.defaultAccountType ?? null; this.defaultAccountType = options.defaultAccountType ?? null;
@@ -134,6 +159,7 @@ export default {
}, },
close() { close() {
this.visible = false; this.visible = false;
this.scheduleReleaseGuard();
}, },
handlePageLeave() { handlePageLeave() {
this.close(); this.close();

View File

@@ -74,7 +74,7 @@ export default {
methods: { methods: {
/** 供页面 onShow 调用:刷新微信绑定状态 */ /** 供页面 onShow 调用:刷新微信绑定状态 */
refreshWxBind() { refreshWxBind() {
if (this.$refs.wxBindEntry) this.$refs.wxBindEntry.refresh(); if (this.$refs.wxBindEntry) return this.$refs.wxBindEntry.refresh();
}, },
/** 进入共享个人中心页(sub_business_shared 分包页面可被任意分包 navigateTo) */ /** 进入共享个人中心页(sub_business_shared 分包页面可被任意分包 navigateTo) */
goProfile() { goProfile() {

View File

@@ -0,0 +1,530 @@
<template>
<view v-if="featureEnabled">
<!-- 工作台只占一行横幅(对齐 PC 公告条),全文进抽屉,避免大卡片占屏且首弹 page-container 锁死页面滑动 -->
<view class="ai-brief-banner" @click="onBannerTap">
<view class="ai-brief-banner__badge">AI</view>
<text class="ai-brief-banner__title">今日简报</text>
<text class="ai-brief-banner__text">{{ bannerText }}</text>
<u-loading v-if="loading" mode="circle" size="22"></u-loading>
<u-icon v-else name="arrow-right" size="22" color="#6acdbb"></u-icon>
</view>
<!-- 查看全文抽屉:page-container 拦截返回键(规则:抽屉必须 page-container + v-if) -->
<!-- pageGuard 与 popupShow 拆开:先 show=false 播完离场再卸节点,避免原生滚动锁残留导致关抽屉后页面无法滑动 -->
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="pageGuard"
:show="popupShow"
:overlay="false"
@leave="onPageLeave"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<u-popup :value="popupShow" mode="bottom" border-radius="24" height="70%" :safe-area-inset-bottom="true" :mask-close-able="true" @input="onPopupInput" @close="closePopup">
<view class="ai-popup">
<view class="ai-popup__header">
<view class="ai-popup__title-wrap">
<view class="ai-brief-banner__badge">AI</view>
<text class="ai-popup__title">{{ popupDateText }}</text>
</view>
<view class="ai-popup__actions">
<text class="ai-popup__refresh" :class="{ 'is-disabled': loading }" @click.stop="onRefreshTap">重新生成</text>
<text class="ai-popup__close" @click="closePopup">关闭</text>
</view>
</view>
<scroll-view scroll-y="true" class="ai-popup__scroll">
<view v-if="chips.length" class="ai-brief-card__chips ai-popup__chips">
<view v-for="chip in chips" :key="chip.label" class="ai-chip" :class="{ 'ai-chip--hl': chip.highlight }">
<text class="ai-chip__value">{{ chip.value }}</text>
<text class="ai-chip__label">{{ chip.label }}</text>
</view>
</view>
<view class="ai-popup__body">
<view v-for="(section, idx) in sections" :key="idx" class="ai-popup__section">
<text v-if="section.title" class="ai-brief-card__section-tag">{{ section.title }}</text>
<text class="ai-popup__section-text">{{ section.text }}</text>
</view>
</view>
<view class="ai-popup__meta">由 AI 生成,仅供参考{{ generatedAtText ? ' · ' + generatedAtText : '' }}</view>
</scroll-view>
<view class="ai-popup__footer">
<view class="ai-popup__btn" @click="closePopup">知道了</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* AI 今日门诊简报:工作台单行横幅 + 点击打开全文抽屉
* 不做每日首弹——page-container 挂在 tab 页上会锁死整页滑动;
* 数据仍走 /doctor-reception-wx/ai-daily-brief(每天首次生成、按天缓存),「重新生成」在抽屉内
*/
import { getAiDailyBriefApi } from '@/api/workbench.js'
import { unwrapXkApi } from '@/utils/api-response.js'
export default {
name: 'ai-brief-card',
props: {
/** 当前门店ID:简报按 doctor+store 缓存(排班/挂号统计都是按店口径),切店需重拉 */
storeId: {
type: [Number, String],
default: ''
}
},
data() {
return {
brief: null,
// 系统配置关闭时整卡不渲染,避免关开关后仍占一行
featureEnabled: true,
loading: false,
// 接口本身失败(网络/鉴权),区别于 brief.status=0 的「AI 生成失败」
fetchError: false,
popupShow: false,
// 原生 page-container 挂载开关:关闭时先关 show,afterleave/超时后再卸,避免锁死页面滑动
pageGuard: false,
releaseTimer: null,
// 去重键(storeId_日期):页面 onShow 会反复触发刷新,同店同天已拉过则跳过
lastFetchKey: ''
}
},
computed: {
/** 生成时间展示(缓存行的更新时间) */
generatedAtText() {
return this.brief && this.brief.generated_at ? this.brief.generated_at : ''
},
/** 弹层标题:简报日期 */
popupDateText() {
const date = this.brief && this.brief.brief_date ? this.brief.brief_date : ''
return date ? date + ' 今日简报' : '今日简报'
},
/** 横幅一行摘要:成功展示首段概览,失败/加载给短提示 */
bannerText() {
if (this.loading) return '正在分析今日门诊…'
if (this.fetchError) return '加载失败,点此重试'
if (this.brief && this.brief.status !== 1) return '生成失败,点此重试'
if (this.sections.length && this.sections[0].text) return this.sections[0].text
if (this.chips.length) {
const first = this.chips[0]
return first.label + ' ' + first.value
}
return '点击查看今日简报'
},
/**
* 正文分段:后端约定 AI 输出「【今日概览】...【重点提醒】...【时间安排建议】...」,
* 按【标题】切段渲染更易读;AI 偶发不按格式时兜底整段展示
*/
sections() {
const content = this.unwrapBriefContent(this.brief && this.brief.content ? this.brief.content : '')
if (!content) return []
const matches = []
const reg = /【([^】]+)】([^【]*)/g
let m = reg.exec(content)
while (m) {
matches.push({ title: m[1] || '', text: (m[2] || '').trim() })
m = reg.exec(content)
}
if (matches.length === 0) return [{ title: '', text: content }]
return matches
},
/**
* 快照统计芯片:读生成时落库的 data_snapshot,保证数字与 AI 文本口径一致
* (值班余号/待接诊/今日到期预约/未来7天预约)
*/
chips() {
const data = this.brief && this.brief.data ? this.brief.data : null
if (!data) return []
const today = data.today || {}
const schedule = data.schedule || {}
const waitAccept = Number(today.wait_accept) || 0
const dueWaiting = Number(today.appointment_due_waiting) || 0
// 未到就诊日的预约不当成今日待办芯片,避免角标式催办
return [
{
label: '今日值班',
value: schedule.is_work ? (Number(schedule.left) || 0) + '/' + (Number(schedule.total) || 0) + '余号' : '休诊',
highlight: false
},
{ label: '待接诊', value: waitAccept, highlight: waitAccept > 0 },
{ label: '今日到期预约', value: dueWaiting, highlight: dueWaiting > 0 }
]
}
},
watch: {
// 弹层开合同步给工作台:简报弹层在 .content(z-index:1)内,盖不住自定义 tabbar(z-index:998),
// 由页面在弹层打开时隐藏 tabbar,避免「知道了」被挡住
popupShow(val) {
this.$emit('popup-change', !!val)
}
},
mounted() {
this.fetchBrief(false)
},
beforeDestroy() {
this.clearReleaseTimer()
},
methods: {
/**
* Agent/模型偶发把简报包成 JSON 或 ```json```,先拆成【标题】正文再分段
*/
unwrapBriefContent(raw, depth) {
let text = raw ? String(raw).trim() : ''
const level = depth || 0
if (!text || level > 4) return text
const fenceStart = text.indexOf('```')
if (fenceStart >= 0) {
const after = text.slice(fenceStart).replace(/^```(?:json)?\s*/, '')
const end = after.lastIndexOf('```')
const inner = end >= 0 ? after.slice(0, end) : after
if (inner) text = inner.trim()
}
try {
const decoded = JSON.parse(text)
if (typeof decoded === 'string') return this.unwrapBriefContent(decoded, level + 1)
if (!decoded || typeof decoded !== 'object') return text
if (Array.isArray(decoded)) {
return this.flattenBriefSections(decoded) || text
}
const wrapFields = ['content', 'text', 'brief', 'summary']
for (let i = 0; i < wrapFields.length; i++) {
const val = decoded[wrapFields[i]]
if (typeof val === 'string' && val.trim()) {
return this.unwrapBriefContent(val, level + 1)
}
if (val && typeof val === 'object') {
return this.unwrapBriefContent(JSON.stringify(val), level + 1)
}
}
if (Array.isArray(decoded.sections)) {
return this.flattenBriefSections(decoded.sections) || text
}
const parts = []
const skip = { content: 1, text: 1, status: 1, brief_status: 1, brief: 1, summary: 1 }
for (const key in decoded) {
if (!Object.prototype.hasOwnProperty.call(decoded, key)) continue
const value = decoded[key]
if (typeof value !== 'string' && typeof value !== 'number') continue
if (skip[key] || !String(key).trim() || !String(value).trim()) continue
parts.push('【' + String(key).trim() + '】' + String(value).trim())
}
return parts.length ? parts.join('') : text
} catch (e) {
return text
}
},
flattenBriefSections(list) {
const parts = []
for (let i = 0; i < list.length; i++) {
const sec = list[i]
if (typeof sec === 'string' && sec.trim()) {
parts.push(sec.trim())
continue
}
const item = sec || {}
const title = String(item.title || item.name || item.label || '').trim()
const body = String(item.text || item.content || item.body || '').trim()
if (title && body) parts.push('【' + title + '】' + body)
else if (body) parts.push(body)
}
return parts.join('')
},
/** 今天日期字符串(去重键用,与后端 brief_date 同格式) */
todayStr() {
const d = new Date()
const pad = (n) => (n < 10 ? '0' + n : '' + n)
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
},
/**
* 拉取简报;refresh=true 强制后端重新生成(用户点「重新生成」按钮)
* 页面 onShow / 切店都会调用本方法,靠 lastFetchKey 去重避免重复请求;
* 首次生成可能耗时十几秒(走 AI),期间维持加载态
*/
async fetchBrief(refresh) {
if (!this.storeId || this.loading) return
const fetchKey = String(this.storeId) + '_' + this.todayStr()
if (!refresh && this.brief && this.lastFetchKey === fetchKey) return
this.loading = true
this.fetchError = false
try {
const params = { store_id: this.storeId }
if (refresh) params.refresh = 1
const res = await getAiDailyBriefApi(params)
const { ok, payload } = unwrapXkApi(res)
if (ok && payload) {
// 系统配置关闭:不展示横幅、不记去重键,避免关开关后仍占位
if (payload.enabled === false) {
this.featureEnabled = false
this.brief = null
this.lastFetchKey = ''
return
}
this.featureEnabled = true
this.brief = payload
this.lastFetchKey = fetchKey
} else {
this.fetchError = true
}
} catch (e) {
this.fetchError = true
} finally {
this.loading = false
}
},
/** 横幅点击:成功打开抽屉,失败则重试;加载中不响应以免打断生成 */
onBannerTap() {
if (this.loading) return
if (this.fetchError) {
this.onRetryTap()
return
}
if (this.brief && this.brief.status !== 1) {
this.onRefreshTap()
return
}
if (this.brief) this.openPopup()
},
/** 重新生成(防抖:加载中不响应) */
onRefreshTap() {
if (this.loading) return
this.fetchBrief(true)
},
/** 网络失败重试(读缓存即可,不强制重生成) */
onRetryTap() {
if (this.loading) return
this.fetchBrief(false)
},
/** 点横幅打开抽屉:先挂 page-container,下一帧再 show,保证原生层能接到进入动画 */
openPopup() {
this.pageGuard = true
this.$nextTick(() => {
this.popupShow = true
})
},
closePopup() {
this.popupShow = false
this.scheduleReleaseGuard()
},
/** u-popup 双向同步(蒙层点击关闭等场景) */
onPopupInput(val) {
this.popupShow = !!val
if (!val) this.scheduleReleaseGuard()
},
/** page-container 返回键:关抽屉,不退出工作台 */
onPageLeave() {
this.popupShow = false
this.scheduleReleaseGuard()
},
/** 离场动画结束立刻卸原生容器(比超时更干净) */
releasePageGuard() {
this.clearReleaseTimer()
this.pageGuard = false
},
/** 点按钮关闭时 afterleave 可能不触发,350ms 兜底卸掉,避免滚动锁残留 */
scheduleReleaseGuard() {
this.clearReleaseTimer()
this.releaseTimer = setTimeout(() => {
this.pageGuard = false
this.releaseTimer = null
}, 350)
},
clearReleaseTimer() {
if (this.releaseTimer) {
clearTimeout(this.releaseTimer)
this.releaseTimer = null
}
}
}
}
</script>
<style lang="scss" scoped>
/* 单行横幅:1px 同色边框 + 微光,不占工作台首屏 */
.ai-brief-banner {
display: flex;
align-items: center;
background: #fff;
border: 1px solid rgba(106, 205, 187, 0.3);
border-radius: 16rpx;
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
padding: 16rpx 20rpx;
margin-bottom: 20rpx;
&__badge {
flex-shrink: 0;
padding: 2rpx 12rpx;
background: linear-gradient(135deg, #6acdbb, #4db6a3);
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
color: #fff;
margin-right: 12rpx;
}
&__title {
flex-shrink: 0;
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-right: 12rpx;
}
&__text {
flex: 1;
min-width: 0;
font-size: 24rpx;
color: #888;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 8rpx;
}
}
.ai-brief-card {
&__chips {
display: flex;
flex-wrap: wrap;
margin-top: 20rpx;
}
&__section-tag {
align-self: flex-start;
padding: 2rpx 16rpx;
background: rgba(106, 205, 187, 0.12);
border-radius: 999rpx;
font-size: 20rpx;
font-weight: 600;
color: #4db6a3;
margin-bottom: 8rpx;
}
}
/* 快照统计芯片 */
.ai-chip {
display: flex;
align-items: center;
padding: 8rpx 18rpx;
border: 1px solid #ebeef5;
border-radius: 999rpx;
margin-right: 12rpx;
margin-bottom: 12rpx;
&--hl {
border-color: rgba(251, 140, 0, 0.45);
background: rgba(251, 140, 0, 0.08);
}
&__value {
font-size: 24rpx;
font-weight: 650;
color: #333;
}
&__label {
font-size: 22rpx;
color: #909399;
margin-left: 8rpx;
}
}
/* 全文弹层 */
.ai-popup {
display: flex;
flex-direction: column;
height: 100%;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx 20rpx;
border-bottom: 1px solid #f2f3f5;
}
&__title-wrap {
display: flex;
align-items: center;
}
&__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
&__actions {
display: flex;
align-items: center;
}
&__refresh {
font-size: 24rpx;
color: #6acdbb;
padding: 8rpx 12rpx;
&.is-disabled {
opacity: 0.5;
}
}
&__close {
font-size: 26rpx;
color: #909399;
padding: 8rpx;
}
&__scroll {
flex: 1;
height: 0;
min-height: 400rpx;
padding: 24rpx 32rpx;
box-sizing: border-box;
}
&__chips {
margin-top: 0;
margin-bottom: 8rpx;
}
&__body {
display: flex;
flex-direction: column;
}
&__section {
display: flex;
flex-direction: column;
margin-bottom: 24rpx;
}
&__section-text {
font-size: 26rpx;
line-height: 1.8;
color: #444;
}
&__meta {
padding: 16rpx 0 32rpx;
border-top: 1px solid #f2f3f5;
font-size: 22rpx;
color: #b0b3b8;
}
&__footer {
padding: 16rpx 32rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
}
&__btn {
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: linear-gradient(135deg, #6acdbb, #4db6a3);
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 600;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,910 @@
<template>
<view>
<!-- 页面级返回拦截:防止用户按返回键直接退出页面(规则要求抽屉必须配 page-container + v-if) -->
<!-- pageGuard 与 show 拆开:先 show=false 播完离场再卸节点,避免关抽屉后页面无法滑动 -->
<!-- 两级抽屉:原生容器一次 leave 后失效,关掉子抽屉后需重挂载才能拦截下一次返回 -->
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="pageGuard"
:show="show"
:overlay="false"
@leave="onPageLeave"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<u-popup
:value="show"
mode="bottom"
border-radius="24"
height="75%"
:safe-area-inset-bottom="true"
:mask-close-able="true"
@input="onPopupInput"
@close="close"
>
<view class="appt-drawer">
<view class="appt-drawer__header">
<text class="appt-drawer__title">未来7天预约明细</text>
<text class="appt-drawer__close" @click="close">关闭</text>
</view>
<!-- 日期筛选:全部 + 7 天横向滚动 pill -->
<scroll-view scroll-x="true" class="appt-drawer__filters">
<view class="appt-drawer__filters-inner">
<view
class="appt-pill"
:class="{ 'appt-pill--active': filterDate === '' }"
@click="switchFilter('')"
>全部7天</view>
<view
v-for="day in dayList"
:key="day.date"
class="appt-pill"
:class="{ 'appt-pill--active': filterDate === day.date }"
@click="switchFilter(day.date)"
>{{ day.label }}({{ day.count }})</view>
</view>
</scroll-view>
<!-- 明细列表:滚动到底自动加载下一页 -->
<scroll-view scroll-y="true" class="appt-drawer__list" @scrolltolower="loadMore">
<view v-if="loading && list.length === 0" class="appt-drawer__loading">
<u-loading mode="circle" size="36"></u-loading>
</view>
<block v-else>
<view v-for="item in list" :key="item.register_id" class="appt-row">
<view class="appt-row__date">
<text class="appt-row__day">{{ item.visit_date_short }}</text>
<text class="appt-row__week">{{ item.week }}</text>
</view>
<view class="appt-row__main">
<view class="appt-row__line1">
<text class="appt-row__patient">{{ item.patient_name || '未知就诊人' }}</text>
<text class="appt-row__slot">{{ item.slot_label }}</text>
</view>
<view class="appt-row__line2">
<text v-if="showDoctor && item.doctor_name" class="appt-row__meta">医生:{{ item.doctor_name }}</text>
<text v-if="showStore && item.store_name" class="appt-row__meta">{{ item.store_name }}</text>
<!-- 脱敏手机号展示(拦截器已解码+脱敏),拨号用未脱敏的 call_mobile -->
<text v-if="item.patient_mobile" class="appt-row__meta">{{ item.patient_mobile }}</text>
</view>
<!-- 行操作:电话沟通 + 医生代改期(仅医生端、已支付待接诊单可改) -->
<view class="appt-row__actions" v-if="item.call_mobile || (canReschedule && item.can_reschedule)">
<view v-if="item.call_mobile" class="appt-action" @click="callPatient(item)">
<u-icon name="phone" size="24" color="#6acdbb"></u-icon>
<text class="appt-action__text">电话</text>
</view>
<view v-if="canReschedule && item.can_reschedule" class="appt-action" @click="openReschedule(item)">
<u-icon name="calendar" size="24" color="#6acdbb"></u-icon>
<text class="appt-action__text">改期</text>
</view>
</view>
</view>
<text class="appt-row__status" :class="item.status_class">{{ item.status_text }}</text>
</view>
<view v-if="!loading && list.length === 0" class="appt-drawer__empty">
<u-icon name="calendar" size="60" color="#C4C7CC"></u-icon>
<text class="appt-drawer__empty-text">暂无预约</text>
</view>
<view v-if="list.length > 0" class="appt-drawer__footer">
<u-loading v-if="loading" mode="circle" size="28"></u-loading>
<text v-else-if="finished" class="appt-drawer__footer-text">没有更多了</text>
<text v-else class="appt-drawer__footer-text" @click="loadMore">加载更多</text>
</view>
</block>
</scroll-view>
</view>
</u-popup>
<!-- 医生代改期子抽屉:日历日期 + 时段选择(数据来自 my-schedule-calendar),z-index 需高于明细抽屉 -->
<u-popup
v-if="role === 'doctor'"
:value="rescheduleShow"
mode="bottom"
border-radius="24"
height="72%"
:safe-area-inset-bottom="true"
:mask-close-able="true"
:z-index="10090"
@input="onReschedulePopupInput"
@close="closeReschedule"
>
<view class="resch">
<view class="appt-drawer__header">
<text class="appt-drawer__title">修改预约时间</text>
<text class="appt-drawer__close" @click="closeReschedule">关闭</text>
</view>
<view class="resch__current" v-if="rescheduleTarget">
{{ rescheduleTarget.patient_name || '就诊人' }} · 当前:{{ rescheduleTarget.visit_date }} {{ rescheduleTarget.slot_label }}
</view>
<scroll-view scroll-y="true" class="resch__body">
<view v-if="calendarLoading" class="appt-drawer__loading">
<u-loading mode="circle" size="36"></u-loading>
</view>
<block v-else>
<view v-if="calendarDays.length > 0">
<scroll-view scroll-x="true" class="resch__days" :show-scrollbar="false">
<view class="resch__days-inner">
<view
v-for="(day, idx) in calendarDays"
:key="day.date"
class="resch-day"
:class="{ 'resch-day--active': idx === selectedDayIndex, 'resch-day--disabled': !day.is_work || day.is_full }"
@click="selectDay(idx)"
>
<view class="resch-day__week">{{ day.week }}</view>
<view class="resch-day__date">{{ day.date_text }}</view>
<view class="resch-day__state" :class="{ 'resch-day__state--rest': !day.is_work }">{{ day.state_text }}</view>
</view>
</view>
</scroll-view>
<view class="resch__slots" v-if="selectedDay && selectedDay.is_work && selectedDay.slots.length > 1">
<view
v-for="slot in selectedDay.slots"
:key="slot.slot_id"
class="resch-slot"
:class="{ 'resch-slot--active': slot.slot_id === selectedSlotId, 'resch-slot--disabled': slot.is_full || slot.is_expired }"
@click="selectSlot(slot)"
>
<view class="resch-slot__name">{{ slot.slot_name }}</view>
<view class="resch-slot__time">{{ slot.start_time }}-{{ slot.end_time }}</view>
<view class="resch-slot__state">{{ slot.state_text }}</view>
</view>
</view>
<view class="resch__tip" v-if="selectedDay && !selectedDay.is_work">该日休诊,请选择其他日期</view>
</view>
<view class="resch__tip" v-else>暂无可约号源</view>
<view class="resch__remark">
<input
class="resch__remark-input"
v-model="rescheduleRemark"
placeholder="备注(选填,如:电话已与患者确认)"
maxlength="100"
/>
</view>
</block>
</scroll-view>
<view class="resch__footer">
<view class="resch__selected" v-if="rescheduleSlotLabel">改期至:{{ rescheduleSlotLabel }}</view>
<view
class="resch__btn"
:class="{ 'resch__btn--disabled': !canConfirmReschedule || rescheduleSubmitting }"
@click="confirmReschedule"
>{{ rescheduleSubmitting ? '提交中...' : '确认修改' }}</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* 未来7天预约明细抽屉(医生 / 诊所管理员 / 平台管理员三端工作台共用)
*
* 为什么按 role 内部选接口而不是传函数 prop:小程序端组件传函数不稳妥,
* 且三端接口鉴权头不同(医生 JWT / _clinicAdmin / _platformAdmin),收敛在组件内按 role 分发最直观。
* 数据分页由后端 appointment-upcoming-list 提供(page/page_size/visit_date),滚动到底自动翻页。
*/
import { getAppointmentUpcomingListApi, rescheduleAppointmentApi, getMyScheduleCalendarApi } from '@/api/reception.js'
import { getClinicAdminAppointmentUpcomingList } from '@/api/clinicAdmin.js'
import { getPlatformAdminAppointmentUpcomingList } from '@/api/platformAdmin.js'
export default {
name: 'AppointmentUpcomingDrawer',
props: {
/** v-model 显隐 */
value: { type: Boolean, default: false },
/** 端角色:doctor 医生(隐藏医生列) / clinic 诊所管理员(隐藏门店列) / platform 平台管理员(全显示) */
role: { type: String, default: 'doctor' },
/** 7 天分布(工作台统计接口返回的 days),用于筛选 pill 展示数量 */
days: { type: Array, default: () => [] },
/** 打开时默认选中的日期(点某天卡格进入时预筛选),空=全部7天 */
defaultDate: { type: String, default: '' },
},
data() {
return {
filterDate: '',
list: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
// 请求序号:快速切日期/连点加载更多时,据此丢弃晚到的过期响应,防止旧数据覆盖新列表
reqSeq: 0,
// page-container 挂载开关:关闭时先关 show,afterleave/超时后再卸
pageGuard: false,
releaseTimer: null,
// ---- 医生代改期子抽屉状态(仅 role=doctor 使用) ----
rescheduleShow: false,
rescheduleTarget: null,
rescheduleRemark: '',
rescheduleSubmitting: false,
calendarLoading: false,
// 医生本人号源日历(my-schedule-calendar),展示文案在 applyCalendar 预计算
calendarDays: [],
selectedDayIndex: 0,
selectedSlotId: -1,
}
},
computed: {
show: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
/** 筛选 pill:父组件偶发传 null,渲染层不能对 undefined 做 v-for */
dayList() {
return Array.isArray(this.days) ? this.days : []
},
/** 是否加载完(用于底部「没有更多」提示与翻页阻断) */
finished() {
return this.list.length >= this.total
},
/** 医生端看自己的号不需要医生列 */
showDoctor() {
return this.role !== 'doctor'
},
/** 诊所管理员端固定本店不需要门店列 */
showStore() {
return this.role !== 'clinic'
},
/** 仅医生端可代患者改期(管理员端明细只读;医生不受「就诊当天不可改」限制) */
canReschedule() {
return this.role === 'doctor'
},
/** 当前选中的日期对象 */
selectedDay() {
return this.calendarDays[this.selectedDayIndex] || null
},
/** 当前选中的时段对象 */
selectedSlot() {
if (!this.selectedDay || this.selectedSlotId < 0) return null
const slots = this.selectedDay.slots || []
for (let i = 0; i < slots.length; i++) {
if (slots[i].slot_id === this.selectedSlotId) return slots[i]
}
return null
},
/** 改期目标文案(底部确认条) */
rescheduleSlotLabel() {
if (!this.selectedDay || !this.selectedSlot) return ''
let label = this.selectedDay.date + ' ' + (this.selectedSlot.slot_name || '')
const s = this.selectedSlot
if (s.start_time && s.end_time && !(s.start_time === '00:00' && s.end_time === '23:59')) {
label += ' ' + s.start_time + '-' + s.end_time
}
return label
},
/** 是否可提交:选中了出诊日的可约时段 */
canConfirmReschedule() {
return !!(this.selectedDay && this.selectedDay.is_work && this.selectedSlot
&& !this.selectedSlot.is_full && !this.selectedSlot.is_expired)
},
},
watch: {
/** 打开时按入口预筛选并拉第一页(每次打开都取最新数据) */
value(v) {
if (v) {
this.pageGuard = true
this.filterDate = this.defaultDate || ''
this.resetAndLoad()
} else {
this.scheduleReleaseGuard()
}
},
},
beforeDestroy() {
this.clearReleaseTimer()
},
methods: {
releasePageGuard() {
this.clearReleaseTimer()
this.pageGuard = false
},
scheduleReleaseGuard() {
this.clearReleaseTimer()
this.releaseTimer = setTimeout(() => {
this.pageGuard = false
this.releaseTimer = null
}, 350)
},
clearReleaseTimer() {
if (this.releaseTimer) {
clearTimeout(this.releaseTimer)
this.releaseTimer = null
}
},
close() {
this.show = false
},
onPopupInput(v) {
this.show = v
},
onPageLeave() {
// 返回手势优先关改期子抽屉,主抽屉保留(微信每页只允许一个 page-container,收口在此分发)
if (this.rescheduleShow) {
this.rescheduleShow = false
// 原生容器一次 leave 后即失效,重挂载后才能继续拦截「关主抽屉」的下一次返回
this.pageGuard = false
this.$nextTick(() => {
this.pageGuard = true
})
return
}
this.close()
},
/** 切换日期筛选:回到第一页重查 */
switchFilter(date) {
if (this.filterDate === date) return
this.filterDate = date
this.resetAndLoad()
},
resetAndLoad() {
this.page = 1
this.list = []
this.total = 0
this.fetchList(1)
},
/** 滚动到底/点击加载更多:请求下一页(页码在成功回包后才提交,失败自然停留原页不跳页) */
loadMore() {
if (this.loading || this.finished) return
this.fetchList(this.page + 1)
},
/**
* 按端角色调用对应明细接口并统一取值
* 医生端接口返回原始 body(code/result),诊所/平台端封装返回 { ok, data }
*
* 竞态与分页防护:
* 1. 每次请求领取自增序号,回包时序号已过期(期间又发了新请求)直接丢弃,防旧响应覆盖新列表
* 2. 页码显式入参、只在成功回包后 this.page = page,失败/丢弃都不动页码,无需回滚逻辑
* @param {number} page 目标页码
*/
fetchList(page = 1) {
const seq = ++this.reqSeq
this.loading = true
const params = {
page,
page_size: this.pageSize,
visit_date: this.filterDate,
}
let task = null
if (this.role === 'clinic') {
task = getClinicAdminAppointmentUpcomingList(params).then((wrap) => {
return wrap && wrap.ok ? wrap.data : null
})
} else if (this.role === 'platform') {
task = getPlatformAdminAppointmentUpcomingList(params).then((wrap) => {
return wrap && wrap.ok ? wrap.data : null
})
} else {
// 医生端:xk-api 原始 body,code=0 时业务在 result
task = getAppointmentUpcomingListApi(params).then((res) => {
return res && res.code === 0 ? res.result : null
})
}
return task.then((payload) => {
if (seq !== this.reqSeq || !payload) return
const items = Array.isArray(payload.items) ? payload.items : []
const rows = items.map((item) => this.decorateItem(item))
// 成功才提交页码:list 拼接按目标页判断,第一页替换、后续页追加
this.page = page
this.list = page === 1 ? rows : this.list.concat(rows)
this.total = payload.total || 0
}).catch(() => {
// 静默失败:保留已有列表与页码,用户可再次触发加载
}).finally(() => {
// 仅最新请求负责收尾 loading,过期请求不动状态(新请求可能仍在途)
if (seq === this.reqSeq) {
this.loading = false
}
})
},
/**
* 行数据装饰:把状态样式类、短日期、可改期标记先算进数据(模板禁止 :class 调函数)
*/
decorateItem(item) {
let statusClass = 'is-normal'
if (item.is_pay !== 1) {
statusClass = 'is-wait-pay'
} else if (item.status === 1) {
statusClass = 'is-wait-accept'
} else if (item.status === 2) {
statusClass = 'is-accepting'
}
return {
...item,
status_class: statusClass,
// 列表里日期缩短为 MM-DD,完整年份对当周预约无增量信息
visit_date_short: (item.visit_date || '').slice(5),
// 仅「已支付 + 待接诊」可改期(待支付单让患者先支付或取消,已接诊单改期无意义)
can_reschedule: item.is_pay === 1 && item.status === 1,
}
},
/** 拨打就诊人电话(call_mobile 拦截器解码不脱敏,专供拨号) */
callPatient(item) {
const phone = item.call_mobile || ''
if (!phone) {
uni.showToast({ title: '暂无联系电话', icon: 'none' })
return
}
uni.makePhoneCall({ phoneNumber: phone })
},
/** 打开改期子抽屉并拉取医生本人号源日历(每次打开取最新余号) */
openReschedule(item) {
this.rescheduleTarget = item
this.rescheduleRemark = ''
this.rescheduleShow = true
this.calendarLoading = true
this.calendarDays = []
this.selectedSlotId = -1
getMyScheduleCalendarApi().then((res) => {
this.calendarLoading = false
if (res && res.code === 0 && res.result) {
this.applyCalendar(res.result)
}
}).catch(() => {
this.calendarLoading = false
})
},
closeReschedule() {
this.rescheduleShow = false
},
onReschedulePopupInput(v) {
this.rescheduleShow = v
},
/**
* 预处理日历:MM-DD 短日期、余号/约满/休诊文案、标记当前预约时段
* 与患者端挂号日历同一套预处理逻辑(模板禁止插值调方法,全部提前算好)
*/
applyCalendar(calendar) {
const target = this.rescheduleTarget || {}
const days = (calendar && calendar.days) || []
const processed = days.map((day) => {
const dateParts = (day.date || '').split('-')
const dateText = dateParts.length === 3 ? dateParts[1] + '-' + dateParts[2] : day.date
let stateText = '休诊'
if (day.is_work) {
stateText = day.is_full ? '约满' : '余' + day.left
}
const slots = (day.slots || []).map((slot) => {
let slotState = '余' + slot.left
if (slot.is_expired) slotState = '已过时'
else if (slot.is_full) slotState = '约满'
if (day.date === target.visit_date && slot.slot_id === target.slot_id) slotState = '当前'
return { ...slot, state_text: slotState }
})
return { ...day, date_text: dateText, state_text: stateText, slots }
})
this.calendarDays = processed
// 默认选第一个可约日期(出诊且未约满),都不可约时停在今天
let idx = 0
for (let i = 0; i < processed.length; i++) {
if (processed[i].is_work && !processed[i].is_full) {
idx = i
break
}
}
this.selectedDayIndex = idx
this.autoSelectSlot()
},
/** 切换改期日期:默认选中该日第一个可约时段 */
selectDay(idx) {
this.selectedDayIndex = idx
this.autoSelectSlot()
},
/** 选择时段:约满/已过时不可选 */
selectSlot(slot) {
if (slot.is_full || slot.is_expired) return
this.selectedSlotId = slot.slot_id
},
/** 自动选中当日第一个可约时段(全天单时段直接选中) */
autoSelectSlot() {
this.selectedSlotId = -1
const day = this.calendarDays[this.selectedDayIndex]
if (!day || !day.is_work) return
const slots = day.slots || []
for (let i = 0; i < slots.length; i++) {
if (!slots[i].is_full && !slots[i].is_expired) {
this.selectedSlotId = slots[i].slot_id
return
}
}
},
/** 提交改期:成功后刷新明细列表并通知父组件刷新工作台统计 */
confirmReschedule() {
if (!this.canConfirmReschedule || this.rescheduleSubmitting || !this.rescheduleTarget) return
this.rescheduleSubmitting = true
rescheduleAppointmentApi({
register_id: this.rescheduleTarget.register_id,
visit_date: this.selectedDay.date,
slot_id: this.selectedSlotId,
remark: this.rescheduleRemark,
}).then((res) => {
this.rescheduleSubmitting = false
if (res && res.code === 0) {
this.rescheduleShow = false
uni.showToast({ title: '改期成功', icon: 'success' })
this.resetAndLoad()
// 父组件(工作台)可据此刷新预约统计卡片
this.$emit('rescheduled', res.result || null)
} else {
uni.showToast({ title: (res && res.message) || '改期失败', icon: 'none' })
}
}).catch(() => {
this.rescheduleSubmitting = false
uni.showToast({ title: '改期失败,请稍后重试', icon: 'none' })
})
},
},
}
</script>
<style lang="scss" scoped>
.appt-drawer {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.appt-drawer__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.appt-drawer__title {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
}
.appt-drawer__close {
font-size: 28rpx;
color: #6acdbb;
}
.appt-drawer__filters {
flex-shrink: 0;
white-space: nowrap;
padding: 20rpx 0;
}
.appt-drawer__filters-inner {
display: inline-flex;
padding: 0 24rpx;
}
.appt-pill {
display: inline-block;
padding: 10rpx 24rpx;
margin-right: 16rpx;
font-size: 24rpx;
color: #6c7380;
background: #f5f6f8;
border-radius: 999rpx;
&--active {
color: #fff;
background: #6acdbb;
font-weight: 500;
}
}
.appt-drawer__list {
flex: 1;
min-height: 0;
padding: 0 24rpx;
box-sizing: border-box;
}
.appt-drawer__loading {
display: flex;
justify-content: center;
padding: 80rpx 0;
}
.appt-row {
display: flex;
align-items: center;
padding: 24rpx 8rpx;
border-bottom: 1rpx solid #f5f6f8;
&__date {
display: flex;
flex-direction: column;
align-items: center;
width: 108rpx;
flex-shrink: 0;
}
&__day {
font-size: 30rpx;
font-weight: 600;
color: #1d2129;
}
&__week {
font-size: 22rpx;
color: #94a3b8;
margin-top: 4rpx;
}
&__main {
flex: 1;
min-width: 0;
margin: 0 16rpx;
}
&__line1 {
display: flex;
align-items: center;
}
&__patient {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-right: 16rpx;
}
&__slot {
font-size: 24rpx;
color: #6acdbb;
}
&__line2 {
margin-top: 8rpx;
}
&__meta {
font-size: 24rpx;
color: #6c7380;
margin-right: 20rpx;
}
&__status {
flex-shrink: 0;
font-size: 24rpx;
padding: 6rpx 18rpx;
border-radius: 999rpx;
&.is-wait-pay {
color: #f97316;
background: rgba(249, 115, 22, 0.1);
}
&.is-wait-accept {
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
&.is-accepting {
color: #00c853;
background: rgba(0, 200, 83, 0.1);
}
&.is-normal {
color: #6c7380;
background: #f5f6f8;
}
}
}
.appt-drawer__empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
&-text {
margin-top: 20rpx;
font-size: 26rpx;
color: #94a3b8;
}
}
.appt-drawer__footer {
display: flex;
justify-content: center;
padding: 24rpx 0 40rpx;
&-text {
font-size: 24rpx;
color: #94a3b8;
}
}
// 行内操作按钮(电话/改期):1px 同色系边框 + 微光,不用左侧色条
.appt-row__actions {
display: flex;
margin-top: 12rpx;
}
.appt-action {
display: inline-flex;
align-items: center;
padding: 6rpx 20rpx;
margin-right: 16rpx;
border: 1px solid rgba(106, 205, 187, 0.3);
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
border-radius: 999rpx;
&__text {
margin-left: 6rpx;
font-size: 24rpx;
color: #6acdbb;
}
}
// 医生代改期子抽屉
.resch {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.resch__current {
flex-shrink: 0;
padding: 16rpx 32rpx 0;
font-size: 26rpx;
color: #6c7380;
}
.resch__body {
flex: 1;
min-height: 0;
padding: 16rpx 24rpx 0;
box-sizing: border-box;
}
.resch__days {
white-space: nowrap;
}
.resch__days-inner {
display: inline-flex;
}
.resch-day {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 124rpx;
padding: 16rpx 0;
margin-right: 16rpx;
border-radius: 12rpx;
border: 1px solid #e5e7eb;
background: #fff;
&__week {
font-size: 26rpx;
font-weight: 600;
color: #1d2129;
}
&__date {
font-size: 22rpx;
color: #6c7380;
margin: 6rpx 0;
}
&__state {
font-size: 22rpx;
color: #6acdbb;
&--rest {
color: #94a3b8;
}
}
&--active {
border-color: rgba(106, 205, 187, 0.6);
background: rgba(106, 205, 187, 0.08);
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
}
&--disabled {
opacity: 0.55;
}
}
.resch__slots {
display: flex;
flex-wrap: wrap;
margin-top: 20rpx;
}
.resch-slot {
display: flex;
flex-direction: column;
align-items: center;
width: 210rpx;
padding: 14rpx 0;
margin: 0 16rpx 16rpx 0;
border-radius: 12rpx;
border: 1px solid #e5e7eb;
background: #fff;
&__name {
font-size: 26rpx;
font-weight: 600;
color: #1d2129;
}
&__time {
font-size: 22rpx;
color: #6c7380;
margin: 4rpx 0;
}
&__state {
font-size: 22rpx;
color: #6acdbb;
}
&--active {
border-color: rgba(106, 205, 187, 0.6);
background: rgba(106, 205, 187, 0.08);
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
}
&--disabled {
opacity: 0.55;
}
}
.resch__tip {
padding: 32rpx 0;
text-align: center;
font-size: 26rpx;
color: #94a3b8;
}
.resch__remark {
margin: 8rpx 0 32rpx;
&-input {
width: 100%;
height: 76rpx;
padding: 0 24rpx;
box-sizing: border-box;
font-size: 26rpx;
color: #1d2129;
background: #f5f6f8;
border-radius: 12rpx;
}
}
.resch__footer {
flex-shrink: 0;
padding: 16rpx 32rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
}
.resch__selected {
font-size: 26rpx;
color: #6acdbb;
margin-bottom: 12rpx;
}
.resch__btn {
height: 86rpx;
line-height: 86rpx;
text-align: center;
font-size: 32rpx;
font-weight: 500;
color: #fff;
background: #6acdbb;
border-radius: 64rpx;
&--disabled {
opacity: 0.5;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="flex-col flex-ali-center state"> <view class="flex-col flex-ali-center state">
<image src="../../static/image/sf.png"></image> <image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/sf.png"></image>
<d-text text="您的认证信息已提交!" className="tips-c m-t-10 m-t-3"></d-text> <d-text text="您的认证信息已提交!" className="tips-c m-t-10 m-t-3"></d-text>
<d-text text="我们会尽快审核,请耐心等待" className="tips-c m-t-1"></d-text> <d-text text="我们会尽快审核,请耐心等待" className="tips-c m-t-1"></d-text>
</view> </view>

View File

@@ -33,8 +33,8 @@
type: [Object,Array], type: [Object,Array],
default: () => { default: () => {
return { return {
'list': '../../static/image/list.png', 'list': 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/list.png',
'search': '../../static/image/search.png' 'search': 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/search.png'
} }
} }
} }

View File

@@ -6,7 +6,7 @@
<page-container <page-container
v-if="usePageContainer && show" v-if="usePageContainer && pageGuard"
:show="show" :show="show"
@@ -14,6 +14,8 @@
@leave="close" @leave="close"
@afterleave="releasePageGuard"
/> />
<!-- #endif --> <!-- #endif -->
@@ -94,6 +96,10 @@
show: this.value, show: this.value,
pageGuard: false,
releaseTimer: null,
canvas: null, canvas: null,
loading: false loading: false
@@ -108,6 +114,16 @@
this.show = v this.show = v
if (v) {
this.pageGuard = true
} else {
this.scheduleReleaseGuard()
}
}, },
show(v) { show(v) {
@@ -134,8 +150,48 @@
}, },
beforeDestroy() {
this.clearReleaseTimer()
},
methods: { methods: {
releasePageGuard() {
this.clearReleaseTimer()
this.pageGuard = false
},
scheduleReleaseGuard() {
this.clearReleaseTimer()
this.releaseTimer = setTimeout(() => {
this.pageGuard = false
this.releaseTimer = null
}, 350)
},
clearReleaseTimer() {
if (this.releaseTimer) {
clearTimeout(this.releaseTimer)
this.releaseTimer = null
}
},
initCanvas() { initCanvas() {
this.canvas = new Mycanvas({ this.canvas = new Mycanvas({

View File

@@ -8,7 +8,7 @@
:maxlength="11" :maxlength="11"
height="96" height="96"
v-model="form.mobile" v-model="form.mobile"
:prefixIcon="require('../../static/image/act.png')" :prefixIcon="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/act.png'"
borderRadius="96rpx" borderRadius="96rpx"
prefixIconSize="40rpx" prefixIconSize="40rpx"
placeholder="请输入手机号" placeholder="请输入手机号"
@@ -22,7 +22,7 @@
height="96" height="96"
v-model="form.password" v-model="form.password"
type="password" type="password"
:prefixIcon="require('../../static/image/pwd.png')" :prefixIcon="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/pwd.png'"
password password
borderRadius="96rpx" borderRadius="96rpx"
prefixIconSize="40rpx" prefixIconSize="40rpx"
@@ -37,12 +37,12 @@
type="number" type="number"
:maxlength="6" :maxlength="6"
placeholder="请填写验证码" placeholder="请填写验证码"
:prefixIcon="require('../../static/image/pwd.png')" :prefixIcon="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/pwd.png'"
:placeholder-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }" :placeholder-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
:custom-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }" :custom-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
> >
<template slot="icon"> <template slot="icon">
<!-- <image src="../../../../static/image/pwd.png"></image>--> <!-- <image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/pwd.png"></image>-->
<u-icon <u-icon
size="40" size="40"
name="chat" name="chat"

View File

@@ -0,0 +1,587 @@
<template>
<!--
识图 Loading(对齐 PC 端 apps/web-antd/src/components/loading/OcrImageLoading.vue 的视觉与交互):
扫描原图 → 进度条 → 结构化处方逐条揭示。
全屏层与「取消识别」出口由调用方提供(ImageOcrDrawer 的 .ocr-scan),本组件只负责卡本身。
-->
<view class="oil">
<!-- 头部:logo + 标题/副标题 + 百分比 -->
<view class="oil__head">
<view class="oil__logo">
<view class="oil__logo-halo"></view>
<view class="oil__logo-mark">
<view class="oil__logo-bar oil__logo-bar--v"></view>
<view class="oil__logo-bar oil__logo-bar--h"></view>
</view>
</view>
<view class="oil__meta">
<text class="oil__title">{{ titleText }}</text>
<view class="oil__sub">
<view class="oil__pulse"></view>
<text class="oil__sub-text">正在解析处方图像…</text>
</view>
</view>
<view class="oil__pct">
<text class="oil__pct-num">{{ pctText }}</text>
<text class="oil__pct-unit">%</text>
</view>
</view>
<!-- 进度条:整段走 WXSS animation,不占 setData -->
<view class="oil__track">
<view class="oil__fill"></view>
</view>
<!-- 原始处方图像:清晰层自上而下揭示 + 扫描线扫过 -->
<view class="oil__label">
<view class="oil__label-dot"></view>
<text class="oil__label-text">原始处方图像</text>
</view>
<view class="oil__doc">
<image
v-if="image"
class="oil__paper oil__paper--blur"
:src="image"
mode="aspectFill"
></image>
<view v-else class="oil__paper oil__paper--blur oil__paper--ph"></view>
<view class="oil__reveal">
<image
v-if="image"
class="oil__paper oil__paper--sharp"
:src="image"
mode="aspectFill"
></image>
<view v-else class="oil__paper oil__paper--sharp oil__paper--ph"></view>
</view>
<view class="oil__scan">
<view class="oil__scan-glow"></view>
<view class="oil__scan-line"></view>
</view>
</view>
<!-- 指向结构化结果 -->
<view class="oil__arrow"></view>
<!-- 结构化处方:只给骨架条,不显示任何具体药名 -->
<view class="oil__label">
<view class="oil__label-dot"></view>
<text class="oil__label-text">结构化处方</text>
</view>
<view class="oil__result">
<view
v-for="row in skeletonRows"
:key="row.key"
class="oil__rrow"
:class="row.lineCls"
:style="row.style"
>
<text class="oil__rrow-tag" :class="row.tagCls">{{ row.tag }}</text>
<view class="oil__rrow-fill">
<view class="oil__rrow-bar" :style="row.barStyle"></view>
</view>
</view>
</view>
</view>
</template>
<script>
/** 扫描时长(ms):必须与样式里 .oil__scan / .oil__reveal / .oil__fill 的 animation-duration 一致 */
const OIL_SCAN_MS = 2400;
/**
* 识图 Loading
*
* 与 PC 端的差异都是小程序平台的硬约束,不是设计变更:
* 1. PC 用 requestAnimationFrame 逐帧改 style;小程序每帧改 DOM 要过 setData(代价高),
* 所以「扫描线 / 清晰层揭示 / 进度条 / 骨架行入场」全部走 WXSS animation —— 零 setData。
* 只有百分比数字必须 JS(文本内容改不了),90ms 一次单字段 setData。
* 2. PC 用 clip-path: inset() 做揭示、mask-composite 做渐变描边 —— 这两个全仓 grep 为 0
* (本仓从未使用),改用 overflow:hidden + height 动画,兼容性更稳。
* 3. **右侧刻意只给骨架条,不显示具体药名**:PC 那个组件里是演示数据(炙黄芪 15g…),
* 真实场景识别还没出结果,显示药名会让人误以为已经识别出来了。
* 4. **只播一次,不循环**:真实识别耗时不定(可能 2s 也可能 20s),循环回绕会出现
* 「进度从 100% 跳回 0%」的反直觉。改为单次播完后停在终态,靠骨架条的 shimmer 持续表明「还在转」。
*/
export default {
name: 'OcrImageLoading',
props: {
/** 原始处方图;识图场景必有,为空时退化为占位纸 */
image: {
type: String,
default: '',
},
/** 主标题,空则用默认 */
title: {
type: String,
default: '',
},
},
data() {
return {
/** 百分比数字:只能 JS 驱动,到顶即停(不空转) */
pctText: '0',
pctTimer: null,
};
},
computed: {
titleText() {
return String(this.title || '').trim() || 'AI 处方识别';
},
/**
* 骨架行:宽度、入场延迟、类名全部预计算成字符串 ——
* 模板里禁止方法调用;也不要用「BEM 前缀 + 下标」拼类名:类名对账只认字面量,
* 拼出来的半截名(前缀结尾停在双连字符上)会被判成 MISSING。
* 揭示时刻对齐 PC 端的 THRESHOLDS(0.08 / 0.24 / 0.4 / 0.54 / 0.68 / 0.82 × 扫描时长)。
*/
skeletonRows() {
const rows = [
{ key: 'p', tag: '患者', rx: false, delay: 190, w: 88 },
{ key: 'd', tag: '诊断', rx: false, delay: 580, w: 62 },
{ key: 'r1', tag: 'Rx', rx: true, delay: 960, w: 80 },
{ key: 'r2', tag: 'Rx', rx: true, delay: 1300, w: 56 },
{ key: 'r3', tag: 'Rx', rx: true, delay: 1640, w: 70 },
{ key: 'a', tag: '医嘱', rx: false, delay: 1970, w: 64 },
];
return rows.map((r, i) => ({
key: r.key,
tag: r.tag,
lineCls: i === 0 ? '' : 'oil__rrow--line',
tagCls: r.rx ? 'oil__rrow-tag--rx' : '',
style: 'animation-delay:' + r.delay + 'ms',
barStyle: 'width:' + r.w + '%',
}));
},
},
mounted() {
this.startPct();
},
beforeDestroy() {
this.stopPct();
},
methods: {
/** 与 .oil__fill / .oil__scan 的 WXSS animation 同一条时间轴(OIL_SCAN_MS),保证数字和条同步 */
startPct() {
this.stopPct();
const t0 = Date.now();
this.pctText = '0';
this.pctTimer = setInterval(() => {
const p = Math.min((Date.now() - t0) / OIL_SCAN_MS, 1);
const next = String(Math.round(p * 100));
if (next !== this.pctText) this.pctText = next;
if (p >= 1) this.stopPct();
}, 90);
},
stopPct() {
if (this.pctTimer) {
clearInterval(this.pctTimer);
this.pctTimer = null;
}
},
},
};
</script>
<style lang="scss" scoped>
/* 玻璃卡:这条业务线的 C 端语言,浅底上靠青绿外环圈边界 */
.oil {
width: 600rpx;
padding: 40rpx 40rpx 36rpx;
box-sizing: border-box;
border-radius: 36rpx;
background: linear-gradient(158deg, rgba(255, 255, 255, 0.86) 0%, rgba(255, 255, 255, 0.58) 100%);
border: 1px solid rgba(255, 255, 255, 0.72);
box-shadow:
0 20rpx 48rpx rgba(12, 48, 44, 0.16),
0 2rpx 8rpx rgba(12, 48, 44, 0.05),
0 0 0 1rpx rgba(0, 135, 113, 0.12),
inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
}
/* ---------- 头部 ---------- */
.oil__head {
display: flex;
flex-direction: row;
align-items: center;
}
.oil__logo {
position: relative;
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 72rpx;
height: 72rpx;
border-radius: 22rpx;
background: linear-gradient(150deg, rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0.22) 52%, rgba(255, 255, 255, 0.4) 100%);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.92),
inset 0 0 20rpx rgba(0, 191, 166, 0.12),
0 6rpx 18rpx -8rpx rgba(5, 95, 70, 0.3),
0 0 0 1rpx rgba(0, 191, 166, 0.14);
}
.oil__logo-halo {
position: absolute;
left: -12rpx;
top: -12rpx;
right: -12rpx;
bottom: -12rpx;
border-radius: 30rpx;
background: radial-gradient(circle, rgba(106, 205, 187, 0.42), rgba(106, 205, 187, 0) 68%);
animation: oil-breath 3.4s ease-in-out infinite;
}
.oil__logo-mark {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 34rpx;
height: 34rpx;
}
.oil__logo-bar {
position: absolute;
border-radius: 4rpx;
background: linear-gradient(135deg, #00BFA6 0%, #008771 100%);
}
.oil__logo-bar--v {
width: 6rpx;
height: 34rpx;
}
.oil__logo-bar--h {
width: 34rpx;
height: 6rpx;
}
.oil__meta {
flex: 1;
min-width: 0;
margin-left: 20rpx;
}
.oil__title {
display: block;
font-size: 30rpx;
font-weight: 700;
line-height: 42rpx;
color: #16302c;
}
.oil__sub {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 6rpx;
}
.oil__pulse {
flex-shrink: 0;
width: 10rpx;
height: 10rpx;
margin-right: 12rpx;
border-radius: 50%;
background: #00BFA6;
animation: oil-beat 1.7s ease-out infinite;
}
.oil__sub-text {
font-size: 24rpx;
line-height: 34rpx;
color: #7a8794;
}
.oil__pct {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: flex-end;
margin-left: 16rpx;
}
.oil__pct-num {
font-size: 40rpx;
font-weight: 700;
line-height: 46rpx;
color: #008771;
}
.oil__pct-unit {
margin-left: 2rpx;
font-size: 22rpx;
line-height: 30rpx;
color: #7a8794;
}
/* ---------- 进度条 ---------- */
.oil__track {
position: relative;
height: 10rpx;
margin: 28rpx 0 26rpx;
border-radius: 99rpx;
overflow: hidden;
background: rgba(11, 58, 44, 0.07);
}
.oil__fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
border-radius: 99rpx;
background: linear-gradient(90deg, #6ACDBB 0%, #00BFA6 58%, #008771 100%);
/* 与 OIL_SCAN_MS 一致,both = 播完停在终态 */
animation: oil-fill 2400ms linear both;
}
.oil__fill::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 40%;
background: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0));
animation: oil-shimmer 1.7s linear infinite;
}
/* ---------- 分组小标题 ---------- */
.oil__label {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 12rpx;
}
.oil__label-dot {
width: 8rpx;
height: 8rpx;
margin-right: 10rpx;
border-radius: 2rpx;
background: #6ACDBB;
}
.oil__label-text {
font-size: 20rpx;
font-weight: 600;
letter-spacing: 2rpx;
color: #98a2b3;
}
/* ---------- 原始处方图像(揭示 + 扫描线) ---------- */
.oil__doc {
position: relative;
width: 100%;
height: 300rpx;
border-radius: 24rpx;
overflow: hidden;
background: #fff;
box-shadow:
0 12rpx 26rpx -16rpx rgba(3, 55, 40, 0.45),
0 0 0 1rpx rgba(11, 58, 44, 0.05);
}
.oil__paper {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 300rpx;
}
/* 未扫描到的部分压暗(PC 用 blur,小程序不引入 filter: blur 的机型兼容风险) */
.oil__paper--blur {
opacity: 0.34;
}
.oil__paper--sharp {
opacity: 1;
}
.oil__paper--ph {
background: linear-gradient(180deg, #f2f7f5 0%, #e8f1ee 100%);
}
/* 揭示:用 overflow hidden + 高度动画,不用 clip-path */
.oil__reveal {
position: absolute;
left: 0;
top: 0;
right: 0;
height: 0;
overflow: hidden;
animation: oil-reveal 2400ms linear both;
}
.oil__scan {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 0;
animation: oil-scan 2400ms linear both;
}
.oil__scan-line {
position: absolute;
left: -4rpx;
right: -4rpx;
top: -1rpx;
height: 4rpx;
background: linear-gradient(
90deg,
rgba(0, 191, 166, 0) 0%,
rgba(0, 191, 166, 0.9) 14%,
#6ACDBB 50%,
rgba(0, 191, 166, 0.9) 86%,
rgba(0, 191, 166, 0) 100%
);
box-shadow:
0 0 10rpx 1rpx rgba(0, 191, 166, 0.9),
0 0 30rpx 6rpx rgba(0, 191, 166, 0.38);
}
.oil__scan-glow {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 100rpx;
background: linear-gradient(
to top,
rgba(0, 191, 166, 0.24) 0%,
rgba(0, 191, 166, 0.09) 46%,
rgba(0, 191, 166, 0) 100%
);
}
/* ---------- 向下箭头 ---------- */
.oil__arrow {
width: 18rpx;
height: 18rpx;
margin: 18rpx auto 20rpx;
border-right: 3rpx solid rgba(0, 191, 166, 0.7);
border-bottom: 3rpx solid rgba(0, 191, 166, 0.7);
transform: rotate(45deg);
animation: oil-arrow 1.6s ease-in-out infinite;
}
/* ---------- 结构化处方(骨架) ---------- */
.oil__result {
border-radius: 20rpx;
overflow: hidden;
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(11, 58, 44, 0.06);
}
.oil__rrow {
display: flex;
flex-direction: row;
align-items: center;
padding: 18rpx 20rpx;
animation: oil-row-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* 分隔线用修饰类,不用相邻兄弟选择器(WXSS 里选择器越简单越稳) */
.oil__rrow--line {
border-top: 1px solid rgba(11, 58, 44, 0.06);
}
.oil__rrow-tag {
flex-shrink: 0;
width: 64rpx;
font-size: 22rpx;
color: #98a2b3;
}
.oil__rrow-tag--rx {
font-family: Georgia, 'Times New Roman', serif;
font-size: 24rpx;
font-weight: 700;
font-style: italic;
color: #008771;
}
.oil__rrow-fill {
flex: 1;
min-width: 0;
margin-left: 16rpx;
}
.oil__rrow-bar {
position: relative;
height: 20rpx;
border-radius: 99rpx;
overflow: hidden;
background: rgba(11, 58, 44, 0.08);
}
.oil__rrow-bar::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 40%;
background: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0));
animation: oil-shimmer 1.6s linear infinite;
}
@keyframes oil-breath {
0%,
100% {
opacity: 0.35;
transform: scale(0.96);
}
50% {
opacity: 0.8;
transform: scale(1.04);
}
}
@keyframes oil-beat {
0% {
box-shadow: 0 0 0 0 rgba(0, 191, 166, 0.5);
}
70% {
box-shadow: 0 0 0 14rpx rgba(0, 191, 166, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(0, 191, 166, 0);
}
}
@keyframes oil-fill {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes oil-reveal {
0% {
height: 0;
}
100% {
height: 300rpx;
}
}
@keyframes oil-scan {
0% {
transform: translateY(0);
opacity: 0;
}
3% {
opacity: 1;
}
96% {
opacity: 1;
}
100% {
transform: translateY(300rpx);
opacity: 0;
}
}
@keyframes oil-arrow {
0%,
100% {
transform: rotate(45deg) translate(0, 0);
opacity: 0.45;
}
50% {
transform: rotate(45deg) translate(5rpx, 5rpx);
opacity: 1;
}
}
@keyframes oil-row-in {
0% {
opacity: 0;
transform: translateX(-20rpx);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes oil-shimmer {
0% {
transform: translateX(-120%);
}
100% {
transform: translateX(320%);
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<!-- 公账逾期条幅:门店逾期即可展示,点击进锁店页(硬锁 reLaunch / 软进 soft) -->
<view v-if="visible" class="pap-banner" :class="toneClass" @click="onTap">
<view class="pap-banner-main">
<text class="pap-banner-title">公账日账单已逾期</text>
<text class="pap-banner-desc">{{ descTxt }}</text>
</view>
<text class="pap-banner-action">去处理</text>
</view>
</template>
<script>
/**
* 工作台顶部公账逾期条幅
* 用 lock-status 现算;store_locked 即显示,不要求当前身份硬锁
*/
import { getPublicAccountLockStatusApi } from '@/api/publicAccountPay.js'
import { unwrapXkApi } from '@/utils/api-response.js'
import {
isPublicAccountStoreLocked,
openPublicAccountLockFromBanner,
} from '@/utils/publicAccountLock.js'
export default {
name: 'PublicAccountLockBanner',
data() {
return {
payload: null,
loading: false,
}
},
computed: {
visible() {
return isPublicAccountStoreLocked(this.payload)
},
amountTxt() {
const p = this.payload || {}
return p.platform_amount_total || p.platform_amount || '0.00'
},
billCount() {
const n = Number((this.payload && this.payload.bill_count) || 0)
return n > 0 ? Math.floor(n) : 0
},
descTxt() {
const range = String((this.payload && this.payload.bill_date_range_txt) || '').trim()
if (this.billCount > 1) {
return '合并 ' + this.billCount + ' 张(' + (range || '多日') + '),合计 ¥' + this.amountTxt + ',点此处理'
}
return '应付款 ¥' + this.amountTxt + ',点此处理'
},
toneClass() {
const color = (this.payload && this.payload.alert_color) || 'yellow'
if (color === 'blue' || color === 'red') {
return 'is-' + color
}
return 'is-yellow'
},
},
methods: {
/**
* 工作台 onShow 调用:拉现算锁态,决定是否展示条幅
*/
async refresh() {
if (this.loading) {
return
}
this.loading = true
try {
const res = await getPublicAccountLockStatusApi()
const wrap = unwrapXkApi(res)
if (!wrap.ok) {
this.payload = null
return
}
this.payload = wrap.payload || null
} catch (e) {
this.payload = null
} finally {
this.loading = false
}
},
onTap() {
if (!this.payload) {
return
}
openPublicAccountLockFromBanner(this.payload)
},
},
}
</script>
<style lang="scss" scoped>
.pap-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin: 16rpx 24rpx 0;
padding: 20rpx 24rpx;
border-radius: 12rpx;
border: 1px solid rgba(217, 119, 6, 0.35);
box-shadow: 0 0 12rpx rgba(217, 119, 6, 0.16);
background: rgba(217, 119, 6, 0.12);
}
.pap-banner.is-blue {
border-color: rgba(37, 99, 235, 0.35);
box-shadow: 0 0 12rpx rgba(37, 99, 235, 0.16);
background: rgba(37, 99, 235, 0.12);
}
.pap-banner.is-red {
border-color: rgba(220, 38, 38, 0.35);
box-shadow: 0 0 12rpx rgba(220, 38, 38, 0.16);
background: rgba(220, 38, 38, 0.12);
}
.pap-banner-main {
flex: 1;
min-width: 0;
}
.pap-banner-title {
display: block;
margin-bottom: 6rpx;
font-size: 28rpx;
font-weight: 600;
color: #d97706;
}
.pap-banner.is-blue .pap-banner-title {
color: #2563eb;
}
.pap-banner.is-red .pap-banner-title {
color: #dc2626;
}
.pap-banner-desc {
display: block;
font-size: 24rpx;
line-height: 1.4;
color: #64748b;
}
.pap-banner-action {
flex-shrink: 0;
font-size: 26rpx;
font-weight: 600;
color: #d97706;
}
.pap-banner.is-blue .pap-banner-action {
color: #2563eb;
}
.pap-banner.is-red .pap-banner-action {
color: #dc2626;
}
</style>

View File

@@ -0,0 +1,678 @@
<template>
<!--
开方医生选择:底部抽屉(顶部「最近选择」头像 chip + 头像/姓名/职称/科室卡片)
放主包:识图抽屉在 sub_workbench,上传页在 sub_image_transfer,
跨分包嵌套异步占位会导致点「开方医生」无弹层;主包同步引用可点
挂载与可见拆开:mounted 控制 v-if,visible 控制进出场 class,关闭时先播离场再卸载
模板禁止 ?? ?. 与 :class 方法调用(类名在 rows 里预先算好)
-->
<view v-if="mounted" class="tdp-root">
<view class="tdp-mask" :class="{ 'tdp-mask--in': visible }" @tap="onClose" @touchmove.stop.prevent="noop"></view>
<view class="tdp-sheet" :class="{ 'tdp-sheet--in': visible }" @tap.stop="noop">
<view class="tdp-grip"></view>
<view class="tdp-head">
<view class="tdp-head__main">
<text class="tdp-title">选择开方医生</text>
<text class="tdp-sub">审方与处方将归属所选医生</text>
</view>
<view class="tdp-close" hover-class="tdp-close--hover" @tap="onClose">
<u-icon name="close" size="24" color="#5f6f7a"></u-icon>
</view>
</view>
<!-- 提交校验拉起时的原因提示:1px 同色边框 + 微光,不用左侧色条 -->
<view v-if="tip" class="tdp-tip">
<u-icon name="info-circle-fill" size="30" color="#D48806"></u-icon>
<text class="tdp-tip__text">{{ tip }}</text>
</view>
<!-- 最近选择:只显示仍在本店列表里的医生(最多 5 位),点 chip 与点下方卡片同一套反馈 -->
<view v-if="recentRows.length" class="tdp-recent">
<text class="tdp-section">最近选择</text>
<scroll-view scroll-x class="tdp-recent__scroll" :show-scrollbar="false">
<view class="tdp-recent__track">
<view
v-for="r in recentRows"
:key="r.id"
class="tdp-recent__chip"
:class="r.chipCls"
:style="r.enterStyle"
hover-class="tdp-recent__chip--press"
:data-id="r.id"
@tap="onPickTap"
>
<image
v-if="r.avatar"
class="tdp-recent__ava"
:src="r.avatar"
mode="aspectFill"
:data-id="r.id"
@error="onAvatarError"
/>
<view v-else class="tdp-recent__ava tdp-avatar--fallback">
<text class="tdp-recent__char">{{ r.initial }}</text>
</view>
<text class="tdp-recent__name">{{ r.name }}</text>
<view v-if="r.on" class="tdp-recent__on">
<u-icon name="checkmark" size="16" color="#FFFFFF"></u-icon>
</view>
</view>
</view>
</scroll-view>
<text class="tdp-section tdp-section--all">全部医生</text>
</view>
<scroll-view scroll-y class="tdp-scroll" :style="scrollStyle">
<view v-if="!rows.length" class="tdp-empty">
<view class="tdp-empty__ic">
<u-icon name="account" size="56" color="#6acdbb"></u-icon>
</view>
<text class="tdp-empty__title">暂无本店医生</text>
<text class="tdp-empty__sub">请先在后台为本诊所绑定医生</text>
</view>
<view
v-for="row in rows"
:key="row.id"
class="tdp-card"
:class="row.cardCls"
:style="row.enterStyle"
hover-class="tdp-card--press"
:data-id="row.id"
@tap="onPickTap"
>
<image
v-if="row.avatar"
class="tdp-avatar"
:src="row.avatar"
mode="aspectFill"
:data-id="row.id"
@error="onAvatarError"
/>
<view v-else class="tdp-avatar tdp-avatar--fallback">
<text class="tdp-avatar__char">{{ row.initial }}</text>
</view>
<view class="tdp-main">
<view class="tdp-name-row">
<text class="tdp-name">{{ row.name }}</text>
<text v-if="row.title" class="tdp-tag">{{ row.title }}</text>
<text v-if="row.suggested" class="tdp-tag tdp-tag--ocr">处方识别</text>
</view>
<text class="tdp-dept">{{ row.dept }}</text>
</view>
<view class="tdp-check" :class="row.checkCls">
<u-icon v-if="row.on" name="checkmark" size="24" color="#FFFFFF"></u-icon>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { getRecentTransferDoctorIds, pushRecentTransferDoctor } from '@/utils/transferRecentDoctors.js';
/** 离场动画时长,需与 .tdp-sheet / .tdp-mask 的 transition 一致 */
const LEAVE_MS = 300;
/** 点选后保留勾选反馈的时长,再关闭抽屉 */
const PICK_FEEDBACK_MS = 180;
/**
* 传方开方医生选择器(主包)
* 列表项约定:id / name / avatar / department / title
* 事件:input(false) 关闭;select(doc) 选中(在勾选反馈后触发)
* 选中时自己记「最近选择」:上传页与驳回重提页共用本组件,记录口径只有一处
*/
export default {
name: 'TransferDoctorPicker',
props: {
value: { type: Boolean, default: false },
list: { type: Array, default: () => [] },
selectedId: { type: [Number, String], default: 0 },
/** 顶部原因提示(如提交时未选医生),空则不显示 */
tip: { type: String, default: '' },
/** 处方识别到的医生候选 id:卡片上打「处方识别」标签,方便在长列表里一眼找到 */
suggestIds: { type: Array, default: () => [] },
},
data() {
return {
/** 是否挂载节点:关闭时延迟到离场动画结束再卸载 */
mounted: false,
/** 是否处于进场态:挂载后下一帧再置 true,transition 才会播放 */
visible: false,
/** 刚点中的医生 id:先亮勾选再关闭,父组件回写 selectedId 前也有反馈 */
pickedId: 0,
picking: false,
enterTimer: null,
leaveTimer: null,
pickTimer: null,
/** 最近选择的医生 id(每次打开从本地重读,别处选过也能同步) */
recentIds: [],
/** 头像加载失败的医生 id → true;失败后回落首字,避免只剩空白圆圈 */
avatarFailed: {},
};
},
computed: {
/**
* 预先算好每张卡的展示字段与类名(微信模板禁止 :class 方法调用)
* enterStyle 做错峰入场:前 8 张依次延迟,再往后不再加延迟,避免长列表等太久
*/
rows() {
const list = Array.isArray(this.list) ? this.list : [];
const currentId = Number(this.pickedId || this.selectedId || 0);
const suggest = (Array.isArray(this.suggestIds) ? this.suggestIds : []).map((v) => Number(v || 0));
// 有「最近选择」区时卡片整体晚一点入场,让视线先落在最近区
const baseDelay = this.recentRows.length ? 200 : 120;
return list.map((doc, i) => {
const id = Number((doc && doc.id) || 0);
const name = String((doc && (doc.name || doc.real_name)) || '').trim() || '医生#' + id;
const on = id > 0 && id === currentId;
return {
id,
name,
initial: name.slice(0, 1),
avatar: this.avatarFailed[id] ? '' : String((doc && doc.avatar) || ''),
title: String((doc && doc.title) || ''),
dept: String((doc && (doc.department || doc.depart)) || '') || '未设置科室',
on,
suggested: id > 0 && suggest.indexOf(id) !== -1,
cardCls: on ? 'tdp-card--on' : '',
checkCls: on ? 'tdp-check--on' : '',
enterStyle: 'animation-delay:' + (baseDelay + Math.min(i, 8) * 45) + 'ms;',
};
});
},
/**
* 「最近选择」chip:按最近顺序从当前列表里回查
* 不在列表里的 id(换店/医生解绑)直接跳过,展示字段一律取列表最新数据
*/
recentRows() {
const list = Array.isArray(this.list) ? this.list : [];
if (!list.length || !this.recentIds.length) return [];
const currentId = Number(this.pickedId || this.selectedId || 0);
const out = [];
this.recentIds.forEach((rid) => {
const doc = list.find((d) => Number((d && d.id) || 0) === rid);
if (!doc) return;
const name = String(doc.name || doc.real_name || '').trim() || '医生#' + rid;
const on = rid === currentId;
out.push({
id: rid,
name,
initial: name.slice(0, 1),
avatar: this.avatarFailed[rid] ? '' : String(doc.avatar || ''),
on,
chipCls: on ? 'tdp-recent__chip--on' : '',
enterStyle: 'animation-delay:' + (100 + out.length * 40) + 'ms;',
});
});
return out;
},
/**
* 医生少时按内容自适应高度,多于 4 位再固定高度滚动,避免短列表下方大片留白
* 抽屉 max-height 80vh:顶部多了「最近选择」或原因提示时要扣掉对应高度,否则小屏底部被裁
*/
scrollStyle() {
if (this.rows.length <= 4) return '';
let vh = 56;
if (this.recentRows.length) vh -= 14;
if (this.tip) vh -= 6;
return 'height:' + vh + 'vh;';
},
},
watch: {
value: {
immediate: true,
handler(v) {
if (v) {
this.enter();
} else {
this.leave();
}
},
},
},
beforeDestroy() {
this.clearTimers();
},
methods: {
noop() {},
clearTimers() {
['enterTimer', 'leaveTimer', 'pickTimer'].forEach((key) => {
if (this[key]) {
clearTimeout(this[key]);
this[key] = null;
}
});
},
/**
* 进场:先挂载(visible=false 的初始位),稍后再加进场类
* 为什么用 setTimeout 而不是只靠 $nextTick:小程序 setData 分批渲染,
* 同一帧挂载 + 加类会直接跳到终态,看不到上滑动画
*/
enter() {
this.clearTimers();
this.pickedId = 0;
this.picking = false;
this.recentIds = getRecentTransferDoctorIds();
this.mounted = true;
this.$nextTick(() => {
this.enterTimer = setTimeout(() => {
this.enterTimer = null;
this.visible = true;
}, 30);
});
},
/** 离场:先撤进场类播放下滑/淡出,结束后再卸载节点 */
leave() {
if (!this.mounted) return;
this.clearTimers();
this.visible = false;
this.leaveTimer = setTimeout(() => {
this.leaveTimer = null;
this.mounted = false;
this.pickedId = 0;
this.picking = false;
}, LEAVE_MS);
},
onClose() {
if (this.picking) return;
this.$emit('input', false);
},
/**
* 点选医生:用 data-id 回查列表(小程序不宜把 v-for 对象直接当 tap 参数)
* 先亮勾选反馈,再 emit select + 关闭,防连点重复提交
*/
onPickTap(e) {
if (this.picking) return;
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {};
const id = Number(ds.id || 0);
if (!id) return;
const list = Array.isArray(this.list) ? this.list : [];
const doc = list.find((d) => Number((d && d.id) || 0) === id);
if (!doc) return;
this.picking = true;
this.pickedId = id;
pushRecentTransferDoctor(id);
this.pickTimer = setTimeout(() => {
this.pickTimer = null;
this.$emit('select', doc);
this.$emit('input', false);
}, PICK_FEEDBACK_MS);
},
/**
* 头像加载失败:记下该医生,rows/recentRows 重新计算后回落首字
* 用 $set:avatarFailed 是动态 key 对象,Vue2 直接赋值侦听不到
*/
onAvatarError(e) {
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {};
const id = Number(ds.id || 0);
if (!id || this.avatarFailed[id]) return;
this.$set(this.avatarFailed, id, true);
},
},
};
</script>
<style lang="scss" scoped>
@import '../../subPackages/sub_image_transfer/common/transfer-theme.scss';
.tdp-root {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 10120;
}
/* 遮罩淡入淡出 */
.tdp-mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(9, 30, 27, 0.46);
opacity: 0;
transition: opacity 0.3s ease;
}
.tdp-mask--in {
opacity: 1;
}
/* 抽屉:从底部上滑,减速曲线收尾;离场沿用同一 transition 下滑 */
.tdp-sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 36rpx 36rpx 0 0;
background: linear-gradient(180deg, #f2fbf9 0%, #f5f7f8 38%, #f3f5f7 100%);
box-shadow: 0 -12rpx 48rpx rgba(9, 42, 37, 0.18);
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.tdp-sheet--in {
transform: translateY(0);
}
.tdp-grip {
width: 72rpx;
height: 8rpx;
margin: 16rpx auto 0;
border-radius: 999rpx;
background: rgba(12, 48, 44, 0.14);
flex-shrink: 0;
}
.tdp-head {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 32rpx 20rpx;
flex-shrink: 0;
}
.tdp-head__main {
flex: 1;
min-width: 0;
}
.tdp-title {
display: block;
font-size: 34rpx;
font-weight: 800;
color: $tf-ink;
letter-spacing: 1rpx;
}
.tdp-sub {
display: block;
margin-top: 6rpx;
font-size: 23rpx;
color: $tf-ink-3;
}
.tdp-close {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: rgba(12, 48, 44, 0.05);
flex-shrink: 0;
transition: transform 0.15s ease;
}
.tdp-close--hover {
@include tf-press;
background: rgba(12, 48, 44, 0.09);
}
/* 原因提示:琥珀色 1px 边框 + 同色微光,进场时轻微下落引起注意 */
.tdp-tip {
display: flex;
flex-direction: row;
align-items: center;
margin: 0 32rpx 12rpx;
padding: 18rpx 22rpx;
border-radius: 20rpx;
background: rgba(255, 248, 235, 0.95);
border: 1px solid rgba(212, 136, 6, 0.3);
box-shadow: 0 0 14rpx rgba(212, 136, 6, 0.12);
flex-shrink: 0;
animation: tdp-tip-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) 0.12s both;
}
.tdp-tip__text {
flex: 1;
margin-left: 12rpx;
font-size: 25rpx;
line-height: 1.5;
font-weight: 600;
color: #b26a00;
}
/* 分区小标题:最近选择 / 全部医生 */
.tdp-section {
display: block;
margin: 4rpx 0 12rpx;
font-size: 24rpx;
font-weight: 700;
color: $tf-ink-2;
}
.tdp-section--all {
margin: 6rpx 0 0;
}
/* 最近选择区:横向滑动的头像 chip,整体轻微下落入场 */
.tdp-recent {
padding: 0 32rpx 4rpx;
flex-shrink: 0;
animation: tdp-tip-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) 0.08s both;
}
.tdp-recent__scroll {
width: 100%;
white-space: nowrap;
}
.tdp-recent__track {
display: flex;
flex-direction: row;
padding: 4rpx 2rpx 14rpx;
}
/* 最近 chip:白玻璃 + 1px 青绿细边,选中加深边框 + 微光(不用左侧色条) */
.tdp-recent__chip {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 124rpx;
margin-right: 16rpx;
padding: 18rpx 8rpx 14rpx;
box-sizing: border-box;
border-radius: 24rpx;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(0, 135, 113, 0.12);
box-shadow: 0 6rpx 18rpx rgba(12, 48, 44, 0.05);
transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease;
/* 与卡片同理用 backwards:入场后释放 transform,按压缩放才生效 */
animation: tdp-card-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.tdp-recent__chip--press {
@include tf-press;
}
.tdp-recent__chip--on {
border-color: rgba(0, 135, 113, 0.5);
background: rgba(240, 251, 248, 0.96);
box-shadow: 0 0 16rpx rgba(0, 191, 166, 0.22);
}
.tdp-recent__ava {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
background: #e8f6f4;
box-shadow: 0 0 0 4rpx rgba(255, 255, 255, 0.95), 0 4rpx 12rpx rgba(0, 128, 106, 0.16);
}
.tdp-recent__char {
font-size: 30rpx;
font-weight: 800;
color: #ffffff;
}
.tdp-recent__name {
max-width: 108rpx;
margin-top: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 23rpx;
font-weight: 600;
color: $tf-ink;
}
/* 选中角标:右上角实心勾,回弹出现 */
.tdp-recent__on {
position: absolute;
top: 10rpx;
right: 12rpx;
display: flex;
align-items: center;
justify-content: center;
width: 30rpx;
height: 30rpx;
border-radius: 50%;
background: $tf-cta-gradient;
animation: tdp-check-pop 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tdp-scroll {
padding: 8rpx 32rpx 8rpx;
box-sizing: border-box;
}
.tdp-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 72rpx 0 88rpx;
}
.tdp-empty__ic {
display: flex;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba(0, 191, 166, 0.08);
border: 1px solid rgba(0, 135, 113, 0.14);
}
.tdp-empty__title {
margin-top: 24rpx;
font-size: 28rpx;
font-weight: 700;
color: $tf-ink-2;
}
.tdp-empty__sub {
margin-top: 8rpx;
font-size: 24rpx;
color: $tf-ink-3;
}
/* 医生卡:白玻璃 + 1px 青绿细边;错峰上浮入场 */
.tdp-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx;
margin-bottom: 18rpx;
border-radius: 26rpx;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(0, 135, 113, 0.12);
box-shadow: 0 8rpx 24rpx rgba(12, 48, 44, 0.05);
transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
/* backwards:只在延迟期保持起始帧,播完即释放 transform,按压缩放才不会被动画终态盖住 */
animation: tdp-card-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.tdp-card--press {
@include tf-press;
}
/* 选中:同色系 1px 边框加深 + 青绿微光,不用左侧色条 */
.tdp-card--on {
border-color: rgba(0, 135, 113, 0.5);
background: rgba(240, 251, 248, 0.96);
box-shadow: 0 0 16rpx rgba(0, 191, 166, 0.22), 0 8rpx 24rpx rgba(12, 48, 44, 0.06);
}
.tdp-avatar {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
flex-shrink: 0;
background: #e8f6f4;
box-shadow: 0 0 0 4rpx rgba(255, 255, 255, 0.95), 0 6rpx 16rpx rgba(0, 128, 106, 0.18);
}
.tdp-avatar--fallback {
display: flex;
align-items: center;
justify-content: center;
background: $tf-cta-gradient;
}
.tdp-avatar__char {
font-size: 36rpx;
font-weight: 800;
color: #ffffff;
}
.tdp-main {
flex: 1;
min-width: 0;
margin-left: 22rpx;
}
.tdp-name-row {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
.tdp-name {
margin-right: 12rpx;
font-size: 30rpx;
font-weight: 700;
color: $tf-ink;
}
.tdp-tag {
padding: 2rpx 12rpx;
border-radius: 999rpx;
font-size: 21rpx;
font-weight: 600;
color: $tf-brand-deep;
background: rgba(0, 191, 166, 0.1);
border: 1px solid rgba(0, 135, 113, 0.2);
}
/* 处方识别候选:琥珀色标签,和职称标签区分开 */
.tdp-tag--ocr {
margin-left: 8rpx;
color: #b26a00;
background: rgba(255, 248, 235, 0.95);
border-color: rgba(212, 136, 6, 0.3);
}
.tdp-dept {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.4;
color: $tf-ink-2;
}
.tdp-check {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
margin-left: 12rpx;
border-radius: 50%;
border: 2rpx solid #d0d7dd;
box-sizing: border-box;
flex-shrink: 0;
background: #ffffff;
}
/* 勾选:深青绿实心 + 回弹放大 */
.tdp-check--on {
border-color: transparent;
background: $tf-cta-gradient;
box-shadow: 0 4rpx 12rpx rgba(0, 128, 106, 0.3);
animation: tdp-check-pop 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes tdp-card-in {
from {
opacity: 0;
transform: translateY(28rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes tdp-check-pop {
0% {
transform: scale(0.6);
}
100% {
transform: scale(1);
}
}
@keyframes tdp-tip-in {
from {
opacity: 0;
transform: translateY(-12rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -1,16 +1,13 @@
<template> <template>
<!-- <!--
工作台卡片样式入口行(四个业务角色「我的」页共用,放 setting-panel 内) 工作台卡片样式入口:与「个人中心 / 绑定微信」同一行规格(单行标题 + 右侧当前值 + 箭头)
由旧版 u-switch 开关改为导航行:显示当前样式文案 + chevron,点击进入图形化单选页 不写副标题,避免行高和信息密度跟菜单卡其它项不一致
--> -->
<view class="switch-row" hover-class="row-hover" @tap="goStylePage"> <view class="entry-row" hover-class="entry-row-hover" :hover-stay-time="100" @tap="goStylePage">
<view class="pref-text"> <text class="entry-label">工作台卡片样式</text>
<text class="switch-label">工作台卡片样式</text> <view class="entry-right">
<text class="pref-desc">长卡片 / 宫格两种形态可选</text> <text class="entry-value">{{ styleLabel }}</text>
</view> <u-icon name="arrow-right" color="#C9CDD4" size="28"></u-icon>
<view class="pref-value">
<text class="value-text">{{ styleLabel }}</text>
<u-icon name="arrow-right" size="24" color="#C0C4CC"></u-icon>
</view> </view>
</view> </view>
</template> </template>
@@ -18,9 +15,7 @@
<script> <script>
/** /**
* card-pref-entry 工作台卡片样式入口 * card-pref-entry 工作台卡片样式入口
* 为什么做成组件:四个业务端「我的」页都需要同一入口,抽组件避免复制粘贴; * 四个业务端「我的」页共用;偏好存本地 wb_card_style,选择页写入后 uni.$emit 同步文案
* 偏好存本地 storage(wb_card_style),选择页写入后 uni.$emit 广播,本组件监听同步文案,
* 各工作台首页 onShow 时读取生效
*/ */
import { getCardStyle } from '@/utils/workbenchCardPref.js'; import { getCardStyle } from '@/utils/workbenchCardPref.js';
@@ -39,7 +34,6 @@ export default {
}, },
mounted() { mounted() {
this.style = getCardStyle(); this.style = getCardStyle();
// 选择页切换后广播同步(组件拿不到页面 onShow,用事件总线保证返回时文案已更新)
uni.$on('wbCardStyleChanged', this.onStyleChanged); uni.$on('wbCardStyleChanged', this.onStyleChanged);
}, },
beforeDestroy() { beforeDestroy() {
@@ -49,7 +43,7 @@ export default {
onStyleChanged(style) { onStyleChanged(style) {
this.style = style === 'long' ? 'long' : 'short'; this.style = style === 'long' ? 'long' : 'short';
}, },
/** 进入图形化单选页(共享分包页面,四端均可 navigateTo) */ /** 进入图形化单选页(共享分包,四端均可 navigateTo) */
goStylePage() { goStylePage() {
uni.navigateTo({ url: '/subPackages/sub_business_shared/card-style/index' }); uni.navigateTo({ url: '/subPackages/sub_business_shared/card-style/index' });
}, },
@@ -58,40 +52,25 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.switch-row { .entry-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 36rpx 0; padding: 36rpx 0;
border-bottom: 1rpx solid #f2f3f5; border-bottom: 1rpx solid #f2f3f5;
} }
.entry-row-hover {
.row-hover { background: #f7f8fa;
opacity: 0.7;
} }
.entry-label {
.pref-text {
display: flex;
flex-direction: column;
}
.switch-label {
font-size: 30rpx; font-size: 30rpx;
color: #31353d; color: #31353d;
} }
.entry-right {
.pref-desc {
margin-top: 8rpx;
font-size: 22rpx;
color: #86909c;
}
.pref-value {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.entry-value {
.value-text {
font-size: 26rpx; font-size: 26rpx;
color: #86909c; color: #86909c;
margin-right: 8rpx; margin-right: 8rpx;

View File

@@ -0,0 +1,374 @@
<template>
<view>
<!-- 编辑「我的常用」抽屉:page-container 拦截返回键,v-if 卸节点避免滚动锁 -->
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="pageGuard"
:show="popupShow"
:overlay="false"
@leave="onPageLeave"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<!-- z-index 高于自定义 tabbar(998) / 工作台看板浮卡,避免编辑抽屉被统计卡盖住 -->
<u-popup
:value="popupShow"
mode="bottom"
border-radius="24"
height="80%"
:z-index="10080"
:safe-area-inset-bottom="true"
:mask-close-able="true"
@input="onPopupInput"
@close="closePopup"
>
<view class="wfe">
<view class="wfe-header">
<text class="wfe-title">编辑常用功能</text>
<text class="wfe-sub">已添加显示减号,未添加显示加号,点完成一次性保存</text>
</view>
<scroll-view scroll-y class="wfe-scroll">
<view v-if="loading" class="wfe-center">
<text class="wfe-center-text">加载中...</text>
</view>
<view v-else-if="groups.length === 0" class="wfe-center">
<text class="wfe-center-text">暂无可用功能</text>
</view>
<view v-else>
<view v-for="group in groups" :key="group.key" class="wfe-group">
<text class="wfe-group-title">{{ group.title }}</text>
<view class="wfe-list">
<view
v-for="item in group.items"
:key="item.key"
class="wfe-row"
hover-class="wfe-row-hover"
@tap="toggleItem(item)"
>
<view class="wfe-icon" :style="item.iconStyle">
<u-icon :name="item.icon" :color="item.iconColor" size="36"></u-icon>
</view>
<view class="wfe-meta">
<text class="wfe-name">{{ item.label }}</text>
<text v-if="item.desc" class="wfe-desc">{{ item.desc }}</text>
</view>
<view class="wfe-op" :class="{ 'is-on': item.selected, 'is-off': !item.selected }">
<u-icon v-if="item.selected" name="minus" color="#EF4444" size="28"></u-icon>
<u-icon v-else name="plus" color="#6ACDBB" size="28"></u-icon>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="wfe-footer">
<view class="wfe-cancel" hover-class="wfe-btn-hover" @tap="closePopup">
<text>取消</text>
</view>
<view class="wfe-ok" hover-class="wfe-btn-hover" @tap="submit">
<text>{{ saving ? '保存中...' : '完成' }}</text>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* 工作台「我的常用」批量编辑抽屉(功能页悬浮按钮 / 首页快捷添加共用)
* 列出当前角色全部入口,已添加减号、未添加加号,点完成调 batchSaveFavorites
* 抽屉必须 page-container + v-if(规则),关抽屉先卸 show 再卸节点避免滚动锁
*/
import { groupEntriesToSections } from '@/utils/workbenchEntries.js';
export default {
name: 'WorkbenchFavEditor',
props: {
/** { getEntries, getUserData, batchSaveFavorites },统一返回 { res, data, ok } */
api: { type: Object, required: true },
},
data() {
return {
popupShow: false,
pageGuard: false,
releaseTimer: null,
loading: false,
saving: false,
groups: [],
};
},
beforeDestroy() {
this.clearReleaseTimer();
},
methods: {
/** 打开抽屉并拉取全部入口 + 当前常用,供加减回显 */
open() {
this.pageGuard = true;
this.$nextTick(() => {
this.popupShow = true;
this.loadData();
});
},
closePopup() {
this.popupShow = false;
this.scheduleReleaseGuard();
},
onPopupInput(val) {
this.popupShow = !!val;
if (!val) this.scheduleReleaseGuard();
},
onPageLeave() {
this.popupShow = false;
this.scheduleReleaseGuard();
},
releasePageGuard() {
this.clearReleaseTimer();
this.pageGuard = false;
},
scheduleReleaseGuard() {
this.clearReleaseTimer();
this.releaseTimer = setTimeout(() => {
this.pageGuard = false;
this.releaseTimer = null;
}, 350);
},
clearReleaseTimer() {
if (this.releaseTimer) {
clearTimeout(this.releaseTimer);
this.releaseTimer = null;
}
},
/**
* 拉入口字典 + 已收藏 code,组装分组列表(selected 存在 data 里,避免模板调方法)
*/
loadData() {
if (!this.api || !this.api.getEntries) return;
this.loading = true;
const entriesP = this.api.getEntries();
const userP = this.api.getUserData ? this.api.getUserData() : Promise.resolve({ ok: true, data: { favorites: [] } });
Promise.all([entriesP, userP]).then(([entryWrap, userWrap]) => {
const entries = entryWrap && entryWrap.ok && Array.isArray(entryWrap.data) ? entryWrap.data : [];
const favs = userWrap && userWrap.ok && userWrap.data && Array.isArray(userWrap.data.favorites)
? userWrap.data.favorites
: [];
const favCodes = {};
favs.forEach((row) => {
if (row && row.code) favCodes[row.code] = true;
});
const sections = groupEntriesToSections(entries);
this.groups = sections.map((section) => {
const items = [];
section.items.forEach((item) => {
items.push(this.decorateItem(item, !!favCodes[item.key]));
});
return { key: section.key, title: section.title, items };
});
}).catch(() => {
this.groups = [];
uni.showToast({ title: '功能列表加载失败', icon: 'none' });
}).finally(() => {
this.loading = false;
});
},
/** 给渲染项补 iconStyle / selected,模板禁止方法调用绑 class/style */
decorateItem(item, selected) {
const theme = item.theme || {};
return {
key: item.key,
label: item.label,
desc: item.desc,
icon: item.icon,
iconColor: theme.color || '#6B7280',
iconStyle: theme.bg ? ('background:' + theme.bg) : 'background:#F3F4F6',
selected: !!selected,
};
},
/** 本地加减,点完成才提交,支持一次勾选多个 */
toggleItem(item) {
item.selected = !item.selected;
},
/** 把当前勾选的 code 一次性提交,成功后通知父页刷新常用 */
submit() {
if (this.saving) return;
if (!this.api || !this.api.batchSaveFavorites) {
uni.showToast({ title: '暂不支持批量保存', icon: 'none' });
return;
}
const codes = [];
this.groups.forEach((group) => {
group.items.forEach((item) => {
if (item.selected) codes.push(item.key);
});
});
this.saving = true;
this.api.batchSaveFavorites(codes).then(({ res, ok }) => {
if (ok) {
uni.showToast({ title: '已更新常用', icon: 'none' });
this.closePopup();
this.$emit('saved');
} else {
uni.showToast({ title: (res && res.message) || '保存失败,请稍后重试', icon: 'none' });
}
}).catch(() => {
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
}).finally(() => {
this.saving = false;
});
},
},
};
</script>
<style lang="scss" scoped>
.wfe {
display: flex;
flex-direction: column;
height: 100%;
background: #f9fafb;
}
.wfe-header {
flex-shrink: 0;
padding: 28rpx 32rpx 16rpx;
background: #FFFFFF;
border-bottom: 1rpx solid rgba(60, 60, 67, 0.06);
}
.wfe-title {
display: block;
font-size: 34rpx;
font-weight: 700;
color: #1E293B;
}
.wfe-sub {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: #8E8E93;
}
.wfe-scroll {
flex: 1;
min-height: 0;
padding: 8rpx 24rpx 24rpx;
box-sizing: border-box;
}
.wfe-group {
margin-top: 20rpx;
}
.wfe-group-title {
display: block;
padding: 8rpx 8rpx 16rpx;
font-size: 26rpx;
font-weight: 600;
color: #1E293B;
}
.wfe-list {
background: #FFFFFF;
border-radius: 24rpx;
border: 1rpx solid rgba(106, 205, 187, 0.3);
box-shadow: 0 0 12rpx rgba(106, 205, 187, 0.18);
overflow: hidden;
}
.wfe-row {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid rgba(60, 60, 67, 0.06);
}
.wfe-row:last-child {
border-bottom: none;
}
.wfe-row-hover {
opacity: 0.75;
}
.wfe-icon {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.wfe-meta {
flex: 1;
min-width: 0;
margin: 0 20rpx;
}
.wfe-name {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1E293B;
}
.wfe-desc {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: #8E8E93;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wfe-op {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.wfe-op.is-on {
background: rgba(239, 68, 68, 0.1);
border: 1rpx solid rgba(239, 68, 68, 0.3);
box-shadow: 0 0 10rpx rgba(239, 68, 68, 0.16);
}
.wfe-op.is-off {
background: rgba(106, 205, 187, 0.12);
border: 1rpx solid rgba(106, 205, 187, 0.3);
box-shadow: 0 0 10rpx rgba(106, 205, 187, 0.16);
}
.wfe-center {
padding: 80rpx 0;
display: flex;
justify-content: center;
}
.wfe-center-text {
font-size: 26rpx;
color: #8E8E93;
}
.wfe-footer {
flex-shrink: 0;
display: flex;
gap: 20rpx;
padding: 16rpx 24rpx 24rpx;
background: #FFFFFF;
border-top: 1rpx solid rgba(60, 60, 67, 0.06);
}
.wfe-cancel,
.wfe-ok {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.wfe-cancel {
background: #F3F4F6;
text {
font-size: 28rpx;
color: #64748B;
}
}
.wfe-ok {
background: #6ACDBB;
text {
font-size: 28rpx;
color: #FFFFFF;
}
}
.wfe-btn-hover {
opacity: 0.8;
}
</style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<!-- <!--
功能中心页共享组件(诊所管理员/平台管理员/业务员/诊所推广员四端「功能」tab 复用) 功能中心页共享组件(诊所管理员/平台管理员/业务员/诊所推广员四端「功能」tab 复用)
结构:顶部搜索框 + u-tabs/swiper 联动(常用 | 全部 | 各分组),金刚区宫格展示入口 结构:顶部搜索框 + u-tabs/swiper 联动(全部 | 各分组),金刚区宫格展示入口
交互:点击入口跳转并静默上报使用;长按入口收藏/取消收藏到「我的常用」;搜索本地过滤 常用/最近只在首页,本页不再重复;长按仍可收藏到首页「我的常用」
放主包 components/ 原因:四端页面分属不同分包,微信不允许跨分包引用组件(同 xk-service-card) 放主包 components/ 原因:四端页面分属不同分包,微信不允许跨分包引用组件(同 xk-service-card)
--> -->
<view class="wf-wrap" :style="{ height: wrapHeight }"> <view class="wf-wrap" :style="{ height: wrapHeight }">
@@ -33,7 +33,14 @@
</view> </view>
<!-- 搜索态:覆盖 tabs/swiper,展示扁平匹配结果 --> <!-- 搜索态:覆盖 tabs/swiper,展示扁平匹配结果 -->
<scroll-view v-else-if="isSearching" scroll-y class="wf-search-scroll"> <scroll-view
v-else-if="isSearching"
scroll-y
class="wf-search-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresherRefresh"
>
<view class="wf-page"> <view class="wf-page">
<view class="wf-panel" v-if="searchItems.length"> <view class="wf-panel" v-if="searchItems.length">
<view class="wf-grid"> <view class="wf-grid">
@@ -65,40 +72,15 @@
</view> </view>
<swiper class="wf-swiper" :current="current" @change="onSwiperChange"> <swiper class="wf-swiper" :current="current" @change="onSwiperChange">
<swiper-item v-for="(page, pi) in swiperPages" :key="pi"> <swiper-item v-for="(page, pi) in swiperPages" :key="pi">
<scroll-view scroll-y class="wf-scroll"> <scroll-view
<!-- 常用 tab:我的常用 + 最近使用两个分区 --> scroll-y
<view v-if="page.type === 'fav'" class="wf-page"> class="wf-scroll"
<view class="wf-group-title">我的常用</view> refresher-enabled
<view class="wf-panel" v-if="favItems.length"> :refresher-triggered="isRefreshing"
<view class="wf-grid"> @refresherrefresh="onRefresherRefresh"
<xk-service-card >
v-for="item in favItems" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
<view class="wf-hint" v-else>
<text>长按任意功能,可添加到我的常用</text>
</view>
<view class="wf-group-title">最近使用</view>
<view class="wf-panel" v-if="recentItems.length">
<view class="wf-grid">
<xk-service-card
v-for="item in recentItems" :key="item.key"
mode="grid"
:icon="item.icon" :label="item.label" :theme="item.theme"
@click="onItemTap(item)" @longpress="onItemLongpress(item)"
></xk-service-card>
</view>
</view>
<view class="wf-hint" v-else>
<text>暂无使用记录,点击功能后会出现在这里</text>
</view>
</view>
<!-- 全部 tab:所有分组一页看全(分组标题 + 各组金刚区) --> <!-- 全部 tab:所有分组一页看全(分组标题 + 各组金刚区) -->
<view v-else-if="page.type === 'all'" class="wf-page"> <view v-if="page.type === 'all'" class="wf-page">
<view v-for="section in sections" :key="section.key"> <view v-for="section in sections" :key="section.key">
<view class="wf-group-title">{{ section.title }}</view> <view class="wf-group-title">{{ section.title }}</view>
<view class="wf-panel"> <view class="wf-panel">
@@ -130,6 +112,12 @@
</swiper-item> </swiper-item>
</swiper> </swiper>
</block> </block>
<!-- 右下角悬浮:打开全部入口的加减编辑(已加减号 / 未加加号,支持批量) -->
<view v-if="!loading && !failed && sections.length" class="wf-fab" hover-class="wf-fab-hover" @tap="openEditor">
<u-icon name="plus" color="#FFFFFF" size="40"></u-icon>
</view>
<workbench-fav-editor ref="favEditor" :api="api" @saved="onEditorSaved"></workbench-fav-editor>
</view> </view>
</template> </template>
@@ -144,17 +132,20 @@
* - getEntries() GET 当前角色启用入口(扁平数组) * - getEntries() GET 当前角色启用入口(扁平数组)
* - getUserData() GET { favorites: [], recents: [] } * - getUserData() GET { favorites: [], recents: [] }
* - toggleFavorite(code) POST 收藏/取消收藏,data = { is_favorite } * - toggleFavorite(code) POST 收藏/取消收藏,data = { is_favorite }
* - batchSaveFavorites(codes) POST 批量保存常用
* - reportUsage(code) POST 使用上报(静默,失败不影响跳转) * - reportUsage(code) POST 使用上报(静默,失败不影响跳转)
* *
* 事件: * 事件:
* - @special(item):点击了无 path 的特殊入口(如推广员端 cs_transfer 传方弹窗),由壳页按 code 处理 * - @special(item):点击了无 path 的特殊入口(如推广员端 cs_transfer 传方弹窗),由壳页按 code 处理
*/ */
import { groupEntriesToSections, normalizeEntry } from '@/utils/workbenchEntries.js'; import { groupEntriesToSections, normalizeEntry } from '@/utils/workbenchEntries.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default { export default {
name: 'WorkbenchFeatures', name: 'WorkbenchFeatures',
mixins: [pullRefreshMixin],
props: { props: {
/** 角色接口集合:{ getEntries, getUserData, toggleFavorite, reportUsage } */ /** 角色接口集合:{ getEntries, getUserData, toggleFavorite, batchSaveFavorites, reportUsage } */
api: { type: Object, required: true }, api: { type: Object, required: true },
}, },
data() { data() {
@@ -163,9 +154,8 @@ export default {
failed: false, failed: false,
/** 分组入口(groupEntriesToSections 输出结构) */ /** 分组入口(groupEntriesToSections 输出结构) */
sections: [], sections: [],
/** 我的常用 / 最近使用(已归一化为渲染项) */ /** 仅用于长按菜单判断是否已收藏,不在本页展示列表(展示在首页) */
favItems: [], favItems: [],
recentItems: [],
keyword: '', keyword: '',
current: 0, current: 0,
/** 内容区高度(px):layout 的 navbar 与 tabbar 之间,swiper 需要确定高度才能滚动 */ /** 内容区高度(px):layout 的 navbar 与 tabbar 之间,swiper 需要确定高度才能滚动 */
@@ -179,10 +169,9 @@ export default {
isSearching() { isSearching() {
return this.keyword.trim() !== ''; return this.keyword.trim() !== '';
}, },
/** swiper 页序列:固定「常用」「全部」两页在前,其后每个分组一页 */ /** swiper 页序列:固定「全部」在前,其后每个分组一页(常用/最近只在首页) */
swiperPages() { swiperPages() {
const pages = [ const pages = [
{ key: '__fav__', type: 'fav', title: '常用', items: [] },
{ key: '__all__', type: 'all', title: '全部', items: [] }, { key: '__all__', type: 'all', title: '全部', items: [] },
]; ];
this.sections.forEach((s) => { this.sections.forEach((s) => {
@@ -238,34 +227,39 @@ export default {
} }
this.bodyHeight = Math.max(400, sys.windowHeight - top - 100 * rpx - safeBottom); this.bodyHeight = Math.max(400, sys.windowHeight - top - 100 * rpx - safeBottom);
}, },
/** 全量加载:入口列表(必须)+ 用户数据(失败静默,常用/历史留空) */ /**
* 全量加载入口 + 收藏状态。
* 下拉刷新时不切整页 loading,否则 scroll-view 被卸掉转圈收不起来。
*/
reload() { reload() {
const silent = this.isRefreshing;
if (!silent) {
this.loading = true; this.loading = true;
this.failed = false; this.failed = false;
this.api.getEntries().then(({ data, ok }) => { }
const entriesP = this.api.getEntries().then(({ data, ok }) => {
if (ok && Array.isArray(data)) { if (ok && Array.isArray(data)) {
this.sections = groupEntriesToSections(data); this.sections = groupEntriesToSections(data);
// 分组数变化时防止 current 越界(固定页为常用+全部两页,如后台调整了分组) if (this.current >= this.sections.length + 1) this.current = 0;
if (this.current >= this.sections.length + 2) this.current = 0; this.failed = false;
} else { } else if (!silent) {
this.failed = true; this.failed = true;
} }
this.loading = false;
}).catch(() => { }).catch(() => {
this.failed = true; if (!silent) this.failed = true;
}).finally(() => {
this.loading = false; this.loading = false;
}); });
this.refreshUserData(); return Promise.all([entriesP, this.refreshUserData()]);
}, },
/** 刷新常用/最近使用(壳页 onShow 也会调用,保证跳转返回后历史及时更新) */ /** 刷新收藏状态(长按文案用;列表本身只在首页展示) */
refreshUserData() { refreshUserData() {
if (!this.api.getUserData) return; if (!this.api.getUserData) return Promise.resolve();
this.api.getUserData().then(({ data, ok }) => { return this.api.getUserData().then(({ data, ok }) => {
if (!ok || !data) return; if (!ok || !data) return;
this.favItems = this.normalizeList(data.favorites); this.favItems = this.normalizeList(data.favorites);
this.recentItems = this.normalizeList(data.recents);
}).catch(() => { }).catch(() => {
// 用户数据是增强信息,失败静默(不打断功能页主流程) // 收藏状态是增强信息,失败静默(不打断功能页主流程)
}); });
}, },
/** 后端入口对象数组 → 渲染项数组(与首页同一映射,字段完全一致) */ /** 后端入口对象数组 → 渲染项数组(与首页同一映射,字段完全一致) */
@@ -318,6 +312,14 @@ export default {
}, },
}); });
}, },
/** 打开全部入口编辑抽屉(悬浮按钮) */
openEditor() {
if (this.$refs.favEditor) this.$refs.favEditor.open();
},
/** 批量保存后刷新收藏状态,长按菜单文案与首页常用保持一致 */
onEditorSaved() {
this.refreshUserData();
},
/** 收藏切换:成功后按后端返回状态提示并刷新常用列表 */ /** 收藏切换:成功后按后端返回状态提示并刷新常用列表 */
doToggleFavorite(item) { doToggleFavorite(item) {
if (!this.api.toggleFavorite) return; if (!this.api.toggleFavorite) return;
@@ -340,6 +342,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
/* 纵向 flex:搜索/tabs 固定高度,swiper 吃满剩余空间(swiper 必须有确定高度才能滚动) */ /* 纵向 flex:搜索/tabs 固定高度,swiper 吃满剩余空间(swiper 必须有确定高度才能滚动) */
.wf-wrap { .wf-wrap {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #f9fafb; background: #f9fafb;
@@ -429,4 +432,22 @@ export default {
.wf-retry-hover { .wf-retry-hover {
opacity: 0.8; opacity: 0.8;
} }
/* 悬浮添加:贴内容区右下,wrap 已扣掉 tabbar 高度 */
.wf-fab {
position: absolute;
right: 32rpx;
bottom: 32rpx;
width: 104rpx;
height: 104rpx;
border-radius: 52rpx;
background: #6ACDBB;
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 20;
}
.wf-fab-hover {
opacity: 0.85;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -34,12 +34,24 @@
</view> </view>
</view> </view>
<!-- 我的常用:空态给引导条(点击也进功能页),避免新用户首页空洞无从下手 --> <!-- 我的常用:平台瓷片风格优先;否则长/短卡 -->
<view class="wue-section"> <view class="wue-section">
<view class="wue-header"> <view class="wue-header">
<text class="wue-title">我的常用</text> <text class="wue-title">我的常用</text>
<view class="wue-add" hover-class="wue-add-hover" @tap="openEditor">
<u-icon name="plus" size="26" color="#6ACDBB"></u-icon>
<text>添加</text>
</view> </view>
<view v-if="favorites.length && cardLong" class="wue-grid is-long"> </view>
<workbench-tile-board
v-if="favorites.length && useTileBoard"
:style-code="tileStyle"
:items="favorites"
:section-title="tileBoardTitle"
@item-click="onItemTap"
@more="$emit('more')"
/>
<view v-else-if="favorites.length && cardLong" class="wue-grid is-long">
<view v-for="item in favorites" :key="item.key" class="wue-cell"> <view v-for="item in favorites" :key="item.key" class="wue-cell">
<xk-service-card <xk-service-card
:icon="item.icon" :label="item.label" :desc="item.desc" :icon="item.icon" :label="item.label" :desc="item.desc"
@@ -60,9 +72,9 @@
</view> </view>
</view> </view>
</view> </view>
<view v-else class="wue-hint" hover-class="wue-hint-hover" @tap="$emit('more')"> <view v-else class="wue-hint" hover-class="wue-hint-hover" @tap="openEditor">
<u-icon name="star" size="30" color="#8E8E93"></u-icon> <u-icon name="plus-circle" size="30" color="#6ACDBB"></u-icon>
<text class="wue-hint-text">暂无常用功能,去功能页长按任意入口即可添加</text> <text class="wue-hint-text">暂无常用功能,点此添加</text>
<u-icon name="arrow-right" size="22" color="#C7C7CC"></u-icon> <u-icon name="arrow-right" size="22" color="#C7C7CC"></u-icon>
</view> </view>
</view> </view>
@@ -75,6 +87,7 @@
@click="$emit('more')" @click="$emit('more')"
></xk-service-card> ></xk-service-card>
</view> </view>
<workbench-fav-editor ref="favEditor" :api="api" @saved="onEditorSaved"></workbench-fav-editor>
</view> </view>
</template> </template>
@@ -86,11 +99,15 @@
* 三段结构与双形态布局完全一致,收敛到主包组件避免四份复制。 * 三段结构与双形态布局完全一致,收敛到主包组件避免四份复制。
* 数据与行为职责划分: * 数据与行为职责划分:
* - 父页面负责拉取 entry-user-data、按 code 注入本端行为(角标/表单预填/传方弹窗)后传入 * - 父页面负责拉取 entry-user-data、按 code 注入本端行为(角标/表单预填/传方弹窗)后传入
* - 本组件只管渲染与转发事件:@item-click(item) 点了某入口、@more 要去功能页 * - 本组件只管渲染与转发事件:@item-click(item) 点了某入口、@more 要去功能页、@saved 常用已批量更新
* 点击上报使用也由父页面在 @item-click 里做(各端上报接口不同) * 点击上报使用也由父页面在 @item-click 里做(各端上报接口不同)
*/ */
import WorkbenchTileBoard from '@/components/workbench-tile-board/workbench-tile-board.vue'
import { isAllowedTileStyle, normalizeTileStyle } from '@/utils/workbenchCardPref.js'
export default { export default {
name: 'WorkbenchUserEntries', name: 'WorkbenchUserEntries',
components: { WorkbenchTileBoard },
props: { props: {
/** 最近使用入口(已归一化 + 行为装饰) */ /** 最近使用入口(已归一化 + 行为装饰) */
recents: { type: Array, default: () => [] }, recents: { type: Array, default: () => [] },
@@ -98,6 +115,10 @@ export default {
favorites: { type: Array, default: () => [] }, favorites: { type: Array, default: () => [] },
/** 卡片形态:true 长卡一行两个 / false 金刚区宫格 */ /** 卡片形态:true 长卡一行两个 / false 金刚区宫格 */
cardLong: { type: Boolean, default: false }, cardLong: { type: Boolean, default: false },
/** 平台瓷片风格 02/03/04/08/17/19;空串表示原版(金刚区/长卡),不走瓷片板 */
tileStyle: { type: String, default: '' },
/** 快捷添加抽屉用:{ getEntries, getUserData, batchSaveFavorites } */
api: { type: Object, default: () => ({}) },
}, },
data() { data() {
return { return {
@@ -105,11 +126,36 @@ export default {
moreTheme: { bg: 'linear-gradient(135deg, #ECFDF5, #D1FAE5)', color: '#6ACDBB' }, moreTheme: { bg: 'linear-gradient(135deg, #ECFDF5, #D1FAE5)', color: '#6ACDBB' },
}; };
}, },
computed: {
/**
* 仅接口下发的合法瓷片码才启用瓷片板;空串/非法一律原版(本地 long/short)
* 为什么不用 normalizeTileStyle:它会把空串回落成 02,管理端原版会误开瓷片
*/
useTileBoard() {
return isAllowedTileStyle(this.tileStyle)
},
/**
* 瓷片板自带的标题:02 已改为主次卡,标题由本组件上方 wue-header 统一提供,
* 再传「我的常用」会连着出现两遍;03/17 的分区标题是版式的一部分,保持原样
*/
tileBoardTitle() {
if (!isAllowedTileStyle(this.tileStyle)) return ''
return normalizeTileStyle(this.tileStyle) === '02' ? '' : '我的常用'
},
},
methods: { methods: {
/** 入口点击统一转发:跳转/上报/特殊行为都由父页面处理 */ /** 入口点击统一转发:跳转/上报/特殊行为都由父页面处理 */
onItemTap(item) { onItemTap(item) {
this.$emit('item-click', item); this.$emit('item-click', item);
}, },
/** 打开常用编辑抽屉(标题「添加」与空态共用) */
openEditor() {
if (this.$refs.favEditor) this.$refs.favEditor.open();
},
/** 批量保存成功:通知首页重拉常用/最近,角标等装饰仍由父页注入 */
onEditorSaved() {
this.$emit('saved');
},
}, },
}; };
</script> </script>
@@ -119,9 +165,29 @@ export default {
margin: 0 30rpx 30rpx; margin: 0 30rpx 30rpx;
} }
.wue-header { .wue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx; margin-bottom: 24rpx;
padding: 0 10rpx; padding: 0 10rpx;
} }
.wue-add {
display: flex;
align-items: center;
padding: 8rpx 16rpx;
border-radius: 28rpx;
border: 1rpx solid rgba(106, 205, 187, 0.3);
box-shadow: 0 0 10rpx rgba(106, 205, 187, 0.16);
background: rgba(106, 205, 187, 0.08);
text {
margin-left: 6rpx;
font-size: 24rpx;
color: #6ACDBB;
}
}
.wue-add-hover {
opacity: 0.75;
}
/* 分区标题:对齐四首页既有 panel-title 规格 */ /* 分区标题:对齐四首页既有 panel-title 规格 */
.wue-title { .wue-title {
font-size: 34rpx; font-size: 34rpx;

View File

@@ -10,6 +10,7 @@ import uView from "uview-ui";
import timeFormatIOSFix from '@/common/js/timeFormat.js' import timeFormatIOSFix from '@/common/js/timeFormat.js'
import store from './store'; import store from './store';
import share from '@/common/share.js' import share from '@/common/share.js'
import { staticOss } from '@/utils/staticOss.js'
Vue.mixin(share) Vue.mixin(share)
Vue.use(uView); Vue.use(uView);
// 覆盖 uView 的 timeFormat / date:修复 iOS 下 `yyyy-MM-dd HH:mm:ss` 无法解析 // 覆盖 uView 的 timeFormat / date:修复 iOS 下 `yyyy-MM-dd HH:mm:ss` 无法解析
@@ -71,8 +72,9 @@ Vue.prototype.$navbarHeight = uni.$d.navbarHeight()
Vue.prototype.$windowHeight = systemInfo.windowHeight Vue.prototype.$windowHeight = systemInfo.windowHeight
Vue.prototype.$screenHeight = systemInfo.screenHeight Vue.prototype.$screenHeight = systemInfo.screenHeight
Vue.prototype.$checkID = m_by_checkID Vue.prototype.$checkID = m_by_checkID
Vue.prototype.$male = require('@/static/image/nan.png') Vue.prototype.$staticOss = staticOss
Vue.prototype.$girl = require('@/static/image/nv.png') Vue.prototype.$male = staticOss('image/nan.png')
Vue.prototype.$girl = staticOss('image/nv.png')
// #ifndef VUE3 // #ifndef VUE3
import Vue from 'vue' import Vue from 'vue'
Vue.config.productionTip = false Vue.config.productionTip = false

View File

@@ -3,7 +3,10 @@
"autoscan": true, "autoscan": true,
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue", "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^d-(.*)": "@/components/d-$1/d-$1.vue", "^d-(.*)": "@/components/d-$1/d-$1.vue",
"^ai-generation-loading$": "@/components/ai-generation-loading/ai-generation-loading.vue" "^ai-generation-loading$": "@/components/ai-generation-loading/ai-generation-loading.vue",
"^transfer-doctor-picker$": "@/components/transfer-doctor-picker/transfer-doctor-picker.vue",
"^proxy-pay-qrcode-modal$": "@/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue",
"^claim-qrcode-modal$": "@/subPackages/sub_workbench/components/claim-qrcode-modal/claim-qrcode-modal.vue"
}, },
"pages": [{ "pages": [{
"path": "pages/login/index", "path": "pages/login/index",
@@ -35,7 +38,8 @@
"path": "pages/patient/index", "path": "pages/patient/index",
"style": { "style": {
"navigationBarTitleText": "患者", "navigationBarTitleText": "患者",
"navigationStyle": "custom" "navigationStyle": "custom",
"enablePullDownRefresh": true
} }
}, },
{ {
@@ -66,8 +70,8 @@
"path": "workbench_upInfo/index", "path": "workbench_upInfo/index",
"style": { "style": {
"navigationBarTitleText": "上传资料", "navigationBarTitleText": "上传资料",
"enablePullDownRefresh ": false, //此设置 是否开启当前页面下拉刷新。 "enablePullDownRefresh": false,
"disableScroll": false //此设置 是否允许页面整体上下滚动。 "disableScroll": false
} }
}, },
@@ -79,6 +83,26 @@
"enablePullDownRefresh": true "enablePullDownRefresh": true
} }
}, },
{
"path": "create_user_patient",
"style": {
"navigationBarTitleText": "新建就诊人",
"navigationStyle": "custom",
"usingComponents": {
"region-address-picker": "/subPackages/sub_salesperson/components/RegionAddressPicker"
},
"componentPlaceholder": {
"region-address-picker": "view"
}
}
},
{
"path": "doctor_created_patient_list",
"style": {
"navigationBarTitleText": "创建记录",
"navigationStyle": "custom"
}
},
{ {
"path": "workbench_succeed", "path": "workbench_succeed",
"style": { "style": {
@@ -124,6 +148,12 @@
} }
} }
}, },
{
"path": "image-transfer-prescription/index",
"style": {
"navigationBarTitleText": "识图传方"
}
},
{ {
"path": "workbench_code", "path": "workbench_code",
"style": { "style": {
@@ -149,6 +179,14 @@
"style": { "style": {
"navigationBarTitleText": "处方详情" "navigationBarTitleText": "处方详情"
} }
},
{
"path": "public_account_lock/index",
"style": {
"navigationBarTitleText": "公账已锁定",
"navigationStyle": "custom",
"disableSwipeBack": true
}
} }
] ]
}, },
@@ -281,7 +319,7 @@
"root": "subPackages/sub_clinic_admin", "root": "subPackages/sub_clinic_admin",
"pages": [{ "pages": [{
"path": "home/index", "path": "home/index",
"style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom", "enablePullDownRefresh": true }
}, },
{ {
"path": "features/index", "path": "features/index",
@@ -289,7 +327,7 @@
}, },
{ {
"path": "my/index", "path": "my/index",
"style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true }
}, },
{ {
"path": "my/vip-detail", "path": "my/vip-detail",
@@ -297,11 +335,11 @@
}, },
{ {
"path": "withdrawal/index", "path": "withdrawal/index",
"style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom", "enablePullDownRefresh": true }
}, },
{ {
"path": "withdrawal/settlement/index", "path": "withdrawal/settlement/index",
"style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true }
}, },
{ {
"path": "withdrawal/settlement/detail", "path": "withdrawal/settlement/detail",
@@ -309,7 +347,7 @@
}, },
{ {
"path": "withdrawal/account-change/index", "path": "withdrawal/account-change/index",
"style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom", "enablePullDownRefresh": true }
}, },
{ {
"path": "withdrawal/apply", "path": "withdrawal/apply",
@@ -323,9 +361,13 @@
"path": "withdrawal/record-detail", "path": "withdrawal/record-detail",
"style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" }
}, },
{
"path": "withdrawal/withdrawable-orders",
"style": { "navigationBarTitleText": "可提订单", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{ {
"path": "reconciliation/index", "path": "reconciliation/index",
"style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom", "enablePullDownRefresh": true }
}, },
{ {
"path": "order/index", "path": "order/index",
@@ -369,18 +411,19 @@
{ "path": "order/index", "style": { "navigationBarTitleText": "商品订单", "navigationStyle": "custom", "disableScroll": true } }, { "path": "order/index", "style": { "navigationBarTitleText": "商品订单", "navigationStyle": "custom", "disableScroll": true } },
{ "path": "order/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom", "enablePullDownRefresh": true } }, { "path": "order/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "order/trace/index", "style": { "navigationBarTitleText": "订单追溯", "navigationStyle": "custom" } }, { "path": "order/trace/index", "style": { "navigationBarTitleText": "订单追溯", "navigationStyle": "custom" } },
{ "path": "reconciliation/index", "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom" } }, { "path": "reconciliation/index", "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/index", "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom" } }, { "path": "withdrawal/index", "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/apply", "style": { "navigationBarTitleText": "申请提现", "navigationStyle": "custom" } }, { "path": "withdrawal/apply", "style": { "navigationBarTitleText": "申请提现", "navigationStyle": "custom" } },
{ "path": "withdrawal/card-edit", "style": { "navigationBarTitleText": "编辑账户", "navigationStyle": "custom" } }, { "path": "withdrawal/card-edit", "style": { "navigationBarTitleText": "编辑账户", "navigationStyle": "custom" } },
{ "path": "withdrawal/record-detail", "style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" } }, { "path": "withdrawal/record-detail", "style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" } },
{ "path": "withdrawal/settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } }, { "path": "withdrawal/withdrawable-orders", "style": { "navigationBarTitleText": "可提订单", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } }, { "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } },
{ "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" } }, { "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom" } }, { "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "warehouse/detail", "style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" } }, { "path": "warehouse/detail", "style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" } },
{ "path": "user-patient/index", "style": { "navigationBarTitleText": "会员管理", "navigationStyle": "custom" } }, { "path": "user-patient/index", "style": { "navigationBarTitleText": "会员管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "user-patient/patients", "style": { "navigationBarTitleText": "就诊人列表", "navigationStyle": "custom" } }, { "path": "user-patient/patients", "style": { "navigationBarTitleText": "就诊人列表", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "user-patient/detail", "style": { "navigationBarTitleText": "就诊人详情", "navigationStyle": "custom" } }, { "path": "user-patient/detail", "style": { "navigationBarTitleText": "就诊人详情", "navigationStyle": "custom" } },
{ "path": "user-patient/register-detail", "style": { "navigationBarTitleText": "挂号详情", "navigationStyle": "custom" } }, { "path": "user-patient/register-detail", "style": { "navigationBarTitleText": "挂号详情", "navigationStyle": "custom" } },
{ "path": "profile/index", "style": { "navigationBarTitleText": "个人中心", "navigationStyle": "custom" } } { "path": "profile/index", "style": { "navigationBarTitleText": "个人中心", "navigationStyle": "custom" } }
@@ -388,28 +431,28 @@
}, { }, {
"root": "subPackages/sub_platform_admin", "root": "subPackages/sub_platform_admin",
"pages": [ "pages": [
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } }, { "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } }, { "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } }, { "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/audit/index", "style": { "navigationBarTitleText": "提现审核", "navigationStyle": "custom" } }, { "path": "withdrawal/audit/index", "style": { "navigationBarTitleText": "提现审核", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store/index", "style": { "navigationBarTitleText": "门店管理", "navigationStyle": "custom" } }, { "path": "store/index", "style": { "navigationBarTitleText": "门店管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store/detail", "style": { "navigationBarTitleText": "门店详情", "navigationStyle": "custom" } }, { "path": "store/detail", "style": { "navigationBarTitleText": "门店详情", "navigationStyle": "custom" } },
{ "path": "store/edit", "style": { "navigationBarTitleText": "编辑诊所", "navigationStyle": "custom" } }, { "path": "store/edit", "style": { "navigationBarTitleText": "编辑诊所", "navigationStyle": "custom" } },
{ "path": "store/drug-price", "style": { "navigationBarTitleText": "药品价格", "navigationStyle": "custom" } }, { "path": "store/drug-price", "style": { "navigationBarTitleText": "药品价格", "navigationStyle": "custom" } },
{ "path": "store/consultation", "style": { "navigationBarTitleText": "在线复诊", "navigationStyle": "custom" } }, { "path": "store/consultation", "style": { "navigationBarTitleText": "在线复诊", "navigationStyle": "custom" } },
{ "path": "store-input/audit/index", "style": { "navigationBarTitleText": "门店审核", "navigationStyle": "custom" } }, { "path": "store-input/audit/index", "style": { "navigationBarTitleText": "门店审核", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store-input/audit/detail", "style": { "navigationBarTitleText": "审核详情", "navigationStyle": "custom" } }, { "path": "store-input/audit/detail", "style": { "navigationBarTitleText": "审核详情", "navigationStyle": "custom" } },
{ "path": "doctor-input/audit/index", "style": { "navigationBarTitleText": "医生审核", "navigationStyle": "custom" } }, { "path": "doctor-input/audit/index", "style": { "navigationBarTitleText": "医生审核", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "doctor-input/audit/detail", "style": { "navigationBarTitleText": "审核详情", "navigationStyle": "custom" } } { "path": "doctor-input/audit/detail", "style": { "navigationBarTitleText": "审核详情", "navigationStyle": "custom" } }
] ]
}, { }, {
"root": "subPackages/sub_salesperson_manage", "root": "subPackages/sub_salesperson_manage",
"pages": [ "pages": [
{ "path": "index", "style": { "navigationBarTitleText": "推广员管理", "navigationStyle": "custom" } }, { "path": "index", "style": { "navigationBarTitleText": "推广员管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "form", "style": { "navigationBarTitleText": "推广员", "navigationStyle": "custom" } }, { "path": "form", "style": { "navigationBarTitleText": "推广员", "navigationStyle": "custom" } },
{ "path": "drug-commission/index", "style": { "navigationBarTitleText": "药品佣金", "navigationStyle": "custom" } }, { "path": "drug-commission/index", "style": { "navigationBarTitleText": "药品佣金", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom" } }, { "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费与结算", "navigationStyle": "custom" } }, { "path": "commission/index", "style": { "navigationBarTitleText": "服务费与结算", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "settlement/preview", "style": { "navigationBarTitleText": "按时间段结算", "navigationStyle": "custom" } }, { "path": "settlement/preview", "style": { "navigationBarTitleText": "按时间段结算", "navigationStyle": "custom" } },
{ "path": "settlement/confirm", "style": { "navigationBarTitleText": "确认结算", "navigationStyle": "custom" } }, { "path": "settlement/confirm", "style": { "navigationBarTitleText": "确认结算", "navigationStyle": "custom" } },
{ "path": "settlement/detail", "style": { "navigationBarTitleText": "结算详情", "navigationStyle": "custom" } } { "path": "settlement/detail", "style": { "navigationBarTitleText": "结算详情", "navigationStyle": "custom" } }
@@ -419,12 +462,12 @@
"pages": [ "pages": [
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } }, { "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
{ "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } }, { "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } }, { "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom" } }, { "path": "store-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } }, { "path": "store-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } },
{ "path": "store-input/detail", "style": { "navigationBarTitleText": "录入详情", "navigationStyle": "custom" } }, { "path": "store-input/detail", "style": { "navigationBarTitleText": "录入详情", "navigationStyle": "custom" } },
{ "path": "store-bank-card/report", "style": { "navigationBarTitleText": "银行卡报备", "navigationStyle": "custom" } }, { "path": "store-bank-card/report", "style": { "navigationBarTitleText": "银行卡报备", "navigationStyle": "custom" } },
{ "path": "doctor-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom" } }, { "path": "doctor-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "doctor-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } }, { "path": "doctor-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } },
{ "path": "doctor-input/detail", "style": { "navigationBarTitleText": "预填详情", "navigationStyle": "custom" } } { "path": "doctor-input/detail", "style": { "navigationBarTitleText": "预填详情", "navigationStyle": "custom" } }
] ]
@@ -436,6 +479,7 @@
"style": { "style": {
"navigationBarTitleText": "工作台", "navigationBarTitleText": "工作台",
"navigationStyle": "custom", "navigationStyle": "custom",
"enablePullDownRefresh": true,
"usingComponents": { "usingComponents": {
"transfer-patient-drawer": "/subPackages/sub_workbench/prescription_v2/components/modals/TransferPatientDrawer" "transfer-patient-drawer": "/subPackages/sub_workbench/prescription_v2/components/modals/TransferPatientDrawer"
}, },
@@ -448,13 +492,13 @@
"path": "features/index", "path": "features/index",
"style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" }
}, },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } }, { "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom" } }, { "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费记录", "navigationStyle": "custom" } }, { "path": "commission/index", "style": { "navigationBarTitleText": "服务费记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "invitees/index", "style": { "navigationBarTitleText": "我邀请的推广员", "navigationStyle": "custom", "enablePullDownRefresh": true } }, { "path": "invitees/index", "style": { "navigationBarTitleText": "我邀请的推广员", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "invitees/detail", "style": { "navigationBarTitleText": "收益明细", "navigationStyle": "custom" } }, { "path": "invitees/detail", "style": { "navigationBarTitleText": "收益明细", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } }, { "path": "settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "transfer-prescription/list", "style": { "navigationBarTitleText": "传方记录", "navigationStyle": "custom" } } { "path": "transfer-prescription/list", "style": { "navigationBarTitleText": "传方记录", "navigationStyle": "custom", "enablePullDownRefresh": true } }
] ]
}, { }, {
"root": "subPackages/sub_agreement", "root": "subPackages/sub_agreement",
@@ -470,7 +514,8 @@
"path": "pages/list", "path": "pages/list",
"style": { "style": {
"navigationBarTitleText": "在线接诊", "navigationBarTitleText": "在线接诊",
"navigationStyle": "custom" "navigationStyle": "custom",
"enablePullDownRefresh": true
} }
}, { }, {
"path": "pages/chat", "path": "pages/chat",
@@ -480,6 +525,56 @@
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}] }]
}, {
"root": "subPackages/sub_image_transfer",
"pages": [{
"path": "upload",
"style": {
"navigationBarTitleText": "识图传方",
"usingComponents": {
"image-ocr-drawer": "/subPackages/sub_workbench/prescription_v2/components/ImageOcrDrawer"
},
"componentPlaceholder": {
"image-ocr-drawer": "view"
}
}
}, {
"path": "order-list",
"style": {
"navigationBarTitleText": "传方订单",
"backgroundColor": "#E8F6F4",
"backgroundTextStyle": "dark"
}
}, {
"path": "order-detail",
"style": {
"navigationBarTitleText": "传方详情",
"usingComponents": {
"chinese-medicine-config": "/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig",
"chinese-medicine-modal": "/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal"
},
"componentPlaceholder": {
"chinese-medicine-config": "view",
"chinese-medicine-modal": "view"
}
}
}]
}, {
"root": "subPackages/sub_transfer_clerk",
"pages": [{
"path": "home/index",
"style": {
"navigationBarTitleText": "传方工作台",
"navigationStyle": "custom"
}
}, {
"path": "my/index",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
}]
} }
], ],
"tabBar": { "tabBar": {

View File

@@ -25,7 +25,11 @@ export default {
onShow() { onShow() {
if (this.pendingAccountSelect) return; if (this.pendingAccountSelect) return;
this.$refs.doctorLogin?.loadWxConfig?.(); this.$refs.doctorLogin?.loadWxConfig?.();
// 等当前页进栈后再跳,避免启动阶段 reLaunch 触发 appLaunch with non-empty page stack
this.$nextTick(() => {
if (this.pendingAccountSelect) return;
redirectIfLoggedIn(); redirectIfLoggedIn();
});
}, },
methods: { methods: {
openAccountPicker({ accounts, defaultAccountId, defaultAccountType }) { openAccountPicker({ accounts, defaultAccountId, defaultAccountType }) {

View File

@@ -105,29 +105,29 @@
userInfo: uni.getStorageSync('userInfo') || {}, userInfo: uni.getStorageSync('userInfo') || {},
functions: [{ functions: [{
name: '我的处方', name: '我的处方',
icon: '../../static/image/wd-cf.png', icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/wd-cf.png',
url: '/subPackages/sub_my/prescriptionList' url: '/subPackages/sub_my/prescriptionList'
}, },
{ {
name: '我的常用方', name: '我的常用方',
icon: '../../static/image/wd-cyf.png', icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/wd-cyf.png',
url: '../../subPackages/sub_workbench/workbench_recipe/common?type=0' url: '../../subPackages/sub_workbench/workbench_recipe/common?type=0'
}, },
// { // {
// name: '挂号费用', // name: '挂号费用',
// icon: '../../static/image/wd-ghfy.png', // icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/wd-ghfy.png',
// url: '../../subPackages/sub_my/my_revenue/index' // url: '../../subPackages/sub_my/my_revenue/index'
// }, // },
{ {
name: '服务设置', name: '服务设置',
icon: '../../static/image/fwsz.png', icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/fwsz.png',
url: '../../subPackages/sub_my/my_service' url: '../../subPackages/sub_my/my_service'
} }
], ],
items: [ items: [
{ {
name: '常用医嘱管理', name: '常用医嘱管理',
icon: '../../static/image/i1.png', icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/i1.png',
url: '../../subPackages/sub_my/my_diagnose_common' url: '../../subPackages/sub_my/my_diagnose_common'
} }
], ],
@@ -141,7 +141,7 @@
computed: { computed: {
// getAvatar() { // getAvatar() {
// let doctorInfo = uni.getStorageSync('doctorInfo') // let doctorInfo = uni.getStorageSync('doctorInfo')
// let url = require(`@/static/image/${doctorInfo.DoctorInfo.user.role%2==0?'nv':'nan'}.png`); // let url = staticOss('image/' + (doctorInfo.DoctorInfo.user.role%2==0?'nv':'nan') + '.png');
// // console.log(info, 'ddd') // // console.log(info, 'ddd')
// return doctorInfo.DoctorInfo['avatar'] ? doctorInfo.DoctorInfo['avatar'] : doctorInfo // return doctorInfo.DoctorInfo['avatar'] ? doctorInfo.DoctorInfo['avatar'] : doctorInfo
// .DoctorInfo.user.role == 1 ? url : url // .DoctorInfo.user.role == 1 ? url : url

View File

@@ -27,7 +27,7 @@
@click="$go('../../subPackages/sub_pharmacist/pharmacist_info?avatar='+avatar+'&info='+JSON.stringify(info))"> @click="$go('../../subPackages/sub_pharmacist/pharmacist_info?avatar='+avatar+'&info='+JSON.stringify(info))">
<view class="flex-row flex-ali-center"> <view class="flex-row flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D" <u-icon margin-left="20" label-size="32" label-color="#31353D"
:name="require('../../static/image/grzl.png')" label="个人资料" size="30"></u-icon> :name="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/grzl.png'" label="个人资料" size="30"></u-icon>
</view> </view>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>
@@ -35,7 +35,7 @@
@click="$go('../../subPackages/sub_pharmacist/pharmacist_setInfo')"> @click="$go('../../subPackages/sub_pharmacist/pharmacist_setInfo')">
<view class="flex-row flex-ali-center"> <view class="flex-row flex-ali-center">
<u-icon margin-left="20" label-size="32" label-color="#31353D" <u-icon margin-left="20" label-size="32" label-color="#31353D"
:name="require('../../static/image/set.png')" label="设置" size="30"></u-icon> :name="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/set.png'" label="设置" size="30"></u-icon>
</view> </view>
<u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon> <u-icon name="arrow-right" size="32" color="#A7ABB0"></u-icon>
</view> </view>

View File

@@ -12,7 +12,7 @@
<view class="flex-row flex-jus-sp"> <view class="flex-row flex-jus-sp">
<view class="entry-card flex-1" hover-class="entry-card-hover" <view class="entry-card flex-1" hover-class="entry-card-hover"
@click="$go('../../subPackages/sub_patient/to-store?id='+1)"> @click="$go('../../subPackages/sub_patient/to-store?id='+1)">
<image class="entry-icon" src="../../static/image/hz_xsjz.png"></image> <image class="entry-icon" src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/hz_xsjz.png"></image>
<view class="entry-info"> <view class="entry-info">
<text class="entry-name">到店接诊</text> <text class="entry-name">到店接诊</text>
<text class="entry-desc">处理到店患者</text> <text class="entry-desc">处理到店患者</text>
@@ -20,7 +20,7 @@
</view> </view>
<view class="entry-card flex-1 m-l-2" hover-class="entry-card-hover" <view class="entry-card flex-1 m-l-2" hover-class="entry-card-hover"
@click="$go('../../subPackages/sub_patient/to-store?id='+2)"> @click="$go('../../subPackages/sub_patient/to-store?id='+2)">
<image class="entry-icon" src="../../static/image/hz_ddjz.png"></image> <image class="entry-icon" src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/hz_ddjz.png"></image>
<view class="entry-info"> <view class="entry-info">
<text class="entry-name">线上接诊</text> <text class="entry-name">线上接诊</text>
<text class="entry-desc">线上问诊接待</text> <text class="entry-desc">线上问诊接待</text>
@@ -30,7 +30,7 @@
<!-- 群发消息入口行 --> <!-- 群发消息入口行 -->
<view class="list-new" hover-class="entry-card-hover" @click="$go('../../subPackages/sub_patient/patient_massInfo')"> <view class="list-new" hover-class="entry-card-hover" @click="$go('../../subPackages/sub_patient/patient_massInfo')">
<view class="l-l-row"> <view class="l-l-row">
<image src="/static/image/hz-qfxx.png" mode=""></image> <image src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/hz-qfxx.png' mode=""></image>
<view class="entry-info"> <view class="entry-info">
<text class="entry-name">群发消息</text> <text class="entry-name">群发消息</text>
<text class="entry-desc">向患者批量发送通知</text> <text class="entry-desc">向患者批量发送通知</text>
@@ -46,9 +46,9 @@
<xk-list-card <xk-list-card
v-for="(item, index) in list" :key="index" v-for="(item, index) in list" :key="index"
mode="flat" mode="flat"
:avatar="item.user.avatarurl || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)" :avatar="item.card_avatar"
:title="item.patient" :title="item.patient"
:meta="(item.sex % 2 != 0 ? '男' : '女') + ' · ' + item.age + '岁'" :meta="item.card_meta"
:arrow="true" :arrow="true"
@click="toDetail(item.patient_id)" @click="toDetail(item.patient_id)"
></xk-list-card> ></xk-list-card>
@@ -127,7 +127,8 @@
}) })
.then((res) => { .then((res) => {
if (res.errcode == 0) { if (res.errcode == 0) {
this.list = this.page === 1 ? res.data.list : [...this.list, ...res.data.list]; const rows = (res.data.list || []).map((item) => this.normalizeItem(item));
this.list = this.page === 1 ? rows : [...this.list, ...rows];
this.totalPage = res.data.pagination.totalPage; this.totalPage = res.data.pagination.totalPage;
if (this.page >= this.totalPage) { if (this.page >= this.totalPage) {
this.status = "nomore"; this.status = "nomore";
@@ -153,6 +154,22 @@
this.loading2 = false; this.loading2 = false;
}); });
}, },
/**
* 列表展示字段提前算好:部分患者 user 为 null,模板读 user.avatarurl 会直接崩
* 微信模板禁止 ?. / ??,头像和性别文案统一在这里兜底
*/
normalizeItem(item) {
const isFemale = item.sex % 2 == 0;
const user = item.user || {};
const fallback = isFemale
? (this.girl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/nv.png')
: (this.male || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/nan.png');
return {
...item,
card_avatar: user.avatarurl || fallback,
card_meta: (isFemale ? '女' : '男') + ' · ' + (item.age || '-') + '岁',
};
},
toDetail(id) { toDetail(id) {
uni.navigateTo({ uni.navigateTo({
url: '/subPackages/sub_patient/patient_detail?id=' + id url: '/subPackages/sub_patient/patient_detail?id=' + id

View File

@@ -13,14 +13,21 @@
<!-- 未通过入驻审核:整页空态守卫,不渲染 swiper --> <!-- 未通过入驻审核:整页空态守卫,不渲染 swiper -->
<view class="empty" v-if="status != 2"> <view class="empty" v-if="status != 2">
<image src="/static/image/none.png" mode="aspectFit"></image> <image src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/none.png' mode="aspectFit"></image>
<text>您还没有通过审核,请稍后再试~</text> <text>您还没有通过审核,请稍后再试~</text>
</view> </view>
<!-- 审方列表:swiper 承载三个状态 tab,支持左右滑动切换;每个 tab 独立分页 + 上拉加载 --> <!-- 审方列表:swiper 承载三个状态 tab,支持左右滑动切换;每个 tab 独立分页 + 上拉加载 -->
<swiper v-else class="tab-swiper" :current="currents" @change="onSwiperChange"> <swiper v-else class="tab-swiper" :current="currents" @change="onSwiperChange">
<swiper-item v-for="(tab, tabIndex) in tabPages" :key="tab.status"> <swiper-item v-for="(tab, tabIndex) in tabPages" :key="tab.status">
<scroll-view scroll-y class="tab-scroll" @scrolltolower="loadMore(tabIndex)"> <scroll-view
scroll-y
class="tab-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresherRefresh"
@scrolltolower="loadMore(tabIndex)"
>
<view class="list-wrap"> <view class="list-wrap">
<xk-list-card <xk-list-card
v-for="item in tab.list" :key="item.id" v-for="item in tab.list" :key="item.id"
@@ -38,7 +45,7 @@
<!-- 各 tab 独立空态:首屏加载完成且无数据时展示 --> <!-- 各 tab 独立空态:首屏加载完成且无数据时展示 -->
<view class="tab-empty" v-if="tab.loaded && tab.list.length === 0"> <view class="tab-empty" v-if="tab.loaded && tab.list.length === 0">
<image src="/static/image/none.png" mode="aspectFit"></image> <image src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/none.png' mode="aspectFit"></image>
<text>暂无{{lists[tabIndex].name}}的处方</text> <text>暂无{{lists[tabIndex].name}}的处方</text>
</view> </view>
@@ -69,7 +76,9 @@
import { import {
unwrapXkApi unwrapXkApi
} from "@/utils/api-response.js"; } from "@/utils/api-response.js";
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default { export default {
mixins: [pullRefreshMixin],
data() { data() {
return { return {
status: uni.getStorageSync('login_status'), status: uni.getStorageSync('login_status'),
@@ -122,6 +131,12 @@
} }
}, },
methods: { methods: {
/**
* 下拉刷新当前审方 tab
*/
reload() {
return this.refreshTab(this.currents)
},
/** tabs 与 swiper 统一切换入口:点 tab、手势左右滑都收敛到这里 */ /** tabs 与 swiper 统一切换入口:点 tab、手势左右滑都收敛到这里 */
switchTab(index) { switchTab(index) {
if (this.currents === index) return if (this.currents === index) return
@@ -140,7 +155,7 @@
const tab = this.tabPages[index] const tab = this.tabPages[index]
tab.page = 1 tab.page = 1
tab.finished = false tab.finished = false
this.fetchTab(index, true) return this.fetchTab(index, true)
}, },
/** 上拉加载下一页 */ /** 上拉加载下一页 */
loadMore(index) { loadMore(index) {

View File

@@ -7,7 +7,7 @@
<view class="head"> <view class="head">
<view class="info"> <view class="info">
<image :src="list.avatar || '/static/image/nv.png'" mode=""></image> <image :src="list.avatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/nv.png'" mode=""></image>
<view class="name"> <view class="name">
<text>{{list.name}}</text> <text>{{list.name}}</text>
<text>药师</text> <text>药师</text>

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="container safe-area-inset-bottom" :style="{ height: screenHeight }"> <view class="container safe-area-inset-bottom" :style="{ height: screenHeight }">
<image class="image" src="/static/login/bg_shh.png" mode=""></image> <image class="image" src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/login/bg_shh.png' mode=""></image>
<view class="bg"> <view class="bg">
<view class="img"> <view class="img">
<image :src="doctorInfo.DoctorInfo.avatar || doctorInfo.avatar" mode=""></image> <image :src="doctorInfo.DoctorInfo.avatar || doctorInfo.avatar" mode=""></image>
@@ -14,7 +14,7 @@
{{doctorInfo.store.store.name || doctorInfo.store.name}} {{doctorInfo.store.store.name || doctorInfo.store.name}}
{{doctorInfo.DoctorInfo.depart.name || doctorInfo.depart.name || ''}} {{doctorInfo.DoctorInfo.depart.name || doctorInfo.depart.name || ''}}
</view> </view>
<image src="/static/login/bg_shhz.png" mode=""></image> <image src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/login/bg_shhz.png' mode=""></image>
<!-- doctorInfo.DoctorInfo.user.status<=1 --> <!-- doctorInfo.DoctorInfo.user.status<=1 -->
<view class="state" v-if="role==1"> <view class="state" v-if="role==1">
{{ doctorInfo.DoctorInfo.user.status<=1&&'您的申请已提交,请耐心等待!感谢您的配合!' ||doctorInfo.DoctorInfo.user.status==3&&'抱歉您的申请已被驳回!'}} {{ doctorInfo.DoctorInfo.user.status<=1&&'您的申请已提交,请耐心等待!感谢您的配合!' ||doctorInfo.DoctorInfo.user.status==3&&'抱歉您的申请已被驳回!'}}

View File

@@ -8,7 +8,7 @@
</view> </view>
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + navbarHeight + 'px' }"> <view class="header-wrap" :style="{ paddingTop: statusBarHeight + navbarHeight + 'px' }">
<view class="box"> <view class="box">
<view class="store_" @click="openShow=true"> <view class="store_" @click="openStoreSelect">
{{doctorInfo.store.store.name || '默认门店'}} {{doctorInfo.store.store.name || '默认门店'}}
<u-icon name="map-fill" color="#fff" size="28" margin-left="12"></u-icon> <u-icon name="map-fill" color="#fff" size="28" margin-left="12"></u-icon>
</view> </view>
@@ -36,7 +36,7 @@
</view> </view>
<view class="box_info_right flex-col flex-jus-sp flex-ali-center" <view class="box_info_right flex-col flex-jus-sp flex-ali-center"
@click="$go('../../subPackages/sub_workbench/workbench_code')"> @click="$go('../../subPackages/sub_workbench/workbench_code')">
<image src="../../static/image/mp.png"></image> <image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/mp.png"></image>
<d-text text="我的名片" className="w-c fs-2"></d-text> <d-text text="我的名片" className="w-c fs-2"></d-text>
</view> </view>
</view> </view>
@@ -45,6 +45,8 @@
<view class="header-spacer" :style="{ height: headerSpacerHeight + 'px' }"></view> <view class="header-spacer" :style="{ height: headerSpacerHeight + 'px' }"></view>
<public-account-lock-banner ref="papLockBanner" />
<view class="content"> <view class="content">
<!-- 看板统计 --> <!-- 看板统计 -->
<view class="dashboard-panel"> <view class="dashboard-panel">
@@ -95,6 +97,36 @@
</view> </view>
</view> </view>
<!-- AI 今日简报:系统配置关闭时不挂载,避免进工作台就打生成接口 -->
<ai-brief-card v-if="aiBriefEnabled" ref="aiBrief" :store-id="store_id" @popup-change="onBriefPopupChange"></ai-brief-card>
<!-- 未来7天预约卡片:点击总数或某天打开明细抽屉 -->
<view class="appt-panel">
<view class="appt-panel__head" @click="openApptDrawer('')">
<view class="appt-panel__title-wrap">
<text class="appt-panel__title">未来7天预约</text>
<text class="appt-panel__sub">含今天</text>
</view>
<view class="appt-panel__total-wrap">
<text class="appt-panel__total">{{ appointmentUpcoming.total }}</text>
<text class="appt-panel__unit">个</text>
<u-icon name="arrow-right" color="#C0C4CC" size="26"></u-icon>
</view>
</view>
<view class="appt-panel__days">
<view
v-for="day in upcomingDayList"
:key="day.date"
class="appt-panel__day"
:class="{ 'appt-panel__day--active': day.count > 0 }"
@click="openApptDrawer(day.date)"
>
<text class="appt-panel__day-label">{{ day.label }}</text>
<text class="appt-panel__day-count">{{ day.count }}</text>
</view>
</view>
</view>
<view class="function p-32" v-if="onlineConsultationConfigReady && canUseOnlineConsultation"> <view class="function p-32" v-if="onlineConsultationConfigReady && canUseOnlineConsultation">
<view class="function_grid"> <view class="function_grid">
<view <view
@@ -106,8 +138,8 @@
<image :src="item.icon"></image> <image :src="item.icon"></image>
<view>{{ item.name }}</view> <view>{{ item.name }}</view>
<u-badge <u-badge
:count="i === 0 ? doctorInfo.wait_accept : 0" :count="offlineWaitAccept"
v-if="i === 0" v-if="i === 0 && offlineWaitAccept > 0"
:offset="[-6, 98]" :offset="[-6, 98]"
bgColor="#F44336" bgColor="#F44336"
></u-badge> ></u-badge>
@@ -125,13 +157,35 @@
<text class="offline-entry-desc">到店开方、处理线下挂号</text> <text class="offline-entry-desc">到店开方、处理线下挂号</text>
</view> </view>
<u-badge <u-badge
v-if="doctorInfo.wait_accept > 0" v-if="offlineWaitAccept > 0"
:count="doctorInfo.wait_accept" :count="offlineWaitAccept"
:offset="[-8, 8]" :offset="[-8, 8]"
bgColor="#F44336" bgColor="#F44336"
></u-badge> ></u-badge>
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon> <u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
</view> </view>
<view
class="offline-entry-row flex-row flex-ali-center m-t-24"
@click="goReceptionEntry(imageTransferEntry.url)"
>
<image class="offline-entry-icon" :src="imageTransferEntry.icon"></image>
<view class="offline-entry-body flex-col flex-1">
<text class="offline-entry-title">{{ imageTransferEntry.name }}</text>
<text class="offline-entry-desc">拍照识方,按配置进接诊或提交传方</text>
</view>
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
</view>
<view
class="offline-entry-row flex-row flex-ali-center m-t-24"
@click="goReceptionEntry(imageTransferOrderEntry.url)"
>
<image class="offline-entry-icon" :src="imageTransferOrderEntry.icon"></image>
<view class="offline-entry-body flex-col flex-1">
<text class="offline-entry-title">{{ imageTransferOrderEntry.name }}</text>
<text class="offline-entry-desc">查看自己上传的传方订单</text>
</view>
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
</view>
</view> </view>
<block> <block>
<!-- 正在接诊模块 --> <!-- 正在接诊模块 -->
@@ -149,7 +203,7 @@
> >
<view class="wb-avatar-wrap"> <view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" <u-image width="80rpx" height="80rpx" shape="circle"
:src="item.user_patient && item.user_patient.avatar || require(`../../static/image/${item.user_patient && item.user_patient.sex%2==0?'nv':'nan'}.png`)"> :src="item.user_patient && item.user_patient.avatar || ('https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/' + (item.user_patient && item.user_patient.sex%2==0?'nv':'nan') + '.png')">
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
<u-badge <u-badge
@@ -242,7 +296,7 @@
<view class="news_item flat-card flex-row flex-ali-start" <view class="news_item flat-card flex-row flex-ali-start"
@click="$go('../../subPackages/sub_patient/patient_label?type=1')" v-if="systemList.num>0"> @click="$go('../../subPackages/sub_patient/patient_label?type=1')" v-if="systemList.num>0">
<view class="wb-avatar-wrap"> <view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" src="/static/image/home1.png"> <u-image width="80rpx" height="80rpx" shape="circle" src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/home1.png'>
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
<u-badge :count="systemList.num" v-if="systemList.num>0" :offset="[-4,-4]" <u-badge :count="systemList.num" v-if="systemList.num>0" :offset="[-4,-4]"
@@ -266,7 +320,7 @@
<view class="news_item flat-card flex-row flex-ali-start" <view class="news_item flat-card flex-row flex-ali-start"
@click="$go('../../subPackages/sub_patient/patient_label?type=2')" v-if="infoList.num>0"> @click="$go('../../subPackages/sub_patient/patient_label?type=2')" v-if="infoList.num>0">
<view class="wb-avatar-wrap"> <view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" :src="'/static/image/home2.png'"> <u-image width="80rpx" height="80rpx" shape="circle" :src="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/home2.png'">
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
<u-badge :count="infoList.num" v-if="infoList.num>0" :offset="[-4,-4]" bgColor="#F44336" <u-badge :count="infoList.num" v-if="infoList.num>0" :offset="[-4,-4]" bgColor="#F44336"
@@ -291,7 +345,7 @@
v-if="!loading&&systemList.length==0" v-if="!loading&&systemList.length==0"
@click="$go('../../subPackages/sub_patient/patient_label?type=1')"> @click="$go('../../subPackages/sub_patient/patient_label?type=1')">
<view class="wb-avatar-wrap"> <view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" src="/static/image/home1.png"> <u-image width="80rpx" height="80rpx" shape="circle" src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/home1.png'>
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
</view> </view>
@@ -313,7 +367,7 @@
<view class="news_item flat-card flex-row flex-ali-start" v-if="!loading&&infoList.length==0" <view class="news_item flat-card flex-row flex-ali-start" v-if="!loading&&infoList.length==0"
@click="$go('../../subPackages/sub_patient/patient_label?type=2')"> @click="$go('../../subPackages/sub_patient/patient_label?type=2')">
<view class="wb-avatar-wrap"> <view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" :src="'/static/image/home2.png'"> <u-image width="80rpx" height="80rpx" shape="circle" :src="'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/home2.png'">
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
</view> </view>
@@ -335,7 +389,7 @@
<view v-if="noticeList.length" class="news_item flat-card flex-row flex-ali-start" <view v-if="noticeList.length" class="news_item flat-card flex-row flex-ali-start"
@click="$go('/subPackages/sub_workbench/prescriptionList')"> @click="$go('/subPackages/sub_workbench/prescriptionList')">
<view class="wb-avatar-wrap"> <view class="wb-avatar-wrap">
<u-image width="80rpx" height="80rpx" shape="circle" src="/static/image/home1.png"> <u-image width="80rpx" height="80rpx" shape="circle" src='https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/home1.png'>
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
<u-badge :count="noticeList.length" :offset="[-4,-4]" <u-badge :count="noticeList.length" :offset="[-4,-4]"
@@ -360,8 +414,17 @@
</block> </block>
</view> </view>
<!-- 切换门店精简 --> <!-- 切换门店:page-container 拦截返回;与预约抽屉/简报互斥(同页只能一个 page-container) -->
<u-popup v-model="openShow" mode="bottom" border-radius="24" length="65%" :safe-area-inset-bottom="true"> <!-- #ifdef MP-WEIXIN -->
<page-container
v-if="storePageGuard"
:show="openShow"
:overlay="false"
@leave="closeStoreSelect"
@afterleave="releaseStorePageGuard"
/>
<!-- #endif -->
<u-popup v-model="openShow" mode="bottom" border-radius="24" length="65%" :safe-area-inset-bottom="true" @close="closeStoreSelect">
<view class="content_popup"> <view class="content_popup">
<view class="top"> <view class="top">
<view class="title">选择诊所</view> <view class="title">选择诊所</view>
@@ -381,14 +444,24 @@
</view> </view>
</scroll-view> </scroll-view>
<view class="confrim-btn"> <view class="confrim-btn">
<view class="u-btn qx" @click="openShow=false">取消</view> <view class="u-btn qx" @click="closeStoreSelect">取消</view>
<view class="u-btn qd" @click="sure">确定</view> <view class="u-btn qd" @click="sure">确定</view>
</view> </view>
</view> </view>
</u-popup> </u-popup>
<!-- 未来7天预约明细抽屉(page-container + v-if 由组件内部处理);改期成功后静默刷新看板统计 -->
<appointment-upcoming-drawer
v-model="apptDrawerShow"
role="doctor"
:days="upcomingDayList"
:default-date="apptDefaultDate"
@rescheduled="onAppointmentRescheduled"
></appointment-upcoming-drawer>
<d-top-message ref="topMessage"></d-top-message> <d-top-message ref="topMessage"></d-top-message>
<d-tabbar></d-tabbar> <!-- 简报弹层在 content 内,层级低于自定义 tabbar;打开时隐藏 tabbar,避免底部按钮被挡住 -->
<d-tabbar v-show="!briefPopupShow"></d-tabbar>
</view> </view>
</template> </template>
@@ -402,15 +475,13 @@ import {
homeSession, homeSession,
getNotice getNotice
} from '@/api/all.js' } from '@/api/all.js'
import {
registerList
} from '../../api/consult';
import { import {
getAcceptingPatientList as fetchAcceptingPatientListApi, getAcceptingPatientList as fetchAcceptingPatientListApi,
getOnlineConsultationPatientListApi, getOnlineConsultationPatientListApi,
getWorkbenchStats, getWorkbenchStats,
} from '../../api/reception'; } from '../../api/reception';
import { checkOnlineConsultationConfigApi } from '../../api/storeConsultation.js'; import { checkOnlineConsultationConfigApi } from '../../api/storeConsultation.js';
import PublicAccountLockBanner from '@/components/public-account-lock-banner/public-account-lock-banner.vue';
/** 房间 ID 归一化(避免引入 chatRoomManager 整模块) */ /** 房间 ID 归一化(避免引入 chatRoomManager 整模块) */
function normalizeRoomId(id) { function normalizeRoomId(id) {
@@ -441,15 +512,26 @@ function formatSessionRelativeTime(item) {
const WORKBENCH_STATS_RANGE_KEY = 'workbench_stats_range'; const WORKBENCH_STATS_RANGE_KEY = 'workbench_stats_range';
const OFFLINE_RECEPTION_ENTRY = { const OFFLINE_RECEPTION_ENTRY = {
name: '线下接诊', name: '线下接诊',
icon: '../../static/image/zxzx.png', icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/zxzx.png',
url: '../../subPackages/sub_workbench/workbench_consult?type=1', url: '../../subPackages/sub_workbench/workbench_consult?type=1',
}; };
const ONLINE_RECEPTION_ENTRY = { const ONLINE_RECEPTION_ENTRY = {
name: '在线接诊', name: '在线接诊',
icon: '../../static/image/xsfz.png', icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/xsfz.png',
url: '../../subPackages/sub_online_reception/pages/list', url: '../../subPackages/sub_online_reception/pages/list',
}; };
const RECEPTION_GRID_ENTRIES = [OFFLINE_RECEPTION_ENTRY, ONLINE_RECEPTION_ENTRY]; /** 医生识图传方:上传处方图 → OCR → 待导入传方 → 接诊中药回填 */
const IMAGE_TRANSFER_ENTRY = {
name: '识图传方',
icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/zxzx.png',
url: '../../subPackages/sub_workbench/image-transfer-prescription/index',
};
const IMAGE_TRANSFER_ORDER_ENTRY = {
name: '我的传方',
icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/zxzx.png',
url: '../../subPackages/sub_image_transfer/order-list?scope=mine',
};
const RECEPTION_GRID_ENTRIES = [OFFLINE_RECEPTION_ENTRY, ONLINE_RECEPTION_ENTRY, IMAGE_TRANSFER_ENTRY, IMAGE_TRANSFER_ORDER_ENTRY];
const EMPTY_WORKBENCH_STATS = { const EMPTY_WORKBENCH_STATS = {
prescription_count: 0, prescription_count: 0,
wait_accept: 0, wait_accept: 0,
@@ -459,6 +541,9 @@ const EMPTY_WORKBENCH_STATS = {
}; };
export default { export default {
components: {
PublicAccountLockBanner,
},
data() { data() {
return { return {
noticeList: [], noticeList: [],
@@ -471,6 +556,8 @@ export default {
}, },
receptionGridEntries: RECEPTION_GRID_ENTRIES, receptionGridEntries: RECEPTION_GRID_ENTRIES,
offlineReceptionEntry: OFFLINE_RECEPTION_ENTRY, offlineReceptionEntry: OFFLINE_RECEPTION_ENTRY,
imageTransferEntry: IMAGE_TRANSFER_ENTRY,
imageTransferOrderEntry: IMAGE_TRANSFER_ORDER_ENTRY,
canUseOnlineConsultation: false, canUseOnlineConsultation: false,
onlineConsultationConfigReady: false, onlineConsultationConfigReady: false,
states: { states: {
@@ -486,7 +573,7 @@ export default {
name: '已结束', name: '已结束',
color: '#C4C7CC' color: '#C4C7CC'
}, },
3: { 4: {
name: '已取消', name: '已取消',
color: '#C4C7CC' color: '#C4C7CC'
} }
@@ -500,6 +587,9 @@ export default {
page: 1, page: 1,
totalPage: 1, totalPage: 1,
openShow: false, openShow: false,
/** 门店选择 page-container 挂载态,与 openShow 拆开 */
storePageGuard: false,
storeReleaseTimer: null,
keyword: "", keyword: "",
store: "", store: "",
storeList: [], storeList: [],
@@ -529,6 +619,14 @@ export default {
workbenchStats: { ...EMPTY_WORKBENCH_STATS }, workbenchStats: { ...EMPTY_WORKBENCH_STATS },
statsLoading: false, statsLoading: false,
statsInitialized: false, statsInitialized: false,
// 未来7天预约统计(医生本人维度,跨门店;随 workbench-stats 一起返回)
appointmentUpcoming: { total: 0, days: [] },
apptDrawerShow: false,
apptDefaultDate: '',
// AI 简报全文弹层是否打开(打开时隐藏自定义 tabbar,避免抽屉被挡住)
briefPopupShow: false,
// 系统配置总开关:默认不展示,看板接口确认开启后再挂载简报卡
aiBriefEnabled: false,
}; };
}, },
watch: { watch: {
@@ -554,12 +652,27 @@ export default {
const contentPx = typeof uni.upx2px === 'function' ? uni.upx2px(312) : 156; const contentPx = typeof uni.upx2px === 'function' ? uni.upx2px(312) : 156;
return this.statusBarHeight + this.navbarHeight + contentPx; return this.statusBarHeight + this.navbarHeight + contentPx;
}, },
/** 未来7天 pill:接口偶发不带 days 时给空数组,避免渲染层对 undefined 做迭代报错 */
upcomingDayList() {
const days = this.appointmentUpcoming && this.appointmentUpcoming.days;
return Array.isArray(days) ? days : [];
},
/**
* 线下接诊角标:优先用今日看板(已剔除未到就诊日的预约),避免 Yii info 把后天预约算进来
*/
offlineWaitAccept() {
const todayStats = this.statsCache && this.statsCache.today;
if (todayStats) return Number(todayStats.wait_accept) || 0;
if (this.statsRange === 'today') return Number(this.workbenchStats.wait_accept) || 0;
return Number(this.doctorInfo && this.doctorInfo.wait_accept) || 0;
},
}, },
onLoad() { onLoad() {
this.initStatsRangeFromStorage(); this.initStatsRangeFromStorage();
}, },
onUnload() { onUnload() {
this.stopRefreshTimer(); this.stopRefreshTimer();
this.clearStoreReleaseTimer();
if (this.__doctorImRoomUpdateHandler) { if (this.__doctorImRoomUpdateHandler) {
uni.$off('doctor-im-room-update', this.__doctorImRoomUpdateHandler); uni.$off('doctor-im-room-update', this.__doctorImRoomUpdateHandler);
this.__doctorImRoomUpdateHandler = null; this.__doctorImRoomUpdateHandler = null;
@@ -601,6 +714,11 @@ export default {
}, },
refreshWorkbenchData() { refreshWorkbenchData() {
const silentStats = this.statsInitialized || this.hasStatsCache(this.statsRange); const silentStats = this.statsInitialized || this.hasStatsCache(this.statsRange);
this.$nextTick(() => {
if (this.$refs.papLockBanner && this.$refs.papLockBanner.refresh) {
this.$refs.papLockBanner.refresh();
}
});
return Promise.all([ return Promise.all([
this.getNotice(), this.getNotice(),
this.getInfo(), this.getInfo(),
@@ -608,7 +726,18 @@ export default {
this.getList(), this.getList(),
this.getAcceptingPatientList(), this.getAcceptingPatientList(),
this.fetchOnlineConsultationConfig(), this.fetchOnlineConsultationConfig(),
this.fetchWorkbenchStats({ silent: silentStats }) this.fetchWorkbenchStats({ silent: silentStats }).then(() => {
// 先确认开关再拉简报:关闭时不请求,避免无谓生成
this.$nextTick(() => {
if (this.aiBriefEnabled && this.$refs.aiBrief) {
this.$refs.aiBrief.fetchBrief(false);
}
});
// 角标用今日待接诊;当前看板不是「今日」时补拉一份 today 缓存
if (this.statsRange !== 'today') {
return this.fetchWorkbenchStats({ silent: true, range: 'today' });
}
})
]).then(() => { ]).then(() => {
this.notifyWorkbenchRefreshed(); this.notifyWorkbenchRefreshed();
}); });
@@ -750,7 +879,7 @@ export default {
this.store_id = uni.getStorageSync('store_id') this.store_id = uni.getStorageSync('store_id')
} }
this.openShow = false this.closeStoreSelect()
this.onlineConsultationConfigReady = false this.onlineConsultationConfigReady = false
this.refreshWorkbenchData() this.refreshWorkbenchData()
}) })
@@ -811,6 +940,82 @@ export default {
if (Number.isNaN(num)) return '0.000' if (Number.isNaN(num)) return '0.000'
return num.toFixed(3) return num.toFixed(3)
}, },
/** AI 简报弹层开合:打开时隐藏自定义 tabbar;与门店/预约抽屉互斥 */
onBriefPopupChange(show) {
this.briefPopupShow = !!show;
if (show) {
// 简报自带 page-container:开之前立刻卸门店拦截,不能等 350ms
this.openShow = false;
this.clearStoreReleaseTimer();
this.storePageGuard = false;
this.apptDrawerShow = false;
}
},
/**
* 打开门店选择:先关预约抽屉/简报,再挂 page-container
* 为什么:微信同页只能一个 page-container,且门店弹层原先无返回拦截
*/
openStoreSelect() {
// 预约/简报关后仍留约 350ms pageGuard,需等卸完再挂门店,否则同页双 page-container
const waitMs = (this.apptDrawerShow || this.briefPopupShow) ? 360 : 0;
this.apptDrawerShow = false;
if (this.briefPopupShow && this.$refs.aiBrief && this.$refs.aiBrief.closePopup) {
this.$refs.aiBrief.closePopup();
}
const open = () => {
this.clearStoreReleaseTimer();
this.storePageGuard = true;
this.$nextTick(() => {
this.openShow = true;
});
};
if (waitMs > 0) {
setTimeout(open, waitMs);
} else {
open();
}
},
/** 关闭门店选择:先卸 show,再延迟卸挂载 */
closeStoreSelect() {
this.openShow = false;
this.scheduleStoreReleaseGuard();
},
releaseStorePageGuard() {
this.clearStoreReleaseTimer();
this.storePageGuard = false;
},
scheduleStoreReleaseGuard() {
this.clearStoreReleaseTimer();
this.storeReleaseTimer = setTimeout(() => {
this.storePageGuard = false;
this.storeReleaseTimer = null;
}, 350);
},
clearStoreReleaseTimer() {
if (this.storeReleaseTimer) {
clearTimeout(this.storeReleaseTimer);
this.storeReleaseTimer = null;
}
},
/** 打开预约抽屉:立刻卸门店 pageGuard;若简报开着则等其卸完再开 */
openApptDrawer(date) {
this.openShow = false;
this.clearStoreReleaseTimer();
this.storePageGuard = false;
this.apptDefaultDate = date || '';
if (this.briefPopupShow && this.$refs.aiBrief && this.$refs.aiBrief.closePopup) {
this.$refs.aiBrief.closePopup();
setTimeout(() => {
this.apptDrawerShow = true;
}, 360);
return;
}
this.apptDrawerShow = true;
},
/** 抽屉内改期成功:预约分布/待接诊数已变化,静默刷新看板(不打断用户继续操作抽屉) */
onAppointmentRescheduled() {
this.fetchWorkbenchStats({ silent: true });
},
fetchWorkbenchStats(options = {}) { fetchWorkbenchStats(options = {}) {
const silent = options.silent === true; const silent = options.silent === true;
const range = options.range || this.statsRange; const range = options.range || this.statsRange;
@@ -829,6 +1034,15 @@ export default {
if (range === this.statsRange) { if (range === this.statsRange) {
this.workbenchStats = { ...stats }; this.workbenchStats = { ...stats };
} }
// 未来7天预约统计不随 range 变化(医生本人全量口径),有值就更新
if (payload.appointment_upcoming) {
this.appointmentUpcoming = {
total: payload.appointment_upcoming.total || 0,
days: payload.appointment_upcoming.days || [],
};
}
// 简报总开关随看板下发,关闭则卸掉卡片
this.aiBriefEnabled = payload.ai_daily_brief_enabled === true || payload.ai_daily_brief_enabled === 1;
this.statsInitialized = true; this.statsInitialized = true;
} }
}).catch(() => { }).catch(() => {
@@ -980,7 +1194,7 @@ export default {
}, },
acceptingStatusLabel(item) { acceptingStatusLabel(item) {
const st = Number(item.status); const st = Number(item.status);
const m = { 1: '待接诊', 2: '接诊中', 3: '已结束' }; const m = { 1: '待接诊', 2: '接诊中', 3: '已结束', 4: '已取消', 7: '已拒诊' };
return m[st] || '接诊中'; return m[st] || '接诊中';
}, },
acceptingStatusColor(item) { acceptingStatusColor(item) {
@@ -1197,6 +1411,95 @@ page {
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06); box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
} }
/* 未来7天预约卡片 */
.appt-panel {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
&__head {
display: flex;
justify-content: space-between;
align-items: center;
}
&__title-wrap {
display: flex;
align-items: baseline;
}
&__title {
font-size: 30rpx;
font-weight: 600;
color: #1d2129;
}
&__sub {
font-size: 22rpx;
color: #94a3b8;
margin-left: 12rpx;
}
&__total-wrap {
display: flex;
align-items: center;
}
&__total {
font-size: 40rpx;
font-weight: 600;
color: #6acdbb;
}
&__unit {
font-size: 24rpx;
color: #94a3b8;
margin: 0 8rpx;
}
&__days {
display: flex;
margin-top: 20rpx;
}
&__day {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 12rpx 0;
margin-right: 8rpx;
border-radius: 12rpx;
background: #f7f8fa;
&:last-child {
margin-right: 0;
}
&--active {
background: rgba(106, 205, 187, 0.12);
.appt-panel__day-count {
color: #6acdbb;
font-weight: 600;
}
}
}
&__day-label {
font-size: 20rpx;
color: #6c7380;
}
&__day-count {
font-size: 28rpx;
color: #c0c4cc;
margin-top: 6rpx;
}
}
.dashboard-tabs { .dashboard-tabs {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

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