Post.vue 9.06 KB
<template>
  <div class="post-page">
    <div class="page-header">
      <h1>邮寄查询</h1>
    </div>

    <el-tabs v-model="activeTab">
      <!-- 快递查询 -->
      <el-tab-pane label="快递单号查询" name="query">
        <div class="tab-card">
          <div class="query-bar">
            <el-input v-model="expressNo" placeholder="输入快递单号(如 SF1234567890)" style="width:360px" clearable @keyup.enter="queryExpress" />
            <el-button type="primary" @click="queryExpress" :loading="querying">查询</el-button>
          </div>

          <div v-if="expressResult" class="express-result">
            <div class="express-header">
              <span class="no-label">单号:{{ expressNo }}</span>
              <el-tag type="success">{{ expressResult.status }}</el-tag>
            </div>
            <el-timeline class="logistics-timeline">
              <el-timeline-item
                v-for="(item, idx) in expressResult.timeline"
                :key="idx"
                :timestamp="item.time"
                :type="idx === 0 ? 'primary' : ''"
                :color="idx === 0 ? '#409eff' : ''"
              >
                <div class="timeline-content">
                  <span class="tl-status">{{ item.status }}</span>
                  <span class="tl-location">{{ item.location }}</span>
                  <span class="tl-desc">{{ item.desc }}</span>
                </div>
              </el-timeline-item>
            </el-timeline>
          </div>

          <div v-if="queried && !expressResult" class="no-result">
            <el-empty description="未查询到该快递单号的物流信息" />
          </div>
        </div>
      </el-tab-pane>

      <!-- 单号录入 -->
      <el-tab-pane label="快递单号录入" name="input">
        <div class="tab-card">
          <div class="section-tip">为申请单录入快递单号,便于实验室接收核对</div>
          <el-table :data="ordersForInput" border style="margin-bottom:16px">
            <el-table-column label="申请单编号" prop="orderNo" width="160" />
            <el-table-column label="患者姓名" prop="patientName" width="100" />
            <el-table-column label="检测项目" prop="productType" width="100" />
            <el-table-column label="提交日期" prop="submitDate" width="110" />
            <el-table-column label="快递单号">
              <template #default="{ row }">
                <el-input
                  v-model="expressInputs[row.id]"
                  placeholder="输入快递单号"
                  size="small"
                  :disabled="!!row.expressNo && !editing[row.id]"
                />
              </template>
            </el-table-column>
            <el-table-column label="操作" width="120">
              <template #default="{ row }">
                <el-button v-if="!row.expressNo || editing[row.id]" size="small" type="primary" @click="saveExpressNo(row)">保存</el-button>
                <el-button v-else size="small" plain @click="editing[row.id] = true">修改</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>

      <!-- 批量录入 -->
      <el-tab-pane label="批量录入" name="batch">
        <div class="tab-card">
          <div class="section-tip">每行一条记录,格式:申请单编号,快递单号</div>
          <div class="batch-example">
            示例:<code>JB2026030001,SF1234567890</code>
          </div>
          <el-input
            v-model="batchText"
            type="textarea"
            :rows="10"
            placeholder="JB2026030001,SF1234567890
JB2026030002,YT9876543210
JB2026030003,ZT1122334455"
            style="font-family:monospace; margin-bottom:12px"
          />
          <el-button type="primary" @click="saveBatch">批量提交</el-button>

          <div v-if="batchResult.length > 0" class="batch-result">
            <div class="result-title">提交结果:</div>
            <div v-for="r in batchResult" :key="r.orderNo" class="result-row">
              <el-icon :color="r.success ? '#52c41a' : '#ff4d4f'">
                <component :is="r.success ? 'Check' : 'Close'" />
              </el-icon>
              <span>{{ r.orderNo }} → {{ r.expressNo }}</span>
              <span class="result-msg" :class="r.success ? 'success' : 'error'">{{ r.msg }}</span>
            </div>
          </div>
        </div>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup lang="ts">
import { usePGXStore } from '@/stores/pgx'
import { ElMessage } from 'element-plus'

const pgxStore = usePGXStore()

const activeTab = ref('query')
const expressNo = ref('SF1234567890')
const querying = ref(false)
const queried = ref(false)
const expressResult = ref<any>(null)

