diff --git a/.cursor/rules/Business-Shared.mdc b/.cursor/rules/Business-Shared.mdc new file mode 100644 index 0000000..a32aabe --- /dev/null +++ b/.cursor/rules/Business-Shared.mdc @@ -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`)加下拉刷新 +- 开方编辑器、录入/申请表单、登录、协议、静态成功页不加 diff --git a/api/clinicAdmin.js b/api/clinicAdmin.js index 6bfb1dd..080132b 100644 --- a/api/clinicAdmin.js +++ b/api/clinicAdmin.js @@ -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' }); +} diff --git a/api/clinicSalesperson.js b/api/clinicSalesperson.js index 5b8c8bc..a7fe0e0 100644 --- a/api/clinicSalesperson.js +++ b/api/clinicSalesperson.js @@ -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 diff --git a/api/platformAdmin.js b/api/platformAdmin.js index f8abe76..9c3cc3f 100644 --- a/api/platformAdmin.js +++ b/api/platformAdmin.js @@ -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 diff --git a/api/reception.js b/api/reception.js index 3a5b8fd..3aeef0d 100644 --- a/api/reception.js +++ b/api/reception.js @@ -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({ diff --git a/api/salesperson.js b/api/salesperson.js index 60bbfd0..4e2ce5b 100644 --- a/api/salesperson.js +++ b/api/salesperson.js @@ -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 diff --git a/components/account-switch/account-switch-panel.vue b/components/account-switch/account-switch-panel.vue index d574cb0..c7cf5c6 100644 --- a/components/account-switch/account-switch-panel.vue +++ b/components/account-switch/account-switch-panel.vue @@ -1,11 +1,13 @@ @@ -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) { diff --git a/subPackages/sub_business_shared/common/display.js b/subPackages/sub_business_shared/common/display.js index dfe1a0c..cb9d403 100644 --- a/subPackages/sub_business_shared/common/display.js +++ b/subPackages/sub_business_shared/common/display.js @@ -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, } } diff --git a/subPackages/sub_business_shared/common/pullRefreshMixin.js b/subPackages/sub_business_shared/common/pullRefreshMixin.js new file mode 100644 index 0000000..cdd694a --- /dev/null +++ b/subPackages/sub_business_shared/common/pullRefreshMixin.js @@ -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) { /* 未开启原生下拉时忽略 */ } + }, + }, +} diff --git a/subPackages/sub_business_shared/components/DrawerPageContainer.vue b/subPackages/sub_business_shared/components/DrawerPageContainer.vue index 0f23d68..1180ef5 100644 --- a/subPackages/sub_business_shared/components/DrawerPageContainer.vue +++ b/subPackages/sub_business_shared/components/DrawerPageContainer.vue @@ -1,11 +1,13 @@