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>
This commit is contained in:
+289
-111
@@ -7,98 +7,207 @@
|
||||
<el-icon :size="20" color="#6366f1"><Connection /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ items.length }}</span>
|
||||
<span class="stat-label">映射规则</span>
|
||||
<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>
|
||||
|
||||
<!-- Main table card -->
|
||||
<!-- Two-tab layout matching GUI -->
|
||||
<div class="card animate-in animate-in-delay-1">
|
||||
<div class="card-head">
|
||||
<h3>条码映射管理</h3>
|
||||
<div class="card-actions">
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="搜索条码..."
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="loadData"
|
||||
@clear="loadData"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button size="small" @click="loadData" :icon="Refresh">刷新</el-button>
|
||||
<el-button size="small" type="primary" @click="openAdd" :icon="Plus">新增映射</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<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="items" v-loading="loading" stripe max-height="600" size="small" class="barcode-table">
|
||||
<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="60" 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 prop="target" 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="editItem(row)">编辑</el-button>
|
||||
<el-button type="danger" link size="small" @click="deleteItem(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<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>
|
||||
|
||||
<!-- Add/Edit dialog -->
|
||||
<el-dialog v-model="showAdd" :title="isEdit ? '编辑映射' : '新增映射'" width="450px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="原始条码">
|
||||
<el-input v-model="form.barcode" :disabled="isEdit" placeholder="输入原始条码" />
|
||||
<!-- 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="form.target" placeholder="输入目标条码" />
|
||||
<el-input v-model="mappingForm.target" placeholder="输入目标条码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明">
|
||||
<el-input v-model="form.description" placeholder="映射说明(可选)" />
|
||||
<el-input v-model="mappingForm.description" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAdd = false">取消</el-button>
|
||||
<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, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Connection } from '@element-plus/icons-vue'
|
||||
import { Search, Refresh, Plus, Connection, Right, Setting } from '@element-plus/icons-vue'
|
||||
import api from '../api'
|
||||
|
||||
const loading = ref(false)
|
||||
const search = ref('')
|
||||
const items = ref<any[]>([])
|
||||
const showAdd = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const rawItems = ref<any[]>([])
|
||||
const activeTab = ref('mapping')
|
||||
|
||||
const form = reactive({
|
||||
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: '',
|
||||
target: '',
|
||||
multiplier: null as number | null,
|
||||
targetUnit: '',
|
||||
fixedPrice: null as number | null,
|
||||
specification: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
@@ -106,7 +215,7 @@ async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/barcodes', { params: { search: search.value } })
|
||||
items.value = res.data.items
|
||||
rawItems.value = res.data.items
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
@@ -114,53 +223,113 @@ async function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
resetForm()
|
||||
showAdd.value = true
|
||||
function onTabChange() {
|
||||
// Keep search across tabs
|
||||
}
|
||||
|
||||
function editItem(row: any) {
|
||||
isEdit.value = true
|
||||
form.barcode = row.barcode
|
||||
form.target = row.target
|
||||
form.description = row.description || ''
|
||||
showAdd.value = true
|
||||
// ── Mapping CRUD ──
|
||||
function openMappingAdd() {
|
||||
mappingEdit.value = false
|
||||
mappingForm.barcode = ''
|
||||
mappingForm.target = ''
|
||||
mappingForm.description = ''
|
||||
showMapping.value = true
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.barcode = ''
|
||||
form.target = ''
|
||||
form.description = ''
|
||||
isEdit.value = false
|
||||
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 (!form.barcode || !form.target) {
|
||||
ElMessage.warning('请填写条码和目标')
|
||||
if (!mappingForm.barcode || !mappingForm.target) {
|
||||
ElMessage.warning('请填写源条码和目标条码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await api.put(`/barcodes/${form.barcode}`, {
|
||||
target: form.target,
|
||||
description: form.description,
|
||||
if (mappingEdit.value) {
|
||||
await api.put(`/barcodes/${mappingForm.barcode}`, {
|
||||
target: mappingForm.target,
|
||||
description: mappingForm.description,
|
||||
})
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await api.post('/barcodes', form)
|
||||
await api.post('/barcodes', {
|
||||
barcode: mappingForm.barcode,
|
||||
target: mappingForm.target,
|
||||
description: mappingForm.description,
|
||||
})
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
showAdd.value = false
|
||||
resetForm()
|
||||
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 {
|
||||
await ElMessageBox.confirm(`确定删除映射 ${row.barcode} → ${row.target}?`, '确认')
|
||||
const desc = row.target ? `${row.barcode} → ${row.target}` : `${row.barcode}`
|
||||
await ElMessageBox.confirm(`确定删除规则 ${desc}?`, '确认')
|
||||
await api.delete(`/barcodes/${row.barcode}`)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
@@ -172,16 +341,16 @@ onMounted(loadData)
|
||||
|
||||
<style scoped>
|
||||
.barcodes-page {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Stats row ── */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 300px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -230,38 +399,23 @@ onMounted(loadData)
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
/* ── Tab toolbar ── */
|
||||
.tab-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-head h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── Table ── */
|
||||
.barcode-table {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Barcode cells ── */
|
||||
.barcode-cell {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
@@ -271,4 +425,28 @@ onMounted(loadData)
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user