async function queryExpress() {
  if (!expressNo.value) { ElMessage.warning('请输入快递单号'); return }
  querying.value = true
  queried.value = false
  await new Promise(r => setTimeout(r, 800))
  querying.value = false
  queried.value = true

  if (expressNo.value.startsWith('SF')) {
    expressResult.value = {
      status: '已签收',
      timeline: [
        { time: '2026-03-03 14:32', status: '已签收', location: '北京市朝阳区', desc: '快递已由前台签收' },
        { time: '2026-03-03 09:15', status: '派送中', location: '北京市朝阳区', desc: '快件正在派送中,请准备签收' },
        { time: '2026-03-03 07:40', status: '到达派件网点', location: '北京朝阳营业部', desc: '快件已到达派件网点' },
        { time: '2026-03-02 18:22', status: '运输中', location: '上海转运中心', desc: '快件已从上海转运,预计明日送达' },
        { time: '2026-03-02 10:05', status: '已揽收', location: '上海市浦东新区', desc: '快递员已上门取件' },
      ]
    }
  } else {
    expressResult.value = null
  }
}

const ordersForInput = computed(() => pgxStore.orders.filter(o => ['submitted','received','testing'].includes(o.status)))
const expressInputs = ref<Record<string, string>>({})
const editing = ref<Record<string, boolean>>({})

onMounted(() => {
  ordersForInput.value.forEach(o => {
    expressInputs.value[o.id] = o.expressNo || ''
  })
})

function saveExpressNo(row: any) {
  const no = expressInputs.value[row.id]
  if (!no) { ElMessage.warning('请输入快递单号'); return }
  pgxStore.updateOrder(row.id, { expressNo: no })
  editing.value[row.id] = false
  ElMessage.success(`已为 ${row.orderNo} 保存快递单号:${no}`)
}

const batchText = ref('')
const batchResult = ref<{ orderNo: string; expressNo: string; success: boolean; msg: string }[]>([])

function saveBatch() {
  if (!batchText.value.trim()) { ElMessage.warning('请输入数据'); return }
  const lines = batchText.value.trim().split('\n')
  const results: typeof batchResult.value = []
  lines.forEach(line => {
    const parts = line.split(',')
    if (parts.length < 2) {
      results.push({ orderNo: line, expressNo: '', success: false, msg: '格式错误' })
      return
    }
    const [orderNo, expressNo] = parts.map(s => s.trim())
    const order = pgxStore.orders.find(o => o.orderNo === orderNo)
    if (!order) {
      results.push({ orderNo, expressNo, success: false, msg: '申请单不存在' })
    } else {
      pgxStore.updateOrder(order.id, { expressNo })
      results.push({ orderNo, expressNo, success: true, msg: '保存成功' })
    }
  })
  batchResult.value = results
  const success = results.filter(r => r.success).length
  ElMessage.success(`批量录入完成:${success}/${results.length} 条成功`)
}
</script>

<style scoped>
.post-page { padding: 24px; }
.page-header { display: flex; align-items: center; margin-bottom: 20px; }
.page-header h1 { font-size: 20px; font-weight: 600; margin: 0; color: #1a1f2e; }

.tab-card { background: #fff; border-radius: 8px; padding: 20px; }
.query-bar { display: flex; gap: 12px; margin-bottom: 20px; }

.express-result { max-width: 600px; }
.express-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f2f5; }
.no-label { font-size: 15px; font-weight: 500; color: #1a1f2e; }
.logistics-timeline { padding-left: 8px; }
.timeline-content { display: flex; gap: 16px; align-items: baseline; flex-wrap: wrap; }
.tl-status { font-size: 14px; font-weight: 600; color: #1a1f2e; }
.tl-location { font-size: 12px; color: #888; }
.tl-desc { font-size: 13px; color: #555; }

.section-tip { color: #888; font-size: 13px; margin-bottom: 14px; }
.batch-example { font-size: 12px; color: #aaa; margin-bottom: 8px; }
code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-family: monospace; }
.batch-result { margin-top: 16px; border: 1px solid #e8ecf0; border-radius: 8px; padding: 12px; }
.result-title { font-size: 14px; font-weight: 500; color: #333; margin-bottom: 8px; }
.result-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; border-bottom: 1px solid #f5f5f5; }
.result-row:last-child { border-bottom: none; }
.result-msg.success { color: #52c41a; }
.result-msg.error { color: #ff4d4f; }
.no-result { padding: 40px; }
</style>