Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd14152ba | ||
|
|
e6e51ac805 | ||
|
|
1dff190e14 | ||
|
|
bd080674bf | ||
|
|
87f21de8de | ||
|
|
8aafb0114b | ||
|
|
158ffd9e6e | ||
|
|
22e93c5dba | ||
|
|
2a1fe884d7 | ||
|
|
62ffb6fd31 | ||
|
|
97bc48c17c | ||
|
|
1fbc14638a |
41
.cursor/rules/Api-Module-Split.mdc
Normal 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`
|
||||||
32
.cursor/rules/Business-Shared.mdc
Normal 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`)加下拉刷新
|
||||||
|
- 开方编辑器、录入/申请表单、登录、协议、静态成功页不加
|
||||||
11
.cursor/rules/Clarify-Before-Act.mdc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
description: 不懂先问,明确目标后再动手
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 先问再做
|
||||||
|
|
||||||
|
1. **不懂就问**:需求、业务口径、技术选型、影响范围等有不确定之处,必须先向你确认,禁止凭猜测直接改代码。
|
||||||
|
2. **一问一答**:每次只提一个最关键的问题,等你回复后再继续;避免一次抛出多个选项把讨论打散。
|
||||||
|
3. **说明提问原因**:每个问题都要附带「我为什么要问」——例如缺什么信息、有哪些备选理解、猜错会有什么后果。
|
||||||
|
4. **目标对齐后再动手**:确认对你的目标(要达成什么、不做什么、验收标准)有明确认知后,再开始查代码、写 SQL、改页面。
|
||||||
@@ -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 再绑定
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
95
.cursor/rules/Page-Container-Drawer.mdc
Normal 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 卸挂载以本规则 + 切换账号组件为准**。
|
||||||
90
.workbuddy/memory/MEMORY.md
Normal 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
@@ -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]))
|
||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
53
api/prescriptionImageOcr.js
Normal 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
@@ -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
@@ -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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
139
api/reception.js
@@ -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
@@ -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(','),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
530
components/ai-brief-card/ai-brief-card.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
587
components/ocr-image-loading/ocr-image-loading.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
678
components/transfer-doctor-picker/transfer-doctor-picker.vue
Normal 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>
|
||||||
@@ -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;
|
||||||
|
|||||||
374
components/workbench-fav-editor/workbench-fav-editor.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
1007
components/workbench-tile-board/workbench-tile-board.vue
Normal 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;
|
||||||
|
|||||||
6
main.js
@@ -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
|
||||||
|
|||||||
169
pages.json
@@ -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": {
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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&&'抱歉您的申请已被驳回!'}}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 594 B |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 351 B |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 931 B |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 837 B |
|
Before Width: | Height: | Size: 1.0 KiB |