feat: 工作台常用入口可批量编辑,简报改为横幅,列表补下拉刷新

各角色工作台支持批量保存常用入口和业务员看板;AI 简报改成单行横幅避免锁死滑动;仓库/提现/对账等列表统一下拉刷新,并补代付码弹窗。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
李琦
2026-08-18 08:53:16 +08:00
parent 97bc48c17c
commit 62ffb6fd31
85 changed files with 2809 additions and 787 deletions

View File

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

View File

@@ -110,6 +110,14 @@ export function toggleClinicAdminFavoriteEntry(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
@@ -271,3 +279,8 @@ export function updateWarehousePrice(data) {
export function getWarehouseProductTypeOptions() {
return clinicRequest({ url: `${PREFIX}-warehouse/product-type-options`, method: 'GET' });
}
/** 诊所/药店可见仓库 Tab xk-api GET /clinic-admin-warehouse/warehouse-tabs */
export function getWarehouseTabs() {
return clinicRequest({ url: `${PREFIX}-warehouse/warehouse-tabs`, method: 'GET' });
}

View File

@@ -114,6 +114,14 @@ export function toggleClinicSalespersonFavoriteEntry(code) {
return clinicSalespersonRequest({ url: '/newApi/clinic-salesperson-dashboard/toggle-favorite-entry', method: 'POST', data: { code } });
}
/**
* 批量保存我的常用(功能页悬浮编辑 / 首页快捷添加)
* 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

View File

@@ -60,6 +60,14 @@ export function togglePlatformAdminFavoriteEntry(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

View File

@@ -61,6 +61,15 @@ export function rescheduleAppointmentApi(data) {
})
}
/** 生成患者代付码 xk-api POST /doctor-reception-wx/proxy-pay-qrcodeorder_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({

View File

@@ -20,6 +20,14 @@ function salespersonDashboardRequest(options) {
});
}
/**
* 业务员工作台首页看板(录入进度 + 名下诊所营业额/处方/挂号)
* xk-api GET /salesperson-dashboard/stats?range=today|week|month
*/
export function getSalespersonDashboardStats(params = {}) {
return salespersonDashboardRequest({ url: '/newApi/salesperson-dashboard/stats', method: 'GET', data: params });
}
/**
* 业务员工作台功能入口下发(后台按角色分配的宫格入口,替代前端硬编码)
* xk-api GET /salesperson-dashboard/entries
@@ -44,6 +52,14 @@ export function toggleSalespersonFavoriteEntry(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

View File

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

View File

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

View File

@@ -1,78 +1,36 @@
<template>
<view>
<!-- AI 今日简报卡片每天首次进工作台后端生成并缓存之后常驻卡片可随时查看 -->
<view class="ai-brief-card">
<view class="ai-brief-card__head">
<view class="ai-brief-card__title-wrap">
<view class="ai-brief-card__badge">AI</view>
<text class="ai-brief-card__title">今日简报</text>
<text v-if="generatedAtText" class="ai-brief-card__time">{{ generatedAtText }}</text>
</view>
<view class="ai-brief-card__refresh" :class="{ 'is-disabled': loading }" @click="onRefreshTap">
<u-icon name="reload" size="22" color="#6acdbb"></u-icon>
<text class="ai-brief-card__refresh-text">重新生成</text>
</view>
</view>
<!-- 加载态AI 生成可能耗时十几秒给出明确等待文案 -->
<view v-if="loading" class="ai-brief-card__loading">
<u-loading mode="circle" size="28"></u-loading>
<text class="ai-brief-card__loading-text">AI 正在分析今日门诊数据</text>
</view>
<!-- 接口失败态网络层 -->
<view v-else-if="fetchError" class="ai-brief-card__error">
<text class="ai-brief-card__error-text">简报加载失败</text>
<view class="ai-brief-card__retry" @click="onRetryTap">重试</view>
</view>
<!-- AI 生成失败态快照芯片仍可看正文位置给重试引导 -->
<block v-else-if="brief && brief.status !== 1">
<view v-if="chips.length" class="ai-brief-card__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-brief-card__error">
<text class="ai-brief-card__error-text">AI 生成失败请重试</text>
<view class="ai-brief-card__retry" @click="onRefreshTap">重新生成</view>
</view>
</block>
<!-- 成功态快照芯片 + 正文预览收纳高度点击弹层看全文 -->
<block v-else-if="brief">
<view v-if="chips.length" class="ai-brief-card__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-brief-card__body" @click="openPopup">
<view v-for="(section, idx) in sections" :key="idx" class="ai-brief-card__section">
<text v-if="section.title" class="ai-brief-card__section-tag">{{ section.title }}</text>
<text class="ai-brief-card__section-text">{{ section.text }}</text>
</view>
</view>
<view class="ai-brief-card__more" @click="openPopup">
<text class="ai-brief-card__more-text">查看全文</text>
<u-icon name="arrow-right" size="22" color="#6acdbb"></u-icon>
</view>
</block>
<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 -->
<!-- 查看全文抽屉page-container 拦截返回键规则抽屉必须 page-container + v-if -->
<!-- pageGuard popupShow 拆开 show=false 播完离场再卸节点避免原生滚动锁残留导致关抽屉后页面无法滑动 -->
<!-- #ifdef MP-WEIXIN -->
<page-container v-if="popupShow" :show="popupShow" :overlay="false" @leave="onPageLeave" />
<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-card__badge">AI</view>
<view class="ai-brief-banner__badge">AI</view>
<text class="ai-popup__title">{{ popupDateText }}</text>
</view>
<text class="ai-popup__close" @click="closePopup">关闭</text>
<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">
@@ -99,17 +57,13 @@
<script>
/**
* AI 今日门诊简报卡片 + 每日首弹弹层(医生工作台)
* 数据流:挂载/切换门店时拉 /doctor-reception-wx/ai-daily-brief后端每天首次生成并按天缓存
* 弹层策略生成成功且本机今天没弹过时自动弹一次全文storage 按天记录),之后可点「查看全文」再看;
* 「重新生成」传 refresh=1 强制后端重生成(原行 UPDATE不新增缓存行
* AI 今日门诊简报:工作台单行横幅 + 点击打开全文抽屉
* 不做每日首弹——page-container 挂在 tab 页上会锁死整页滑动
* 数据仍走 /doctor-reception-wx/ai-daily-brief每天首次生成、按天缓存「重新生成」在抽屉内
*/
import { getAiDailyBriefApi } from '@/api/workbench.js'
import { unwrapXkApi } from '@/utils/api-response.js'
/** 每日首弹的 storage key按天记录避免当天反复弹打扰 */
const POPUP_STORAGE_KEY = 'ai_brief_popup_date'
export default {
name: 'ai-brief-card',
props: {
@@ -122,12 +76,16 @@ export default {
data() {
return {
brief: null,
// 系统配置关闭时整卡不渲染,避免关开关后仍占一行
featureEnabled: true,
loading: false,
// 接口本身失败(网络/鉴权),区别于 brief.status=0 的「AI 生成失败」
fetchError: false,
popupShow: false,
// 去重键storeId_日期页面 onShow 会反复触发刷新,同店同天已拉过则跳过,
// 跨天/切店后 key 变化自动重拉(跨天首次会重新生成并触发每日首弹)
// 原生 page-container 挂载开关:关闭时先关 showafterleave/超时后再卸,避免锁死页面滑动
pageGuard: false,
releaseTimer: null,
// 去重键storeId_日期页面 onShow 会反复触发刷新,同店同天已拉过则跳过
lastFetchKey: ''
}
},
@@ -141,12 +99,24 @@ export default {
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.brief && this.brief.content ? this.brief.content : ''
const content = this.unwrapBriefContent(this.brief && this.brief.content ? this.brief.content : '')
if (!content) return []
const matches = []
const reg = /【([^】]+)】([^【]*)/g
@@ -167,9 +137,9 @@ export default {
if (!data) return []
const today = data.today || {}
const schedule = data.schedule || {}
const upcoming = data.upcoming || {}
const waitAccept = Number(today.wait_accept) || 0
const dueWaiting = Number(today.appointment_due_waiting) || 0
// 未到就诊日的预约不当成今日待办芯片,避免角标式催办
return [
{
label: '今日值班',
@@ -177,15 +147,88 @@ export default {
highlight: false
},
{ label: '待接诊', value: waitAccept, highlight: waitAccept > 0 },
{ label: '今日到期预约', value: dueWaiting, highlight: dueWaiting > 0 },
{ label: '未来7天预约', value: Number(upcoming.total) || 0, highlight: false }
{ label: '今日到期预约', value: dueWaiting, highlight: dueWaiting > 0 }
]
}
},
watch: {
// 弹层开合同步给工作台:简报弹层在 .contentz-index:1盖不住自定义 tabbarz-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()
@@ -209,9 +252,16 @@ export default {
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
this.maybeAutoPopup()
} else {
this.fetchError = true
}
@@ -221,17 +271,18 @@ export default {
this.loading = false
}
},
/**
* 每日首弹:生成成功且本机今天没弹过才自动弹全文
* 失败态不自动弹(打扰用户),卡片内保留重试入口
*/
maybeAutoPopup() {
if (!this.brief || this.brief.status !== 1) return
const today = this.brief.brief_date
if (!today) return
if (uni.getStorageSync(POPUP_STORAGE_KEY) === today) return
uni.setStorageSync(POPUP_STORAGE_KEY, today)
this.popupShow = true
/** 横幅点击:成功打开抽屉,失败则重试;加载中不响应以免打断生成 */
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() {
@@ -243,47 +294,64 @@ export default {
if (this.loading) return
this.fetchBrief(false)
},
/** 点「查看全文」手动打开弹层 */
/** 点横幅打开抽屉:先挂 page-container下一帧再 show保证原生层能接到进入动画 */
openPopup() {
this.popupShow = true
this.pageGuard = true
this.$nextTick(() => {
this.popupShow = true
})
},
closePopup() {
this.popupShow = false
this.scheduleReleaseGuard()
},
/** u-popup 双向同步(蒙层点击关闭等场景) */
onPopupInput(val) {
this.popupShow = val
this.popupShow = !!val
if (!val) this.scheduleReleaseGuard()
},
/** page-container 返回键触发:关闭弹层而非退出页面 */
/** 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>
/* 卡片:白底 + AI 主题绿 1px 边框 + 微光(规则:不用左侧竖色条,用同色系边框+外发光强调) */
.ai-brief-card {
/* 单行横幅: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: 24rpx;
padding: 16rpx 20rpx;
margin-bottom: 20rpx;
&__head {
display: flex;
justify-content: space-between;
align-items: center;
}
&__title-wrap {
display: flex;
align-items: center;
}
&__badge {
flex-shrink: 0;
padding: 2rpx 12rpx;
background: linear-gradient(135deg, #6acdbb, #4db6a3);
border-radius: 8rpx;
@@ -294,94 +362,32 @@ export default {
}
&__title {
font-size: 30rpx;
flex-shrink: 0;
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-right: 12rpx;
}
&__time {
font-size: 22rpx;
color: #b0b3b8;
margin-left: 16rpx;
}
&__refresh {
display: flex;
align-items: center;
padding: 6rpx 16rpx;
border: 1px solid rgba(106, 205, 187, 0.4);
border-radius: 999rpx;
&.is-disabled {
opacity: 0.5;
}
}
&__refresh-text {
font-size: 22rpx;
color: #6acdbb;
margin-left: 6rpx;
}
&__loading {
display: flex;
align-items: center;
padding: 28rpx 8rpx;
}
&__loading-text {
&__text {
flex: 1;
min-width: 0;
font-size: 24rpx;
color: #909399;
margin-left: 16rpx;
}
&__error {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 8rpx;
}
&__error-text {
font-size: 24rpx;
color: #909399;
}
&__retry {
padding: 6rpx 24rpx;
background: rgba(106, 205, 187, 0.08);
border: 1px solid rgba(106, 205, 187, 0.4);
border-radius: 999rpx;
font-size: 22rpx;
color: #6acdbb;
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;
}
/* 正文预览:限高收纳,点开弹层看全文 */
&__body {
max-height: 300rpx;
overflow: hidden;
background: #f7f9f8;
border-radius: 12rpx;
padding: 20rpx 24rpx;
margin-top: 20rpx;
}
&__section {
display: flex;
flex-direction: column;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
&__section-tag {
align-self: flex-start;
padding: 2rpx 16rpx;
@@ -392,25 +398,6 @@ export default {
color: #4db6a3;
margin-bottom: 8rpx;
}
&__section-text {
font-size: 24rpx;
line-height: 1.7;
color: #555;
}
&__more {
display: flex;
align-items: center;
justify-content: center;
padding-top: 16rpx;
}
&__more-text {
font-size: 22rpx;
color: #6acdbb;
margin-right: 4rpx;
}
}
/* 快照统计芯片 */
@@ -466,6 +453,21 @@ export default {
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;
@@ -474,7 +476,8 @@ export default {
&__scroll {
flex: 1;
min-height: 0;
height: 0;
min-height: 400rpx;
padding: 24rpx 32rpx;
box-sizing: border-box;
}

View File

@@ -1,13 +1,15 @@
<template>
<view>
<!-- 页面级返回拦截防止用户按返回键直接退出页面规则要求抽屉必须配 page-container + v-if -->
<!-- pageGuard 用于两级抽屉场景原生容器一次 leave 后失效关掉子抽屉后需重挂载才能拦截下一次返回 -->
<!-- pageGuard show 拆开 show=false 播完离场再卸节点避免关抽屉后页面无法滑动 -->
<!-- 两级抽屉原生容器一次 leave 后失效关掉子抽屉后需重挂载才能拦截下一次返回 -->
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="pageGuard && show"
v-if="pageGuard"
:show="show"
:overlay="false"
@leave="onPageLeave"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<u-popup
@@ -35,7 +37,7 @@
@click="switchFilter('')"
>全部7天</view>
<view
v-for="day in days"
v-for="day in dayList"
:key="day.date"
class="appt-pill"
:class="{ 'appt-pill--active': filterDate === day.date }"
@@ -209,8 +211,9 @@ export default {
loading: false,
// 请求序号:快速切日期/连点加载更多时,据此丢弃晚到的过期响应,防止旧数据覆盖新列表
reqSeq: 0,
// page-container 挂载开关(见模板注释)
pageGuard: true,
// page-container 挂载开关:关闭时先关 showafterleave/超时后再卸
pageGuard: false,
releaseTimer: null,
// ---- 医生代改期子抽屉状态(仅 role=doctor 使用) ----
rescheduleShow: false,
rescheduleTarget: null,
@@ -228,6 +231,10 @@ export default {
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
@@ -277,12 +284,35 @@ export default {
/** 打开时按入口预筛选并拉第一页(每次打开都取最新数据) */
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
},

View File

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

View File

@@ -0,0 +1,267 @@
<template>
<view>
<!-- 页面级返回拦截pageGuard show 拆开关弹窗后先离场再卸避免锁死页面滑动 -->
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="pageGuard"
:show="show"
:overlay="false"
@leave="close"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<u-popup :value="show" mode="center" border-radius="24" :mask-close-able="true" @input="onPopupInput" @close="close">
<view class="ppay">
<view class="ppay__header">
<text class="ppay__title">患者代付码</text>
<text class="ppay__close" @click="close">关闭</text>
</view>
<view class="ppay__body">
<view v-if="loading" class="ppay__loading">
<u-loading mode="circle" size="40"></u-loading>
<text class="ppay__loading-text">代付码生成中...</text>
</view>
<block v-else-if="qrcode">
<!-- 后端返回完整 data:image/png;base64 数据长按可转发给患者 -->
<image class="ppay__qrcode" :src="qrcode" mode="aspectFit" show-menu-by-longpress></image>
<view class="ppay__meta">
<view class="ppay__meta-row">
<text class="ppay__meta-label">订单号</text>
<text class="ppay__meta-value">{{ orderNo }}</text>
</view>
<view class="ppay__meta-row">
<text class="ppay__meta-label">待支付金额</text>
<text class="ppay__meta-value ppay__meta-value--price">{{ totalPayPrice }}</text>
</view>
</view>
<view class="ppay__tip">请让患者用微信扫一扫进入小程序完成支付</view>
</block>
<view v-else class="ppay__error">
<text class="ppay__error-text">{{ errorText || '代付码生成失败' }}</text>
<view class="ppay__retry" @click="load">重试</view>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* 医生端「患者代付码」弹窗
*
* 用途:本次挂号处方存在待支付订单时,医生出示小程序码给患者扫码代付
* (复用 PC 接诊台同款后端 proxy-pay-qrcodescene=ppay={orderId},患者端解析链路零改动)。
* 读值xk-api 形态,拦截器已返回 body → code / result / messageunwrapXkApi
*/
import { getProxyPayQrcodeApi } from '@/api/reception.js'
import { unwrapXkApi } from '@/utils/api-response.js'
export default {
name: 'ProxyPayQrcodeModal',
props: {
/** v-model 显隐 */
value: { type: Boolean, default: false },
/** 待支付订单ID处方关联的商品订单 */
orderId: { type: [Number, String], default: 0 },
},
data() {
return {
loading: false,
qrcode: '',
orderNo: '',
totalPayPrice: '',
errorText: '',
pageGuard: false,
releaseTimer: null,
}
},
computed: {
show: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
},
watch: {
/** 每次打开都重新生成:订单可能已被支付/取消,后端会按最新状态校验 */
value(v) {
if (v) {
this.pageGuard = true
this.load()
} 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
},
/** 请求代付码xk-api取值 code/result/message */
async load() {
if (!this.orderId) {
this.qrcode = ''
this.errorText = '缺少订单信息'
return
}
this.loading = true
this.qrcode = ''
this.errorText = ''
try {
const res = await getProxyPayQrcodeApi({ order_id: this.orderId })
const { ok, payload, message } = unwrapXkApi(res)
if (ok && payload && payload.qrcode) {
this.qrcode = payload.qrcode
this.orderNo = payload.order_no || ''
this.totalPayPrice = payload.total_pay_price || ''
} else {
this.errorText = message || '代付码生成失败'
}
} catch (e) {
this.errorText = (e && e.message) || '代付码生成失败'
} finally {
this.loading = false
}
},
},
}
</script>
<style lang="scss" scoped>
.ppay {
width: 600rpx;
padding: 32rpx;
box-sizing: border-box;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
&__title {
font-size: 32rpx;
font-weight: bold;
color: #2a2e35;
}
&__close {
font-size: 26rpx;
color: #8a92a3;
padding: 8rpx;
}
&__body {
display: flex;
flex-direction: column;
align-items: center;
}
&__loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
}
&__loading-text {
margin-top: 16rpx;
font-size: 26rpx;
color: #8a92a3;
}
&__qrcode {
width: 400rpx;
height: 400rpx;
}
/* 订单信息卡1px 同色系边框 + 微光(规范禁止左侧竖色条强调) */
&__meta {
width: 100%;
margin-top: 24rpx;
padding: 20rpx 24rpx;
box-sizing: border-box;
border: 1px solid rgba(0, 168, 138, 0.3);
border-radius: 16rpx;
box-shadow: 0 0 6px rgba(0, 168, 138, 0.18);
}
&__meta-row {
display: flex;
align-items: center;
justify-content: space-between;
& + & {
margin-top: 12rpx;
}
}
&__meta-label {
font-size: 26rpx;
color: #8a92a3;
}
&__meta-value {
font-size: 26rpx;
color: #2a2e35;
&--price {
font-size: 30rpx;
font-weight: bold;
color: #fa3534;
}
}
&__tip {
margin-top: 20rpx;
font-size: 24rpx;
color: #8a92a3;
text-align: center;
}
&__error {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
}
&__error-text {
font-size: 26rpx;
color: #fa3534;
}
&__retry {
margin-top: 24rpx;
padding: 12rpx 48rpx;
font-size: 26rpx;
color: #00a88a;
border: 1px solid rgba(0, 168, 138, 0.5);
border-radius: 999rpx;
}
}
</style>

View File

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

View File

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

View File

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

View File

@@ -34,10 +34,14 @@
</view>
</view>
<!-- 我的常用空态给引导条点击也进功能页避免新用户首页空洞无从下手 -->
<!-- 我的常用标题旁快捷添加空态点开编辑抽屉不再引导去功能页长按 -->
<view class="wue-section">
<view class="wue-header">
<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 v-for="item in favorites" :key="item.key" class="wue-cell">
@@ -60,9 +64,9 @@
</view>
</view>
</view>
<view v-else class="wue-hint" hover-class="wue-hint-hover" @tap="$emit('more')">
<u-icon name="star" size="30" color="#8E8E93"></u-icon>
<text class="wue-hint-text">暂无常用功能去功能页长按任意入口即可添加</text>
<view v-else class="wue-hint" hover-class="wue-hint-hover" @tap="openEditor">
<u-icon name="plus-circle" size="30" color="#6ACDBB"></u-icon>
<text class="wue-hint-text">暂无常用功能点此添加</text>
<u-icon name="arrow-right" size="22" color="#C7C7CC"></u-icon>
</view>
</view>
@@ -75,6 +79,7 @@
@click="$emit('more')"
></xk-service-card>
</view>
<workbench-fav-editor ref="favEditor" :api="api" @saved="onEditorSaved"></workbench-fav-editor>
</view>
</template>
@@ -86,7 +91,7 @@
* 三段结构与双形态布局完全一致,收敛到主包组件避免四份复制。
* 数据与行为职责划分:
* - 父页面负责拉取 entry-user-data、按 code 注入本端行为(角标/表单预填/传方弹窗)后传入
* - 本组件只管渲染与转发事件:@item-click(item) 点了某入口、@more 要去功能页
* - 本组件只管渲染与转发事件:@item-click(item) 点了某入口、@more 要去功能页、@saved 常用已批量更新
* 点击上报使用也由父页面在 @item-click 里做(各端上报接口不同)
*/
export default {
@@ -98,6 +103,8 @@ export default {
favorites: { type: Array, default: () => [] },
/** 卡片形态true 长卡一行两个 / false 金刚区宫格 */
cardLong: { type: Boolean, default: false },
/** 快捷添加抽屉用:{ getEntries, getUserData, batchSaveFavorites } */
api: { type: Object, default: () => ({}) },
},
data() {
return {
@@ -110,6 +117,14 @@ export default {
onItemTap(item) {
this.$emit('item-click', item);
},
/** 打开常用编辑抽屉(标题「添加」与空态共用) */
openEditor() {
if (this.$refs.favEditor) this.$refs.favEditor.open();
},
/** 批量保存成功:通知首页重拉常用/最近,角标等装饰仍由父页注入 */
onEditorSaved() {
this.$emit('saved');
},
},
};
</script>
@@ -119,9 +134,29 @@ export default {
margin: 0 30rpx 30rpx;
}
.wue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
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 规格 */
.wue-title {
font-size: 34rpx;

View File

@@ -35,7 +35,8 @@
"path": "pages/patient/index",
"style": {
"navigationBarTitleText": "患者",
"navigationStyle": "custom"
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
@@ -281,7 +282,7 @@
"root": "subPackages/sub_clinic_admin",
"pages": [{
"path": "home/index",
"style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" }
"style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "features/index",
@@ -289,7 +290,7 @@
},
{
"path": "my/index",
"style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" }
"style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "my/vip-detail",
@@ -297,11 +298,11 @@
},
{
"path": "withdrawal/index",
"style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom" }
"style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "withdrawal/settlement/index",
"style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" }
"style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "withdrawal/settlement/detail",
@@ -309,7 +310,7 @@
},
{
"path": "withdrawal/account-change/index",
"style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" }
"style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "withdrawal/apply",
@@ -325,7 +326,7 @@
},
{
"path": "reconciliation/index",
"style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom" }
"style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "order/index",
@@ -369,18 +370,18 @@
{ "path": "order/index", "style": { "navigationBarTitleText": "商品订单", "navigationStyle": "custom", "disableScroll": true } },
{ "path": "order/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "order/trace/index", "style": { "navigationBarTitleText": "订单追溯", "navigationStyle": "custom" } },
{ "path": "reconciliation/index", "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom" } },
{ "path": "withdrawal/index", "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom" } },
{ "path": "reconciliation/index", "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/index", "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/apply", "style": { "navigationBarTitleText": "申请提现", "navigationStyle": "custom" } },
{ "path": "withdrawal/card-edit", "style": { "navigationBarTitleText": "编辑账户", "navigationStyle": "custom" } },
{ "path": "withdrawal/record-detail", "style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" } },
{ "path": "withdrawal/settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } },
{ "path": "withdrawal/settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } },
{ "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" } },
{ "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom" } },
{ "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "warehouse/detail", "style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" } },
{ "path": "user-patient/index", "style": { "navigationBarTitleText": "会员管理", "navigationStyle": "custom" } },
{ "path": "user-patient/patients", "style": { "navigationBarTitleText": "就诊人列表", "navigationStyle": "custom" } },
{ "path": "user-patient/index", "style": { "navigationBarTitleText": "会员管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "user-patient/patients", "style": { "navigationBarTitleText": "就诊人列表", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "user-patient/detail", "style": { "navigationBarTitleText": "就诊人详情", "navigationStyle": "custom" } },
{ "path": "user-patient/register-detail", "style": { "navigationBarTitleText": "挂号详情", "navigationStyle": "custom" } },
{ "path": "profile/index", "style": { "navigationBarTitleText": "个人中心", "navigationStyle": "custom" } }
@@ -388,28 +389,28 @@
}, {
"root": "subPackages/sub_platform_admin",
"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": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
{ "path": "withdrawal/audit/index", "style": { "navigationBarTitleText": "提现审核", "navigationStyle": "custom" } },
{ "path": "store/index", "style": { "navigationBarTitleText": "门店管理", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/audit/index", "style": { "navigationBarTitleText": "提现审核", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store/index", "style": { "navigationBarTitleText": "门店管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store/detail", "style": { "navigationBarTitleText": "门店详情", "navigationStyle": "custom" } },
{ "path": "store/edit", "style": { "navigationBarTitleText": "编辑诊所", "navigationStyle": "custom" } },
{ "path": "store/drug-price", "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": "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" } }
]
}, {
"root": "subPackages/sub_salesperson_manage",
"pages": [
{ "path": "index", "style": { "navigationBarTitleText": "推广员管理", "navigationStyle": "custom" } },
{ "path": "index", "style": { "navigationBarTitleText": "推广员管理", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "form", "style": { "navigationBarTitleText": "推广员", "navigationStyle": "custom" } },
{ "path": "drug-commission/index", "style": { "navigationBarTitleText": "药品佣金", "navigationStyle": "custom" } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom" } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费与结算", "navigationStyle": "custom" } },
{ "path": "drug-commission/index", "style": { "navigationBarTitleText": "药品佣金", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费与结算", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "settlement/preview", "style": { "navigationBarTitleText": "按时间段结算", "navigationStyle": "custom" } },
{ "path": "settlement/confirm", "style": { "navigationBarTitleText": "确认结算", "navigationStyle": "custom" } },
{ "path": "settlement/detail", "style": { "navigationBarTitleText": "结算详情", "navigationStyle": "custom" } }
@@ -419,12 +420,12 @@
"pages": [
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
{ "path": "features/index", "style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
{ "path": "store-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "store-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } },
{ "path": "store-input/detail", "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/detail", "style": { "navigationBarTitleText": "预填详情", "navigationStyle": "custom" } }
]
@@ -436,6 +437,7 @@
"style": {
"navigationBarTitleText": "工作台",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"usingComponents": {
"transfer-patient-drawer": "/subPackages/sub_workbench/prescription_v2/components/modals/TransferPatientDrawer"
},
@@ -448,13 +450,13 @@
"path": "features/index",
"style": { "navigationBarTitleText": "功能", "navigationStyle": "custom" }
},
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom" } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费记录", "navigationStyle": "custom" } },
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "commission/index", "style": { "navigationBarTitleText": "服务费记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "invitees/index", "style": { "navigationBarTitleText": "我邀请的推广员", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "invitees/detail", "style": { "navigationBarTitleText": "收益明细", "navigationStyle": "custom" } },
{ "path": "settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } },
{ "path": "transfer-prescription/list", "style": { "navigationBarTitleText": "传方记录", "navigationStyle": "custom" } }
{ "path": "invitees/detail", "style": { "navigationBarTitleText": "收益明细", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "transfer-prescription/list", "style": { "navigationBarTitleText": "传方记录", "navigationStyle": "custom", "enablePullDownRefresh": true } }
]
}, {
"root": "subPackages/sub_agreement",
@@ -470,7 +472,8 @@
"path": "pages/list",
"style": {
"navigationBarTitleText": "在线接诊",
"navigationStyle": "custom"
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
}, {
"path": "pages/chat",

View File

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

View File

@@ -46,9 +46,9 @@
<xk-list-card
v-for="(item, index) in list" :key="index"
mode="flat"
:avatar="item.user.avatarurl || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)"
:avatar="item.card_avatar"
:title="item.patient"
:meta="(item.sex % 2 != 0 ? '男' : '女') + ' · ' + item.age + '岁'"
:meta="item.card_meta"
:arrow="true"
@click="toDetail(item.patient_id)"
></xk-list-card>
@@ -127,7 +127,8 @@
})
.then((res) => {
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;
if (this.page >= this.totalPage) {
this.status = "nomore";
@@ -153,6 +154,22 @@
this.loading2 = false;
});
},
/**
* 列表展示字段提前算好:部分患者 user 为 null模板读 user.avatarurl 会直接崩
* 微信模板禁止 ?. / ??,头像和性别文案统一在这里兜底
*/
normalizeItem(item) {
const isFemale = item.sex % 2 == 0;
const user = item.user || {};
const fallback = isFemale
? (this.girl || require('../../static/image/nv.png'))
: (this.male || require('../../static/image/nan.png'));
return {
...item,
card_avatar: user.avatarurl || fallback,
card_meta: (isFemale ? '女' : '男') + ' · ' + (item.age || '-') + '岁',
};
},
toDetail(id) {
uni.navigateTo({
url: '/subPackages/sub_patient/patient_detail?id=' + id

View File

@@ -20,7 +20,14 @@
<!-- 审方列表swiper 承载三个状态 tab支持左右滑动切换每个 tab 独立分页 + 上拉加载 -->
<swiper v-else class="tab-swiper" :current="currents" @change="onSwiperChange">
<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">
<xk-list-card
v-for="item in tab.list" :key="item.id"
@@ -69,7 +76,9 @@
import {
unwrapXkApi
} from "@/utils/api-response.js";
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default {
mixins: [pullRefreshMixin],
data() {
return {
status: uni.getStorageSync('login_status'),
@@ -122,6 +131,12 @@
}
},
methods: {
/**
* 下拉刷新当前审方 tab
*/
reload() {
return this.refreshTab(this.currents)
},
/** tabs 与 swiper 统一切换入口:点 tab、手势左右滑都收敛到这里 */
switchTab(index) {
if (this.currents === index) return
@@ -140,7 +155,7 @@
const tab = this.tabPages[index]
tab.page = 1
tab.finished = false
this.fetchTab(index, true)
return this.fetchTab(index, true)
},
/** 上拉加载下一页 */
loadMore(index) {

View File

@@ -95,8 +95,8 @@
</view>
</view>
<!-- AI 今日简报每天首次进入自动弹全文storage 按天记录之后常驻卡片可随时查看 -->
<ai-brief-card ref="aiBrief" :store-id="store_id"></ai-brief-card>
<!-- AI 今日简报系统配置关闭时不挂载避免进工作台就打生成接口 -->
<ai-brief-card v-if="aiBriefEnabled" ref="aiBrief" :store-id="store_id" @popup-change="onBriefPopupChange"></ai-brief-card>
<!-- 未来7天预约卡片点击总数或某天打开明细抽屉 -->
<view class="appt-panel">
@@ -113,7 +113,7 @@
</view>
<view class="appt-panel__days">
<view
v-for="day in appointmentUpcoming.days"
v-for="day in upcomingDayList"
:key="day.date"
class="appt-panel__day"
:class="{ 'appt-panel__day--active': day.count > 0 }"
@@ -136,8 +136,8 @@
<image :src="item.icon"></image>
<view>{{ item.name }}</view>
<u-badge
:count="i === 0 ? doctorInfo.wait_accept : 0"
v-if="i === 0"
:count="offlineWaitAccept"
v-if="i === 0 && offlineWaitAccept > 0"
:offset="[-6, 98]"
bgColor="#F44336"
></u-badge>
@@ -155,8 +155,8 @@
<text class="offline-entry-desc">到店开方处理线下挂号</text>
</view>
<u-badge
v-if="doctorInfo.wait_accept > 0"
:count="doctorInfo.wait_accept"
v-if="offlineWaitAccept > 0"
:count="offlineWaitAccept"
:offset="[-8, 8]"
bgColor="#F44336"
></u-badge>
@@ -421,13 +421,14 @@
<appointment-upcoming-drawer
v-model="apptDrawerShow"
role="doctor"
:days="appointmentUpcoming.days"
:days="upcomingDayList"
:default-date="apptDefaultDate"
@rescheduled="onAppointmentRescheduled"
></appointment-upcoming-drawer>
<d-top-message ref="topMessage"></d-top-message>
<d-tabbar></d-tabbar>
<!-- 简报弹层在 content 层级低于自定义 tabbar打开时隐藏 tabbar避免底部按钮被挡住 -->
<d-tabbar v-show="!briefPopupShow"></d-tabbar>
</view>
</template>
@@ -522,7 +523,7 @@ export default {
name: '已结束',
color: '#C4C7CC'
},
3: {
4: {
name: '已取消',
color: '#C4C7CC'
}
@@ -569,6 +570,10 @@ export default {
appointmentUpcoming: { total: 0, days: [] },
apptDrawerShow: false,
apptDefaultDate: '',
// AI 简报全文弹层是否打开(打开时隐藏自定义 tabbar避免抽屉被挡住
briefPopupShow: false,
// 系统配置总开关:默认不展示,看板接口确认开启后再挂载简报卡
aiBriefEnabled: false,
};
},
watch: {
@@ -594,6 +599,20 @@ export default {
const contentPx = typeof uni.upx2px === 'function' ? uni.upx2px(312) : 156;
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() {
this.initStatsRangeFromStorage();
@@ -641,10 +660,6 @@ export default {
},
refreshWorkbenchData() {
const silentStats = this.statsInitialized || this.hasStatsCache(this.statsRange);
// AI 简报随工作台一起刷新:组件内部按「门店+日期」去重,同店同天已拉过会直接跳过
if (this.$refs.aiBrief) {
this.$refs.aiBrief.fetchBrief(false);
}
return Promise.all([
this.getNotice(),
this.getInfo(),
@@ -652,7 +667,18 @@ export default {
this.getList(),
this.getAcceptingPatientList(),
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(() => {
this.notifyWorkbenchRefreshed();
});
@@ -855,6 +881,10 @@ export default {
if (Number.isNaN(num)) return '0.000'
return num.toFixed(3)
},
/** AI 简报弹层开合:打开时隐藏自定义 tabbar避免抽屉底部「知道了」被挡住 */
onBriefPopupChange(show) {
this.briefPopupShow = !!show;
},
/** 打开未来7天预约明细抽屉date 为空看全部,点某天卡格预筛选该天 */
openApptDrawer(date) {
this.apptDefaultDate = date || '';
@@ -889,6 +919,8 @@ export default {
days: payload.appointment_upcoming.days || [],
};
}
// 简报总开关随看板下发,关闭则卸掉卡片
this.aiBriefEnabled = payload.ai_daily_brief_enabled === true || payload.ai_daily_brief_enabled === 1;
this.statsInitialized = true;
}
}).catch(() => {
@@ -1040,7 +1072,7 @@ export default {
},
acceptingStatusLabel(item) {
const st = Number(item.status);
const m = { 1: '待接诊', 2: '接诊中', 3: '已结束' };
const m = { 1: '待接诊', 2: '接诊中', 3: '已结束', 4: '已取消', 7: '已拒诊' };
return m[st] || '接诊中';
},
acceptingStatusColor(item) {

View File

@@ -515,6 +515,8 @@ export function mapWarehouseRow(item, warehouseType = 'store') {
statusText: warehouseStatusText(item.status),
isOnShelf: item.status === 2,
pinyin: drug.pinyin || '-',
// 绑定配送仓:商家不可改价,列表/详情按钮置灰
priceLocked: Number(item.has_delivery_warehouse) === 1,
_raw: item,
}
}

View File

@@ -0,0 +1,62 @@
/**
* 列表/看板下拉刷新:自定义导航用 scroll-view refresher整页滚动用 onPullDownRefresh。
* 页面需实现 reload();请求结束务必调 finishRefresh(),否则转圈不收起。
* 最短转圈 400ms接口太快时微信 refresher 动画来不及展开,看起来像「下拉没反应」。
*/
const MIN_REFRESH_MS = 400
export default {
data() {
return {
isRefreshing: false,
}
},
methods: {
/**
* 统一走 reload();若返回 Promise 则在 finally 收起转圈
*/
runPullRefresh() {
this.isRefreshing = true
const started = Date.now()
const done = () => {
const wait = Math.max(0, MIN_REFRESH_MS - (Date.now() - started))
setTimeout(() => this.finishRefresh(), wait)
}
if (typeof this.reload !== 'function') {
done()
return
}
try {
const ret = this.reload()
if (ret && typeof ret.then === 'function') {
ret.finally(done)
} else {
this.$nextTick(done)
}
} catch (e) {
done()
}
},
/**
* scroll-view 下拉:先亮转圈再重拉当前列表
*/
onRefresherRefresh() {
this.runPullRefresh()
},
/**
* 整页原生下拉:重拉后必须 stop否则系统转圈卡住
*/
onPullDownRefresh() {
this.runPullRefresh()
},
/**
* 请求 finally 里关掉两种转圈
*/
finishRefresh() {
this.isRefreshing = false
try {
uni.stopPullDownRefresh()
} catch (e) { /* 未开启原生下拉时忽略 */ }
},
},
}

View File

@@ -1,11 +1,13 @@
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<!-- pageGuard show 拆开关抽屉先离场再卸避免原生滚动锁残留 -->
<page-container
v-if="usePageGuard && show"
v-if="usePageGuard && pageGuard"
:show="show"
:overlay="false"
@leave="onPageLeave"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<u-popup
@@ -46,13 +48,51 @@ export default {
height: { type: String, default: '' },
tall: { type: Boolean, default: false },
},
data() {
return {
pageGuard: false,
releaseTimer: null,
}
},
computed: {
show: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
},
watch: {
value: {
immediate: true,
handler(v) {
if (v) {
this.pageGuard = true
} 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
this.$emit('close')

View File

@@ -137,7 +137,7 @@
</view>
<view class="info">
<text>发药人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<image v-if="view.senderSignImage" :src="checkImageUrl(view.senderSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
@@ -147,7 +147,7 @@
</view>
<view class="info">
<text>调配人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<image v-if="view.dispenserSignImage" :src="checkImageUrl(view.dispenserSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
</view>
@@ -223,6 +223,8 @@ export default {
|| (w.data.pharmacist_info && w.data.pharmacist_info.identity && w.data.pharmacist_info.identity.sign_image)
|| (w.data.pharmacistInfo && w.data.pharmacistInfo.identity && w.data.pharmacistInfo.identity.sign_image)
|| ''
mapped.dispenserSignImage = w.data.dispenser_sign_image || ''
mapped.senderSignImage = w.data.sender_sign_image || ''
mapped.validHours = w.data.valid_hours || 72
mapped.status = w.data.status
mapped.patientMobile = w.data.patient ? w.data.patient.mobile : ''

View File

@@ -234,9 +234,10 @@ import {
} from '@/subPackages/sub_business_shared/common/filterCache.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [businessMixin],
mixins: [businessMixin, pullRefreshMixin],
components: { BusinessPageLayout, PageStickyHeader, CollapsibleFilterPanel },
data() {
return {

View File

@@ -47,8 +47,10 @@
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import { getWxUserList, pickProfileItems } from '@/api/userPatientProfile.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {

View File

@@ -39,8 +39,10 @@
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import { getPatientListByUser, pickProfileItems } from '@/api/userPatientProfile.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {

View File

@@ -43,8 +43,13 @@
<!-- 底部操作按钮组 -->
<view v-if="info.id" class="action-group">
<view class="btn-large btn-outline" hover-class="btn-hover-opacity" @click="openPriceEdit">
{{ isPlatformWarehouse ? '修改价格' : '修改售价' }}
<view
class="btn-large btn-outline"
:class="{ 'btn-disabled': priceLocked }"
hover-class="btn-hover-opacity"
@click="openPriceEdit"
>
{{ priceLocked ? '不可改价' : (isPlatformWarehouse ? '修改价格' : '修改售价') }}
</view>
<view
class="btn-large"
@@ -91,6 +96,13 @@ export default {
isPlatformWarehouse() {
return this.bizCap.warehouseType === 'platform'
},
// 门店仓详情带 has_delivery_warehouse绑定配送仓则改价置灰
priceLocked() {
if (this.isPlatformWarehouse) {
return false
}
return Number(this.info.has_delivery_warehouse) === 1
},
},
onLoad(q) {
this.id = Number(q.id || 0)
@@ -102,7 +114,14 @@ export default {
if (w.ok) this.info = w.data || {}
})
},
/**
* 打开改价;锁定商品只提示,不弹窗
*/
openPriceEdit() {
if (this.priceLocked) {
uni.showToast({ title: '该商品不可改价', icon: 'none' })
return
}
this.priceModalVisible = true
},
closePriceEdit() {
@@ -314,6 +333,13 @@ $danger-color-light: rgba(255, 107, 107, 0.1);
border: 2rpx solid $theme-color;
}
.btn-large.btn-disabled {
color: $text-light;
background: $bg-color;
border-color: $border-color;
box-shadow: none;
}
.btn-hover-opacity {
opacity: 0.7;
}

View File

@@ -37,8 +37,13 @@
<!-- 底部操作按钮横向排列增加点击区域与反馈 -->
<view class="action-row" @click.stop>
<view class="action-btn outline" hover-class="btn-hover-opacity" @click.stop="openPriceEdit(item)">
改价
<view
class="action-btn outline"
:class="{ 'btn-disabled': item._priceLocked }"
hover-class="btn-hover-opacity"
@click.stop="openPriceEdit(item)"
>
{{ item._priceLocked ? '不可改价' : '改价' }}
</view>
<view class="action-btn" :class="rowDisplay(item).isOnShelf ? 'btn-danger' : 'btn-primary'" hover-class="btn-hover-opacity" @click.stop="toggleStatus(item)">
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
@@ -70,9 +75,10 @@ import { withWxKey } from '@/utils/wxListKey.js'
import { mapWarehouseRow } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [businessMixin],
mixins: [businessMixin, pullRefreshMixin],
components: { BusinessPageLayout, PriceEditModal },
data() {
return {
@@ -116,6 +122,11 @@ export default {
}
this.bizApi.getWarehouseList(params).then(w => {
const items = withWxKey((w.data && w.data.items) || [], 'id', 'wh')
// 门店仓绑定配送仓不可改价;平台仓不受此限
const lockPrice = this.bizCap.warehouseType !== 'platform'
items.forEach((it) => {
it._priceLocked = lockPrice && Number(it.has_delivery_warehouse) === 1
})
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
@@ -123,7 +134,14 @@ export default {
goDetail(item) {
uni.navigateTo({ url: `${this.bizRoot}/warehouse/detail?id=${item.id}` })
},
/**
* 打开改价;门店仓绑定配送仓的商品不可改价
*/
openPriceEdit(item) {
if (item && item._priceLocked) {
uni.showToast({ title: '该商品不可改价', icon: 'none' })
return
}
this.priceEditItem = item
this.priceModalVisible = true
},
@@ -327,6 +345,12 @@ $danger-color-light: rgba(255, 107, 107, 0.1);
border: 1rpx solid $border-color;
}
.action-btn.btn-disabled {
color: $text-light;
background: $bg-color;
border-color: $border-color;
}
.action-btn.btn-primary {
color: #ffffff;
background: $theme-color;

View File

@@ -45,9 +45,10 @@
<script>
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [businessMixin],
mixins: [businessMixin, pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {

View File

@@ -80,9 +80,10 @@ import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
import { withdrawalCheckStatusText, formatDateTime } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [businessMixin],
mixins: [businessMixin, pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {
@@ -173,6 +174,14 @@ export default {
},
// 刷新提现列表
/**
* 下拉刷新:卡、余额、列表一起重拉
*/
reload() {
this.loadCard()
this.loadBalance()
this.reloadList()
},
reloadList() {
this.page = 1;
this.loadList(1, false);

View File

@@ -74,9 +74,10 @@
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import { mapSettlementRow } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [businessMixin],
mixins: [businessMixin, pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {

View File

@@ -514,6 +514,8 @@ export function mapWarehouseRow(item, warehouseType = 'store') {
statusText: warehouseStatusText(item.status),
isOnShelf: item.status === 2,
pinyin: drug.pinyin || '-',
// 绑定配送仓:商家不可改价,列表/详情按钮置灰
priceLocked: Number(item.has_delivery_warehouse) === 1,
_raw: item,
}
}

View File

@@ -9,6 +9,7 @@ import {
const KEY_ORDER = 'clinic_admin.order.filter'
const KEY_RECONCILIATION = 'clinic_admin.reconciliation.filter'
const KEY_WAREHOUSE_TAB = 'clinic_admin.warehouse.tab'
function readJson(key) {
try {
@@ -119,3 +120,21 @@ export function clearReconciliationFilter() {
uni.removeStorageSync(KEY_RECONCILIATION)
} catch (e) { /* ignore */ }
}
/**
* 读取上次选中的仓库分类 type。
* 存 type 不存下标,接口增减 Tab 后不会对错位0 / 空表示商城仓(列表不按 type 过滤)。
*/
export function loadWarehouseTabType() {
const cached = readJson(KEY_WAREHOUSE_TAB)
if (!cached || typeof cached !== 'object') return ''
const n = Number(cached.type)
if (!n || Number.isNaN(n) || n <= 0) return ''
return n
}
/** 记住当前仓库 Tab 的 type下次进页优先还原 */
export function saveWarehouseTabType(type) {
const n = type === '' || type == null ? 0 : Number(type)
writeJson(KEY_WAREHOUSE_TAB, { type: Number.isNaN(n) ? 0 : n })
}

View File

@@ -15,6 +15,7 @@ import {
getClinicAdminWorkbenchEntries,
getClinicAdminEntryUserData,
toggleClinicAdminFavoriteEntry,
batchSaveClinicAdminFavoriteEntry,
reportClinicAdminEntryUsage,
} from '@/api/clinicAdmin.js'
@@ -27,12 +28,13 @@ export default {
getEntries: getClinicAdminWorkbenchEntries,
getUserData: getClinicAdminEntryUserData,
toggleFavorite: toggleClinicAdminFavoriteEntry,
batchSaveFavorites: batchSaveClinicAdminFavoriteEntry,
reportUsage: reportClinicAdminEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在
// 返回时刷新收藏状态(长按菜单用;常用/最近只在首页展示
if (this.$refs.features) this.$refs.features.refreshUserData()
},
}

View File

@@ -88,8 +88,10 @@
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
:api="favEditorApi"
@item-click="onEntryTap"
@more="goFeatures"
@saved="loadUserEntries"
></workbench-user-entries>
</view>
@@ -106,12 +108,14 @@
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import { getClinicAdminStoreQr, getClinicAdminDashboardStats, getClinicAdminEntryUserData, reportClinicAdminEntryUsage } from '@/api/clinicAdmin.js'
import { getClinicAdminStoreQr, getClinicAdminDashboardStats, getClinicAdminWorkbenchEntries, getClinicAdminEntryUserData, batchSaveClinicAdminFavoriteEntry, reportClinicAdminEntryUsage } from '@/api/clinicAdmin.js'
import { openQrCodePreview, buildClinicQrPayload } from '@/utils/qrCodePreview.js'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout },
data() {
return {
@@ -125,6 +129,12 @@ export default {
recentRaw: [],
/** 卡片形态偏好true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
/** 首页快捷添加抽屉接口(与功能页同一套) */
favEditorApi: {
getEntries: getClinicAdminWorkbenchEntries,
getUserData: getClinicAdminEntryUserData,
batchSaveFavorites: batchSaveClinicAdminFavoriteEntry,
},
/** 经营看板数据(接口返回前展示 0避免布局跳动 */
dashboard: {
today_order_count: 0,
@@ -168,6 +178,16 @@ export default {
this.loadStoreQr()
},
methods: {
/**
* 下拉刷新工作台:看板、入口、门店码一起重拉
*/
reload() {
return Promise.all([
this.loadDashboard(),
this.loadUserEntries(),
Promise.resolve(this.loadStoreQr()),
])
},
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底,功能不可达时去功能 tab */
async loadUserEntries() {
try {

View File

@@ -13,6 +13,7 @@
<template #after-profile>
<vip-member-card :vip="vipInfo" />
</template>
</admin-my-panel>
</clinic-page-layout>
</template>
@@ -26,8 +27,10 @@ import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-p
import VipMemberCard from '@/subPackages/sub_clinic_admin/components/vip-member-card.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getClinicAdminMyInfo } from '@/api/clinicAdmin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout, VipMemberCard, AdminMyPanel },
data() {
return {
@@ -43,6 +46,16 @@ export default {
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
methods: {
/**
* 下拉刷新:重拉 VIP/资料 + 微信绑定
*/
reload() {
this.loadLocalUser()
const wx = this.$refs.myPanel && this.$refs.myPanel.refreshWxBind
? this.$refs.myPanel.refreshWxBind()
: Promise.resolve()
return Promise.all([this.refreshVip(), Promise.resolve(wx)])
},
/** 先读本地缓存,保证首屏有基础信息(个人中心改完昵称/头像返回时也走这里立即生效) */
loadLocalUser() {
const user = uni.getStorageSync('clinic_admin_user') || {}

View File

@@ -129,7 +129,7 @@
</view>
<view class="info">
<text>发药人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<image v-if="view.senderSignImage" :src="checkImageUrl(view.senderSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
<view class="info">
@@ -139,7 +139,7 @@
</view>
<view class="info">
<text>调配人</text>
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
<image v-if="view.dispenserSignImage" :src="checkImageUrl(view.dispenserSignImage)" mode="aspectFit"></image>
<text v-else class="name"></text>
</view>
</view>
@@ -212,6 +212,8 @@ export default {
|| (w.data.pharmacist_info && w.data.pharmacist_info.identity && w.data.pharmacist_info.identity.sign_image)
|| (w.data.pharmacistInfo && w.data.pharmacistInfo.identity && w.data.pharmacistInfo.identity.sign_image)
|| ''
mapped.dispenserSignImage = w.data.dispenser_sign_image || ''
mapped.senderSignImage = w.data.sender_sign_image || ''
mapped.validHours = w.data.valid_hours || 72
mapped.status = w.data.status
mapped.patientMobile = w.data.patient ? w.data.patient.mobile : ''

View File

@@ -238,8 +238,10 @@ import {
clearReconciliationFilter,
defaultReconciliationFilter,
} from '@/subPackages/sub_clinic_admin/common/filterCache.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel },
data() {
return {

View File

@@ -43,8 +43,13 @@
<!-- 底部操作按钮组 -->
<view v-if="info.id" class="action-group">
<view class="btn-large btn-outline" hover-class="btn-hover-opacity" @click="openPriceEdit">
修改售价
<view
class="btn-large btn-outline"
:class="{ 'btn-disabled': priceLocked }"
hover-class="btn-hover-opacity"
@click="openPriceEdit"
>
{{ priceLocked ? '不可改价' : '修改售价' }}
</view>
<view
class="btn-large"
@@ -90,6 +95,10 @@ export default {
display() {
return mapWarehouseRow(this.info)
},
// 详情接口补了 has_delivery_warehouse绑定配送仓则改价置灰
priceLocked() {
return Number(this.info.has_delivery_warehouse) === 1
},
},
onLoad(q) {
this.id = Number(q.id || 0)
@@ -101,7 +110,14 @@ export default {
if (w.ok) this.info = w.data || {}
})
},
/**
* 打开改价;锁定商品只提示,不弹窗
*/
openPriceEdit() {
if (this.priceLocked) {
uni.showToast({ title: '该商品不可改价', icon: 'none' })
return
}
this.priceModalVisible = true
},
closePriceEdit() {
@@ -310,6 +326,13 @@ $danger-color-light: rgba(255, 107, 107, 0.1);
border: 2rpx solid $theme-color;
}
.btn-large.btn-disabled {
color: $text-light;
background: $bg-color;
border-color: $border-color;
box-shadow: none;
}
.btn-hover-opacity {
opacity: 0.7;
}

View File

@@ -1,57 +1,88 @@
<template>
<clinic-page-layout :title="pageTitle" :show-back="true">
<!-- 搜索栏优化胶囊式设计增加视觉呼吸感 -->
<view class="search-wrapper">
<view class="search-bar">
<input class="search-input" v-model="keyword" placeholder="搜索药品名称" placeholder-class="search-placeholder" @confirm="reload" />
<view class="search-btn" hover-class="btn-hover" @click="reload">搜索</view>
</view>
</view>
<view class="list-container">
<view
v-for="item in list"
:key="item._wxKey"
class="item-card"
hover-class="card-hover"
@click="goDetail(item)"
>
<!-- 左侧缩略图 -->
<image class="thumb" :src="rowDisplay(item).imageUrl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg'" mode="aspectFill" />
<!-- 右侧内容区 -->
<view class="content">
<!-- 顶部标题与状态标签对齐 -->
<view class="header-row">
<text class="title">{{ rowDisplay(item).drugName }}</text>
<view class="status-tag" :class="rowDisplay(item).isOnShelf ? 'tag-on' : 'tag-off'">
{{ rowDisplay(item).statusText }}
</view>
</view>
<!-- 中部详细信息 -->
<view class="info-group">
<text class="info-text">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</text>
<text class="info-text">库存 {{ rowDisplay(item).stock }}</text>
</view>
<!-- 底部操作按钮横向排列增加点击区域与反馈 -->
<view class="action-row" @click.stop>
<view class="action-btn outline" hover-class="btn-hover-opacity" @click.stop="openPriceEdit(item)">
改价
</view>
<view class="action-btn" :class="rowDisplay(item).isOnShelf ? 'btn-danger' : 'btn-primary'" hover-class="btn-hover-opacity" @click.stop="toggleStatus(item)">
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
</view>
</view>
<view
class="page-flex"
:style="{ height: 'calc(100vh - ' + navTopOffset + 'px - 32rpx)' }"
>
<view class="search-wrapper rx-flex-fixed">
<view v-if="tabList.length" class="tabs-wrap">
<u-tabs
:list="tabList"
:is-scroll="true"
:current="tabIndex"
active-color="#6ACDBB"
inactive-color="#86909C"
bg-color="#ffffff"
:bold="true"
@change="onTabChange"
/>
</view>
<view class="search-bar">
<input class="search-input" v-model="keyword" placeholder="搜索药品名称" placeholder-class="search-placeholder" @confirm="reload" />
<view class="search-btn" hover-class="btn-hover" @click="reload">搜索</view>
</view>
</view>
</view>
<!-- 空状态优化 -->
<view v-if="!loading && !list.length" class="empty-state">
<view class="empty-icon"></view>
<text class="empty-text">暂无药品数据</text>
<swiper
class="wh-swiper"
:current="tabIndex"
:disable-touch="warehouseTabs.length < 2"
@change="onSwiperChange"
>
<swiper-item v-for="(tab, idx) in warehouseTabs" :key="idx">
<scroll-view
scroll-y
class="wh-scroll"
refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresherRefresh"
@scrolltolower="onScrollToLower"
>
<block v-if="tabIndex == idx">
<view class="list-container">
<view
v-for="item in list"
:key="item._wxKey"
class="item-card"
hover-class="card-hover"
@click="goDetail(item)"
>
<image class="thumb" :src="rowDisplay(item).imageUrl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg'" mode="aspectFill" />
<view class="content">
<view class="header-row">
<text class="title">{{ rowDisplay(item).drugName }}</text>
<view class="status-tag" :class="rowDisplay(item).isOnShelf ? 'tag-on' : 'tag-off'">
{{ rowDisplay(item).statusText }}
</view>
</view>
<view class="info-group">
<text class="info-text">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</text>
<text class="info-text">库存 {{ rowDisplay(item).stock }}</text>
</view>
<view class="action-row" @click.stop>
<view
class="action-btn outline"
:class="{ 'btn-disabled': item.has_delivery_warehouse == 1 }"
hover-class="btn-hover-opacity"
@click.stop="openPriceEdit(item)"
>
{{ item.has_delivery_warehouse == 1 ? '不可改价' : '改价' }}
</view>
<view class="action-btn" :class="rowDisplay(item).isOnShelf ? 'btn-danger' : 'btn-primary'" hover-class="btn-hover-opacity" @click.stop="toggleStatus(item)">
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
</view>
</view>
</view>
</view>
</view>
<view v-if="!loading && !list.length" class="empty-state">
<view class="empty-icon"></view>
<text class="empty-text">暂无药品数据</text>
</view>
</block>
</scroll-view>
</swiper-item>
</swiper>
</view>
<price-edit-modal
@@ -68,11 +99,16 @@ import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-p
import PriceEditModal from './components/PriceEditModal.vue'
import {
getWarehouseList,
getWarehouseTabs,
updateWarehouseStatus,
updateWarehousePrice,
} from '@/api/clinicAdmin.js'
import { withWxKey } from '@/utils/wxListKey.js'
import { mapWarehouseRow } from '@/subPackages/sub_clinic_admin/common/display.js'
import {
loadWarehouseTabType,
saveWarehouseTabType,
} from '@/subPackages/sub_clinic_admin/common/filterCache.js'
export default {
components: { ClinicPageLayout, PriceEditModal },
@@ -80,9 +116,16 @@ export default {
return {
pageTitle: '仓库管理',
warehouseType: '',
pendingType: '',
warehouseTabs: [],
tabIndex: 0,
tabChanging: false,
navTopOffset: 0,
keyword: '',
list: [],
page: 1,
hasMore: true,
isRefreshing: false,
loading: false,
priceModalVisible: false,
priceEditItem: null,
@@ -90,23 +133,118 @@ export default {
}
},
onLoad(q) {
this.navTopOffset = (this.$statusBarHeight || 0) + (this.$navbarHeight || 44)
if (q.label) {
this.pageTitle = decodeURIComponent(q.label)
}
// 深链 type 优先于本地记忆
if (q.type != null && q.type !== '') {
this.warehouseType = q.type
this.pendingType = q.type
} else {
this.pendingType = loadWarehouseTabType()
}
this.reload()
this.loadTabs().then(() => {
this.reload()
})
},
onReachBottom() {
this.load(this.page + 1, true)
computed: {
// u-tabs 只要 nametype 存在 warehouseTabs 里
tabList() {
return (this.warehouseTabs || []).map((t) => ({ name: t.name }))
},
},
methods: {
rowDisplay(item) {
return mapWarehouseRow(item)
},
/**
* 仓库 Tab 只信接口,前端不写死分类
*/
loadTabs() {
return getWarehouseTabs().then((w) => {
const rows = (w.ok && Array.isArray(w.data)) ? w.data : []
this.warehouseTabs = rows.map((row) => ({
name: row.label || row.name || '',
type: row.value != null && Number(row.value) > 0 ? Number(row.value) : '',
})).filter((t) => t.name)
this.applyPendingType()
}).catch(() => {
this.warehouseTabs = []
this.applyPendingType()
})
},
/**
* 深链 / 记忆 type 对上 Tab对不上落到第一项
*/
applyPendingType() {
const tabs = this.warehouseTabs || []
if (!tabs.length) {
this.warehouseType = ''
this.tabIndex = 0
return
}
let idx = 0
if (this.pendingType !== '' && this.pendingType != null) {
const want = Number(this.pendingType)
const hit = tabs.findIndex((t) => Number(t.type) === want)
if (hit >= 0) idx = hit
} else if (this.pendingType === '' || this.pendingType === 0) {
const shopHit = tabs.findIndex((t) => t.type === '' || t.type === 0)
if (shopHit >= 0) idx = shopHit
}
this.applyTabIndex(idx, false)
},
/**
* 统一切 Tab改下标、记本地、按需刷新列表
*/
applyTabIndex(idx, reloadList) {
const tabs = this.warehouseTabs || []
const safeIdx = idx >= 0 && idx < tabs.length ? idx : 0
this.tabIndex = safeIdx
const cur = tabs[safeIdx]
this.warehouseType = cur && cur.type !== '' && cur.type != null ? cur.type : ''
saveWarehouseTabType(this.warehouseType)
if (reloadList) {
this.reload()
}
},
onTabChange(index) {
if (this.tabChanging) return
const idx = typeof index === 'object' ? Number(index.index != null ? index.index : 0) : Number(index)
if (Number.isNaN(idx) || idx === this.tabIndex) return
this.tabChanging = true
this.applyTabIndex(idx, true)
this.$nextTick(() => {
this.tabChanging = false
})
},
/**
* 左右滑切 Tab与顶部 u-tabs 双向同步
*/
onSwiperChange(e) {
if (this.tabChanging) return
const idx = Number(e.detail && e.detail.current)
if (Number.isNaN(idx) || idx === this.tabIndex) return
this.tabChanging = true
this.applyTabIndex(idx, true)
this.$nextTick(() => {
this.tabChanging = false
})
},
/**
* 下拉刷新当前 Tab 列表isRefreshing 必须在请求结束后关掉,否则转圈不收起
*/
onRefresherRefresh() {
this.isRefreshing = true
this.reload()
},
onScrollToLower() {
if (this.loading || !this.hasMore) return
this.load(this.page + 1, true)
},
reload() {
this.page = 1
this.hasMore = true
this.load(1, false)
},
load(page, append) {
@@ -120,12 +258,23 @@ export default {
const items = withWxKey((w.data && w.data.items) || [], 'id', 'wh')
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
this.hasMore = items.length >= 20
}).finally(() => {
this.loading = false
if (this.isRefreshing) this.isRefreshing = false
})
},
goDetail(item) {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/warehouse/detail?id=' + item.id })
},
/**
* 打开改价;绑定配送仓的商品不可改价,只提示不弹窗
*/
openPriceEdit(item) {
if (item && Number(item.has_delivery_warehouse) === 1) {
uni.showToast({ title: '该商品不可改价', icon: 'none' })
return
}
this.priceEditItem = item
this.priceModalVisible = true
},
@@ -176,18 +325,42 @@ $border-color: #ebedf0;
$danger-color: #ff6b6b;
$danger-color-light: rgba(255, 107, 107, 0.1);
.page-flex {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
box-sizing: border-box;
}
.rx-flex-fixed {
flex-shrink: 0;
}
.wh-swiper {
flex: 1;
height: 0;
min-height: 0;
width: 100%;
}
.wh-scroll {
height: 100%;
box-sizing: border-box;
}
.search-wrapper {
padding: 16rpx 24rpx;
background-color: transparent;
position: sticky;
top: 0;
z-index: 10;
padding: 0 0 16rpx;
background-color: #ffffff;
}
.tabs-wrap {
background: #ffffff;
border-bottom: 1rpx solid $border-color;
}
.search-bar {
display: flex;
align-items: center;
background: #ffffff;
margin: 16rpx 24rpx 0;
padding: 12rpx 16rpx 12rpx 32rpx;
border-radius: 40rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
@@ -216,7 +389,7 @@ $danger-color-light: rgba(255, 107, 107, 0.1);
}
.list-container {
padding: 0 24rpx 24rpx;
padding: 16rpx 24rpx 24rpx;
}
.item-card {
@@ -326,6 +499,12 @@ $danger-color-light: rgba(255, 107, 107, 0.1);
border: 1rpx solid $border-color;
}
.action-btn.btn-disabled {
color: $text-light;
background: $bg-color;
border-color: $border-color;
}
.action-btn.btn-primary {
color: #ffffff;
background: $theme-color;

View File

@@ -45,8 +45,10 @@
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import { getAccountChangeLogList } from '@/api/clinicAdmin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout },
data() {
return {

View File

@@ -79,8 +79,10 @@ import { getClinicAdminCard, getClinicAdminBalance, getWithdrawalList } from '@/
import { withWxKey } from '@/utils/wxListKey.js'
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
import { withdrawalCheckStatusText, formatDateTime } from '@/subPackages/sub_clinic_admin/common/display.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout },
data() {
return {
@@ -168,6 +170,14 @@ export default {
});
},
/**
* 下拉刷新:卡、余额、列表一起重拉
*/
reload() {
this.loadCard()
this.loadBalance()
this.reloadList()
},
// 刷新提现列表
reloadList() {
this.page = 1;

View File

@@ -74,8 +74,10 @@
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import { mapSettlementRow } from '@/subPackages/sub_clinic_admin/common/display.js'
import { getLedgerLogList } from '@/api/clinicAdmin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout },
data() {
return {

View File

@@ -20,8 +20,10 @@
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonCommissionList } from '@/api/clinicSalesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicSalespersonPageLayout },
data() {
return { list: [] }
@@ -30,6 +32,9 @@ export default {
this.loadList()
},
methods: {
reload() {
return this.loadList()
},
async loadList() {
const wrap = await getClinicSalespersonCommissionList({ page: 1, pageSize: 50 })
if (wrap && wrap.ok) this.list = wrap.data?.items || []

View File

@@ -15,6 +15,7 @@ import {
getClinicSalespersonWorkbenchEntries,
getClinicSalespersonEntryUserData,
toggleClinicSalespersonFavoriteEntry,
batchSaveClinicSalespersonFavoriteEntry,
reportClinicSalespersonEntryUsage,
} from '@/api/clinicSalesperson.js'
@@ -27,12 +28,13 @@ export default {
getEntries: getClinicSalespersonWorkbenchEntries,
getUserData: getClinicSalespersonEntryUserData,
toggleFavorite: toggleClinicSalespersonFavoriteEntry,
batchSaveFavorites: batchSaveClinicSalespersonFavoriteEntry,
reportUsage: reportClinicSalespersonEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在
// 返回时刷新收藏状态(长按菜单用;常用/最近只在首页展示
if (this.$refs.features) this.$refs.features.refreshUserData()
},
methods: {

View File

@@ -81,8 +81,10 @@
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
:api="favEditorApi"
@item-click="onEntryTap"
@more="goFeatures"
@saved="loadUserEntries"
></workbench-user-entries>
</view>
@@ -101,12 +103,14 @@
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import TransferModeModal from '@/subPackages/sub_clinic_salesperson/components/TransferModeModal.vue'
import { getClinicSalespersonMyInfo, createClinicSalespersonTransferPrescription, getClinicSalespersonEntryUserData, reportClinicSalespersonEntryUsage } from '@/api/clinicSalesperson.js'
import { getClinicSalespersonMyInfo, createClinicSalespersonTransferPrescription, getClinicSalespersonWorkbenchEntries, getClinicSalespersonEntryUserData, batchSaveClinicSalespersonFavoriteEntry, reportClinicSalespersonEntryUsage } from '@/api/clinicSalesperson.js'
import { openQrCodePreview, buildSalespersonQrPayload } from '@/utils/qrCodePreview.js'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
// transfer-patient-drawer 跨分包引用,在 pages.json 通过 usingComponents + componentPlaceholder 注册
components: { ClinicSalespersonPageLayout, TransferModeModal },
data() {
@@ -124,6 +128,12 @@ export default {
recentRaw: [],
/** 卡片形态偏好true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
/** 首页快捷添加抽屉接口(与功能页同一套) */
favEditorApi: {
getEntries: getClinicSalespersonWorkbenchEntries,
getUserData: getClinicSalespersonEntryUserData,
batchSaveFavorites: batchSaveClinicSalespersonFavoriteEntry,
},
}
},
computed: {
@@ -149,6 +159,12 @@ export default {
this.loadUserEntries()
},
methods: {
/**
* 下拉刷新工作台:资料与入口一起重拉
*/
reload() {
return Promise.all([this.loadInfo(), this.loadUserEntries()])
},
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底) */
async loadUserEntries() {
try {

View File

@@ -36,8 +36,10 @@
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonCommissionList } from '@/api/clinicSalesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicSalespersonPageLayout },
data() {
return {
@@ -70,6 +72,9 @@ export default {
this.loadList()
},
methods: {
reload() {
return this.loadList()
},
async loadList() {
if (this.sourceSalespersonId <= 0) return
this.loading = true

View File

@@ -18,8 +18,10 @@
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonUserBindList } from '@/api/clinicSalesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicSalespersonPageLayout },
data() {
return { list: [] }
@@ -28,6 +30,9 @@ export default {
this.loadList()
},
methods: {
reload() {
return this.loadList()
},
async loadList() {
const wrap = await getClinicSalespersonUserBindList({ page: 1, pageSize: 50 })
if (wrap && wrap.ok) this.list = wrap.data?.items || []

View File

@@ -21,8 +21,10 @@
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicSalespersonPageLayout, AdminMyPanel },
data() {
return {
@@ -40,6 +42,16 @@ export default {
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
methods: {
/**
* 下拉刷新:重拉资料 + 微信绑定
*/
reload() {
this.loadLocalUser()
const wx = this.$refs.myPanel && this.$refs.myPanel.refreshWxBind
? this.$refs.myPanel.refreshWxBind()
: Promise.resolve()
return Promise.all([this.loadProfile(), Promise.resolve(wx)])
},
/** 读本地缓存兜底首屏my-info 缓存结构:{ salesperson, store } */
loadLocalUser() {
const info = uni.getStorageSync('clinic_salesperson_user') || {}

View File

@@ -16,8 +16,10 @@
<script>
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import { getClinicSalespersonSettlementList, getClinicSalespersonSettlementDetail } from '@/api/clinicSalesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicSalespersonPageLayout },
data() {
return { list: [] }
@@ -26,6 +28,9 @@ export default {
this.loadList()
},
methods: {
reload() {
return this.loadList()
},
async loadList() {
const wrap = await getClinicSalespersonSettlementList({ page: 1, pageSize: 50 })
if (wrap && wrap.ok) this.list = wrap.data?.items || []

View File

@@ -35,8 +35,10 @@
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
import TransferDetailDrawer from '@/subPackages/sub_clinic_salesperson/components/TransferDetailDrawer.vue'
import { getClinicSalespersonTransferPrescriptionList } from '@/api/clinicSalesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicSalespersonPageLayout, TransferDetailDrawer },
data() {
return { list: [], loading: false }
@@ -45,6 +47,9 @@ export default {
this.loadList()
},
methods: {
reload() {
return this.loadList()
},
async loadList() {
this.loading = true
try {

View File

@@ -187,7 +187,7 @@
</view>
<view class="info">
<text>发药人</text>
<image :src="checkImageUrl(lists.Pharmacist_sign_image)" mode="aspectFit"></image>
<image v-if="lists.sender_sign_image" :src="checkImageUrl(lists.sender_sign_image)" mode="aspectFit"></image>
</view>
<view class="info">
<text>核对人</text>
@@ -195,7 +195,7 @@
</view>
<view class="info">
<text>调配人</text>
<image :src="checkImageUrl(lists.Pharmacist_sign_image)" mode="aspectFit"></image>
<image v-if="lists.dispenser_sign_image" :src="checkImageUrl(lists.dispenser_sign_image)" mode="aspectFit"></image>
</view>
</view>

View File

@@ -525,7 +525,7 @@ export default {
return map[status] || '状态未知';
},
getRegisterStatusText(status) {
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
const map = { 0: '待支付', 1: '待接诊', 2: '接诊中', 3: '已结束', 4: '已取消', 5: '待评价', 6: '已评价', 7: '已拒诊' };
return map[status] || '状态未知';
}
}

View File

@@ -82,7 +82,7 @@ export default {
},
methods: {
registerStatusText(status) {
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
const map = { 0: '待支付', 1: '待接诊', 2: '接诊中', 3: '已结束', 4: '已取消', 5: '待评价', 6: '已评价', 7: '已拒诊' };
const n = Number(status);
return map[n] != null ? map[n] : String(status);
}

View File

@@ -903,7 +903,7 @@ export default {
}
},
registerOrderStatusText(status) {
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
const map = { 0: '待支付', 1: '待接诊', 2: '接诊中', 3: '已结束', 4: '已取消', 5: '待评价', 6: '已评价', 7: '已拒诊' };
const n = Number(status);
return map[n] != null ? map[n] : String(status);
},

View File

@@ -118,8 +118,10 @@
import { getOnlineConsultationPatientListApi, getMyStoreListApi, switchStoreApi } from '@/api/reception.js';
import { normalizeRoomId } from '@/utils/chat/chatRoomManager.js';
import { formatSessionRelativeTime } from '@/utils/chat/sessionListFormat.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default {
mixins: [pullRefreshMixin],
data() {
return {
patientList: [],
@@ -151,6 +153,9 @@ export default {
}
},
methods: {
reload() {
return this.bootstrap()
},
applyDoctorImRoomUpdate(payload) {
const rid = normalizeRoomId(payload && payload.roomId);
if (!rid) return;
@@ -297,7 +302,7 @@ export default {
return row.user_patient?.mobile || row.user?.mobile || row.mobile || '';
},
statusText(st) {
const m = { 1: '待接诊', 2: '接诊中', 3: '已结束' };
const m = { 1: '待接诊', 2: '接诊中', 3: '已结束', 4: '已取消', 7: '已拒诊' };
return m[st] || '其他';
},
goChat(row) {

View File

@@ -187,7 +187,7 @@
</view>
<view class="info">
<text>发药人</text>
<image :src="checkImageUrl(lists.Pharmacist_sign_image)" mode="aspectFit"></image>
<image v-if="lists.sender_sign_image" :src="checkImageUrl(lists.sender_sign_image)" mode="aspectFit"></image>
</view>
<view class="info">
<text>核对人</text>
@@ -195,7 +195,7 @@
</view>
<view class="info">
<text>调配人</text>
<image :src="checkImageUrl(lists.Pharmacist_sign_image)" mode="aspectFit"></image>
<image v-if="lists.dispenser_sign_image" :src="checkImageUrl(lists.dispenser_sign_image)" mode="aspectFit"></image>
</view>
</view>

View File

@@ -10,9 +10,17 @@
<script>
import InputListPage from '@/subPackages/sub_platform_admin/input/InputListPage.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { InputListPage },
provide() { return { businessContext: getPlatformAdminContext() } },
methods: {
reload() {
const p = this.$refs.listPage
if (p && typeof p.reload === 'function') return p.reload()
},
},
onShow() {
this.$nextTick(() => this.$refs.listPage?.reload?.())
},

View File

@@ -15,6 +15,7 @@ import {
getPlatformAdminWorkbenchEntries,
getPlatformAdminEntryUserData,
togglePlatformAdminFavoriteEntry,
batchSavePlatformAdminFavoriteEntry,
reportPlatformAdminEntryUsage,
} from '@/api/platformAdmin.js'
@@ -27,12 +28,13 @@ export default {
getEntries: getPlatformAdminWorkbenchEntries,
getUserData: getPlatformAdminEntryUserData,
toggleFavorite: togglePlatformAdminFavoriteEntry,
batchSaveFavorites: batchSavePlatformAdminFavoriteEntry,
reportUsage: reportPlatformAdminEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在
// 返回时刷新收藏状态(长按菜单用;常用/最近只在首页展示
if (this.$refs.features) this.$refs.features.refreshUserData()
},
}

View File

@@ -71,8 +71,10 @@
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
:api="favEditorApi"
@item-click="onEntryTap"
@more="goFeatures"
@saved="loadUserEntries"
></workbench-user-entries>
</view>
@@ -91,11 +93,13 @@
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import { navigateToInputForm } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
import { getPlatformAdminDashboardStats, getPlatformAdminEntryUserData, reportPlatformAdminEntryUsage } from '@/api/platformAdmin.js'
import { getPlatformAdminDashboardStats, getPlatformAdminWorkbenchEntries, getPlatformAdminEntryUserData, batchSavePlatformAdminFavoriteEntry, reportPlatformAdminEntryUsage } from '@/api/platformAdmin.js'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout },
provide() { return { businessContext: getPlatformAdminContext() } },
data() {
@@ -122,6 +126,12 @@ export default {
recentRaw: [],
/** 卡片形态偏好true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
/** 首页快捷添加抽屉接口(与功能页同一套) */
favEditorApi: {
getEntries: getPlatformAdminWorkbenchEntries,
getUserData: getPlatformAdminEntryUserData,
batchSaveFavorites: batchSavePlatformAdminFavoriteEntry,
},
}
},
computed: {
@@ -143,6 +153,12 @@ export default {
this.loadUserEntries()
},
methods: {
/**
* 下拉刷新工作台:看板与入口一起重拉
*/
reload() {
return Promise.all([this.loadDashboard(), this.loadUserEntries()])
},
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底) */
async loadUserEntries() {
try {

View File

@@ -56,11 +56,11 @@ export default {
if (data && Array.isArray(data.data)) return data.data
return []
},
reload() { this.page = 1; this.load(1, false) },
reload() { this.page = 1; return this.load(1, false) },
load(page, append) {
const params = { page, pageSize: 15 }
if (this.auditMode) params.status = 0
this.listApi(params)
return this.listApi(params)
.then(w => {
const items = this.parseListItems(w.data)
if (!items.length && !w.ok) {

View File

@@ -19,20 +19,43 @@
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import { getPlatformAdminMyInfo } from '@/api/platformAdmin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout, AdminMyPanel },
provide() { return { businessContext: getPlatformAdminContext() } },
data() {
return { displayName: '', avatar: '', roleText: '平台管理员' }
},
onShow() {
// 个人中心改完昵称/头像返回时重读缓存立即生效
const user = uni.getStorageSync('platform_admin_user') || {}
this.displayName = user.nick_name || '管理员'
this.avatar = user.avatar || ''
this.roleText = (user.roles && user.roles.name) || '平台管理员'
this.applyLocalUser()
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
methods: {
/**
* 下拉刷新:拉 my-info 写回缓存,并刷新微信绑定
*/
reload() {
return this.refreshProfile()
},
applyLocalUser() {
const user = uni.getStorageSync('platform_admin_user') || {}
this.displayName = user.nick_name || '管理员'
this.avatar = user.avatar || ''
this.roleText = (user.roles && user.roles.name) || '平台管理员'
},
async refreshProfile() {
try {
const wrap = await getPlatformAdminMyInfo()
if (wrap && wrap.ok && wrap.data) {
uni.setStorageSync('platform_admin_user', wrap.data)
}
} catch (e) { /* 保留本地缓存 */ }
this.applyLocalUser()
if (this.$refs.myPanel) return this.$refs.myPanel.refreshWxBind()
},
},
}
</script>

View File

@@ -10,9 +10,17 @@
<script>
import InputListPage from '@/subPackages/sub_platform_admin/input/InputListPage.vue'
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { InputListPage },
provide() { return { businessContext: getPlatformAdminContext() } },
methods: {
reload() {
const p = this.$refs.listPage
if (p && typeof p.reload === 'function') return p.reload()
},
},
onShow() {
this.$nextTick(() => this.$refs.listPage?.reload?.())
},

View File

@@ -46,10 +46,12 @@
*/
import StorePageLayout from '@/subPackages/sub_platform_admin/components/store-page-layout.vue';
import { getStoreList, isSalespersonStoreMode } from '@/api/storeManage.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
const ROOT = '/subPackages/sub_platform_admin/store';
export default {
mixins: [pullRefreshMixin],
components: { StorePageLayout },
data() {
return {

View File

@@ -30,8 +30,10 @@ import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common
import platformAdminApi from '@/api/platformAdmin.js'
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
import { formatDateTime, withdrawalCheckStatusText } from '@/subPackages/sub_business_shared/common/display.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout },
provide() { return { businessContext: getPlatformAdminContext() } },
data() {
@@ -52,6 +54,9 @@ export default {
statusText: item.check_status_txt || withdrawalCheckStatusText(item.check_status),
}
},
reload() {
return this.load()
},
load() {
platformAdminApi.getWithdrawalAuditList({ page: 1, pageSize: 50, check_status: 1 }).then(w => {
const items = (w.data && w.data.items) || []

View File

@@ -89,7 +89,7 @@ export function getStoreDetailSections() {
{ key: 'id', label: 'ID' },
{ key: 'name', label: '门店名称' },
{ key: 'type', label: '门店类型', format: (_, info) => storeTypeText(info.type) },
{ key: 'clinic_type', label: '诊所类型', format: (_, info) => clinicTypeText(info.clinic_type) },
{ key: 'clinic_type', label: '诊所类型', format: (_, info) => clinicTypeText(info.clinic_type), show: (info) => Number(info.type) !== 1 },
{ key: 'contact', label: '联系人' },
{ key: 'mobile', label: '联系电话' },
{ key: 'region', label: '省市区', format: (_, info) => regionText(info) },

View File

@@ -21,7 +21,8 @@ export function applyStoreDraft(vm, draft) {
if (!Array.isArray(vm.form.contract_files)) vm.form.contract_files = []
const ui = draft.ui || {}
vm.currentStep = ui.currentStep != null ? ui.currentStep : 0
vm.clinicTypeIndex = ui.clinicTypeIndex != null ? ui.clinicTypeIndex : (Number(vm.form.clinic_type) === 1 ? 0 : 1)
vm.storeTypeIndex = Number(vm.form.type) === 1 ? 1 : 0
vm.clinicTypeIndex = ui.clinicTypeIndex != null ? ui.clinicTypeIndex : (Number(vm.form.clinic_type) === 2 ? 1 : 0)
vm.bankTypeIndex = ui.bankTypeIndex != null
? ui.bankTypeIndex
: Math.max(0, vm.bankTypes.findIndex((b) => b.value === Number(vm.form.bank_account_type)))

View File

@@ -1,11 +1,13 @@
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<!-- pageGuard show 拆开关抽屉先离场再卸避免原生滚动锁残留 -->
<page-container
v-if="show"
v-if="pageGuard"
:show="show"
:overlay="false"
@leave="onPageLeave"
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<u-popup
@@ -38,13 +40,51 @@ export default {
showClose: { type: Boolean, default: true },
maskCloseable: { type: Boolean, default: true },
},
data() {
return {
pageGuard: false,
releaseTimer: null,
}
},
computed: {
show: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
},
watch: {
value: {
immediate: true,
handler(v) {
if (v) {
this.pageGuard = true
} 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
this.$emit('close')

View File

@@ -1,10 +1,11 @@
<template>
<business-page-layout :title="pageTitle" :show-back="true">
<view class="form-page">
<view class="step-header">
<u-steps :list="stepList" :current="currentStep" active-color="#6acdbb"></u-steps>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-body">
<u-form :model="form" ref="uForm" label-width="220" label-align="left">
<view v-show="currentStep === 0" class="step-content">
<view class="section-title">基础信息</view>
@@ -141,7 +142,8 @@
</view>
</view>
</u-form>
</scroll-view>
</view>
</view>
<view class="footer-actions">
<u-button v-if="canSaveDraft" class="btn-draft" @click="openSaveDraft">存草稿</u-button>
@@ -231,6 +233,7 @@ export default {
],
form: createDefaultDoctorInputForm(),
}
}
},
computed: {
pageTitle() { return this.id ? '编辑医生预填' : '新建医生预填' },
@@ -521,8 +524,11 @@ export default {
</script>
<style scoped>
.step-header { background-color: #fff; padding: 30rpx 40rpx; margin-bottom: 20rpx; }
.form-scroll { height: calc(100vh - 480rpx); box-sizing: border-box; }
.form-page {
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.step-header { background-color: #fff; padding: 16rpx 24rpx; }
.step-content { background: #fff; padding: 0 32rpx; border-radius: 16rpx; margin: 0 24rpx; }
.section-title {
font-size: 32rpx;
@@ -606,11 +612,22 @@ export default {
.summary-row .k { width: 180rpx; color: #86909c; flex-shrink: 0; }
.summary-row .v { flex: 1; color: #1d2129; }
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-wrap: wrap;
padding: 40rpx 24rpx 80rpx;
gap: 24rpx;
background: transparent;
flex-wrap: nowrap;
align-items: center;
padding: 16rpx 20rpx calc(16rpx + env(safe-area-inset-bottom));
gap: 16rpx;
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
box-sizing: border-box;
}
.footer-actions ::v-deep .u-btn {
margin: 0;
}
.btn-draft { flex: 1; min-width: 160rpx; }
.btn-prev { flex: 1; }

View File

@@ -10,9 +10,17 @@
<script>
import InputListPage from '@/subPackages/sub_salesperson/input/InputListPage.vue'
import { getInputBusinessContext } from '@/subPackages/sub_salesperson/common/context.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { InputListPage },
provide() { return { businessContext: getInputBusinessContext() } },
methods: {
reload() {
const p = this.$refs.listPage
if (p && typeof p.reload === 'function') return p.reload()
},
},
onShow() {
this.$refs.listPage?.reload?.()
},

View File

@@ -15,6 +15,7 @@ import {
getSalespersonWorkbenchEntries,
getSalespersonEntryUserData,
toggleSalespersonFavoriteEntry,
batchSaveSalespersonFavoriteEntry,
reportSalespersonEntryUsage,
} from '@/api/salesperson.js'
@@ -27,12 +28,13 @@ export default {
getEntries: getSalespersonWorkbenchEntries,
getUserData: getSalespersonEntryUserData,
toggleFavorite: toggleSalespersonFavoriteEntry,
batchSaveFavorites: batchSaveSalespersonFavoriteEntry,
reportUsage: reportSalespersonEntryUsage,
},
}
},
onShow() {
// 从功能跳转的页面返回时刷新常用/最近使用(首屏加载由组件自身 mounted 完成,此时 ref 还不存在
// 返回时刷新收藏状态(长按菜单用;常用/最近只在首页展示
if (this.$refs.features) this.$refs.features.refreshUserData()
},
}

View File

@@ -1,5 +1,14 @@
<template>
<business-page-layout title="业务工作台" :show-tabbar="true" :tab-index="0">
<!-- 自定义导航必须用 scroll-view 下拉刷新原生 onPullDownRefresh 在本页不会发请求 -->
<scroll-view
scroll-y
class="home-scroll"
:style="homeScrollStyle"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresherRefresh"
>
<view class="workspace-container">
<!-- 极简顶部英雄区 (保持原风格) -->
@@ -8,29 +17,78 @@
<view class="brand-text">
<view class="badge"><text class="badge-text">业务工作台</text></view>
<text class="store-name">{{ displayName }}</text>
<text class="store-sub">今日业务进度更新</text>
<text class="store-sub">名下诊所经营进度</text>
</view>
</view>
</view>
<!-- 核心资产看板 (优化布局与数值层级) -->
<!-- 经营看板今日/本周/本月经营三项与录入进度同一接口同一时间窗 -->
<view class="dashboard-wrapper">
<view class="dashboard-card">
<view class="stat-item">
<text class="stat-label">门店 (/)</text>
<view class="stat-value-group">
<text class="stat-value">{{ stats.storeCount || 0 }}</text>
<text class="stat-slash">/</text>
<text class="stat-value-highlight">{{ stats.storeApprovedCount || 0 }}</text>
</view>
<view class="dashboard-tabs">
<view
v-for="(tab, tabIndex) in statsRangeTabs"
:key="tab.value"
class="dashboard-tab"
:class="{ active: statsRange === tab.value }"
@click="selectStatsTab(tabIndex)"
>{{ tab.label }}</view>
</view>
<view class="stat-item">
<text class="stat-label">医生 (/)</text>
<view class="stat-value-group">
<text class="stat-value">{{ stats.doctorCount || 0 }}</text>
<text class="stat-slash">/</text>
<text class="stat-value-highlight">{{ stats.doctorApprovedCount || 0 }}</text>
<view class="dashboard-body">
<view v-if="statsLoading && !statsLoaded[statsRange]" class="dashboard-loading-overlay">
<u-loading mode="circle" size="36"></u-loading>
</view>
<swiper
class="dashboard-swiper"
:current="statsTabIndex"
:duration="200"
@change="onStatsSwiperChange"
>
<swiper-item v-for="tab in statsRangeTabs" :key="tab.value">
<view class="stat-row is-3">
<view class="stat-item">
<text class="stat-label">营业额</text>
<view class="stat-value-group">
<text class="stat-value highlight">{{ statsCache[tab.value].revenue }}</text>
<text class="stat-unit"></text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">处方</text>
<view class="stat-value-group">
<text class="stat-value">{{ statsCache[tab.value].prescription_count }}</text>
<text class="stat-unit"></text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">挂号</text>
<view class="stat-value-group">
<text class="stat-value">{{ statsCache[tab.value].register_count }}</text>
<text class="stat-unit"></text>
</view>
</view>
</view>
<view class="stat-divider"></view>
<view class="stat-row is-2">
<view class="stat-item">
<text class="stat-label">门店 (/)</text>
<view class="stat-value-group">
<text class="stat-value">{{ statsCache[tab.value].store_count }}</text>
<text class="stat-slash">/</text>
<text class="stat-value-highlight">{{ statsCache[tab.value].store_approved_count }}</text>
</view>
</view>
<view class="stat-item">
<text class="stat-label">医生 (/)</text>
<view class="stat-value-group">
<text class="stat-value">{{ statsCache[tab.value].doctor_count }}</text>
<text class="stat-slash">/</text>
<text class="stat-value-highlight">{{ statsCache[tab.value].doctor_approved_count }}</text>
</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
</view>
@@ -41,33 +99,78 @@
:recents="recentItems"
:favorites="favItems"
:card-long="cardLong"
:api="favEditorApi"
@item-click="onEntryTap"
@more="goFeatures"
@saved="loadUserEntries"
></workbench-user-entries>
</view>
</view>
</scroll-view>
</business-page-layout>
</template>
<script>
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import { getSalespersonInputStats, getSalespersonEntryUserData, reportSalespersonEntryUsage } from '@/api/salesperson.js'
import { getSalespersonDashboardStats, getSalespersonWorkbenchEntries, getSalespersonEntryUserData, batchSaveSalespersonFavoriteEntry, reportSalespersonEntryUsage } from '@/api/salesperson.js'
import { navigateToInputForm } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
import { normalizeEntry } from '@/utils/workbenchEntries.js'
import { getCardStyle } from '@/utils/workbenchCardPref.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
/** 记住业务员看板时间窗,与医生工作台同一套 today/week/month */
const SP_STATS_RANGE_KEY = 'sp_workbench_stats_range'
function emptyClinicStats() {
return {
revenue: '0.00',
prescription_count: 0,
register_count: 0,
store_count: 0,
store_approved_count: 0,
doctor_count: 0,
doctor_approved_count: 0,
}
}
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {
displayName: '',
stats: { storeCount: 0, storeApprovedCount: 0, doctorCount: 0, doctorApprovedCount: 0 },
/** 经营+录入都按时间窗缓存,切 Tab 时整卡一起换,不再单独留一份累计录入 */
statsRange: 'today',
statsTabIndex: 0,
statsSwiperChanging: false,
statsLoading: false,
statsRangeTabs: [
{ label: '今日', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' },
],
statsCache: {
today: emptyClinicStats(),
week: emptyClinicStats(),
month: emptyClinicStats(),
},
statsLoaded: {
today: false,
week: false,
month: false,
},
/** 我的常用/最近使用原始入口(归一化后存储,表单预填装饰在 computed 注入) */
favRaw: [],
recentRaw: [],
/** 卡片形态偏好true 长卡一行两个 / false 金刚区宫格(我的页可切换,存本地) */
cardLong: false,
/** 首页快捷添加抽屉接口(与功能页同一套) */
favEditorApi: {
getEntries: getSalespersonWorkbenchEntries,
getUserData: getSalespersonEntryUserData,
batchSaveFavorites: batchSaveSalespersonFavoriteEntry,
},
}
},
computed: {
@@ -79,16 +182,44 @@ export default {
recentItems() {
return this.recentRaw.map((item) => this.decorateItem(item))
},
/**
* 滚动区高度:窗口 状态栏 导航 tabbar 安全区
* 与 business-page-layout 的 body 预留一致scroll-view 没有明确高度时 refresher 不会触发请求
*/
homeScrollStyle() {
const sys = uni.getSystemInfoSync() || {}
const windowHeight = Number(sys.windowHeight || 0)
const statusBar = Number(this.$statusBarHeight || sys.statusBarHeight || 0)
const navbar = Number(this.$navbarHeight || 44)
const rpx = Number(sys.windowWidth || 375) / 750
const tabbar = 100 * rpx
const safeBottom = Number((sys.safeAreaInsets && sys.safeAreaInsets.bottom) || 0)
const h = Math.max(200, windowHeight - statusBar - navbar - tabbar - safeBottom)
return { height: h + 'px' }
},
},
onLoad() {
this.initStatsRangeFromStorage()
},
onShow() {
const user = uni.getStorageSync('salesperson_user') || {}
this.displayName = user.nick_name || '业务员'
// 卡片形态偏好每次 onShow 重读:我的页切换后返回立即生效
this.cardLong = getCardStyle() === 'long'
this.fetchDashboardData()
this.fetchDashboardData({ range: this.statsRange })
this.loadUserEntries()
},
methods: {
/**
* 下拉刷新:重拉当前时间窗经营数据 + 常用入口
*/
reload() {
this.statsLoaded[this.statsRange] = false
return Promise.all([
this.fetchDashboardData({ range: this.statsRange }),
this.loadUserEntries(),
])
},
/** 拉取我的常用 + 最近使用:失败静默留空(首页有引导条 + 更多功能卡兜底) */
async loadUserEntries() {
try {
@@ -128,18 +259,85 @@ export default {
goFeatures() {
uni.reLaunch({ url: '/subPackages/sub_salesperson/features/index' })
},
async fetchDashboardData() {
/**
* 拉取首页看板:录入进度 + 指定时间窗的名下诊所经营
* 切 Tab 时有缓存先展示再静默刷新;无缓存才出 loading
*/
async fetchDashboardData(options) {
const opts = options || {}
const range = opts.range || this.statsRange
const silent = opts.silent === true
if (!silent && !this.statsLoaded[range]) {
this.statsLoading = true
}
try {
const wrap = await getSalespersonInputStats()
const wrap = await getSalespersonDashboardStats({ range })
if (!wrap || !wrap.ok) return
const data = wrap.data || {}
this.stats = {
storeCount: data.store_count || 0,
storeApprovedCount: data.store_approved_count || 0,
doctorCount: data.doctor_count || 0,
doctorApprovedCount: data.doctor_approved_count || 0,
}
} catch (e) { console.error('获取统计数据失败', e) }
this.$set(this.statsCache, range, {
revenue: this.formatAmount(data.revenue),
prescription_count: data.prescription_count || 0,
register_count: data.register_count || 0,
store_count: data.store_count || 0,
store_approved_count: data.store_approved_count || 0,
doctor_count: data.doctor_count || 0,
doctor_approved_count: data.doctor_approved_count || 0,
})
this.$set(this.statsLoaded, range, true)
} catch (e) {
console.error('获取统计数据失败', e)
} finally {
if (range === this.statsRange) this.statsLoading = false
}
},
/** 营业额保留两位小数,接口未返回时展示 0.00 */
formatAmount(value) {
const n = Number(value)
if (isNaN(n)) return '0.00'
return n.toFixed(2)
},
/** 恢复上次选中的时间窗(与医生端 localStorage 记住 Tab 同类) */
initStatsRangeFromStorage() {
const saved = uni.getStorageSync(SP_STATS_RANGE_KEY)
const tabIndex = this.statsRangeTabs.findIndex((tab) => tab.value === saved)
if (tabIndex >= 0) {
this.statsRange = saved
this.statsTabIndex = tabIndex
}
},
/** 点 Tab带动 swiper再按时间窗拉数 */
selectStatsTab(index) {
if (this.statsSwiperChanging) return
const tab = this.statsRangeTabs[index]
if (!tab) return
this.statsTabIndex = index
this.changeStatsRange(tab.value)
},
/** 左右滑看板:同步 Tab 高亮并拉对应时间窗 */
onStatsSwiperChange(e) {
const index = e.detail.current
const tab = this.statsRangeTabs[index]
if (!tab || tab.value === this.statsRange) return
this.statsSwiperChanging = true
this.statsTabIndex = index
this.changeStatsRange(tab.value)
this.$nextTick(() => {
this.statsSwiperChanging = false
})
},
/**
* 切换今日/本周/本月:有缓存立即展示并后台静默刷新,无缓存出 loading
*/
changeStatsRange(range) {
if (this.statsRange === range && this.statsLoaded[range]) return
this.statsRange = range
uni.setStorageSync(SP_STATS_RANGE_KEY, range)
const tabIndex = this.statsRangeTabs.findIndex((tab) => tab.value === range)
if (tabIndex >= 0) this.statsTabIndex = tabIndex
this.fetchDashboardData({
range,
silent: !!this.statsLoaded[range],
})
},
go(item) {
uni.vibrateShort(); // 增加轻微触感反馈
@@ -154,8 +352,8 @@ export default {
</script>
<style lang="scss" scoped>
/* 保持你的容器风格 */
.workspace-container { padding-bottom: 80rpx; background-color: #F4F6F8; min-height: 100vh; }
.home-scroll { box-sizing: border-box; }
.workspace-container { padding-bottom: 80rpx; background-color: #F4F6F8; min-height: 100%; }
.hero-section { background: linear-gradient(135deg, #6acdbb 0%, #4db6a8 100%); padding: 40rpx 40rpx 80rpx; border-bottom-left-radius: 48rpx; border-bottom-right-radius: 48rpx; box-shadow: 0 4rpx 20rpx rgba(77, 182, 168, 0.2); }
.brand-text { flex: 1; }
.badge { display: inline-flex; background: rgba(255, 255, 255, 0.15); padding: 6rpx 20rpx; border-radius: 30rpx; margin-bottom: 20rpx; border: 1px solid rgba(255, 255, 255, 0.2); }
@@ -163,15 +361,27 @@ export default {
.store-name { font-size: 42rpx; font-weight: 600; color: #fff; }
.store-sub { font-size: 26rpx; color: rgba(255, 255, 255, 0.85); }
/* 看板区域与各端工作台看板统一规格24rpx 圆角 + 轻阴影 + DIN 数字 */
/* 看板:上排 3 列经营、下排 2 列入录(与各端工作台浮卡规格一致 */
.dashboard-wrapper { margin: -40rpx 30rpx 30rpx; }
.dashboard-card { background: #fff; border-radius: 24rpx; padding: 40rpx 30rpx; display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03); }
.dashboard-card { background: #fff; border-radius: 24rpx; padding: 32rpx 20rpx; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03); }
.stat-row { display: grid; }
.stat-row.is-3 { grid-template-columns: repeat(3, 1fr); }
.stat-row.is-2 { grid-template-columns: repeat(2, 1fr); }
.dashboard-tabs { display: flex; flex-direction: row; margin-bottom: 8rpx; background: #f5f6f8; border-radius: 12rpx; padding: 6rpx; }
.dashboard-tab { flex: 1; text-align: center; font-size: 24rpx; color: #6c7380; padding: 12rpx 0; border-radius: 8rpx; }
.dashboard-tab.active { background: #fff; color: #6acdbb; font-weight: 600; }
.dashboard-body { position: relative; min-height: 280rpx; }
.dashboard-swiper { height: 280rpx; }
.dashboard-loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 2; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.72); }
.stat-divider { height: 1px; background: #F1F5F9; margin: 8rpx 16rpx 28rpx; }
.stat-item { display: flex; flex-direction: column; align-items: center; }
.stat-label { font-size: 22rpx; color: #94A3B8; margin-bottom: 12rpx; }
.stat-value-group { display: flex; align-items: baseline; }
.stat-value { font-size: 42rpx; font-weight: 700; color: #1E293B; font-family: "DIN Alternate", sans-serif; }
.stat-value { font-size: 36rpx; font-weight: 700; color: #1E293B; font-family: "DIN Alternate", sans-serif; }
.stat-value.highlight { color: #059669; }
.stat-slash { font-size: 32rpx; color: #CBD5E1; margin: 0 8rpx; }
.stat-value-highlight { font-size: 42rpx; font-weight: 700; color: #059669; font-family: "DIN Alternate", sans-serif; }
.stat-value-highlight { font-size: 36rpx; font-weight: 700; color: #059669; font-family: "DIN Alternate", sans-serif; }
.stat-unit { font-size: 20rpx; color: #94A3B8; margin-left: 4rpx; }
/* 个人功能区(分区/宫格样式在 workbench-user-entries 组件内) */
.content-wrapper { position: relative; z-index: 10; }

View File

@@ -72,11 +72,11 @@ export default {
? this.bizApi.getDoctorInputList(params)
: this.bizApi.getStoreInputList(params)
},
reload() { this.page = 1; this.load(1, false) },
reload() { this.page = 1; return this.load(1, false) },
load(page, append) {
const params = { page, pageSize: 15 }
if (this.auditMode) params.status = 0
this.listApi(params)
return this.listApi(params)
.then((w) => {
const items = this.parseListItems(w.data)
if (!items.length && !w.ok) {

View File

@@ -19,20 +19,43 @@
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import AdminMyPanel from '@/components/admin-my-panel/admin-my-panel.vue'
import { getSalespersonContext } from '@/subPackages/sub_salesperson/common/context.js'
import { getSalespersonMyInfo } from '@/api/salesperson.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { BusinessPageLayout, AdminMyPanel },
provide() { return { businessContext: getSalespersonContext() } },
data() {
return { displayName: '', avatar: '', roleText: '业务员' }
},
onShow() {
// 个人中心改完昵称/头像返回时重读缓存立即生效
const user = uni.getStorageSync('salesperson_user') || {}
this.displayName = user.nick_name || '业务员'
this.avatar = user.avatar || ''
this.roleText = (user.roles && user.roles.name) || '业务员'
this.applyLocalUser()
if (this.$refs.myPanel) this.$refs.myPanel.refreshWxBind()
},
methods: {
/**
* 下拉刷新:拉 my-info 写回缓存,并刷新微信绑定
*/
reload() {
return this.refreshProfile()
},
applyLocalUser() {
const user = uni.getStorageSync('salesperson_user') || {}
this.displayName = user.nick_name || '业务员'
this.avatar = user.avatar || ''
this.roleText = (user.roles && user.roles.name) || '业务员'
},
async refreshProfile() {
try {
const wrap = await getSalespersonMyInfo()
if (wrap && wrap.ok && wrap.data) {
uni.setStorageSync('salesperson_user', wrap.data)
}
} catch (e) { /* 保留本地缓存 */ }
this.applyLocalUser()
if (this.$refs.myPanel) return this.$refs.myPanel.refreshWxBind()
},
},
}
</script>

View File

@@ -1,13 +1,14 @@
<template>
<business-page-layout :title="pageTitle" :show-back="true">
<!-- 顶部步骤条 -->
<view v-if="!submitSuccess" class="step-header">
<!-- 顶部步骤条 + 表单滚动 + 底栏三栏撑满底栏始终可见 -->
<view v-if="!submitSuccess" class="form-page">
<view class="step-header">
<u-steps :list="stepList" :current="currentStep" active-color="#6acdbb"></u-steps>
</view>
<scroll-view v-if="!submitSuccess" scroll-y class="form-scroll">
<u-form :model="form" ref="uForm" label-width="220" label-align="left">
<view class="form-body">
<u-form :model="form" ref="uForm" class="compact-form" label-width="180" label-align="left">
<!-- ================= 步骤 1基础与地址 ================= -->
<view v-show="currentStep === 0" class="step-content">
@@ -15,11 +16,34 @@
<u-form-item label="门店名称" :required="true">
<u-input v-model="form.name" placeholder="请输入门店名称" />
</u-form-item>
<u-form-item label="门店类型" right-icon="arrow-right" :required="true">
<u-input :value="storeTypeLabel" type="select" @click="showStoreType = true" placeholder="请选择" />
<u-select v-model="showStoreType" :list="storeTypes" @confirm="onStoreTypeConfirm"></u-select>
<u-form-item label="门店类型" :required="true" :border-bottom="false">
<view class="chip-row">
<view
v-for="(item, idx) in storeTypes"
:key="item.value"
class="chip"
:class="{ active: storeTypeIndex === idx }"
@click="selectStoreType(idx)"
>
<text class="chip-label">{{ item.label }}</text>
</view>
</view>
</u-form-item>
<u-form-item label="联系人姓名" :required="true">
<!-- 诊所类型只对诊所有意义选药店时隐藏以免占位和误填 -->
<u-form-item v-if="isClinicStore" label="诊所类型" :border-bottom="false">
<view class="chip-row">
<view
v-for="(item, idx) in clinicTypes"
:key="item.value"
class="chip"
:class="{ active: clinicTypeIndex === idx }"
@click="selectClinicType(idx)"
>
<text class="chip-label">{{ item.label }}</text>
</view>
</view>
</u-form-item>
<u-form-item label="联系人" :required="true">
<u-input v-model="form.contact" placeholder="请输入联系人姓名" />
</u-form-item>
<u-form-item label="手机号" :required="true">
@@ -31,47 +55,30 @@
:options="promoterOptions"
:loading="promoterLoading"
:disabled="promoterDisabled"
@input="form.code = $event"
@input="onPromoterInput"
/>
</u-form-item>
<u-form-item label="诊所类型" :border-bottom="false">
<view class="clinic-type-cards">
<view
v-for="(item, idx) in clinicTypes"
:key="item.value"
class="clinic-type-card"
:class="{ active: clinicTypeIndex === idx }"
@click="selectClinicType(idx)"
>
<text class="clinic-type-label">{{ item.label }}</text>
</view>
</view>
</u-form-item>
<u-form-item label="是否包邮">
<view class="switch-row">
<text class="switch-hint">{{ Number(form.is_shipping_free) === 1 ? '包邮' : '不包邮' }}</text>
<view class="toggle-grid">
<view class="toggle-cell">
<text class="toggle-label">包邮</text>
<switch
:checked="Number(form.is_shipping_free) === 1"
color="#6acdbb"
@change="onShippingFreeChange"
/>
</view>
</u-form-item>
<u-form-item label="订阅价格波动">
<view class="switch-row">
<text class="switch-hint">{{ Number(form.subscribe_price_change) === 0 ? '订阅' : '不订阅' }}</text>
<view class="toggle-cell">
<text class="toggle-label">订阅波动</text>
<switch
:checked="Number(form.subscribe_price_change) === 0"
color="#6acdbb"
@change="onSubscribeChange"
/>
</view>
</u-form-item>
</view>
<u-form-item label="营业时间" right-icon="arrow-right">
<u-form-item label="营业时间">
<view class="time-flex">
<view class="time-box" @click="showStartTime = true">{{ form.start_time || '开始时间' }}</view>
<text class="time-sep">-</text>
@@ -157,9 +164,15 @@
</view>
</u-form>
</view>
</view>
</scroll-view>
<view v-if="!submitSuccess" class="footer-actions">
<u-button v-if="canSaveDraft" class="btn-draft" @click="openSaveDraft">存草稿</u-button>
<u-button v-if="currentStep > 0" class="btn-prev" @click="prevStep">上一步</u-button>
<u-button v-if="currentStep < 2" class="btn-next" type="primary" :custom-style="primaryStyle" @click="nextStep">下一步</u-button>
<u-button v-if="currentStep === 2" class="btn-submit" type="primary" :custom-style="primaryStyle" :loading="submitting" :disabled="submitting" @click="submit">确认保存</u-button>
</view>
<view v-if="submitSuccess" class="success-panel">
<u-icon name="checkmark-circle-fill" color="#6acdbb" size="120"></u-icon>
@@ -171,14 +184,6 @@
</view>
</view>
<!-- 底部操作按钮 -->
<view v-if="!submitSuccess" class="footer-actions">
<u-button v-if="canSaveDraft" class="btn-draft" @click="openSaveDraft">存草稿</u-button>
<u-button v-if="currentStep > 0" class="btn-prev" @click="prevStep">上一步</u-button>
<u-button v-if="currentStep < 2" class="btn-next" type="primary" :custom-style="primaryStyle" @click="nextStep">下一步</u-button>
<u-button v-if="currentStep === 2" class="btn-submit" type="primary" :custom-style="primaryStyle" :loading="submitting" :disabled="submitting" @click="submit">确认保存</u-button>
</view>
<u-popup v-model="showDraftNameModal" mode="center" border-radius="16" width="600rpx">
<view class="draft-name-box">
<text class="draft-name-title">草稿名称</text>
@@ -238,7 +243,6 @@ export default {
},
// uView select/picker 显示状态控制
showStoreType: false,
showStartTime: false,
showEndTime: false,
showBankType: false,
@@ -256,13 +260,15 @@ export default {
promoterOptions: [],
promoterLoading: false,
promoterDisabled: false,
// 用户已手选省市区后,不再跟业务员地区覆盖
regionTouched: false,
}
},
computed: {
pageTitle() { return this.id ? '编辑门店录入' : '新建门店录入' },
storeTypeLabel() {
const item = this.storeTypes.find((t) => t.value === Number(this.form.type))
return item ? item.label : '请选择'
/** 药店没有诊所类型,选药店时整行隐藏 */
isClinicStore() {
return Number(this.form.type) !== 1
},
canSaveDraft() { return !this.id },
regionValue() {
@@ -316,7 +322,11 @@ export default {
}
}
}
}).catch(() => { this.promoterLoading = false })
this.applyDefaultRegionIfNeeded()
}).catch(() => {
this.promoterLoading = false
this.applyDefaultRegionIfNeeded()
})
},
loadDetailAsync() {
return this.bizApi.getStoreInputDetail(this.id).then((w) => {
@@ -329,11 +339,25 @@ export default {
this.form.city_id = d.city_id || null
this.form.address = d.province_id && d.city_id ? [d.province_id, d.city_id] : []
this.storeTypeIndex = Number(d.type) === 1 ? 1 : 0
this.clinicTypeIndex = Number(d.clinic_type) === 1 ? 0 : 1
this.clinicTypeIndex = Number(d.clinic_type) === 2 ? 1 : 0
this.bankTypeIndex = Math.max(0, this.bankTypes.findIndex((b) => b.value === Number(d.bank_account_type)))
})
},
/** 切换门店类型:药店清空诊所类型,切回诊所时恢复上次选择 */
selectStoreType(idx) {
const item = this.storeTypes[idx]
if (!item) return
this.storeTypeIndex = idx
this.form.type = item.value
if (item.value === 1) {
this.form.clinic_type = 0
return
}
if (Number(this.form.clinic_type) !== 1 && Number(this.form.clinic_type) !== 2) {
this.selectClinicType(this.clinicTypeIndex)
}
},
selectClinicType(idx) {
this.clinicTypeIndex = idx
this.form.clinic_type = this.clinicTypes[idx].value
@@ -346,11 +370,6 @@ export default {
},
// --- 下拉及选择器确认事件 ---
onStoreTypeConfirm(e) {
const selected = e[0]
this.form.type = selected.value
this.storeTypeIndex = this.storeTypes.findIndex((item) => item.value === selected.value)
},
onBankTypeConfirm(e) {
const selected = e[0];
this.form.bank_account_type = selected.value;
@@ -364,6 +383,49 @@ export default {
this.form.province_id = val[0]
this.form.city_id = val[1]
this.form.address = [val[0], val[1]]
this.regionTouched = true
},
/**
* 选择业务员后:新建且用户还没手选省市区时,跟所选业务员的省市
*/
onPromoterInput(code) {
this.form.code = code
if (this.id || this.regionTouched) return
this.fillRegion(this.regionFromPromoterCode(code))
},
/**
* 新建门店:业务员端默认带出当前账号/已选业务员所在省市
* 超管共用本页不预填;编辑和已有草稿不覆盖
*/
applyDefaultRegionIfNeeded() {
if (this.id || this.regionTouched) return
if (this.form.province_id && this.form.city_id) return
if (uni.getStorageSync('loginMode') !== 'salesperson') return
const region = this.regionFromPromoterCode(this.form.code) || this.regionFromLoginUser()
this.fillRegion(region)
},
regionFromLoginUser() {
const userKey = this.bizCtx.userStorageKey || 'salesperson_user'
const user = uni.getStorageSync(userKey) || {}
return this.normalizeRegion(user.province_id, user.city_id)
},
regionFromPromoterCode(code) {
if (!code) return null
const item = this.promoterOptions.find((p) => p.code === code)
if (!item) return null
return this.normalizeRegion(item.province_id, item.city_id)
},
normalizeRegion(provinceId, cityId) {
const p = Number(provinceId || 0)
const c = Number(cityId || 0)
if (!p || !c) return null
return { province_id: p, city_id: c }
},
fillRegion(region) {
if (!region) return
this.form.province_id = region.province_id
this.form.city_id = region.city_id
this.form.address = [region.province_id, region.city_id]
},
// --- 图片上传相关 ---
@@ -441,6 +503,9 @@ export default {
// 联系人手机号与后端 checkPhone 一致
if (!assertPhone(f.mobile)) return false;
if (!f.code?.trim()) return this.showError('请选择业务员');
if (Number(f.type) === 0 && Number(f.clinic_type) !== 1 && Number(f.clinic_type) !== 2) {
return this.showError('请选择诊所类型')
}
if (!f.province_id || !f.city_id) return this.showError('请选择省市区');
if (!f.position?.trim()) return this.showError('请输入详细地址');
}
@@ -471,9 +536,12 @@ export default {
runSubmit(this, {
validate: () => this.validateOptionalBankFields(),
request: () => {
const isPharmacy = Number(this.form.type) === 1
const payload = {
...this.form,
type: Number(this.form.type) === 1 ? 1 : 0,
type: isPharmacy ? 1 : 0,
// 药店不提交诊所类型,避免后台把药店记成西医/中医诊所
clinic_type: isPharmacy ? 0 : Number(this.form.clinic_type || 0),
address: [Number(this.form.province_id), Number(this.form.city_id)],
}
delete payload.erp_id
@@ -517,79 +585,125 @@ export default {
flex: 1;
min-width: 0;
}
/* 容器及结构 */
.step-header { background-color: #fff; padding: 30rpx 40rpx; margin-bottom: 20rpx; }
.form-scroll { height: calc(100vh - 480rpx); box-sizing: border-box; }
.step-content { background: #fff; padding: 0 32rpx; border-radius: 16rpx; margin: 0 24rpx; }
/* 标题 */
.section-title { font-size: 32rpx; font-weight: 600; padding: 40rpx 0 20rpx; color: #1D2129; position: relative; }
.step-tip { font-size: 24rpx; color: #86909c; margin: -8rpx 0 16rpx; line-height: 1.5; }
.section-title::after { content: ''; position: absolute; left: -32rpx; top: 46rpx; width: 8rpx; height: 32rpx; background: #6acdbb; border-radius: 0 4rpx 4rpx 0; }
/* 营业时间特别处理 */
.form-item-promoter ::v-deep .promoter-picker {
min-height: 56rpx;
padding: 0;
}
.form-item-promoter ::v-deep .avatar {
width: 48rpx;
height: 48rpx;
}
.form-item-promoter ::v-deep .sub {
margin-top: 2rpx;
}
.form-page {
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.compact-form ::v-deep .u-form-item {
padding: 12rpx 0;
}
.compact-form ::v-deep .u-form-item__body {
padding: 8rpx 0;
}
.step-header { background-color: #fff; padding: 16rpx 24rpx; }
.step-content { background: #fff; padding: 0 24rpx 16rpx; border-radius: 12rpx; margin: 0 20rpx; }
.section-title {
font-size: 28rpx;
font-weight: 600;
padding: 20rpx 0 8rpx;
color: #1D2129;
}
.step-tip { font-size: 22rpx; color: #86909c; margin: 0 0 8rpx; line-height: 1.4; }
.time-flex { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.time-box { flex: 1; text-align: center; background: #f5f7fa; padding: 12rpx 0; border-radius: 8rpx; font-size: 28rpx; color: #606266; }
.time-sep { margin: 0 20rpx; color: #999; }
.clinic-type-cards { display: flex; gap: 12rpx; width: 100%; }
.clinic-type-card {
.time-box { flex: 1; text-align: center; background: #f5f7fa; padding: 8rpx 0; border-radius: 8rpx; font-size: 26rpx; color: #606266; }
.time-sep { margin: 0 12rpx; color: #999; }
.chip-row { display: flex; gap: 12rpx; width: 100%; }
.chip {
flex: 1;
padding: 12rpx 10rpx;
padding: 6rpx 10rpx;
text-align: center;
background: #f5f7fa;
border: 2rpx solid #e4e7ed;
border: 1px solid rgba(106, 205, 187, 0.3);
border-radius: 8rpx;
box-sizing: border-box;
}
.clinic-type-card.active {
background: rgba(106, 205, 187, 0.08);
border-color: #6acdbb;
.chip.active {
background: rgba(106, 205, 187, 0.1);
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
}
.clinic-type-label { font-size: 24rpx; color: #303133; }
.clinic-type-card.active .clinic-type-label { color: #6acdbb; font-weight: 600; }
.switch-row {
.chip-label { font-size: 22rpx; line-height: 1.4; color: #303133; }
.chip.active .chip-label { color: #6acdbb; font-weight: 600; }
.toggle-grid {
display: flex;
gap: 12rpx;
padding: 8rpx 0 16rpx;
}
.toggle-cell {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
gap: 16rpx;
justify-content: space-between;
gap: 8rpx;
padding: 10rpx 16rpx;
background: #f8faf9;
border: 1px solid rgba(106, 205, 187, 0.3);
border-radius: 8rpx;
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
box-sizing: border-box;
}
.switch-hint { font-size: 26rpx; color: #86909c; }
/* 图片资料区域 */
.upload-group { margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; }
.toggle-label { font-size: 24rpx; color: #303133; flex-shrink: 0; }
.toggle-cell switch {
transform: scale(0.82);
transform-origin: right center;
}
.upload-group { margin-bottom: 16rpx; padding-bottom: 12rpx; border-bottom: 1rpx solid #f5f5f5; }
.upload-group:last-child { border-bottom: none; }
.upload-label { font-size: 28rpx; color: #303133; margin-bottom: 20rpx; display: flex; align-items: center;}
.upload-label { font-size: 26rpx; color: #303133; margin-bottom: 10rpx; display: flex; align-items: center; }
.required { color: #fa3534; margin-right: 8rpx; font-weight: bold; }
.img-row, .img-list { display: flex; flex-wrap: wrap; gap: 24rpx; }
.thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); }
.upload-btn { width: 160rpx; height: 160rpx; background: #f4f5f7; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 2rpx dashed #dcdfe6; }
.upload-txt { font-size: 24rpx; color: #999; margin-top: 12rpx; }
.img-row, .img-list { display: flex; flex-wrap: wrap; gap: 16rpx; }
.thumb { width: 120rpx; height: 120rpx; border-radius: 8rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); }
.upload-btn { width: 120rpx; height: 120rpx; background: #f4f5f7; border-radius: 8rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 2rpx dashed #dcdfe6; }
.upload-txt { font-size: 22rpx; color: #999; margin-top: 8rpx; }
.gallery-link {
display: inline-block;
margin-top: 12rpx;
font-size: 24rpx;
margin-top: 8rpx;
font-size: 22rpx;
color: #6acdbb;
}
.contract-btn { width: 100%; height: 100rpx; flex-direction: row; gap: 12rpx; background: rgba(106, 205, 187, 0.05); border-color: #6acdbb; }
/* 底部操作区 */
.footer-actions { display: flex; flex-wrap: wrap; padding: 40rpx 24rpx 80rpx; gap: 24rpx; background: transparent; }
.btn-draft { flex: 1; min-width: 160rpx; }
.contract-btn { width: 100%; height: 80rpx; flex-direction: row; gap: 12rpx; background: rgba(106, 205, 187, 0.05); border-color: #6acdbb; }
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: 16rpx 20rpx calc(16rpx + env(safe-area-inset-bottom));
gap: 16rpx;
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
box-sizing: border-box;
}
.footer-actions ::v-deep .u-btn {
margin: 0;
}
.btn-draft { flex: 1; min-width: 140rpx; }
.btn-prev { flex: 1; }
.btn-next, .btn-submit { flex: 2; }
.draft-name-box { padding: 40rpx 32rpx; background: #fff; }
.draft-name-title { display: block; font-size: 32rpx; font-weight: 600; margin-bottom: 24rpx; color: #1d2129; }
.draft-name-actions { display: flex; gap: 24rpx; margin-top: 32rpx; justify-content: flex-end; }
.draft-name-box { padding: 32rpx 28rpx; background: #fff; }
.draft-name-title { display: block; font-size: 30rpx; font-weight: 600; margin-bottom: 20rpx; color: #1d2129; }
.draft-name-actions { display: flex; gap: 16rpx; margin-top: 24rpx; justify-content: flex-end; }
.success-panel {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 48rpx 80rpx;
padding: 80rpx 48rpx 64rpx;
min-height: 50vh;
}
.success-title { font-size: 36rpx; font-weight: 600; color: #1d2129; margin-top: 32rpx; }
.success-sub { font-size: 28rpx; color: #86909c; margin-top: 16rpx; text-align: center; }
.success-actions { display: flex; flex-direction: column; gap: 24rpx; width: 100%; margin-top: 64rpx; padding: 0 24rpx; box-sizing: border-box; }
.success-title { font-size: 34rpx; font-weight: 600; color: #1d2129; margin-top: 24rpx; }
.success-sub { font-size: 26rpx; color: #86909c; margin-top: 12rpx; text-align: center; }
.success-actions { display: flex; flex-direction: column; gap: 20rpx; width: 100%; margin-top: 48rpx; padding: 0 24rpx; box-sizing: border-box; }
</style>

View File

@@ -11,9 +11,17 @@
<script>
import InputListPage from '@/subPackages/sub_salesperson/input/InputListPage.vue'
import { getInputBusinessContext } from '@/subPackages/sub_salesperson/common/context.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
export default {
mixins: [pullRefreshMixin],
components: { InputListPage },
provide() { return { businessContext: getInputBusinessContext() } },
methods: {
reload() {
const p = this.$refs.listPage
if (p && typeof p.reload === 'function') return p.reload()
},
},
onShow() {
this.$refs.listPage?.reload?.()
},

View File

@@ -60,9 +60,12 @@
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export default {
mixins: [pullRefreshMixin],
components: { PageLayout },
data() {
return {
@@ -100,14 +103,17 @@ export default {
this.loadData();
},
methods: {
reload() {
return this.loadData()
},
switchTab(key) {
this.activeTab = key;
this.selectedIds = [];
this.loadData();
},
loadData() {
if (this.activeTab === 'history') this.loadSettlements();
else this.loadOrders();
if (this.activeTab === 'history') return this.loadSettlements();
return this.loadOrders();
},
buildParams() {
const params = {

View File

@@ -37,8 +37,10 @@
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default {
mixins: [pullRefreshMixin],
components: { PageLayout },
data() {
return {

View File

@@ -72,9 +72,12 @@ import { getSalespersonManageContext, resolveManageMode } from './common/context
import { splitTypeLabel } from './common/labels.js';
import { openQrCodePreview } from '@/utils/qrCodePreview.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
const ROOT = '/subPackages/sub_salesperson_manage';
export default {
mixins: [pullRefreshMixin],
components: { PageLayout },
data() {
return {

View File

@@ -16,8 +16,10 @@
<script>
import PageLayout from '../components/PageLayout.vue';
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
export default {
mixins: [pullRefreshMixin],
components: { PageLayout },
data() {
return {
@@ -40,6 +42,9 @@ export default {
this.loadList();
},
methods: {
reload() {
return this.loadList()
},
async loadList() {
if (!this.salespersonId) return;
this.loading = true;

View File

@@ -1,188 +1,226 @@
<template>
<view class="safe-area-inset-bottom">
<u-navbar title='到店接诊' title-size="34" title-color="#000000" back-icon-color="#000"
<view class="page-wrap safe-area-inset-bottom">
<u-navbar title="到店接诊" title-size="34" title-color="#000000" back-icon-color="#000"
:background="{ background: '#FFF' }"></u-navbar>
<view class="tabs white" :style="{ top: statusBarHeight + navbarHeight - 1 + 'px' }">
<u-tabs bar-width="20" :list="tabs.filter((item, index) => (type === 1 ? true : index !== 1))"
:current="current" :is-scroll="false" @change="change" active-color="#6ACDBB"></u-tabs>
<!-- 头部 tabs与下方 swiper 双向联动 tab / 左右滑都走 switchTab -->
<view class="head">
<u-tabs bar-width="20" :list="tabList" :current="current" :is-scroll="false"
@change="switchTab" active-color="#6ACDBB"></u-tabs>
</view>
<view style="margin-top: 114rpx; padding: 20rpx 24rpx 0;" v-if="!loading && list[current].length >= 0">
<!-- 到店接诊列表卡统一走 xk-list-card信息分层 + 状态色 + /次操作按钮 -->
<xk-list-card
v-for="item in list[current]" :key="item.id"
:avatar="item.avatar || require(`../../static/image/${item.sex%2==0?'nv':'nan'}.png`)"
:title="item.patient"
:meta="(item.sex % 2 != 0 ? '男' : '女') + ' · ' + item.age + '岁'"
:time="item.created_at"
:rows="item.info_rows"
:status="{ text: tabs[current].name, color: tabs[current].color }"
:actions="[{ key: 'info', text: current === 0 ? '去接诊' : '查看详情', type: current === 0 ? 'primary' : 'ghost' }]"
@action="toInfo(item.user_patient_id, item.id)"
@click="toInfo(item.user_patient_id, item.id)"
></xk-list-card>
<u-loadmore :status="status" :load-text="loadText" v-if="!loading && list[current].length > 0" />
</view>
<view class="flex-row flex-jus-center" v-if="!loading && list[current].length == 0">
<!-- <d-empty text="暂无信息" mode="search"></d-empty> -->
<view class="none-box">
<image src="../../static/image/none.png" class="none"></image>
<p class="none-text">当前暂无接诊患者</p>
</view>
</view>
<d-loading-page :loading="loading"></d-loading-page>
<!-- swiper 必须有确定高度才能横向滑动每页独立 scroll-view 负责上下滚和分页 -->
<swiper class="tab-swiper" :current="current" :disable-touch="visibleTabs.length < 2"
@change="onSwiperChange">
<swiper-item v-for="(tab, tabIndex) in visibleTabs" :key="tab.status">
<scroll-view
scroll-y
class="tab-scroll"
refresher-enabled
:refresher-triggered="tab.refreshing"
@refresherrefresh="onRefresh(tabIndex)"
@scrolltolower="loadMore(tabIndex)"
>
<view class="list-wrap">
<xk-list-card
v-for="item in tab.list" :key="item.id"
:avatar="item.card_avatar"
:title="item.patient"
:meta="item.card_meta"
:time="item.created_at"
:rows="item.info_rows"
:status="{ text: tab.name, color: tab.color }"
:actions="[{ key: 'info', text: tabIndex === 0 ? '去接诊' : '查看详情', type: tabIndex === 0 ? 'primary' : 'ghost' }]"
@action="toInfo(item.user_patient_id, item.id)"
@click="toInfo(item.user_patient_id, item.id)"
></xk-list-card>
<view class="tab-empty" v-if="tab.loaded && tab.list.length === 0">
<image src="../../static/image/none.png" class="none"></image>
<p class="none-text">当前暂无接诊患者</p>
</view>
<u-loadmore
v-if="tab.list.length > 0"
:status="tab.finished ? 'nomore' : (tab.loading ? 'loading' : 'loadmore')"
:load-text="loadText"
/>
</view>
</scroll-view>
</swiper-item>
</swiper>
<d-loading-page :loading="pageLoading"></d-loading-page>
</view>
</template>
<script>
// 到店接诊列表:已从老 Yii register/list 迁移到 xk-api patient-liststatus 单状态筛选 + 分页)
// 取值按 xk-api 约定 code/result见 Api-Response.mdc
import {
getPatientList
} from "@/api/reception.js";
/**
* 到店接诊列表xk-api patient-liststatus 单状态筛选 + 分页
* 交互u-tabs + swiper 双向联动,左右滑切 tab每个 tab 独立分页,切换不丢已加载内容
* 取值按 xk-api 约定 code/result见 Api-Response.mdc
*/
import { getPatientList } from '@/api/reception.js';
/** 单个 tab 的初始分页状态(提到 data 外,避免 data() 里调 methods */
function buildTab(status, name, color) {
return {
status,
name,
color,
list: [],
page: 1,
finished: false,
loading: false,
loaded: false,
refreshing: false,
};
}
export default {
data() {
return {
flag: false,
item: {},
status: "loading",
pageLoading: true,
loadText: {
loadmore: "轻轻上拉",
loading: "努力加载中",
nomore: "实在没有了",
loadmore: '轻轻上拉',
loading: '努力加载中',
nomore: '实在没有了',
},
list: {
0: [],
1: [],
2: [],
},
loading: true,
male: this.$male,
girl: this.$girl,
tabs: [{
name: "待接诊",
color: "#F44336",
},
{
name: "已接诊",
color: "#00C853",
},
{
name: "已完成",
color: "#C4C7CC",
},
// 三个状态 tab 独立缓存type!==1 时隐藏「已接诊」(与改版前 filter 一致)
tabPages: [
buildTab(1, '待接诊', '#F44336'),
buildTab(2, '已接诊', '#00C853'),
buildTab(3, '已完成', '#C4C7CC'),
],
current: 0,
statusBarHeight: this.$statusBarHeight,
navbarHeight: this.$navbarHeight,
page: 1,
type: 1,
totalPage: 1,
isHide: false,
register_id: "",
statued: 1,
};
},
computed: {
/** 实际展示的 tab对象引用 tabPages方便共用 loaded/list */
visibleTabs() {
if (this.type === 1) return this.tabPages;
return this.tabPages.filter((t, i) => i !== 1);
},
/** u-tabs 只要 name */
tabList() {
return this.visibleTabs.map((t) => ({ name: t.name }));
},
},
onHide() {
this.isHide = true;
},
onLoad(op) {
this.type = op["type"] ? Number(op["type"]) : 1;
this.init();
this.type = op['type'] ? Number(op['type']) : 1;
this.refreshTab(0);
},
onShow() {
if (this.isHide) {
this.list[this.current] = [];
this.page = 1;
this.loading = true;
this.init();
this.refreshTab(this.current);
}
},
onPullDownRefresh() {
this.init();
},
onReachBottom() {
if (this.page >= this.totalPage) {
this.$toast("数据已全部加载");
return;
}
this.page++;
this.getList();
// this.init();
this.refreshTab(this.current);
},
methods: {
init() {
this.list[this.current] = [];
this.page = 1;
this.loading = true;
this.getList(true);
/** tabs 与 swiper 统一切换:点 tab、手势左右滑都走这里 */
switchTab(index) {
if (this.current === index) return;
this.current = index;
const tab = this.visibleTabs[index];
if (tab && !tab.loaded) {
this.refreshTab(index);
}
},
/** swiper 手势滑动:回写 current 让顶部 tab 高亮跟随 */
onSwiperChange(e) {
this.switchTab(e.detail.current);
},
/** 下拉刷新当前 swiper 页 */
onRefresh(index) {
const tab = this.visibleTabs[index];
if (tab) tab.refreshing = true;
this.refreshTab(index);
},
/** 重置到第一页再拉 */
refreshTab(index) {
const tab = this.visibleTabs[index];
if (!tab) return;
tab.page = 1;
tab.finished = false;
this.fetchTab(index, true);
},
/** 上拉加载下一页 */
loadMore(index) {
const tab = this.visibleTabs[index];
if (!tab || tab.loading || tab.finished) return;
tab.page += 1;
this.fetchTab(index, false);
},
/**
* 拉取当前 tab 的挂号列表xk-api patient-list 分页版)
* 第一页替换、后续页追加(老代码把新页拼在前面会导致越翻越乱,迁移时一并修正)
* 拉取某个可见 tab 的挂号列表
* @param index 可见 tab 下标
* @param isRefresh true=覆盖刷新false=追加(加载更多)
*/
getList(is_stop = false) {
fetchTab(index, isRefresh) {
const tab = this.visibleTabs[index];
if (!tab || tab.loading) return;
tab.loading = true;
getPatientList({
store_id: uni.getStorageSync('store_id') || '11001',
status: this.statued,
page: this.page,
status: tab.status,
page: tab.page,
page_size: 10,
}).then((res) => {
let success = false;
if (res.code === 0 && res.result) {
// 行数据预处理:卡片次要信息行提前拼好(模板里不要写复杂表达式/方法调用)
const rows = (res.result.list || []).map((item) => {
const infoRows = ['推广员:' + (item.salesperson_name || '无')];
// 预约单补显预约时段,医生一眼区分「当天现场号」和「预约到期号」
if (item.appointment && item.appointment.visit_date) {
infoRows.push('预约:' + item.appointment.visit_date + ' ' + (item.appointment.slot_name || '全天'));
}
return { ...item, info_rows: infoRows };
});
this.list[this.current] = this.page === 1 ? rows : [
...this.list[this.current],
...rows,
];
this.totalPage = res.result.page_count || 1;
if (this.page >= this.totalPage) {
this.status = "nomore";
} else {
this.status = "loadmore";
}
this.$nextTick(() => {
this.loading = false;
is_stop && uni.stopPullDownRefresh();
});
const rows = (res.result.list || []).map((item) => this.normalizeItem(item));
tab.list = isRefresh ? rows : tab.list.concat(rows);
tab.finished = tab.page >= (res.result.page_count || 1);
success = true;
} else {
this.$toast(res.message || '加载失败');
is_stop && uni.stopPullDownRefresh();
this.loading = false;
if (this.page >= this.totalPage) {
this.status = "nomore";
} else {
this.status = "loadmore";
}
if (isRefresh) tab.list = [];
}
if (!success && !isRefresh) {
tab.page = Math.max(1, tab.page - 1);
}
tab.loading = false;
tab.refreshing = false;
if (success) tab.loaded = true;
this.$nextTick(() => {
this.pageLoading = false;
uni.stopPullDownRefresh();
});
}).catch(() => {
if (!isRefresh) tab.page = Math.max(1, tab.page - 1);
tab.loading = false;
tab.refreshing = false;
this.pageLoading = false;
uni.stopPullDownRefresh();
this.$toast('网络异常,请稍后重试');
});
},
change(index) {
this.current = index;
if (index == 0) {
this.statued = 1
/**
* 卡片展示字段提前算好:模板禁止方法调用 / ?? / ?.
*/
normalizeItem(item) {
const isFemale = item.sex % 2 == 0;
const infoRows = ['推广员:' + (item.salesperson_name || '无')];
if (item.appointment && item.appointment.visit_date) {
infoRows.push('预约:' + item.appointment.visit_date + ' ' + (item.appointment.slot_name || '全天'));
}
if (index == 1) {
this.statued = 2
}
if (index == 2) {
this.statued = 3
}
this.list[this.current] = [];
this.page = 1;
this.getList();
const fallback = isFemale
? (this.girl || require('../../static/image/nv.png'))
: (this.male || require('../../static/image/nan.png'));
return {
...item,
card_avatar: item.avatar || fallback,
card_meta: (isFemale ? '女' : '男') + ' · ' + item.age + '岁',
info_rows: infoRows,
};
},
toInfo(e, re_id) {
this.$go('../../subPackages/sub_workbench/workbench_infoPatient?id=' + e + '&re_id=' + re_id)
}
this.$go('../../subPackages/sub_workbench/workbench_infoPatient?id=' + e + '&re_id=' + re_id);
},
},
};
</script>
@@ -192,41 +230,40 @@
}
</style>
<style lang="scss" scoped>
.content {
padding: 25rpx;
/* 纵向 flexnavbar / tabs 固定swiper 吃满剩余高度(无确定高度则左右滑失效) */
.page-wrap {
display: flex;
flex-direction: column;
height: 100vh;
}
view {
position: relative;
}
.tabs {
width: 100vw;
position: fixed;
left: 0;
.head {
width: 100%;
height: 82rpx;
padding-top: 16rpx;
z-index: 99;
}
.fun {
position: fixed;
left: 0;
z-index: 99;
width: 750rpx;
background-color: #fff;
padding: 12rpx 32rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
bottom: env(safe-area-inset-bottom);
flex-shrink: 0;
z-index: 2;
}
.none-box {
.tab-swiper {
flex: 1;
min-height: 0;
}
.tab-scroll {
height: 100%;
}
.list-wrap {
padding: 20rpx 24rpx 40rpx;
}
.tab-empty {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 200.87rpx;
padding: 200rpx 0 80rpx;
.none {
width: 350rpx;
@@ -239,4 +276,4 @@
color: #999999;
}
}
</style>
</style>

View File

@@ -219,6 +219,8 @@
</view>
<view class="rx-actions flex-row flex-jus-end m-t-16">
<view v-if="item.can_withdraw" class="action-btn-ghost danger" @click="withdrawRxByIndex('current', rxIdx)">撤回</view>
<!-- 代付码仅本次挂号且订单待支付时出示条件对齐 PC 接诊台 -->
<view v-if="item.order_id && item.order_pay_text == '待支付'" class="action-btn-ghost primary m-l-16" @click="openProxyPayQrcode(item)">代付码</view>
<view class="action-btn-ghost primary m-l-16" @click="goToReusePrescription(item.id)">复用</view>
<view class="action-btn-ghost default m-l-16" @click="openPrescriptionDetail(item.id)">查看处方</view>
</view>
@@ -282,6 +284,9 @@
<u-modal v-model="show" title="操作提示" :show-cancel-button="true" :content="content" @confirm="confirm"
confirm-color="#00A88A" cancel-color="#8A92A3"></u-modal>
<!-- 患者代付码弹窗出示小程序码给患者扫码支付待支付处方订单 -->
<proxy-pay-qrcode-modal v-model="proxyPayShow" :order-id="proxyPayOrderId"></proxy-pay-qrcode-modal>
<d-loading-page :loading="loading"></d-loading-page>
<!-- 底部操作栏 -->
@@ -320,6 +325,7 @@ import {
VIP_FEATURE,
} from '@/utils/vip.js'
import VipGate from '@/components/vip-gate/vip-gate.vue'
import ProxyPayQrcodeModal from '@/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue'
function emptyHealthInquiry() {
return {
@@ -372,6 +378,7 @@ function normalizeRegisterDetail(raw) {
export default {
components: {
VipGate,
ProxyPayQrcodeModal,
},
data() {
return {
@@ -401,6 +408,10 @@ export default {
rxLoadingMore: false,
/** 履约诊所 VIP permissions全局判断用 */
storeVipPermissions: [],
/** 患者代付码弹窗显隐 */
proxyPayShow: false,
/** 代付码目标订单ID处方关联的待支付商品订单 */
proxyPayOrderId: 0,
};
},
computed: {
@@ -453,6 +464,18 @@ export default {
payTextByIsPay(isPay) {
return Number(isPay) === 1 ? '已支付' : '未支付'
},
/**
* 打开患者代付码弹窗:出示小程序码给患者扫码支付本单
* 弹窗打开时组件内部按最新订单状态请求出码(已支付/已取消会被后端拦截并提示)
*/
openProxyPayQrcode(item) {
if (!item || !item.order_id) {
uni.showToast({ title: '该处方暂无待支付订单', icon: 'none' })
return
}
this.proxyPayOrderId = item.order_id
this.proxyPayShow = true
},
apiOk(res) {
return res && (res.code === 0 || res.errcode === 0)
},

View File

@@ -80,11 +80,16 @@ export function resolveLoggedInTarget() {
export function redirectIfLoggedIn() {
const target = resolveLoggedInTarget();
if (target && target.url) {
uni.reLaunch({ url: target.url });
return true;
}
return false;
if (!target || !target.url) return false;
// 启动尚未挂上页面时不要 reLaunch否则微信会报 appLaunch with non-empty page stack
const pages = getCurrentPages();
if (!pages || !pages.length) return false;
const cur = pages[pages.length - 1];
const curPath = cur && cur.route ? '/' + cur.route : '';
const targetPath = String(target.url).split('?')[0];
if (curPath === targetPath) return true;
uni.reLaunch({ url: target.url });
return true;
}
export function saveLoginSession(result) {