diff --git a/api/consult.js b/api/consult.js
index 96afc8f..2558c78 100644
--- a/api/consult.js
+++ b/api/consult.js
@@ -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
diff --git a/api/reception.js b/api/reception.js
index dd9619c..3a5b8fd 100644
--- a/api/reception.js
+++ b/api/reception.js
@@ -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({
diff --git a/api/workbench.js b/api/workbench.js
new file mode 100644
index 0000000..96fd6f2
--- /dev/null
+++ b/api/workbench.js
@@ -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
+ })
+}
diff --git a/common/common.js b/common/common.js
index 2742c82..aa816b8 100644
--- a/common/common.js
+++ b/common/common.js
@@ -1,7 +1,4 @@
import $store from '@/store/index.js';
-import {
- registerList,
-} from '@/api/consult.js'
class common {
constructor(arg) {
this.timer = null
diff --git a/components/ai-brief-card/ai-brief-card.vue b/components/ai-brief-card/ai-brief-card.vue
new file mode 100644
index 0000000..db1b39e
--- /dev/null
+++ b/components/ai-brief-card/ai-brief-card.vue
@@ -0,0 +1,527 @@
+
+
+
+
+
+
+ AI
+ 今日简报
+ {{ generatedAtText }}
+
+
+
+ 重新生成
+
+
+
+
+
+
+ AI 正在分析今日门诊数据…
+
+
+
+
+ 简报加载失败
+ 重试
+
+
+
+
+
+
+ {{ chip.value }}
+ {{ chip.label }}
+
+
+
+ AI 生成失败,请重试
+ 重新生成
+
+
+
+
+
+
+
+ {{ chip.value }}
+ {{ chip.label }}
+
+
+
+
+ {{ section.title }}
+ {{ section.text }}
+
+
+
+ 查看全文
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/appointment-upcoming-drawer/appointment-upcoming-drawer.vue b/components/appointment-upcoming-drawer/appointment-upcoming-drawer.vue
index 2dbe62a..33476b6 100644
--- a/components/appointment-upcoming-drawer/appointment-upcoming-drawer.vue
+++ b/components/appointment-upcoming-drawer/appointment-upcoming-drawer.vue
@@ -1,9 +1,10 @@
+
医生:{{ item.doctor_name }}
{{ item.store_name }}
+
+ {{ item.patient_mobile }}
+
+
+
+
+
+ 电话
+
+
+
+ 改期
+
{{ item.status_text }}
@@ -79,6 +93,85 @@
+
+
+
+
+
+
+ {{ rescheduleTarget.patient_name || '就诊人' }} · 当前:{{ rescheduleTarget.visit_date }} {{ rescheduleTarget.slot_label }}
+
+
+
+
+
+
+
+
+
+
+ {{ day.week }}
+ {{ day.date_text }}
+ {{ day.state_text }}
+
+
+
+
+
+ {{ slot.slot_name }}
+ {{ slot.start_time }}-{{ slot.end_time }}
+ {{ slot.state_text }}
+
+
+ 该日休诊,请选择其他日期
+
+ 暂无可约号源
+
+
+
+
+
+
@@ -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' })
+ })
+ },
},
}
@@ -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;
+ }
+}
diff --git a/pages/workbench/index.vue b/pages/workbench/index.vue
index b210263..efa60c8 100644
--- a/pages/workbench/index.vue
+++ b/pages/workbench/index.vue
@@ -95,6 +95,9 @@
+
+
+
@@ -414,12 +417,13 @@
-
+
@@ -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;
diff --git a/store/index.js b/store/index.js
index 065a2bf..1bfcc72 100644
--- a/store/index.js
+++ b/store/index.js
@@ -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
diff --git a/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue b/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
index f523a94..46eda6e 100644
--- a/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
+++ b/subPackages/sub_workbench/prescription_v2/components/AiPrescriptionDrawer.vue
@@ -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"
>
{{ row.nameText }}
+ 未读
{{ row.typeText }}
@@ -65,6 +67,33 @@
/>
加载历史处方…
+
+
+
+
+ {{ tab.label }}
+ 新
+
+
+
+
+ {{ multiFailMsg }}
+
请选择历史或生成处方
本次生成未产出对照结果(耗时 {{ 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;
diff --git a/subPackages/sub_workbench/workbench_consult.vue b/subPackages/sub_workbench/workbench_consult.vue
index f19dbfd..44d0044 100644
--- a/subPackages/sub_workbench/workbench_consult.vue
+++ b/subPackages/sub_workbench/workbench_consult.vue
@@ -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 @@