feat: 按 API 拆分规则新建 api/workbench.js(xk-api、unwrapXkApi 取值)。
新增 components/ai-brief-card/ai-brief-card.vue:卡片 + 全文底部弹层(page-container + v-if 拦截返回键),每日首弹用 storage 按天记录;模板遵守小程序限制(无 ?./??、无 :class 函数调用)。
工作台页面挂载卡片并接入 refreshWorkbenchData,组件内部按「门店+日期」去重——页面 onShow 反复触发也只拉一次,切店/跨天自动重拉(跨天首次会重新生成并触发新一天的首弹)。
This commit is contained in:
@@ -89,16 +89,7 @@ export function imageOrderInfo(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/*咨询列表 挂号列表
|
||||
order_id int 是 订单id
|
||||
*/
|
||||
export function registerList(data) {
|
||||
return req.request({
|
||||
url: 'register/list',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// 老 Yii register/list(挂号列表)已迁移到 xk-api doctor-reception-wx/patient-list(见 api/reception.js getPatientList)
|
||||
// 咨询列表角标
|
||||
/**
|
||||
* @param {Object} data
|
||||
|
||||
@@ -52,6 +52,24 @@ export function getAppointmentUpcomingListApi(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 医生代患者改期 xk-api POST /doctor-reception-wx/reschedule-appointment(register_id/visit_date/slot_id/remark,写改期日志) */
|
||||
export function rescheduleAppointmentApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/reschedule-appointment',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 医生本人号源日历 xk-api GET /doctor-reception-wx/my-schedule-calendar(改期抽屉选新日期/时段) */
|
||||
export function getMyScheduleCalendarApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/my-schedule-calendar',
|
||||
method: 'GET',
|
||||
data: data || {}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取患者详情(挂号维度的就诊信息,不含处方列表)
|
||||
export function getPatientItem(id) {
|
||||
return req.request({
|
||||
@@ -611,6 +629,15 @@ export function aiGenerationDetailApi(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 标记 AI 生成记录为已读(熄灭未读微光) */
|
||||
export function aiMarkGenerationReadApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/ai-mark-generation-read',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** AI 处方药名模糊对照 */
|
||||
export function aiMatchPrescriptionDrugsApi(data) {
|
||||
return req.request({
|
||||
|
||||
16
api/workbench.js
Normal file
16
api/workbench.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 医生工作台扩展接口(xk-api,取值用 code / result / message,配合 unwrapXkApi)
|
||||
* 看板统计/预约明细等历史接口在 api/reception.js,本文件放工作台新增能力(AI 简报等)
|
||||
*/
|
||||
import {
|
||||
req
|
||||
} from '@/common/js/index.js';
|
||||
|
||||
/** AI 今日门诊简报 xk-api GET /doctor-reception-wx/ai-daily-brief(store_id 必传;refresh=1 强制重新生成,后端每天首次生成后按天缓存) */
|
||||
export function getAiDailyBriefApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/ai-daily-brief',
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import $store from '@/store/index.js';
|
||||
import {
|
||||
registerList,
|
||||
} from '@/api/consult.js'
|
||||
class common {
|
||||
constructor(arg) {
|
||||
this.timer = null
|
||||
|
||||
527
components/ai-brief-card/ai-brief-card.vue
Normal file
527
components/ai-brief-card/ai-brief-card.vue
Normal file
@@ -0,0 +1,527 @@
|
||||
<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>
|
||||
|
||||
<!-- 每日首弹/查看全文弹层:page-container 拦截返回键防误退(规则:抽屉必须 page-container + v-if) -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container v-if="popupShow" :show="popupShow" :overlay="false" @leave="onPageLeave" />
|
||||
<!-- #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>
|
||||
<text class="ai-popup__title">{{ popupDateText }}</text>
|
||||
</view>
|
||||
<text class="ai-popup__close" @click="closePopup">关闭</text>
|
||||
</view>
|
||||
<scroll-view scroll-y="true" class="ai-popup__scroll">
|
||||
<view v-if="chips.length" class="ai-brief-card__chips ai-popup__chips">
|
||||
<view v-for="chip in chips" :key="chip.label" class="ai-chip" :class="{ 'ai-chip--hl': chip.highlight }">
|
||||
<text class="ai-chip__value">{{ chip.value }}</text>
|
||||
<text class="ai-chip__label">{{ chip.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-popup__body">
|
||||
<view v-for="(section, idx) in sections" :key="idx" class="ai-popup__section">
|
||||
<text v-if="section.title" class="ai-brief-card__section-tag">{{ section.title }}</text>
|
||||
<text class="ai-popup__section-text">{{ section.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-popup__meta">由 AI 生成,仅供参考{{ generatedAtText ? ' · ' + generatedAtText : '' }}</view>
|
||||
</scroll-view>
|
||||
<view class="ai-popup__footer">
|
||||
<view class="ai-popup__btn" @click="closePopup">知道了</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* AI 今日门诊简报卡片 + 每日首弹弹层(医生工作台)
|
||||
* 数据流:挂载/切换门店时拉 /doctor-reception-wx/ai-daily-brief(后端每天首次生成并按天缓存);
|
||||
* 弹层策略:生成成功且本机今天没弹过时自动弹一次全文(storage 按天记录),之后可点「查看全文」再看;
|
||||
* 「重新生成」传 refresh=1 强制后端重生成(原行 UPDATE,不新增缓存行)
|
||||
*/
|
||||
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: {
|
||||
/** 当前门店ID:简报按 doctor+store 缓存(排班/挂号统计都是按店口径),切店需重拉 */
|
||||
storeId: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
brief: null,
|
||||
loading: false,
|
||||
// 接口本身失败(网络/鉴权),区别于 brief.status=0 的「AI 生成失败」
|
||||
fetchError: false,
|
||||
popupShow: false,
|
||||
// 去重键(storeId_日期):页面 onShow 会反复触发刷新,同店同天已拉过则跳过,
|
||||
// 跨天/切店后 key 变化自动重拉(跨天首次会重新生成并触发每日首弹)
|
||||
lastFetchKey: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** 生成时间展示(缓存行的更新时间) */
|
||||
generatedAtText() {
|
||||
return this.brief && this.brief.generated_at ? this.brief.generated_at : ''
|
||||
},
|
||||
/** 弹层标题:简报日期 */
|
||||
popupDateText() {
|
||||
const date = this.brief && this.brief.brief_date ? this.brief.brief_date : ''
|
||||
return date ? date + ' 今日简报' : '今日简报'
|
||||
},
|
||||
/**
|
||||
* 正文分段:后端约定 AI 输出「【今日概览】...【重点提醒】...【时间安排建议】...」,
|
||||
* 按【标题】切段渲染更易读;AI 偶发不按格式时兜底整段展示
|
||||
*/
|
||||
sections() {
|
||||
const content = this.brief && this.brief.content ? this.brief.content : ''
|
||||
if (!content) return []
|
||||
const matches = []
|
||||
const reg = /【([^】]+)】([^【]*)/g
|
||||
let m = reg.exec(content)
|
||||
while (m) {
|
||||
matches.push({ title: m[1] || '', text: (m[2] || '').trim() })
|
||||
m = reg.exec(content)
|
||||
}
|
||||
if (matches.length === 0) return [{ title: '', text: content }]
|
||||
return matches
|
||||
},
|
||||
/**
|
||||
* 快照统计芯片:读生成时落库的 data_snapshot,保证数字与 AI 文本口径一致
|
||||
* (值班余号/待接诊/今日到期预约/未来7天预约)
|
||||
*/
|
||||
chips() {
|
||||
const data = this.brief && this.brief.data ? this.brief.data : null
|
||||
if (!data) return []
|
||||
const today = data.today || {}
|
||||
const schedule = data.schedule || {}
|
||||
const upcoming = data.upcoming || {}
|
||||
const waitAccept = Number(today.wait_accept) || 0
|
||||
const dueWaiting = Number(today.appointment_due_waiting) || 0
|
||||
return [
|
||||
{
|
||||
label: '今日值班',
|
||||
value: schedule.is_work ? (Number(schedule.left) || 0) + '/' + (Number(schedule.total) || 0) + '余号' : '休诊',
|
||||
highlight: false
|
||||
},
|
||||
{ label: '待接诊', value: waitAccept, highlight: waitAccept > 0 },
|
||||
{ label: '今日到期预约', value: dueWaiting, highlight: dueWaiting > 0 },
|
||||
{ label: '未来7天预约', value: Number(upcoming.total) || 0, highlight: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchBrief(false)
|
||||
},
|
||||
methods: {
|
||||
/** 今天日期字符串(去重键用,与后端 brief_date 同格式) */
|
||||
todayStr() {
|
||||
const d = new Date()
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n)
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
||||
},
|
||||
/**
|
||||
* 拉取简报;refresh=true 强制后端重新生成(用户点「重新生成」按钮)
|
||||
* 页面 onShow / 切店都会调用本方法,靠 lastFetchKey 去重避免重复请求;
|
||||
* 首次生成可能耗时十几秒(走 AI),期间维持加载态
|
||||
*/
|
||||
async fetchBrief(refresh) {
|
||||
if (!this.storeId || this.loading) return
|
||||
const fetchKey = String(this.storeId) + '_' + this.todayStr()
|
||||
if (!refresh && this.brief && this.lastFetchKey === fetchKey) return
|
||||
this.loading = true
|
||||
this.fetchError = false
|
||||
try {
|
||||
const params = { store_id: this.storeId }
|
||||
if (refresh) params.refresh = 1
|
||||
const res = await getAiDailyBriefApi(params)
|
||||
const { ok, payload } = unwrapXkApi(res)
|
||||
if (ok && payload) {
|
||||
this.brief = payload
|
||||
this.lastFetchKey = fetchKey
|
||||
this.maybeAutoPopup()
|
||||
} else {
|
||||
this.fetchError = true
|
||||
}
|
||||
} catch (e) {
|
||||
this.fetchError = true
|
||||
} finally {
|
||||
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
|
||||
},
|
||||
/** 重新生成(防抖:加载中不响应) */
|
||||
onRefreshTap() {
|
||||
if (this.loading) return
|
||||
this.fetchBrief(true)
|
||||
},
|
||||
/** 网络失败重试(读缓存即可,不强制重生成) */
|
||||
onRetryTap() {
|
||||
if (this.loading) return
|
||||
this.fetchBrief(false)
|
||||
},
|
||||
/** 点「查看全文」手动打开弹层 */
|
||||
openPopup() {
|
||||
this.popupShow = true
|
||||
},
|
||||
closePopup() {
|
||||
this.popupShow = false
|
||||
},
|
||||
/** u-popup 双向同步(蒙层点击关闭等场景) */
|
||||
onPopupInput(val) {
|
||||
this.popupShow = val
|
||||
},
|
||||
/** page-container 返回键触发:关闭弹层而非退出页面 */
|
||||
onPageLeave() {
|
||||
this.popupShow = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 卡片:白底 + AI 主题绿 1px 边框 + 微光(规则:不用左侧竖色条,用同色系边框+外发光强调) */
|
||||
.ai-brief-card {
|
||||
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;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
padding: 2rpx 12rpx;
|
||||
background: linear-gradient(135deg, #6acdbb, #4db6a3);
|
||||
border-radius: 8rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&__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 {
|
||||
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;
|
||||
}
|
||||
|
||||
&__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;
|
||||
background: rgba(106, 205, 187, 0.12);
|
||||
border-radius: 999rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 快照统计芯片 */
|
||||
.ai-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 18rpx;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 999rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&--hl {
|
||||
border-color: rgba(251, 140, 0, 0.45);
|
||||
background: rgba(251, 140, 0, 0.08);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 650;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全文弹层 */
|
||||
.ai-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx 20rpx;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
&__title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&__close {
|
||||
font-size: 26rpx;
|
||||
color: #909399;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__chips {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__section-text {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.8;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
padding: 16rpx 0 32rpx;
|
||||
border-top: 1px solid #f2f3f5;
|
||||
font-size: 22rpx;
|
||||
color: #b0b3b8;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 16rpx 32rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
&__btn {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #6acdbb, #4db6a3);
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 页面级返回拦截:防止用户按返回键直接退出页面(规则要求抽屉必须配 page-container + v-if) -->
|
||||
<!-- pageGuard 用于两级抽屉场景:原生容器一次 leave 后失效,关掉子抽屉后需重挂载才能拦截下一次返回 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="show"
|
||||
v-if="pageGuard && show"
|
||||
:show="show"
|
||||
:overlay="false"
|
||||
@leave="onPageLeave"
|
||||
@@ -62,6 +63,19 @@
|
||||
<view class="appt-row__line2">
|
||||
<text v-if="showDoctor && item.doctor_name" class="appt-row__meta">医生:{{ item.doctor_name }}</text>
|
||||
<text v-if="showStore && item.store_name" class="appt-row__meta">{{ item.store_name }}</text>
|
||||
<!-- 脱敏手机号展示(拦截器已解码+脱敏),拨号用未脱敏的 call_mobile -->
|
||||
<text v-if="item.patient_mobile" class="appt-row__meta">{{ item.patient_mobile }}</text>
|
||||
</view>
|
||||
<!-- 行操作:电话沟通 + 医生代改期(仅医生端、已支付待接诊单可改) -->
|
||||
<view class="appt-row__actions" v-if="item.call_mobile || (canReschedule && item.can_reschedule)">
|
||||
<view v-if="item.call_mobile" class="appt-action" @click="callPatient(item)">
|
||||
<u-icon name="phone" size="24" color="#6acdbb"></u-icon>
|
||||
<text class="appt-action__text">电话</text>
|
||||
</view>
|
||||
<view v-if="canReschedule && item.can_reschedule" class="appt-action" @click="openReschedule(item)">
|
||||
<u-icon name="calendar" size="24" color="#6acdbb"></u-icon>
|
||||
<text class="appt-action__text">改期</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="appt-row__status" :class="item.status_class">{{ item.status_text }}</text>
|
||||
@@ -79,6 +93,85 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 医生代改期子抽屉:日历日期 + 时段选择(数据来自 my-schedule-calendar),z-index 需高于明细抽屉 -->
|
||||
<u-popup
|
||||
v-if="role === 'doctor'"
|
||||
:value="rescheduleShow"
|
||||
mode="bottom"
|
||||
border-radius="24"
|
||||
height="72%"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="true"
|
||||
:z-index="10090"
|
||||
@input="onReschedulePopupInput"
|
||||
@close="closeReschedule"
|
||||
>
|
||||
<view class="resch">
|
||||
<view class="appt-drawer__header">
|
||||
<text class="appt-drawer__title">修改预约时间</text>
|
||||
<text class="appt-drawer__close" @click="closeReschedule">关闭</text>
|
||||
</view>
|
||||
<view class="resch__current" v-if="rescheduleTarget">
|
||||
{{ rescheduleTarget.patient_name || '就诊人' }} · 当前:{{ rescheduleTarget.visit_date }} {{ rescheduleTarget.slot_label }}
|
||||
</view>
|
||||
<scroll-view scroll-y="true" class="resch__body">
|
||||
<view v-if="calendarLoading" class="appt-drawer__loading">
|
||||
<u-loading mode="circle" size="36"></u-loading>
|
||||
</view>
|
||||
<block v-else>
|
||||
<view v-if="calendarDays.length > 0">
|
||||
<scroll-view scroll-x="true" class="resch__days" :show-scrollbar="false">
|
||||
<view class="resch__days-inner">
|
||||
<view
|
||||
v-for="(day, idx) in calendarDays"
|
||||
:key="day.date"
|
||||
class="resch-day"
|
||||
:class="{ 'resch-day--active': idx === selectedDayIndex, 'resch-day--disabled': !day.is_work || day.is_full }"
|
||||
@click="selectDay(idx)"
|
||||
>
|
||||
<view class="resch-day__week">{{ day.week }}</view>
|
||||
<view class="resch-day__date">{{ day.date_text }}</view>
|
||||
<view class="resch-day__state" :class="{ 'resch-day__state--rest': !day.is_work }">{{ day.state_text }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="resch__slots" v-if="selectedDay && selectedDay.is_work && selectedDay.slots.length > 1">
|
||||
<view
|
||||
v-for="slot in selectedDay.slots"
|
||||
:key="slot.slot_id"
|
||||
class="resch-slot"
|
||||
:class="{ 'resch-slot--active': slot.slot_id === selectedSlotId, 'resch-slot--disabled': slot.is_full || slot.is_expired }"
|
||||
@click="selectSlot(slot)"
|
||||
>
|
||||
<view class="resch-slot__name">{{ slot.slot_name }}</view>
|
||||
<view class="resch-slot__time">{{ slot.start_time }}-{{ slot.end_time }}</view>
|
||||
<view class="resch-slot__state">{{ slot.state_text }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="resch__tip" v-if="selectedDay && !selectedDay.is_work">该日休诊,请选择其他日期</view>
|
||||
</view>
|
||||
<view class="resch__tip" v-else>暂无可约号源</view>
|
||||
<view class="resch__remark">
|
||||
<input
|
||||
class="resch__remark-input"
|
||||
v-model="rescheduleRemark"
|
||||
placeholder="备注(选填,如:电话已与患者确认)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
<view class="resch__footer">
|
||||
<view class="resch__selected" v-if="rescheduleSlotLabel">改期至:{{ rescheduleSlotLabel }}</view>
|
||||
<view
|
||||
class="resch__btn"
|
||||
:class="{ 'resch__btn--disabled': !canConfirmReschedule || rescheduleSubmitting }"
|
||||
@click="confirmReschedule"
|
||||
>{{ rescheduleSubmitting ? '提交中...' : '确认修改' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -90,7 +183,7 @@
|
||||
* 且三端接口鉴权头不同(医生 JWT / _clinicAdmin / _platformAdmin),收敛在组件内按 role 分发最直观。
|
||||
* 数据分页由后端 appointment-upcoming-list 提供(page/page_size/visit_date),滚动到底自动翻页。
|
||||
*/
|
||||
import { getAppointmentUpcomingListApi } from '@/api/reception.js'
|
||||
import { getAppointmentUpcomingListApi, rescheduleAppointmentApi, getMyScheduleCalendarApi } from '@/api/reception.js'
|
||||
import { getClinicAdminAppointmentUpcomingList } from '@/api/clinicAdmin.js'
|
||||
import { getPlatformAdminAppointmentUpcomingList } from '@/api/platformAdmin.js'
|
||||
|
||||
@@ -116,6 +209,18 @@ export default {
|
||||
loading: false,
|
||||
// 请求序号:快速切日期/连点加载更多时,据此丢弃晚到的过期响应,防止旧数据覆盖新列表
|
||||
reqSeq: 0,
|
||||
// page-container 重挂载开关(见模板注释)
|
||||
pageGuard: true,
|
||||
// ---- 医生代改期子抽屉状态(仅 role=doctor 使用) ----
|
||||
rescheduleShow: false,
|
||||
rescheduleTarget: null,
|
||||
rescheduleRemark: '',
|
||||
rescheduleSubmitting: false,
|
||||
calendarLoading: false,
|
||||
// 医生本人号源日历(my-schedule-calendar),展示文案在 applyCalendar 预计算
|
||||
calendarDays: [],
|
||||
selectedDayIndex: 0,
|
||||
selectedSlotId: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -135,6 +240,38 @@ export default {
|
||||
showStore() {
|
||||
return this.role !== 'clinic'
|
||||
},
|
||||
/** 仅医生端可代患者改期(管理员端明细只读;医生不受「就诊当天不可改」限制) */
|
||||
canReschedule() {
|
||||
return this.role === 'doctor'
|
||||
},
|
||||
/** 当前选中的日期对象 */
|
||||
selectedDay() {
|
||||
return this.calendarDays[this.selectedDayIndex] || null
|
||||
},
|
||||
/** 当前选中的时段对象 */
|
||||
selectedSlot() {
|
||||
if (!this.selectedDay || this.selectedSlotId < 0) return null
|
||||
const slots = this.selectedDay.slots || []
|
||||
for (let i = 0; i < slots.length; i++) {
|
||||
if (slots[i].slot_id === this.selectedSlotId) return slots[i]
|
||||
}
|
||||
return null
|
||||
},
|
||||
/** 改期目标文案(底部确认条) */
|
||||
rescheduleSlotLabel() {
|
||||
if (!this.selectedDay || !this.selectedSlot) return ''
|
||||
let label = this.selectedDay.date + ' ' + (this.selectedSlot.slot_name || '')
|
||||
const s = this.selectedSlot
|
||||
if (s.start_time && s.end_time && !(s.start_time === '00:00' && s.end_time === '23:59')) {
|
||||
label += ' ' + s.start_time + '-' + s.end_time
|
||||
}
|
||||
return label
|
||||
},
|
||||
/** 是否可提交:选中了出诊日的可约时段 */
|
||||
canConfirmReschedule() {
|
||||
return !!(this.selectedDay && this.selectedDay.is_work && this.selectedSlot
|
||||
&& !this.selectedSlot.is_full && !this.selectedSlot.is_expired)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/** 打开时按入口预筛选并拉第一页(每次打开都取最新数据) */
|
||||
@@ -153,6 +290,16 @@ export default {
|
||||
this.show = v
|
||||
},
|
||||
onPageLeave() {
|
||||
// 返回手势优先关改期子抽屉,主抽屉保留(微信每页只允许一个 page-container,收口在此分发)
|
||||
if (this.rescheduleShow) {
|
||||
this.rescheduleShow = false
|
||||
// 原生容器一次 leave 后即失效,重挂载后才能继续拦截「关主抽屉」的下一次返回
|
||||
this.pageGuard = false
|
||||
this.$nextTick(() => {
|
||||
this.pageGuard = true
|
||||
})
|
||||
return
|
||||
}
|
||||
this.close()
|
||||
},
|
||||
/** 切换日期筛选:回到第一页重查 */
|
||||
@@ -222,7 +369,7 @@ export default {
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 行数据装饰:把状态样式类、短日期先算进数据(模板禁止 :class 调函数)
|
||||
* 行数据装饰:把状态样式类、短日期、可改期标记先算进数据(模板禁止 :class 调函数)
|
||||
*/
|
||||
decorateItem(item) {
|
||||
let statusClass = 'is-normal'
|
||||
@@ -238,8 +385,125 @@ export default {
|
||||
status_class: statusClass,
|
||||
// 列表里日期缩短为 MM-DD,完整年份对当周预约无增量信息
|
||||
visit_date_short: (item.visit_date || '').slice(5),
|
||||
// 仅「已支付 + 待接诊」可改期(待支付单让患者先支付或取消,已接诊单改期无意义)
|
||||
can_reschedule: item.is_pay === 1 && item.status === 1,
|
||||
}
|
||||
},
|
||||
/** 拨打就诊人电话(call_mobile 拦截器解码不脱敏,专供拨号) */
|
||||
callPatient(item) {
|
||||
const phone = item.call_mobile || ''
|
||||
if (!phone) {
|
||||
uni.showToast({ title: '暂无联系电话', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.makePhoneCall({ phoneNumber: phone })
|
||||
},
|
||||
/** 打开改期子抽屉并拉取医生本人号源日历(每次打开取最新余号) */
|
||||
openReschedule(item) {
|
||||
this.rescheduleTarget = item
|
||||
this.rescheduleRemark = ''
|
||||
this.rescheduleShow = true
|
||||
this.calendarLoading = true
|
||||
this.calendarDays = []
|
||||
this.selectedSlotId = -1
|
||||
getMyScheduleCalendarApi().then((res) => {
|
||||
this.calendarLoading = false
|
||||
if (res && res.code === 0 && res.result) {
|
||||
this.applyCalendar(res.result)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.calendarLoading = false
|
||||
})
|
||||
},
|
||||
closeReschedule() {
|
||||
this.rescheduleShow = false
|
||||
},
|
||||
onReschedulePopupInput(v) {
|
||||
this.rescheduleShow = v
|
||||
},
|
||||
/**
|
||||
* 预处理日历:MM-DD 短日期、余号/约满/休诊文案、标记当前预约时段
|
||||
* 与患者端挂号日历同一套预处理逻辑(模板禁止插值调方法,全部提前算好)
|
||||
*/
|
||||
applyCalendar(calendar) {
|
||||
const target = this.rescheduleTarget || {}
|
||||
const days = (calendar && calendar.days) || []
|
||||
const processed = days.map((day) => {
|
||||
const dateParts = (day.date || '').split('-')
|
||||
const dateText = dateParts.length === 3 ? dateParts[1] + '-' + dateParts[2] : day.date
|
||||
let stateText = '休诊'
|
||||
if (day.is_work) {
|
||||
stateText = day.is_full ? '约满' : '余' + day.left
|
||||
}
|
||||
const slots = (day.slots || []).map((slot) => {
|
||||
let slotState = '余' + slot.left
|
||||
if (slot.is_expired) slotState = '已过时'
|
||||
else if (slot.is_full) slotState = '约满'
|
||||
if (day.date === target.visit_date && slot.slot_id === target.slot_id) slotState = '当前'
|
||||
return { ...slot, state_text: slotState }
|
||||
})
|
||||
return { ...day, date_text: dateText, state_text: stateText, slots }
|
||||
})
|
||||
this.calendarDays = processed
|
||||
// 默认选第一个可约日期(出诊且未约满),都不可约时停在今天
|
||||
let idx = 0
|
||||
for (let i = 0; i < processed.length; i++) {
|
||||
if (processed[i].is_work && !processed[i].is_full) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
this.selectedDayIndex = idx
|
||||
this.autoSelectSlot()
|
||||
},
|
||||
/** 切换改期日期:默认选中该日第一个可约时段 */
|
||||
selectDay(idx) {
|
||||
this.selectedDayIndex = idx
|
||||
this.autoSelectSlot()
|
||||
},
|
||||
/** 选择时段:约满/已过时不可选 */
|
||||
selectSlot(slot) {
|
||||
if (slot.is_full || slot.is_expired) return
|
||||
this.selectedSlotId = slot.slot_id
|
||||
},
|
||||
/** 自动选中当日第一个可约时段(全天单时段直接选中) */
|
||||
autoSelectSlot() {
|
||||
this.selectedSlotId = -1
|
||||
const day = this.calendarDays[this.selectedDayIndex]
|
||||
if (!day || !day.is_work) return
|
||||
const slots = day.slots || []
|
||||
for (let i = 0; i < slots.length; i++) {
|
||||
if (!slots[i].is_full && !slots[i].is_expired) {
|
||||
this.selectedSlotId = slots[i].slot_id
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
/** 提交改期:成功后刷新明细列表并通知父组件刷新工作台统计 */
|
||||
confirmReschedule() {
|
||||
if (!this.canConfirmReschedule || this.rescheduleSubmitting || !this.rescheduleTarget) return
|
||||
this.rescheduleSubmitting = true
|
||||
rescheduleAppointmentApi({
|
||||
register_id: this.rescheduleTarget.register_id,
|
||||
visit_date: this.selectedDay.date,
|
||||
slot_id: this.selectedSlotId,
|
||||
remark: this.rescheduleRemark,
|
||||
}).then((res) => {
|
||||
this.rescheduleSubmitting = false
|
||||
if (res && res.code === 0) {
|
||||
this.rescheduleShow = false
|
||||
uni.showToast({ title: '改期成功', icon: 'success' })
|
||||
this.resetAndLoad()
|
||||
// 父组件(工作台)可据此刷新预约统计卡片
|
||||
this.$emit('rescheduled', res.result || null)
|
||||
} else {
|
||||
uni.showToast({ title: (res && res.message) || '改期失败', icon: 'none' })
|
||||
}
|
||||
}).catch(() => {
|
||||
this.rescheduleSubmitting = false
|
||||
uni.showToast({ title: '改期失败,请稍后重试', icon: 'none' })
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -422,4 +686,195 @@ export default {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
// 行内操作按钮(电话/改期):1px 同色系边框 + 微光,不用左侧色条
|
||||
.appt-row__actions {
|
||||
display: flex;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.appt-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 20rpx;
|
||||
margin-right: 16rpx;
|
||||
border: 1px solid rgba(106, 205, 187, 0.3);
|
||||
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
|
||||
border-radius: 999rpx;
|
||||
|
||||
&__text {
|
||||
margin-left: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
}
|
||||
|
||||
// 医生代改期子抽屉
|
||||
.resch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.resch__current {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #6c7380;
|
||||
}
|
||||
|
||||
.resch__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16rpx 24rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resch__days {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resch__days-inner {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.resch-day {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 124rpx;
|
||||
padding: 16rpx 0;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
|
||||
&__week {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: 22rpx;
|
||||
color: #6c7380;
|
||||
margin: 6rpx 0;
|
||||
}
|
||||
|
||||
&__state {
|
||||
font-size: 22rpx;
|
||||
color: #6acdbb;
|
||||
|
||||
&--rest {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: rgba(106, 205, 187, 0.6);
|
||||
background: rgba(106, 205, 187, 0.08);
|
||||
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
.resch__slots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.resch-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 210rpx;
|
||||
padding: 14rpx 0;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
|
||||
&__name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 22rpx;
|
||||
color: #6c7380;
|
||||
margin: 4rpx 0;
|
||||
}
|
||||
|
||||
&__state {
|
||||
font-size: 22rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: rgba(106, 205, 187, 0.6);
|
||||
background: rgba(106, 205, 187, 0.08);
|
||||
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
.resch__tip {
|
||||
padding: 32rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.resch__remark {
|
||||
margin: 8rpx 0 32rpx;
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
height: 76rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 26rpx;
|
||||
color: #1d2129;
|
||||
background: #f5f6f8;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.resch__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.resch__selected {
|
||||
font-size: 26rpx;
|
||||
color: #6acdbb;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.resch__btn {
|
||||
height: 86rpx;
|
||||
line-height: 86rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: #6acdbb;
|
||||
border-radius: 64rpx;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 今日简报:每天首次进入自动弹全文(storage 按天记录),之后常驻卡片可随时查看 -->
|
||||
<ai-brief-card ref="aiBrief" :store-id="store_id"></ai-brief-card>
|
||||
|
||||
<!-- 未来7天预约卡片:点击总数或某天打开明细抽屉 -->
|
||||
<view class="appt-panel">
|
||||
<view class="appt-panel__head" @click="openApptDrawer('')">
|
||||
@@ -414,12 +417,13 @@
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 未来7天预约明细抽屉(page-container + v-if 由组件内部处理) -->
|
||||
<!-- 未来7天预约明细抽屉(page-container + v-if 由组件内部处理);改期成功后静默刷新看板统计 -->
|
||||
<appointment-upcoming-drawer
|
||||
v-model="apptDrawerShow"
|
||||
role="doctor"
|
||||
:days="appointmentUpcoming.days"
|
||||
:default-date="apptDefaultDate"
|
||||
@rescheduled="onAppointmentRescheduled"
|
||||
></appointment-upcoming-drawer>
|
||||
|
||||
<d-top-message ref="topMessage"></d-top-message>
|
||||
@@ -437,9 +441,6 @@ import {
|
||||
homeSession,
|
||||
getNotice
|
||||
} from '@/api/all.js'
|
||||
import {
|
||||
registerList
|
||||
} from '../../api/consult';
|
||||
import {
|
||||
getAcceptingPatientList as fetchAcceptingPatientListApi,
|
||||
getOnlineConsultationPatientListApi,
|
||||
@@ -640,6 +641,10 @@ 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(),
|
||||
@@ -855,6 +860,10 @@ export default {
|
||||
this.apptDefaultDate = date || '';
|
||||
this.apptDrawerShow = true;
|
||||
},
|
||||
/** 抽屉内改期成功:预约分布/待接诊数已变化,静默刷新看板(不打断用户继续操作抽屉) */
|
||||
onAppointmentRescheduled() {
|
||||
this.fetchWorkbenchStats({ silent: true });
|
||||
},
|
||||
fetchWorkbenchStats(options = {}) {
|
||||
const silent = options.silent === true;
|
||||
const range = options.range || this.statsRange;
|
||||
|
||||
@@ -2,21 +2,13 @@ import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { disconnectWebSocket } from '@/utils/ws/initWebSocket.js';
|
||||
|
||||
import {
|
||||
registerList
|
||||
} from '@/api/consult.js'
|
||||
Vue.use(Vuex)
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
utils: null,
|
||||
consult: [], //咨询会话列表
|
||||
docSession: [], //医生首页消息会话列表
|
||||
status: 1
|
||||
},
|
||||
mutations: {
|
||||
SET_CONSULT(state, res) {
|
||||
state.consult = res;
|
||||
},
|
||||
SET_DOCSESSION(state, res) {
|
||||
state.docSession = res;
|
||||
},
|
||||
@@ -54,20 +46,8 @@ const store = new Vuex.Store({
|
||||
closeWebSocket() {
|
||||
disconnectWebSocket();
|
||||
},
|
||||
// 获取挂号信息
|
||||
getConsult({
|
||||
state,
|
||||
commit
|
||||
}) {
|
||||
registerList({
|
||||
status: state.status,
|
||||
page: 1
|
||||
}).then(res => {
|
||||
if (res.errcode == 0) {
|
||||
commit('SET_CONSULT', res.data);
|
||||
}
|
||||
})
|
||||
},
|
||||
// 老 getConsult(Yii register/list 轮询挂号列表)已随接口迁移 xk-api 一并移除:
|
||||
// 挂号列表页自身分页拉取(patient-list),无页面消费该全局状态
|
||||
}
|
||||
})
|
||||
export default store
|
||||
|
||||
@@ -48,11 +48,13 @@
|
||||
:class="{
|
||||
'ai-hist-item--active': activeId == row.id,
|
||||
'ai-hist-item--pending': row.pending,
|
||||
'ai-hist-item--unread': row.unread,
|
||||
}"
|
||||
:data-id="row.id"
|
||||
@click="onSelectHistoryTap"
|
||||
>
|
||||
<text class="fs-26 font-bold ai-hist-name">{{ row.nameText }}</text>
|
||||
<text v-if="row.unread" class="ai-unread-badge fs-20">未读</text>
|
||||
<text class="fs-22 color-sub">{{ row.typeText }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -65,6 +67,33 @@
|
||||
/>
|
||||
<view v-else-if="matchLoading" class="p-32 color-sub fs-24">加载历史处方…</view>
|
||||
<block v-else>
|
||||
<!-- 多方案对比 Tab(模板禁止方法调用,用 multiTabView) -->
|
||||
<scroll-view
|
||||
v-if="multiTabView.length > 1"
|
||||
scroll-x
|
||||
class="ai-multi-tabs"
|
||||
>
|
||||
<view class="ai-multi-tabs-inner flex-row">
|
||||
<view
|
||||
v-for="tab in multiTabView"
|
||||
:key="tab.index"
|
||||
class="ai-multi-tab"
|
||||
:class="{
|
||||
'ai-multi-tab--active': tab.active,
|
||||
'ai-multi-tab--fail': tab.failed,
|
||||
'ai-multi-tab--unread': tab.unread,
|
||||
}"
|
||||
:data-index="tab.index"
|
||||
@click="onMultiTabTap"
|
||||
>
|
||||
<text class="fs-22">{{ tab.label }}</text>
|
||||
<text v-if="tab.unread" class="ai-unread-badge fs-18">新</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-if="multiFailMsg" class="ai-multi-fail">
|
||||
<text class="fs-22">{{ multiFailMsg }}</text>
|
||||
</view>
|
||||
<view v-if="!matchResult && !lastDurationMs" class="p-32 color-sub fs-24">请选择历史或生成处方</view>
|
||||
<view v-else-if="!matchResult && lastDurationMs" class="p-32 color-sub fs-24">
|
||||
本次生成未产出对照结果(耗时 {{ durationText }})
|
||||
@@ -315,6 +344,7 @@ import {
|
||||
aiFeatureGateApi,
|
||||
aiGeneratePrescriptionApi,
|
||||
aiListGenerationsApi,
|
||||
aiMarkGenerationReadApi,
|
||||
aiMatchPrescriptionDrugsApi,
|
||||
aiSavePrescriptionDrugSelectionApi,
|
||||
getProcessRuleList,
|
||||
@@ -408,6 +438,11 @@ export default {
|
||||
consentText: '',
|
||||
consentVisible: false,
|
||||
consentSubmitting: false,
|
||||
/** 多方案对比:后端 prescriptions[] */
|
||||
multiPrescriptions: [],
|
||||
multiActiveIndex: 0,
|
||||
multiTabView: [],
|
||||
multiFailMsg: '',
|
||||
importBtnStyle: {
|
||||
backgroundColor: '#6ACDBB',
|
||||
color: '#fff',
|
||||
@@ -539,6 +574,10 @@ export default {
|
||||
this.rxChildProcessRuleName = ''
|
||||
this.rxProcessRuleNote = ''
|
||||
this.activeId = 0
|
||||
this.multiPrescriptions = []
|
||||
this.multiActiveIndex = 0
|
||||
this.multiTabView = []
|
||||
this.multiFailMsg = ''
|
||||
this.rebuildMatchView()
|
||||
},
|
||||
onClose() {
|
||||
@@ -603,9 +642,44 @@ export default {
|
||||
typeText: typeLabel + ' · ' + cnt + '味 · ' + this.formatTime(row.created_at) + (row.duration_ms ? (' · ' + this.formatDurationMs(row.duration_ms)) : ''),
|
||||
prescription_type: pt,
|
||||
pending: false,
|
||||
unread: Number(row.is_read) !== 1,
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 本地同步已读状态(历史 + 多方案)
|
||||
*/
|
||||
patchLocalRead(generationId) {
|
||||
const gid = Number(generationId || 0)
|
||||
if (gid <= 0) return
|
||||
const hist = Array.isArray(this.historyList) ? this.historyList : []
|
||||
for (let i = 0; i < hist.length; i++) {
|
||||
if (Number(hist[i].id) === gid) {
|
||||
this.$set(this.historyList[i], 'is_read', 1)
|
||||
}
|
||||
}
|
||||
const multi = Array.isArray(this.multiPrescriptions) ? this.multiPrescriptions : []
|
||||
for (let j = 0; j < multi.length; j++) {
|
||||
if (Number((multi[j] && multi[j].generation_id) || 0) === gid) {
|
||||
this.$set(this.multiPrescriptions[j], 'is_read', 1)
|
||||
}
|
||||
}
|
||||
this.rebuildHistoryView()
|
||||
this.rebuildMultiTabView()
|
||||
},
|
||||
/**
|
||||
* 查看方案后标记已读(失败不阻断)
|
||||
*/
|
||||
async markReadIfNeeded(generationId, alreadyRead) {
|
||||
const gid = Number(generationId || 0)
|
||||
if (gid <= 0 || Number(alreadyRead) === 1) return
|
||||
try {
|
||||
await aiMarkGenerationReadApi({ generation_id: gid })
|
||||
this.patchLocalRead(gid)
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 开始生成:取消旧选中、清空对照预览,历史顶部插入「正在生成中」占位
|
||||
* 避免仍高亮旧方案,造成「还在看上一次结果」的错觉
|
||||
@@ -908,6 +982,66 @@ export default {
|
||||
onGenFieldBlur() {
|
||||
this.editingKey = ''
|
||||
},
|
||||
/**
|
||||
* 重建多方案 Tab 展示数据(模板禁止方法调用,先算进 data)
|
||||
*/
|
||||
rebuildMultiTabView() {
|
||||
const list = Array.isArray(this.multiPrescriptions) ? this.multiPrescriptions : []
|
||||
const active = Number(this.multiActiveIndex || 0)
|
||||
const tabs = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const p = list[i] || {}
|
||||
const failed = p.ok === false
|
||||
const unread = !failed && Number(p.is_read) !== 1
|
||||
tabs.push({
|
||||
index: i,
|
||||
label: failed ? ('方案' + (i + 1) + '失败') : ('方案' + (i + 1)),
|
||||
failed: failed,
|
||||
active: i === active,
|
||||
unread: unread,
|
||||
})
|
||||
}
|
||||
this.multiTabView = tabs
|
||||
const cur = list[active] || null
|
||||
this.multiFailMsg =
|
||||
cur && cur.ok === false ? String(cur.error_msg || '该方案生成失败') : ''
|
||||
},
|
||||
/**
|
||||
* 应用某一份多方案到右侧对照区
|
||||
*/
|
||||
applyMultiSlot(slot, index) {
|
||||
this.multiActiveIndex = Number(index || 0)
|
||||
this.rebuildMultiTabView()
|
||||
if (!slot || slot.ok === false) {
|
||||
this.matchResult = null
|
||||
this.matchedList = []
|
||||
this.unmatchedList = []
|
||||
this.conflictView = []
|
||||
this.applyRxMeta({
|
||||
dosage: 0,
|
||||
day_dosage: 0,
|
||||
reason: '',
|
||||
basis: '',
|
||||
prescription_name: '',
|
||||
duration_ms: slot ? slot.duration_ms : 0,
|
||||
})
|
||||
this.activeId = Number((slot && slot.generation_id) || 0)
|
||||
this.rebuildMatchView()
|
||||
return
|
||||
}
|
||||
this.activeId = Number(slot.generation_id || 0)
|
||||
this.applyMatch(slot.match || null)
|
||||
this.applyConflict(slot.conflict || null)
|
||||
this.applyRxMeta(slot)
|
||||
this.markReadIfNeeded(this.activeId, slot.is_read)
|
||||
},
|
||||
onMultiTabTap(e) {
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
|
||||
const idx = Number(ds.index || 0)
|
||||
const slot = this.multiPrescriptions[idx]
|
||||
if (!slot) return
|
||||
this.applyMultiSlot(slot, idx)
|
||||
},
|
||||
async onGenerate() {
|
||||
if (this.generating) return
|
||||
const chief = String(this.aiChief || '').trim()
|
||||
@@ -928,6 +1062,9 @@ export default {
|
||||
this.closeGenModal()
|
||||
this.beginGeneratingPlaceholder()
|
||||
this.generating = true
|
||||
this.multiPrescriptions = []
|
||||
this.multiActiveIndex = 0
|
||||
this.rebuildMultiTabView()
|
||||
try {
|
||||
const req = {
|
||||
register_id: Number(this.registerId),
|
||||
@@ -946,10 +1083,15 @@ export default {
|
||||
const res = await aiGeneratePrescriptionApi(req)
|
||||
const data = this.unwrapBiz(res) || {}
|
||||
const durationText = this.formatDurationMs(data.duration_ms)
|
||||
const list = Array.isArray(data.prescriptions) ? data.prescriptions : []
|
||||
// 业务软失败:HTTP 成功但 ok=false,toast 警告而非失败
|
||||
if (data.ok === false) {
|
||||
this.endGeneratingPlaceholder()
|
||||
this.resetPreview()
|
||||
if (list.length > 1) {
|
||||
this.multiPrescriptions = list
|
||||
this.rebuildMultiTabView()
|
||||
}
|
||||
if (data.generation_id) {
|
||||
await this.loadHistory()
|
||||
this.activeId = Number(data.generation_id)
|
||||
@@ -958,10 +1100,36 @@ export default {
|
||||
return
|
||||
}
|
||||
await this.loadHistory()
|
||||
if (list.length > 1) {
|
||||
this.multiPrescriptions = list
|
||||
let firstOk = 0
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i] && list[i].ok !== false) {
|
||||
firstOk = i
|
||||
break
|
||||
}
|
||||
}
|
||||
this.applyMultiSlot(list[firstOk], firstOk)
|
||||
let okN = 0
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i] && list[i].ok !== false) okN++
|
||||
}
|
||||
const failN = list.length - okN
|
||||
const base =
|
||||
failN > 0
|
||||
? '已生成' + okN + '套可用(' + failN + '套失败)'
|
||||
: '已生成' + okN + '套方案'
|
||||
uni.showToast({
|
||||
title: durationText ? base + '(' + durationText + ')' : base,
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
this.activeId = Number(data.generation_id || 0)
|
||||
this.applyMatch(data.match || null)
|
||||
this.applyConflict(data.conflict || null)
|
||||
this.applyRxMeta(data)
|
||||
this.markReadIfNeeded(this.activeId, 0)
|
||||
uni.showToast({
|
||||
title: durationText ? ('已生成(耗时 ' + durationText + ')') : '已生成',
|
||||
icon: 'success',
|
||||
@@ -986,6 +1154,11 @@ export default {
|
||||
},
|
||||
async selectHistoryById(id) {
|
||||
this.activeId = id
|
||||
// 点历史时退出本次多方案对比
|
||||
this.multiPrescriptions = []
|
||||
this.multiActiveIndex = 0
|
||||
this.multiTabView = []
|
||||
this.multiFailMsg = ''
|
||||
this.matchLoading = true
|
||||
try {
|
||||
let pt = Number(this.prescriptionType || 0)
|
||||
@@ -1008,6 +1181,8 @@ export default {
|
||||
prescription_type: pt,
|
||||
})
|
||||
const data = this.unwrapBiz(res) || {}
|
||||
// match 后端已标已读,本地熄灭未读微光
|
||||
this.patchLocalRead(id)
|
||||
this.applyMatch(data)
|
||||
this.applyConflict(data.conflict || null)
|
||||
this.applyRxMeta({
|
||||
@@ -1099,6 +1274,13 @@ export default {
|
||||
this.visible = false
|
||||
},
|
||||
onConfirmImport() {
|
||||
if (this.multiPrescriptions && this.multiPrescriptions.length > 1) {
|
||||
const cur = this.multiPrescriptions[this.multiActiveIndex]
|
||||
if (!cur || cur.ok === false) {
|
||||
uni.showToast({ title: '请切换到成功方案后再导入', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
const drugs = []
|
||||
for (let i = 0; i < this.matchedList.length; i++) {
|
||||
const m = this.matchedList[i]
|
||||
@@ -1175,6 +1357,44 @@ export default {
|
||||
font-size: 24rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
.ai-multi-tabs {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.ai-multi-tabs-inner {
|
||||
padding: 0 8rpx 8rpx;
|
||||
}
|
||||
.ai-multi-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 20rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid rgba(106, 205, 187, 0.35);
|
||||
background: #fff;
|
||||
color: #666;
|
||||
box-shadow: 0 0 6px rgba(106, 205, 187, 0.12);
|
||||
}
|
||||
.ai-multi-tab--active {
|
||||
border-color: rgba(106, 205, 187, 0.8);
|
||||
color: #2a9d8f;
|
||||
background: rgba(106, 205, 187, 0.12);
|
||||
box-shadow: 0 0 8px rgba(106, 205, 187, 0.22);
|
||||
}
|
||||
.ai-multi-tab--fail {
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
color: #ef4444;
|
||||
box-shadow: 0 0 6px rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
.ai-multi-fail {
|
||||
margin: 0 8rpx 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
color: #ef4444;
|
||||
}
|
||||
.ai-rx-meta {
|
||||
margin: 0 0 12rpx;
|
||||
background: #fff;
|
||||
@@ -1280,6 +1500,38 @@ export default {
|
||||
border: 1px dashed #6acdbb;
|
||||
background: #f0faf8;
|
||||
}
|
||||
/* 未读:同色系边框 + 微光脉冲(禁止左侧色条) */
|
||||
.ai-hist-item--unread {
|
||||
margin: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid rgba(106, 205, 187, 0.55);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(106, 205, 187, 0.12),
|
||||
0 0 12rpx rgba(106, 205, 187, 0.35);
|
||||
animation: ai-unread-glow 2.2s ease-in-out infinite;
|
||||
}
|
||||
.ai-unread-badge {
|
||||
color: #2a9d8f;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.ai-multi-tab--unread {
|
||||
border-color: rgba(106, 205, 187, 0.7);
|
||||
box-shadow: 0 0 10rpx rgba(106, 205, 187, 0.35);
|
||||
animation: ai-unread-glow 2.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ai-unread-glow {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(106, 205, 187, 0.1),
|
||||
0 0 8rpx rgba(106, 205, 187, 0.25);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(106, 205, 187, 0.22),
|
||||
0 0 16rpx rgba(106, 205, 187, 0.45);
|
||||
}
|
||||
}
|
||||
.ai-hist-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:title="item.patient"
|
||||
:meta="(item.sex % 2 != 0 ? '男' : '女') + ' · ' + item.age + '岁'"
|
||||
:time="item.created_at"
|
||||
:rows="['推广员:' + (item.salesperson_name || '无')]"
|
||||
: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)"
|
||||
@@ -36,10 +36,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 到店接诊列表:已从老 Yii register/list 迁移到 xk-api patient-list(status 单状态筛选 + 分页)
|
||||
// 取值按 xk-api 约定 code/result(见 Api-Response.mdc)
|
||||
import {
|
||||
registerList,
|
||||
listMain
|
||||
} from "@/api/consult.js";
|
||||
getPatientList
|
||||
} from "@/api/reception.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -85,31 +86,6 @@
|
||||
},
|
||||
onHide() {
|
||||
this.isHide = true;
|
||||
},
|
||||
watch: {
|
||||
consult: {
|
||||
deep: true,
|
||||
immediate: false,
|
||||
handler(e) {
|
||||
this.list[this.current] = e.list;
|
||||
this.page = 1;
|
||||
this.totalPage = e.pagination.totalPage;
|
||||
this.$nextTick(() => {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: 300,
|
||||
});
|
||||
});
|
||||
if (this.page >= this.totalPage) {
|
||||
this.status = "nomore";
|
||||
} else {
|
||||
this.status = "loadmore";
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
onLoad(op) {
|
||||
this.type = op["type"] ? Number(op["type"]) : 1;
|
||||
@@ -142,19 +118,32 @@
|
||||
this.loading = true;
|
||||
this.getList(true);
|
||||
},
|
||||
/**
|
||||
* 拉取当前 tab 的挂号列表(xk-api patient-list 分页版)
|
||||
* 第一页替换、后续页追加(老代码把新页拼在前面会导致越翻越乱,迁移时一并修正)
|
||||
*/
|
||||
getList(is_stop = false) {
|
||||
registerList({
|
||||
type: this.type,
|
||||
getPatientList({
|
||||
store_id: uni.getStorageSync('store_id') || '11001',
|
||||
status: this.statued,
|
||||
page: this.page,
|
||||
page_size: 10,
|
||||
}).then((res) => {
|
||||
if (res.errcode == 0) {
|
||||
this.list[this.current] = [
|
||||
...res.data.list,
|
||||
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.data.pagination.totalPage;
|
||||
this.totalPage = res.result.page_count || 1;
|
||||
if (this.page >= this.totalPage) {
|
||||
this.status = "nomore";
|
||||
} else {
|
||||
@@ -165,7 +154,7 @@
|
||||
is_stop && uni.stopPullDownRefresh();
|
||||
});
|
||||
} else {
|
||||
this.$toast(res.msg);
|
||||
this.$toast(res.message || '加载失败');
|
||||
is_stop && uni.stopPullDownRefresh();
|
||||
this.loading = false;
|
||||
if (this.page >= this.totalPage) {
|
||||
@@ -178,9 +167,6 @@
|
||||
},
|
||||
change(index) {
|
||||
this.current = index;
|
||||
this.$store.state.status = index + 1;
|
||||
// this.init();
|
||||
|
||||
if (index == 0) {
|
||||
this.statued = 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user