Files
orc-order-v2/web/frontend/src/views/Barcodes.vue
T
houhuan 81bafaf557 fix: sync/barcode/memory overhaul + detailed logs + preview + result tracking
- Sync: fix GiteaSync constructor + add push()/pull() methods
- Barcode: two-tab layout matching GUI (mapping + special rules)
- Memory: spec→specification unification, manual add, confidence/price tracking
- Processing: TaskLogHandler captures detailed logs (barcode mapping, unit conversion)
- Preview: fullscreen dialog for file preview (image/Excel) in Orders/Tables/Images
- Detail: per-file log filtering in file pages
- Tasks: result files now per-task, add copy path button
- Config: reactive edited state + save_config fix
- Dashboard: sync task isolation, log limit 10

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 19:37:10 +08:00

453 lines
14 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>
<div class="barcodes-page">
<!-- Stats row -->
<div class="stats-row animate-in">
<div class="stat-card">
<div class="stat-icon" style="background: rgba(99,102,241,0.1)">
<el-icon :size="20" color="#6366f1"><Connection /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ mappingItems.length + specialItems.length }}</span>
<span class="stat-label">总规则数</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: rgba(16,185,129,0.1)">
<el-icon :size="20" color="#10b981"><Right /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ mappingItems.length }}</span>
<span class="stat-label">条码映射</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: rgba(245,158,11,0.1)">
<el-icon :size="20" color="#f59e0b"><Setting /></el-icon>
</div>
<div class="stat-info">
<span class="stat-value">{{ specialItems.length }}</span>
<span class="stat-label">特殊处理</span>
</div>
</div>
</div>
<!-- Two-tab layout matching GUI -->
<div class="card animate-in animate-in-delay-1">
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<!-- Tab 1: 条码映射 -->
<el-tab-pane label="条码映射" name="mapping">
<div class="tab-toolbar">
<el-input
v-model="search"
placeholder="搜索条码..."
clearable
style="width: 220px"
@keyup.enter="loadData"
@clear="loadData"
>
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<div class="tab-actions">
<el-button size="small" @click="loadData" :icon="Refresh">刷新</el-button>
<el-button size="small" type="primary" @click="openMappingAdd">新增映射</el-button>
</div>
</div>
<el-table :data="mappingItems" v-loading="loading" stripe max-height="500" size="small">
<el-table-column prop="barcode" label="源条码" width="200">
<template #default="{ row }">
<span class="barcode-cell">{{ row.barcode }}</span>
</template>
</el-table-column>
<el-table-column label="" width="40" align="center">
<template #default>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--amber-500)" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</template>
</el-table-column>
<el-table-column label="目标条码" width="200">
<template #default="{ row }">
<span class="barcode-cell target">{{ row.target }}</span>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="130" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="editMapping(row)">编辑</el-button>
<el-button type="danger" link size="small" @click="deleteItem(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- Tab 2: 特殊处理 -->
<el-tab-pane label="特殊处理" name="special">
<div class="tab-toolbar">
<el-input
v-model="search"
placeholder="搜索条码..."
clearable
style="width: 220px"
@keyup.enter="loadData"
@clear="loadData"
>
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<div class="tab-actions">
<el-button size="small" @click="loadData" :icon="Refresh">刷新</el-button>
<el-button size="small" type="primary" @click="openSpecialAdd">新增特殊处理</el-button>
</div>
</div>
<el-table :data="specialItems" v-loading="loading" stripe max-height="500" size="small">
<el-table-column prop="barcode" label="条码" width="180">
<template #default="{ row }">
<span class="barcode-cell special-type">{{ row.barcode }}</span>
</template>
</el-table-column>
<el-table-column prop="multiplier" label="乘数" width="70" align="center">
<template #default="{ row }">
<span class="multiplier-badge">{{ row.multiplier }}</span>
</template>
</el-table-column>
<el-table-column prop="target_unit" label="目标单位" width="90" align="center" />
<el-table-column prop="fixed_price" label="固定单价" width="100" align="center">
<template #default="{ row }">
<span v-if="row.fixed_price != null" class="price-cell">{{ row.fixed_price.toFixed(4) }}</span>
<span v-else class="text-muted">--</span>
</template>
</el-table-column>
<el-table-column prop="specification" label="规格" width="90" align="center" />
<el-table-column prop="description" label="描述" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="130" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="editSpecial(row)">编辑</el-button>
<el-button type="danger" link size="small" @click="deleteItem(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
<!-- Mapping add/edit dialog -->
<el-dialog v-model="showMapping" :title="mappingEdit ? '编辑条码映射' : '新增条码映射'" width="450px" :close-on-click-modal="false">
<el-form :model="mappingForm" label-width="80px">
<el-form-item label="源条码">
<el-input v-model="mappingForm.barcode" :disabled="mappingEdit" placeholder="输入原始条码" />
</el-form-item>
<el-form-item label="目标条码">
<el-input v-model="mappingForm.target" placeholder="输入目标条码" />
</el-form-item>
<el-form-item label="说明">
<el-input v-model="mappingForm.description" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showMapping = false">取消</el-button>
<el-button type="primary" @click="saveMapping">保存</el-button>
</template>
</el-dialog>
<!-- Special rule add/edit dialog -->
<el-dialog v-model="showSpecial" :title="specialEdit ? '编辑特殊处理' : '新增特殊处理'" width="480px" :close-on-click-modal="false">
<el-form :model="specialForm" label-width="80px">
<el-form-item label="条码">
<el-input v-model="specialForm.barcode" :disabled="specialEdit" placeholder="输入条码" />
</el-form-item>
<el-form-item label="乘数">
<el-input-number v-model="specialForm.multiplier" :min="1" :step="1" style="width: 100%" placeholder="如: 10" />
</el-form-item>
<el-form-item label="目标单位">
<el-input v-model="specialForm.targetUnit" placeholder="如: 瓶、个、对" />
</el-form-item>
<el-form-item label="固定单价">
<el-input-number v-model="specialForm.fixedPrice" :precision="4" :step="0.01" :min="0" style="width: 100%" placeholder="可选" />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="specialForm.specification" placeholder="如: 1*30" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="specialForm.description" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showSpecial = false">取消</el-button>
<el-button type="primary" @click="saveSpecial">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus, Connection, Right, Setting } from '@element-plus/icons-vue'
import api from '../api'
const loading = ref(false)
const search = ref('')
const rawItems = ref<any[]>([])
const activeTab = ref('mapping')
const mappingItems = computed(() => rawItems.value.filter(r => !r.multiplier))
const specialItems = computed(() => rawItems.value.filter(r => r.multiplier))
// ── Mapping form ──
const showMapping = ref(false)
const mappingEdit = ref(false)
const mappingForm = reactive({ barcode: '', target: '', description: '' })
// ── Special form ──
const showSpecial = ref(false)
const specialEdit = ref(false)
const specialForm = reactive({
barcode: '',
multiplier: null as number | null,
targetUnit: '',
fixedPrice: null as number | null,
specification: '',
description: '',
})
async function loadData() {
loading.value = true
try {
const res = await api.get('/barcodes', { params: { search: search.value } })
rawItems.value = res.data.items
} catch {
ElMessage.error('加载失败')
} finally {
loading.value = false
}
}
function onTabChange() {
// Keep search across tabs
}
// ── Mapping CRUD ──
function openMappingAdd() {
mappingEdit.value = false
mappingForm.barcode = ''
mappingForm.target = ''
mappingForm.description = ''
showMapping.value = true
}
function editMapping(row: any) {
mappingEdit.value = true
mappingForm.barcode = row.barcode
mappingForm.target = row.target
mappingForm.description = row.description || ''
showMapping.value = true
}
async function saveMapping() {
if (!mappingForm.barcode || !mappingForm.target) {
ElMessage.warning('请填写源条码和目标条码')
return
}
try {
if (mappingEdit.value) {
await api.put(`/barcodes/${mappingForm.barcode}`, {
target: mappingForm.target,
description: mappingForm.description,
})
ElMessage.success('已更新')
} else {
await api.post('/barcodes', {
barcode: mappingForm.barcode,
target: mappingForm.target,
description: mappingForm.description,
})
ElMessage.success('已创建')
}
showMapping.value = false
loadData()
} catch (err: any) {
ElMessage.error(err.response?.data?.detail || '操作失败')
}
}
// ── Special CRUD ──
function openSpecialAdd() {
specialEdit.value = false
specialForm.barcode = ''
specialForm.multiplier = null
specialForm.targetUnit = ''
specialForm.fixedPrice = null
specialForm.specification = ''
specialForm.description = ''
showSpecial.value = true
}
function editSpecial(row: any) {
specialEdit.value = true
specialForm.barcode = row.barcode
specialForm.multiplier = row.multiplier
specialForm.targetUnit = row.target_unit || ''
specialForm.fixedPrice = row.fixed_price ?? null
specialForm.specification = row.specification || ''
specialForm.description = row.description || ''
showSpecial.value = true
}
async function saveSpecial() {
if (!specialForm.barcode) {
ElMessage.warning('请填写条码')
return
}
if (!specialForm.multiplier) {
ElMessage.warning('请填写乘数')
return
}
try {
const body: any = {
multiplier: specialForm.multiplier,
target_unit: specialForm.targetUnit || null,
fixed_price: specialForm.fixedPrice ?? null,
specification: specialForm.specification || null,
description: specialForm.description,
}
if (specialEdit.value) {
await api.put(`/barcodes/${specialForm.barcode}`, body)
ElMessage.success('已更新')
} else {
await api.post('/barcodes', { barcode: specialForm.barcode, ...body })
ElMessage.success('已创建')
}
showSpecial.value = false
loadData()
} catch (err: any) {
ElMessage.error(err.response?.data?.detail || '操作失败')
}
}
// ── Shared ──
async function deleteItem(row: any) {
try {
const desc = row.target ? `${row.barcode}${row.target}` : `${row.barcode}`
await ElMessageBox.confirm(`确定删除规则 ${desc}`, '确认')
await api.delete(`/barcodes/${row.barcode}`)
ElMessage.success('已删除')
loadData()
} catch {}
}
onMounted(loadData)
</script>
<style scoped>
.barcodes-page {
width: 100%;
}
/* ── Stats row ── */
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 20px;
max-width: 560px;
}
.stat-card {
display: flex;
align-items: center;
gap: 14px;
padding: 18px 20px;
background: #fff;
border-radius: 12px;
border: 1px solid var(--border-light);
transition: all 0.2s var(--ease-out);
}
.stat-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.stat-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-value {
display: block;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
line-height: 1;
}
.stat-label {
font-size: 13px;
color: var(--text-secondary);
margin-top: 2px;
}
/* ── Card ── */
.card {
background: #fff;
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 20px;
}
/* ── Tab toolbar ── */
.tab-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.tab-actions {
display: flex;
gap: 8px;
align-items: center;
}
/* ── Barcode cells ── */
.barcode-cell {
font-family: var(--font-mono);
font-size: 13px;
color: var(--info);
}
.barcode-cell.target {
color: var(--success);
}
.barcode-cell.special-type {
color: var(--warning);
}
.multiplier-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
background: rgba(245,158,11,0.1);
color: var(--warning);
font-weight: 600;
font-family: var(--font-mono);
}
.price-cell {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-primary);
}
.text-muted {
color: var(--text-muted);
}
</style>