81bafaf557
- 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>
453 lines
14 KiB
Vue
453 lines
14 KiB
Vue
<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>
|