feat: 工作台常用入口可批量编辑,简报改为横幅,列表补下拉刷新
各角色工作台支持批量保存常用入口和业务员看板;AI 简报改成单行横幅避免锁死滑动;仓库/提现/对账等列表统一下拉刷新,并补代付码弹窗。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
32
.cursor/rules/Business-Shared.mdc
Normal file
32
.cursor/rules/Business-Shared.mdc
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
description: 后台角色(诊所/平台/业务员)必须复用 sub_business_shared,禁止再复制商品订单等业务页
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 后台角色共享页(禁止重复造轮子)
|
||||
|
||||
诊所管理员 / 平台管理员 / 业务员(含省/市经理)的业务页优先复用 `subPackages/sub_business_shared`,用 `businessMixin` + `resolveBusinessContext` 按 `loginMode` 切 API,不要再复制一套列表/详情。
|
||||
|
||||
## 商品订单
|
||||
|
||||
- **必须**走 `sub_business_shared/order`(平台、业务员)
|
||||
- 诊所管理员继续用历史页 `sub_clinic_admin/order`,**本次不迁、也不要再抄一份**
|
||||
- API:平台 `platform-admin-order/*`,业务员 `salesperson-order/*`(`api/businessApi.js`)
|
||||
- **数据范围跟 PC**:业务员/省/市经理看名下门店全部订单(后端 `getMyStoreIds()` / `store.code = admin.code`),**禁止**前端再按 `salesperson_id` 过滤
|
||||
- **操作跟 PC**:业务员 `canShip/canRefund = false`(`context.js` 的 `bizCap`),详情按钮按能力显隐
|
||||
|
||||
```js
|
||||
// ❌ BAD:再写一套业务员订单页,或按推广员 ID 滤单
|
||||
// ✅ GOOD:工作台入口指向 /subPackages/sub_business_shared/order/index
|
||||
```
|
||||
|
||||
## 可复用清单(先找再写)
|
||||
|
||||
`business-page-layout`、`CollapsibleFilterPanel`、`ListSkeleton`、`InputListPage`、`xk-list-card`、`filterCache`、`pullRefreshMixin`
|
||||
|
||||
## 下拉刷新
|
||||
|
||||
- 自定义导航 + 内部列表:`scroll-view` 的 `refresher-enabled` + `isRefreshing`(对齐仓库/订单),mixin 用 `pullRefreshMixin`
|
||||
- 整页滚动列表:`pages.json` 开 `enablePullDownRefresh` + `onPullDownRefresh` + `uni.stopPullDownRefresh`
|
||||
- **禁止**给聊天页(`sub_online_reception/pages/chat`)加下拉刷新
|
||||
- 开方编辑器、录入/申请表单、登录、协议、静态成功页不加
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,6 +61,15 @@ export function rescheduleAppointmentApi(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 生成患者代付码 xk-api POST /doctor-reception-wx/proxy-pay-qrcode(order_id;返回 base64 小程序码 + 订单号/金额) */
|
||||
export function getProxyPayQrcodeApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/proxy-pay-qrcode',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 医生本人号源日历 xk-api GET /doctor-reception-wx/my-schedule-calendar(改期抽屉选新日期/时段) */
|
||||
export function getMyScheduleCalendarApi(data) {
|
||||
return req.request({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 挂载开关:关闭时先关 show,afterleave/超时后再卸,避免锁死页面滑动
|
||||
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: {
|
||||
// 弹层开合同步给工作台:简报弹层在 .content(z-index:1)内,盖不住自定义 tabbar(z-index:998),
|
||||
// 由页面在弹层打开时隐藏 tabbar,避免「知道了」被挡住
|
||||
popupShow(val) {
|
||||
this.$emit('popup-change', !!val)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchBrief(false)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Agent/模型偶发把简报包成 JSON 或 ```json```,先拆成【标题】正文再分段
|
||||
*/
|
||||
unwrapBriefContent(raw, depth) {
|
||||
let text = raw ? String(raw).trim() : ''
|
||||
const level = depth || 0
|
||||
if (!text || level > 4) return text
|
||||
const fenceStart = text.indexOf('```')
|
||||
if (fenceStart >= 0) {
|
||||
const after = text.slice(fenceStart).replace(/^```(?:json)?\s*/, '')
|
||||
const end = after.lastIndexOf('```')
|
||||
const inner = end >= 0 ? after.slice(0, end) : after
|
||||
if (inner) text = inner.trim()
|
||||
}
|
||||
try {
|
||||
const decoded = JSON.parse(text)
|
||||
if (typeof decoded === 'string') return this.unwrapBriefContent(decoded, level + 1)
|
||||
if (!decoded || typeof decoded !== 'object') return text
|
||||
if (Array.isArray(decoded)) {
|
||||
return this.flattenBriefSections(decoded) || text
|
||||
}
|
||||
const wrapFields = ['content', 'text', 'brief', 'summary']
|
||||
for (let i = 0; i < wrapFields.length; i++) {
|
||||
const val = decoded[wrapFields[i]]
|
||||
if (typeof val === 'string' && val.trim()) {
|
||||
return this.unwrapBriefContent(val, level + 1)
|
||||
}
|
||||
if (val && typeof val === 'object') {
|
||||
return this.unwrapBriefContent(JSON.stringify(val), level + 1)
|
||||
}
|
||||
}
|
||||
if (Array.isArray(decoded.sections)) {
|
||||
return this.flattenBriefSections(decoded.sections) || text
|
||||
}
|
||||
const parts = []
|
||||
const skip = { content: 1, text: 1, status: 1, brief_status: 1, brief: 1, summary: 1 }
|
||||
for (const key in decoded) {
|
||||
if (!Object.prototype.hasOwnProperty.call(decoded, key)) continue
|
||||
const value = decoded[key]
|
||||
if (typeof value !== 'string' && typeof value !== 'number') continue
|
||||
if (skip[key] || !String(key).trim() || !String(value).trim()) continue
|
||||
parts.push('【' + String(key).trim() + '】' + String(value).trim())
|
||||
}
|
||||
return parts.length ? parts.join('') : text
|
||||
} catch (e) {
|
||||
return text
|
||||
}
|
||||
},
|
||||
flattenBriefSections(list) {
|
||||
const parts = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const sec = list[i]
|
||||
if (typeof sec === 'string' && sec.trim()) {
|
||||
parts.push(sec.trim())
|
||||
continue
|
||||
}
|
||||
const item = sec || {}
|
||||
const title = String(item.title || item.name || item.label || '').trim()
|
||||
const body = String(item.text || item.content || item.body || '').trim()
|
||||
if (title && body) parts.push('【' + title + '】' + body)
|
||||
else if (body) parts.push(body)
|
||||
}
|
||||
return parts.join('')
|
||||
},
|
||||
/** 今天日期字符串(去重键用,与后端 brief_date 同格式) */
|
||||
todayStr() {
|
||||
const d = new Date()
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 挂载开关:关闭时先关 show,afterleave/超时后再卸
|
||||
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
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
267
components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue
Normal file
267
components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue
Normal 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-qrcode,scene=ppay={orderId},患者端解析链路零改动)。
|
||||
* 读值:xk-api 形态,拦截器已返回 body → code / result / message(unwrapXkApi)
|
||||
*/
|
||||
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>
|
||||
@@ -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 工作台卡片样式入口
|
||||
* 为什么做成组件:四个业务端「我的」页都需要同一入口,抽组件避免复制粘贴;
|
||||
* 偏好存本地 storage(wb_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;
|
||||
|
||||
372
components/workbench-fav-editor/workbench-fav-editor.vue
Normal file
372
components/workbench-fav-editor/workbench-fav-editor.vue
Normal 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>
|
||||
@@ -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,
|
||||
/** 内容区高度(px):layout 的 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
71
pages.json
71
pages.json
@@ -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",
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
62
subPackages/sub_business_shared/common/pullRefreshMixin.js
Normal file
62
subPackages/sub_business_shared/common/pullRefreshMixin.js
Normal 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) { /* 未开启原生下拉时忽略 */ }
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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 : ''
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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') || {}
|
||||
|
||||
@@ -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 : ''
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 只要 name;type 存在 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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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') || {}
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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] || '状态未知';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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?.())
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?.())
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) || []
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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?.()
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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?.()
|
||||
},
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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-list(status 单状态筛选 + 分页)
|
||||
// 取值按 xk-api 约定 code/result(见 Api-Response.mdc)
|
||||
import {
|
||||
getPatientList
|
||||
} from "@/api/reception.js";
|
||||
/**
|
||||
* 到店接诊列表:xk-api patient-list(status 单状态筛选 + 分页)
|
||||
* 交互: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;
|
||||
/* 纵向 flex:navbar / 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>
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user