Phase 0 基础设施:
- statusTag.ts: getStatusInlineStyle() 移除内联 borderRadius/padding/fontSize,仅返回 {background, color}
- 新增 SEVERITY_COLORS + getSeverityStyle() + getSeverityLabel() 统一告警严重程度样式
- variables.scss: 新增 9 个语义颜色别名 ($success/$danger/$warning/$info 等)
- mixins.scss: 新增 status-inline mixin 统一状态标签样式
- 7 个消费者页面添加 @include status-inline CSS 补偿
Phase 1 HIGH 修复 (4 页面):
- P46 随访管理: 移除 getTypeStyle() 硬编码 fontSize,替换文字 Loading 为组件
- P45 咨询详情医护: 添加 Loading/ErrorState 三态模板 + error ref
- P02 健康数据: 添加 loading ref + Loading 组件 + 错误 toast 提示
- P48 告警中心: 替换本地 SEVERITY_COLORS/SEVERITY_LABELS 为 statusTag.ts 导出
Phase 2 全局一致性:
- 2.1 触控补全: 17 页面为可点击元素添加 min-height: $touch-min
- 2.2 字号替换: 19 文件 31 处硬编码 px → Design Token CSS 变量
- 2.3 颜色替换: 18 文件 ~50 处硬编码十六进制 → SCSS 语义变量
- 2.4 elder-mode.scss: 新增 9 个选择器到触控放大清单
Phase 3 LOW 修复:
- 3.1 统一 Loading: 21 页面旧式文字加载 → <Loading> 组件
- 3.2 useElderClass: 8 页面补全长者模式 class 绑定
- 3.3 零散修复: 按钮 44px→48px,诊断记录添加 scroll-view 无限加载
同时新增 UniApp (Vue 3 + Vite) 小程序完整代码库 (146 文件)
133 lines
6.9 KiB
Vue
133 lines
6.9 KiB
Vue
<template>
|
||
<view :class="['detail-page', elderClass]">
|
||
<view class="detail-header">
|
||
<view class="back-btn" @tap="goBack"><text class="back-text">返回</text></view>
|
||
<text class="header-title">预约详情</text>
|
||
<view class="header-placeholder" />
|
||
</view>
|
||
|
||
<Loading v-if="loading" text="加载中..." />
|
||
<ErrorState v-else-if="error || !appointment" text="未找到预约信息" />
|
||
<template v-else>
|
||
<view class="status-card">
|
||
<view :class="['status-tag', statusInfo.className]">
|
||
<text class="status-tag-text">{{ statusInfo.label }}</text>
|
||
</view>
|
||
<text class="status-doctor">{{ appointment.doctor_name }}</text>
|
||
<text class="status-dept">{{ appointment.department || '' }}</text>
|
||
</view>
|
||
|
||
<view class="info-section">
|
||
<text class="section-title">预约信息</text>
|
||
<view class="info-item">
|
||
<view class="info-label-wrap"><text class="info-icon-serif">患</text><text class="info-label">就诊人</text></view>
|
||
<text class="info-value">{{ appointment.patient_name }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<view class="info-label-wrap"><text class="info-icon-serif">日</text><text class="info-label">就诊日期</text></view>
|
||
<text class="info-value info-date">{{ appointment.appointment_date }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<view class="info-label-wrap"><text class="info-icon-serif">时</text><text class="info-label">就诊时段</text></view>
|
||
<text class="info-value info-time">{{ appointment.start_time }} - {{ appointment.end_time }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<view class="info-label-wrap"><text class="info-icon-serif">号</text><text class="info-label">预约单号</text></view>
|
||
<text class="info-value info-id">{{ appointment.id }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="appointment.status === 'pending' || appointment.status === 'confirmed'" class="tips-card">
|
||
<text class="tips-title">温馨提示</text>
|
||
<text class="tips-text">请按预约时间提前15分钟到达,携带有效身份证件和医保卡。</text>
|
||
</view>
|
||
|
||
<view v-if="canCancel" class="bottom-bar">
|
||
<view :class="['cancel-btn', cancelling ? 'cancel-disabled' : '']" @tap="cancelling ? undefined : handleCancel">
|
||
<text class="cancel-text">{{ cancelling ? '处理中...' : '取消预约' }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { getAppointment, cancelAppointment, type Appointment } from '@/services/appointment'
|
||
import ErrorState from '@/components/ErrorState.vue'
|
||
import Loading from '@/components/Loading.vue'
|
||
import { useElderClass } from '@/composables/useElderClass'
|
||
|
||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||
pending: { label: '待确认', className: 'tag-pending' },
|
||
confirmed: { label: '已确认', className: 'tag-confirmed' },
|
||
cancelled: { label: '已取消', className: 'tag-cancelled' },
|
||
completed: { label: '已完成', className: 'tag-completed' },
|
||
}
|
||
|
||
const { elderClass } = useElderClass()
|
||
const appointment = ref<Appointment | null>(null)
|
||
const loading = ref(true)
|
||
const error = ref(false)
|
||
const cancelling = ref(false)
|
||
let id = ''
|
||
|
||
const statusInfo = computed(() => appointment.value ? (STATUS_MAP[appointment.value.status] || { label: appointment.value.status, className: 'tag-pending' }) : { label: '未知', className: 'tag-pending' })
|
||
const canCancel = computed(() => appointment.value && (appointment.value.status === 'pending' || appointment.value.status === 'confirmed'))
|
||
const goBack = () => uni.navigateBack()
|
||
|
||
const handleCancel = async () => {
|
||
if (!appointment.value || cancelling.value) return
|
||
const res = await uni.showModal({ title: '确认取消', content: '确定要取消此预约吗?取消后需重新预约。' })
|
||
if (!res.confirm) return
|
||
cancelling.value = true
|
||
try {
|
||
await cancelAppointment(appointment.value.id, appointment.value.version)
|
||
uni.showToast({ title: '已取消预约', icon: 'success' })
|
||
setTimeout(() => uni.navigateBack(), 1500)
|
||
} catch { uni.showToast({ title: '取消失败', icon: 'none' }) }
|
||
finally { cancelling.value = false }
|
||
}
|
||
|
||
onLoad((query) => {
|
||
id = query?.id || ''
|
||
if (!id) { error.value = true; loading.value = false; return }
|
||
loading.value = true
|
||
getAppointment(id).then(data => { appointment.value = data }).catch(() => { error.value = true }).finally(() => { loading.value = false })
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.detail-page { min-height: 100vh; background: $bg; }
|
||
.detail-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: $card; }
|
||
.back-btn { padding: 6px 12px; }
|
||
.back-text { font-size: var(--tk-font-body); color: $pri; }
|
||
.header-title { font-size: var(--tk-font-body); font-weight: 500; color: $tx; }
|
||
.header-placeholder { width: 50px; }
|
||
.status-card { @include card; margin: 16px 24px; text-align: center; }
|
||
.status-tag { display: inline-block; padding: 4px 16px; border-radius: 20px; margin-bottom: 8px; }
|
||
.tag-pending { background: rgba(250,173,20,0.15); }
|
||
.tag-confirmed { background: rgba($pri, 0.1); }
|
||
.tag-cancelled { background: rgba(0,0,0,0.05); }
|
||
.tag-completed { background: rgba(82,196,26,0.1); }
|
||
.status-tag-text { font-size: var(--tk-font-cap); }
|
||
.status-doctor { font-size: var(--tk-font-title); font-weight: 600; color: $tx; display: block; margin-top: 8px; }
|
||
.status-dept { font-size: var(--tk-font-caption); color: $tx3; display: block; margin-top: 4px; }
|
||
.info-section { @include card; margin: 0 24px 16px; }
|
||
.section-title { font-size: var(--tk-font-body); font-weight: 500; color: $tx; margin-bottom: 12px; display: block; }
|
||
.info-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid rgba(0,0,0,0.04); }
|
||
.info-item:last-child { border-bottom: none; }
|
||
.info-label-wrap { display: flex; align-items: center; gap: 8px; }
|
||
.info-icon-serif { width: 24px; height: 24px; border-radius: 4px; background: rgba($pri, 0.08); @include flex-center; font-size: var(--tk-font-micro); color: $pri; }
|
||
.info-label { font-size: var(--tk-font-cap); color: $tx3; }
|
||
.info-value { font-size: var(--tk-font-body); color: $tx; }
|
||
.tips-card { @include card; margin: 0 24px 16px; background: rgba(250,173,20,0.08); }
|
||
.tips-title { font-size: var(--tk-font-cap); font-weight: 500; color: $wrn; display: block; margin-bottom: 6px; }
|
||
.tips-text { font-size: var(--tk-font-cap); color: $tx2; line-height: 1.6; }
|
||
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 12px 24px; background: $card; box-shadow: $shadow-sm; }
|
||
.cancel-btn { height: $touch-min; border: 1px solid $dan; border-radius: $r; @include flex-center; }
|
||
.cancel-disabled { opacity: 0.5; }
|
||
.cancel-text { font-size: var(--tk-font-body); color: $dan; }
|
||
</style>
|