163 lines
7.2 KiB
Vue
163 lines
7.2 KiB
Vue
<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>
|