Files
track/track-uniapp/pages/scan/index.vue

163 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<!-- ======== 扫码按钮 ======== -->
<view class="scan-area">
<view class="scan-btn" @tap="handleScanCode">
<text class="scan-icon">📷</text>
<text class="scan-text">点击扫码</text>
<text class="scan-hint">调用原生摄像头扫描二维码/条码</text>
</view>
<!-- 手动输入 -->
<view class="manual-input">
<input
v-model="serialNumber"
class="input"
type="text"
maxlength="16"
placeholder="手动输入16位序列号"
@confirm="handleManualSearch"
/>
<button class="search-btn" @tap="handleManualSearch" :disabled="loading">
{{ loading ? '查询中' : '查询' }}
</button>
</view>
</view>
<!-- ======== 最近扫描 ======== -->
<view v-if="lastScanned" class="last-scan">
最近扫描: <text class="sn-text">{{ lastScanned }}</text>
</view>
<!-- ======== 加载中 / 错误 / 结果 ======== -->
<view v-if="loading" class="loading">查询中...</view>
<view v-if="error && !loading" class="error-box">{{ error }}</view>
<view v-if="product && !loading" class="result">
<view class="card">
<view class="card-header">
<text class="card-title">📦 产品信息</text>
<text :class="['status', statusClass(product.status)]">{{ statusLabel(product.status) }}</text>
</view>
<view class="info-grid">
<view class="info-item"><text class="label">序列号</text><text class="value sn">{{ product.serial_number }}</text></view>
<view class="info-item"><text class="label">订单编号</text><text class="value">{{ product.order_no }}</text></view>
<view v-if="product.material_id" class="info-item"><text class="label">物料ID</text><text class="value">{{ product.material_id }}</text></view>
</view>
</view>
<view class="card">
<view class="card-header">
<text class="card-title">📋 当前进度</text>
<text class="task-count">{{ product.top_level_tasks.length }} 个任务</text>
</view>
<view v-if="product.top_level_tasks.length === 0" class="empty-tasks">暂无关联任务</view>
<view v-for="task in product.top_level_tasks" :key="task.id" class="task-item">
<view class="task-info">
<text class="task-name">{{ task.task_name }}</text>
<text class="task-assignee">负责人: {{ task.assignee_id || '未分配' }}</text>
</view>
<text :class="['status-sm', statusClass(task.status)]">{{ statusLabel(task.status) }}</text>
</view>
</view>
</view>
<view v-if="!product && !loading && !error" class="empty">
<text class="empty-icon">📱</text>
<text class="empty-text">扫码或手动输入序列号查询产品进度</text>
</view>
<view class="version">T1.0.0</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { get } from "../../utils/request";
const serialNumber = ref("");
const lastScanned = ref("");
const loading = ref(false);
const error = ref("");
const product = ref(null);
const STATUS_MAP = { pending: "待处理", in_progress: "进行中", completed: "已完成", cancelled: "已取消" };
function statusLabel(s) { return STATUS_MAP[s] || s; }
function statusClass(s) {
switch (s) {
case "pending": return "status-yellow";
case "in_progress": return "status-blue";
case "completed": return "status-green";
default: return "status-gray";
}
}
async function doQuery(sn) {
if (!sn || sn.length < 8) { error.value = "序列号至少需要 8 位"; return; }
serialNumber.value = sn;
lastScanned.value = sn;
loading.value = true;
error.value = "";
product.value = null;
try { product.value = await get(`/products/scan/${sn}`); }
catch { /* request.js 已弹 toast */ }
finally { loading.value = false; }
}
function handleScanCode() {
uni.scanCode({
onlyFromCamera: true,
scanType: ["qrCode", "barCode"],
success(res) {
const sn = (res.result || "").replace(/[^a-zA-Z0-9]/g, "").slice(0, 16);
doQuery(sn);
},
fail(err) {
if (err.errMsg && err.errMsg.includes("cancel")) return;
uni.showToast({ title: "扫码失败,请重试", icon: "none" });
},
});
}
function handleManualSearch() { doQuery(serialNumber.value.trim()); }
</script>
<style scoped>
.page { min-height: 100vh; padding: 16px; padding-bottom: 80px; }
.scan-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 180px; background: linear-gradient(135deg, #2563EB, #3B82F6); border-radius: 16px; color: #fff; box-shadow: 0 4px 16px rgba(37,99,235,0.3); }
.scan-icon { font-size: 44px; margin-bottom: 6px; }
.scan-text { font-size: 18px; font-weight: 700; }
.scan-hint { font-size: 12px; opacity: 0.8; margin-top: 4px; }
.manual-input { display: flex; gap: 8px; margin-top: 12px; }
.input { flex: 1; height: 44px; padding: 0 12px; border: 1px solid #e5e7eb; border-radius: 10px; font-size: 14px; background: #fff; }
.search-btn { height: 44px; padding: 0 20px; background: #2563EB; color: #fff; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; line-height: 44px; }
.search-btn[disabled] { opacity: 0.6; }
.last-scan { font-size: 12px; color: #9ca3af; margin: 12px 0; }
.sn-text { font-family: monospace; color: #4b5563; }
.loading { text-align: center; padding: 32px 0; color: #6b7280; }
.error-box { padding: 12px; border-radius: 10px; background: #fef2f2; color: #dc2626; font-size: 13px; border: 1px solid #fecaca; }
.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.card-title { font-size: 15px; font-weight: 700; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.label { font-size: 12px; color: #9ca3af; }
.value { font-size: 14px; color: #1f2937; font-weight: 600; }
.sn { font-family: monospace; }
.status { font-size: 11px; padding: 2px 10px; border-radius: 20px; font-weight: 600; }
.status-sm { font-size: 11px; padding: 2px 8px; border-radius: 20px; font-weight: 600; flex-shrink: 0; }
.status-yellow { background: #fef3c7; color: #b45309; }
.status-blue { background: #dbeafe; color: #1d4ed8; }
.status-green { background: #dcfce7; color: #15803d; }
.status-gray { background: #f3f4f6; color: #6b7280; }
.task-count { font-size: 12px; color: #9ca3af; }
.empty-tasks { text-align: center; padding: 24px 0; color: #9ca3af; font-size: 13px; }
.task-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-top: 1px solid #f3f4f6; }
.task-info { flex: 1; min-width: 0; }
.task-name { font-size: 14px; font-weight: 600; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-assignee { font-size: 12px; color: #9ca3af; }
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 60px; color: #9ca3af; }
.empty-icon { font-size: 64px; margin-bottom: 12px; }
.empty-text { font-size: 14px; }
.version { text-align: center; font-size: 11px; color: #d1d5db; padding: 16px 0; }
</style>