first
This commit is contained in:
354
admin-web/src/pages/materials/index.vue
Normal file
354
admin-web/src/pages/materials/index.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { adminApi, type AdminMaterialBatchDetail, type AdminMaterialBatchItem, type AdminMaterialTagCode } from "../../api/admin";
|
||||
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||
|
||||
const loading = ref(false);
|
||||
const creating = ref(false);
|
||||
const downloadingId = ref<number | null>(null);
|
||||
const detailLoading = ref(false);
|
||||
const createDialogVisible = ref(false);
|
||||
const detailDrawerVisible = ref(false);
|
||||
const detailKeyword = ref("");
|
||||
|
||||
const keyword = ref("");
|
||||
const qrUrl = ref("");
|
||||
const verifyCode = ref("");
|
||||
const dateRange = ref<[string, string] | null>(null);
|
||||
const batches = ref<AdminMaterialBatchItem[]>([]);
|
||||
const detail = ref<AdminMaterialBatchDetail | null>(null);
|
||||
|
||||
const createForm = reactive({
|
||||
count: 100,
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const stats = computed(() => {
|
||||
const totalCodes = batches.value.reduce((sum, item) => sum + item.total_count, 0);
|
||||
const totalBound = batches.value.reduce((sum, item) => sum + item.bound_count, 0);
|
||||
const totalDownloads = batches.value.reduce((sum, item) => sum + item.download_count, 0);
|
||||
return [
|
||||
{ title: "批次数", value: batches.value.length, desc: "当前筛选结果内的物料批次" },
|
||||
{ title: "二维码数", value: totalCodes, desc: "已生成的吊牌二维码链接" },
|
||||
{ title: "已绑定", value: totalBound, desc: "已关联鉴定报告的吊牌" },
|
||||
{ title: "下载次数", value: totalDownloads, desc: "Excel 打包下载总次数" },
|
||||
];
|
||||
});
|
||||
|
||||
function buildQueryParams() {
|
||||
return {
|
||||
keyword: keyword.value.trim(),
|
||||
qr_url: qrUrl.value.trim(),
|
||||
verify_code: verifyCode.value.trim(),
|
||||
date_start: dateRange.value?.[0] || "",
|
||||
date_end: dateRange.value?.[1] || "",
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchBatches() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await adminApi.getMaterialBatches(buildQueryParams());
|
||||
batches.value = response.data.list;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("物料批次加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
keyword.value = "";
|
||||
qrUrl.value = "";
|
||||
verifyCode.value = "";
|
||||
dateRange.value = null;
|
||||
fetchBatches();
|
||||
}
|
||||
|
||||
async function createBatch() {
|
||||
const count = Number(createForm.count);
|
||||
if (!Number.isInteger(count) || count < 1 || count > 10000) {
|
||||
ElMessage.warning("链接数量需为 1-10000 的整数");
|
||||
return;
|
||||
}
|
||||
|
||||
creating.value = true;
|
||||
try {
|
||||
await adminApi.createMaterialBatch({
|
||||
count,
|
||||
remark: createForm.remark.trim(),
|
||||
});
|
||||
ElMessage.success("物料批次已生成");
|
||||
createDialogVisible.value = false;
|
||||
createForm.count = 100;
|
||||
createForm.remark = "";
|
||||
await fetchBatches();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "物料批次生成失败");
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBatch(row: Pick<AdminMaterialBatchItem, "id" | "batch_no">) {
|
||||
try {
|
||||
await ElMessageBox.confirm("将打包下载完整批次的二维码链接与验真编码,并记录一次下载次数。", "下载物料批次", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认下载",
|
||||
cancelButtonText: "取消",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadingId.value = row.id;
|
||||
try {
|
||||
const blob = await adminApi.downloadMaterialBatch(row.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `material-batch-${row.batch_no}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
ElMessage.success("物料批次已下载");
|
||||
await fetchBatches();
|
||||
if (detail.value?.batch.id === row.id) {
|
||||
await loadDetail(row.id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "物料批次下载失败");
|
||||
} finally {
|
||||
downloadingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(id: number) {
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
const response = await adminApi.getMaterialBatchDetail(id, detailKeyword.value.trim());
|
||||
detail.value = response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("批次详情加载失败");
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(row: AdminMaterialBatchItem) {
|
||||
detailKeyword.value = "";
|
||||
detailDrawerVisible.value = true;
|
||||
await loadDetail(row.id);
|
||||
}
|
||||
|
||||
async function copyText(value: string, label: string) {
|
||||
if (!value) {
|
||||
ElMessage.warning(`${label}为空`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
ElMessage.success(`${label}已复制`);
|
||||
} catch {
|
||||
const input = document.createElement("textarea");
|
||||
input.value = value;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
ElMessage.success(`${label}已复制`);
|
||||
}
|
||||
}
|
||||
|
||||
function openReport(row: AdminMaterialTagCode) {
|
||||
if (!row.report_id) return;
|
||||
window.location.hash = `#/reports?report_id=${row.report_id}`;
|
||||
}
|
||||
|
||||
onMounted(fetchBatches);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="metric-grid" style="margin-bottom: 18px">
|
||||
<div v-for="item in stats" :key="item.title" class="metric-card">
|
||||
<div class="metric-card__label">{{ item.title }}</div>
|
||||
<div class="metric-card__value">{{ item.value }}</div>
|
||||
<div class="metric-card__desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<div class="filters-row" style="justify-content: space-between;">
|
||||
<div class="filters-row">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<el-input v-model="keyword" placeholder="搜索二维码链接 / token / 验真编码" clearable style="width: 320px" />
|
||||
<el-input v-model="qrUrl" placeholder="二维码链接" clearable style="width: 260px" />
|
||||
<el-input v-model="verifyCode" placeholder="验真编码" clearable style="width: 160px" />
|
||||
<el-button type="primary" @click="fetchBatches">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="createDialogVisible = true">批量建码</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="panel-card orders-table" shadow="never">
|
||||
<el-table :data="batches" stripe row-key="id">
|
||||
<el-table-column prop="batch_no" label="批次号" min-width="180" />
|
||||
<el-table-column prop="total_count" label="链接数量" min-width="100" />
|
||||
<el-table-column label="绑定进度" min-width="130">
|
||||
<template #default="{ row }">{{ row.bound_count }} / {{ row.total_count }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="download_count" label="下载次数" min-width="100" />
|
||||
<el-table-column prop="created_by_name" label="创建人" min-width="110" />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="170" />
|
||||
<el-table-column prop="last_downloaded_at" label="最近下载" min-width="170" />
|
||||
<el-table-column prop="remark" label="备注" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="命中条码" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.matched_codes.length" class="material-match-list">
|
||||
<div v-for="item in row.matched_codes" :key="item.id" class="material-match-item">
|
||||
<div class="material-match-item__main">{{ item.qr_url }}</div>
|
||||
<div class="material-match-item__meta">
|
||||
验真编码 {{ item.verify_code }} · 扫码 {{ item.scan_count }} · 验真 {{ item.verify_count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="color: var(--admin-text-subtle);">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="210">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">查看详情</el-button>
|
||||
<el-button link type="success" :loading="downloadingId === row.id" @click="downloadBatch(row)">下载 Excel</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="createDialogVisible" title="批量建码" width="520px">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="创建链接数量">
|
||||
<el-input-number v-model="createForm.count" :min="1" :max="10000" :step="100" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="createForm.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="可填写生产用途、工厂批次或内部说明" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="createBatch">提交建码</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="detailDrawerVisible" size="72%" title="物料批次详情">
|
||||
<div v-loading="detailLoading" v-if="detail" class="material-detail">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__title">批次信息</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">批次号</div>
|
||||
<div class="detail-value">{{ detail.batch.batch_no }}</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">数量 / 下载次数</div>
|
||||
<div class="detail-value">{{ detail.batch.total_count }} / {{ detail.batch.download_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__title">生产备注</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">备注</div>
|
||||
<div class="detail-value">{{ detail.batch.remark || "-" }}</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">最近下载</div>
|
||||
<div class="detail-value">{{ detail.batch.last_downloaded_at || "-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="panel-card" shadow="never" style="margin-top: 18px">
|
||||
<div class="filters-row" style="justify-content: space-between;">
|
||||
<div class="filters-row">
|
||||
<el-input v-model="detailKeyword" placeholder="筛选二维码链接 / token / 验真编码" clearable style="width: 340px" />
|
||||
<el-button type="primary" @click="loadDetail(detail.batch.id)">筛选</el-button>
|
||||
</div>
|
||||
<el-button type="success" :loading="downloadingId === detail.batch.id" @click="downloadBatch(detail.batch)">下载 Excel</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="panel-card orders-table" shadow="never">
|
||||
<el-table :data="detail.codes" stripe>
|
||||
<el-table-column prop="qr_url" label="二维码链接" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div style="word-break: break-all;">{{ row.qr_url }}</div>
|
||||
<el-button link type="primary" @click="copyText(row.qr_url, '二维码链接')">复制</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verify_code" label="验真编码" min-width="120" />
|
||||
<el-table-column label="绑定状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<OrderStatusTag :status="row.bind_status_text" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联报告编号" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.report_id" link type="primary" @click="openReport(row)">{{ row.report_no }}</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scan_count" label="扫码次数" min-width="100" />
|
||||
<el-table-column prop="verify_count" label="验真次数" min-width="100" />
|
||||
<el-table-column prop="bound_by_name" label="绑定人" min-width="110" />
|
||||
<el-table-column prop="bound_at" label="绑定时间" min-width="170" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.material-match-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.material-match-item {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: #fffdfa;
|
||||
}
|
||||
|
||||
.material-match-item__main {
|
||||
word-break: break-all;
|
||||
color: var(--admin-text-main);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.material-match-item__meta {
|
||||
margin-top: 4px;
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.material-detail {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user