feat: complete web application — FastAPI backend + Vue 3 SPA frontend
- Full FastAPI backend with JWT auth, file management, processing pipeline, memory CRUD, barcode mappings, config management, cloud sync - Vue 3 + Element Plus frontend with dashboard, task history, HTTP logs, memory editor, barcode editor, config editor, sync page - HTTP request logging middleware with SQLite persistence - Task history tracking with progress and retry support - File metadata recording for upload/download operations - WebAuth section in config.ini for bcrypt password storage - Bug fix: logs.py count query returns tuple not dict Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<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">{{ items.length }}</span>
|
||||
<span class="stat-label">映射规则</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main table card -->
|
||||
<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-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>
|
||||
</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="输入原始条码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="目标条码">
|
||||
<el-input v-model="form.target" placeholder="输入目标条码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明">
|
||||
<el-input v-model="form.description" placeholder="映射说明(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAdd = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveMapping">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Connection } 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 form = reactive({
|
||||
barcode: '',
|
||||
target: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/barcodes', { params: { search: search.value } })
|
||||
items.value = res.data.items
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
resetForm()
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
function editItem(row: any) {
|
||||
isEdit.value = true
|
||||
form.barcode = row.barcode
|
||||
form.target = row.target
|
||||
form.description = row.description || ''
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.barcode = ''
|
||||
form.target = ''
|
||||
form.description = ''
|
||||
isEdit.value = false
|
||||
}
|
||||
|
||||
async function saveMapping() {
|
||||
if (!form.barcode || !form.target) {
|
||||
ElMessage.warning('请填写条码和目标')
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await api.put(`/barcodes/${form.barcode}`, {
|
||||
target: form.target,
|
||||
description: form.description,
|
||||
})
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await api.post('/barcodes', form)
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
showAdd.value = false
|
||||
resetForm()
|
||||
loadData()
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItem(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除映射 ${row.barcode} → ${row.target}?`, '确认')
|
||||
await api.delete(`/barcodes/${row.barcode}`)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.barcodes-page {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
/* ── Stats row ── */
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.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;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-head h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── Table ── */
|
||||
.barcode-table {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.barcode-cell {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.barcode-cell.target {
|
||||
color: var(--success);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user