|
|
@@ -0,0 +1,1229 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container upload-page">
|
|
|
+ <div class="page-header">
|
|
|
+ <h2><i class="el-icon-upload2"></i> 数据上传</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-row :gutter="20" class="metrics-row">
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <el-card class="metric-card">
|
|
|
+ <div class="metric-content">
|
|
|
+ <div class="metric-icon icon-blue">
|
|
|
+ <i class="el-icon-collection-tag"></i>
|
|
|
+ </div>
|
|
|
+ <div class="metric-info">
|
|
|
+ <div class="metric-label">数据表总数</div>
|
|
|
+ <div class="metric-value">{{ uploadTables.length }}</div>
|
|
|
+ <div class="metric-sub">按文档固定顺序上传</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <el-card class="metric-card">
|
|
|
+ <div class="metric-content">
|
|
|
+ <div class="metric-icon icon-green">
|
|
|
+ <i class="el-icon-circle-check"></i>
|
|
|
+ </div>
|
|
|
+ <div class="metric-info">
|
|
|
+ <div class="metric-label">已完成上传</div>
|
|
|
+ <div class="metric-value">{{ completedTableCount }}</div>
|
|
|
+ <div class="metric-sub">{{ progressPercent }}% 流程进度</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <el-card class="metric-card">
|
|
|
+ <div class="metric-content">
|
|
|
+ <div class="metric-icon icon-orange">
|
|
|
+ <i class="el-icon-document-copy"></i>
|
|
|
+ </div>
|
|
|
+ <div class="metric-info">
|
|
|
+ <div class="metric-label">当前样例数据量</div>
|
|
|
+ <div class="metric-value">{{ currentRecordTotal }}</div>
|
|
|
+ <div class="metric-sub">来自各表预览快照</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <el-card class="metric-card">
|
|
|
+ <div class="metric-content">
|
|
|
+ <div class="metric-icon icon-red">
|
|
|
+ <i class="el-icon-right"></i>
|
|
|
+ </div>
|
|
|
+ <div class="metric-info">
|
|
|
+ <div class="metric-label">下一待上传</div>
|
|
|
+ <div class="metric-value next-step">{{ nextPendingLabel }}</div>
|
|
|
+ <div class="metric-sub">严格按顺序解锁</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-card class="overview-card">
|
|
|
+ <div class="overview-top">
|
|
|
+ <div>
|
|
|
+ <div class="overview-title">上传规则</div>
|
|
|
+ <div class="overview-desc">
|
|
|
+ 当前页面仅实现前端交互。文件选择、顺序校验、状态流转和数据预览均为本地演示,后端接口后续接入。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-actions">
|
|
|
+ <el-button type="primary" size="small" @click="focusNextPending">
|
|
|
+ <i class="el-icon-position"></i> 定位到下一步
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" @click="resetAllProgress">
|
|
|
+ <i class="el-icon-refresh-left"></i> 重置进度
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-progress">
|
|
|
+ <span>整体上传进度</span>
|
|
|
+ <el-progress :percentage="progressPercent" :stroke-width="10" />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-row :gutter="20" class="content-row">
|
|
|
+ <el-col :xs="24" :lg="10">
|
|
|
+ <el-card class="order-card">
|
|
|
+ <template slot="header">
|
|
|
+ <div class="card-header">
|
|
|
+ <span>上传顺序面板</span>
|
|
|
+ <el-tag size="mini" type="info">共 {{ uploadTables.length }} 步</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="order-list">
|
|
|
+ <div
|
|
|
+ v-for="table in orderedTables"
|
|
|
+ :key="table.key"
|
|
|
+ class="order-item"
|
|
|
+ :class="{
|
|
|
+ active: activeTableKey === table.key,
|
|
|
+ uploaded: table.uploaded,
|
|
|
+ locked: !canUpload(table) && !table.uploaded
|
|
|
+ }"
|
|
|
+ @click="selectTable(table.key)"
|
|
|
+ >
|
|
|
+ <div class="order-index">{{ table.order }}</div>
|
|
|
+ <div class="order-main">
|
|
|
+ <div class="order-title-row">
|
|
|
+ <div>
|
|
|
+ <div class="order-title">{{ table.name }}</div>
|
|
|
+ <div class="order-table">{{ table.tableName }}</div>
|
|
|
+ </div>
|
|
|
+ <el-tag :type="getStatusType(table)" size="mini">
|
|
|
+ {{ getStatusText(table) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="order-desc">{{ table.description }}</div>
|
|
|
+ <div class="order-tags">
|
|
|
+ <el-tag
|
|
|
+ v-if="table.dependencies.length === 0"
|
|
|
+ size="mini"
|
|
|
+ effect="plain"
|
|
|
+ type="success"
|
|
|
+ >
|
|
|
+ 无外键依赖
|
|
|
+ </el-tag>
|
|
|
+ <el-tag
|
|
|
+ v-for="dependency in table.dependencies"
|
|
|
+ :key="dependency"
|
|
|
+ size="mini"
|
|
|
+ effect="plain"
|
|
|
+ >
|
|
|
+ {{ dependency }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="order-footer">
|
|
|
+ <span>当前数据 {{ table.recordCount }} 条</span>
|
|
|
+ <span v-if="table.fileName">已选文件:{{ table.fileName }}</span>
|
|
|
+ <span v-else-if="!table.uploaded && !canUpload(table)">{{ getBlockedReason(table) }}</span>
|
|
|
+ <span v-else>待选择上传文件</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :xs="24" :lg="14">
|
|
|
+ <el-card class="detail-card">
|
|
|
+ <template slot="header">
|
|
|
+ <div class="card-header">
|
|
|
+ <span>当前表详情</span>
|
|
|
+ <el-tag size="mini">{{ selectedTable.tableName }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="detail-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :disabled="selectedTable.uploaded"
|
|
|
+ @click="triggerFileSelect(selectedTable)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-folder-opened"></i> 选择文件
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ size="small"
|
|
|
+ :disabled="selectedTable.uploaded || !canUpload(selectedTable)"
|
|
|
+ @click="submitTable(selectedTable)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-upload"></i> 模拟上传
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ :disabled="!selectedTable.fileName && !selectedTable.uploaded"
|
|
|
+ @click="resetFromTable(selectedTable)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-delete"></i> 清空当前及后续
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-if="!canUpload(selectedTable) && !selectedTable.uploaded"
|
|
|
+ :closable="false"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :title="getBlockedReason(selectedTable)"
|
|
|
+ class="detail-alert"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-else
|
|
|
+ :closable="false"
|
|
|
+ type="info"
|
|
|
+ show-icon
|
|
|
+ class="detail-alert"
|
|
|
+ :title="selectedTable.uploaded ? '当前步骤已完成,可继续下一张表。' : '当前步骤已解锁,可选择文件后提交。'"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-descriptions :column="2" border class="table-descriptions">
|
|
|
+ <el-descriptions-item label="中文名称">{{ selectedTable.name }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="上传顺序">第 {{ selectedTable.order }} 步</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="依赖关系">{{ formatDependencies(selectedTable) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="当前状态">{{ getStatusText(selectedTable) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="当前数据量">{{ selectedTable.recordCount }} 条</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="最近操作">
|
|
|
+ {{ selectedTable.updatedAt || '暂无操作' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="selected-file" v-if="selectedTable.fileName">
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
+ <span>{{ selectedTable.fileName }}</span>
|
|
|
+ <span class="file-size">{{ selectedTable.fileSize }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section-title">
|
|
|
+ <span>字段摘要</span>
|
|
|
+ <el-tag size="mini" type="info">{{ selectedTable.fields.length }} 个核心字段</el-tag>
|
|
|
+ </div>
|
|
|
+ <el-table :data="selectedTable.fields" stripe size="small" class="data-table">
|
|
|
+ <el-table-column prop="name" label="字段名" min-width="140" />
|
|
|
+ <el-table-column prop="type" label="类型" min-width="120" />
|
|
|
+ <el-table-column prop="constraint" label="约束" min-width="160" />
|
|
|
+ <el-table-column prop="description" label="说明" min-width="220" show-overflow-tooltip />
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="section-title section-with-gap">
|
|
|
+ <span>当前表部分数据</span>
|
|
|
+ <el-tag size="mini" type="success">{{ selectedTable.recordCount }} 条</el-tag>
|
|
|
+ </div>
|
|
|
+ <el-table :data="selectedTable.sampleRows" stripe size="small" class="data-table">
|
|
|
+ <el-table-column
|
|
|
+ v-for="column in selectedTable.previewColumns"
|
|
|
+ :key="column.prop"
|
|
|
+ :prop="column.prop"
|
|
|
+ :label="column.label"
|
|
|
+ :min-width="column.minWidth || 120"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-card class="snapshot-card">
|
|
|
+ <template slot="header">
|
|
|
+ <div class="card-header">
|
|
|
+ <span>当前各数据表部分数据</span>
|
|
|
+ <el-tag size="mini" type="info">可切换查看 15 张表</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-tabs v-model="activePreviewTab" @tab-click="handlePreviewTabChange">
|
|
|
+ <el-tab-pane
|
|
|
+ v-for="table in orderedTables"
|
|
|
+ :key="table.key"
|
|
|
+ :name="table.key"
|
|
|
+ >
|
|
|
+ <span slot="label">{{ table.order }}. {{ table.name }}</span>
|
|
|
+ <div class="snapshot-meta">
|
|
|
+ <el-tag size="mini" :type="getStatusType(table)">{{ getStatusText(table) }}</el-tag>
|
|
|
+ <el-tag size="mini" effect="plain" type="info">{{ table.tableName }}</el-tag>
|
|
|
+ <el-tag size="mini" effect="plain" type="success">当前数据 {{ table.recordCount }} 条</el-tag>
|
|
|
+ <span class="snapshot-desc">{{ table.description }}</span>
|
|
|
+ </div>
|
|
|
+ <el-table :data="table.sampleRows" stripe size="mini">
|
|
|
+ <el-table-column
|
|
|
+ v-for="column in table.previewColumns"
|
|
|
+ :key="column.prop"
|
|
|
+ :prop="column.prop"
|
|
|
+ :label="column.label"
|
|
|
+ :min-width="column.minWidth || 120"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <div class="hidden-inputs">
|
|
|
+ <input
|
|
|
+ v-for="table in orderedTables"
|
|
|
+ :key="table.key"
|
|
|
+ :ref="'fileInput-' + table.key"
|
|
|
+ type="file"
|
|
|
+ accept=".xlsx,.xls,.csv"
|
|
|
+ @change="handleFileChange(table, $event)"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+const UPLOAD_TABLES = [
|
|
|
+ {
|
|
|
+ key: 'warehouse',
|
|
|
+ order: 1,
|
|
|
+ name: '仓库表',
|
|
|
+ tableName: 'warehouse',
|
|
|
+ description: '仓库主数据,无外键依赖。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 12,
|
|
|
+ fields: [
|
|
|
+ { name: 'warehouse_code', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '仓库编码(主键)' },
|
|
|
+ { name: 'warehouse_name', type: 'VARCHAR(100)', constraint: 'NOT NULL', description: '仓库名称' },
|
|
|
+ { name: 'warehouse_type', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '仓库类型(普通仓 / 冷链仓 / 电商仓等)' },
|
|
|
+ { name: 'warehouse_address', type: 'VARCHAR(200)', constraint: 'NOT NULL', description: '仓库地址' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'warehouse_code', label: '仓库编码', minWidth: 120 },
|
|
|
+ { prop: 'warehouse_name', label: '仓库名称', minWidth: 160 },
|
|
|
+ { prop: 'warehouse_type', label: '仓库类型', minWidth: 120 },
|
|
|
+ { prop: 'warehouse_address', label: '仓库地址', minWidth: 220 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { warehouse_code: 'WH-SH-01', warehouse_name: '上海中心仓', warehouse_type: '电商仓', warehouse_address: '上海市嘉定区叶城路 88 号' },
|
|
|
+ { warehouse_code: 'WH-HZ-02', warehouse_name: '华中周转仓', warehouse_type: '普通仓', warehouse_address: '武汉市东西湖区高桥五路 26 号' },
|
|
|
+ { warehouse_code: 'WH-GZ-03', warehouse_name: '广州冷链仓', warehouse_type: '冷链仓', warehouse_address: '广州市黄埔区开源大道 16 号' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'supplier',
|
|
|
+ order: 2,
|
|
|
+ name: '供应商表',
|
|
|
+ tableName: 'supplier',
|
|
|
+ description: '供应商主数据,无外键依赖。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 18,
|
|
|
+ fields: [
|
|
|
+ { name: 'supplier_id', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '供应商编号(主键)' },
|
|
|
+ { name: 'supplier_name', type: 'VARCHAR(100)', constraint: 'NOT NULL', description: '供应商名称' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'supplier_id', label: '供应商编号', minWidth: 140 },
|
|
|
+ { prop: 'supplier_name', label: '供应商名称', minWidth: 220 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { supplier_id: 'SUP-001', supplier_name: '华东包装材料有限公司' },
|
|
|
+ { supplier_id: 'SUP-007', supplier_name: '宁波晨光辅料工厂' },
|
|
|
+ { supplier_id: 'SUP-015', supplier_name: '青岛智造电子配件厂' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'semi_finished_product',
|
|
|
+ order: 3,
|
|
|
+ name: '半成品表',
|
|
|
+ tableName: 'semi_finished_product',
|
|
|
+ description: '半成品基础档案,无外键依赖。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 26,
|
|
|
+ fields: [
|
|
|
+ { name: 'sku', type: 'VARCHAR(64)', constraint: 'PRIMARY KEY', description: '半成品 SKU(主键)' },
|
|
|
+ { name: 'semi_name', type: 'VARCHAR(100)', constraint: 'NOT NULL', description: '半成品名称' },
|
|
|
+ { name: 'price', type: 'DECIMAL(10,2)', constraint: 'NOT NULL', description: '半成品单价' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'sku', label: '半成品 SKU', minWidth: 160 },
|
|
|
+ { prop: 'semi_name', label: '半成品名称', minWidth: 220 },
|
|
|
+ { prop: 'price', label: '单价', minWidth: 100 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { sku: 'SEMI-AX1001', semi_name: '主控板组件 A', price: '38.50' },
|
|
|
+ { sku: 'SEMI-BX2040', semi_name: '电源模块 B', price: '21.00' },
|
|
|
+ { sku: 'SEMI-CX3012', semi_name: '外壳组件 C', price: '12.80' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'product',
|
|
|
+ order: 4,
|
|
|
+ name: '产品表',
|
|
|
+ tableName: 'product',
|
|
|
+ description: '成品主数据,无外键依赖。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 48,
|
|
|
+ fields: [
|
|
|
+ { name: 'sku', type: 'VARCHAR(64)', constraint: 'PRIMARY KEY', description: '产品 SKU(主键)' },
|
|
|
+ { name: 'spu', type: 'VARCHAR(64)', constraint: 'NOT NULL', description: '标准产品单元 SPU' },
|
|
|
+ { name: 'product_code', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '产品代码' },
|
|
|
+ { name: 'product_name', type: 'VARCHAR(128)', constraint: 'NOT NULL', description: '产品名称' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'sku', label: 'SKU', minWidth: 160 },
|
|
|
+ { prop: 'spu', label: 'SPU', minWidth: 120 },
|
|
|
+ { prop: 'product_code', label: '产品代码', minWidth: 140 },
|
|
|
+ { prop: 'product_name', label: '产品名称', minWidth: 220 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { sku: 'SKU-P1001-BLK', spu: 'SPU-P1001', product_code: 'PD-1001', product_name: '智能水杯 黑色款' },
|
|
|
+ { sku: 'SKU-P1001-WHT', spu: 'SPU-P1001', product_code: 'PD-1002', product_name: '智能水杯 白色款' },
|
|
|
+ { sku: 'SKU-P2100-GRY', spu: 'SPU-P2100', product_code: 'PD-2100', product_name: '便携保温壶 灰色款' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'product_structure',
|
|
|
+ order: 5,
|
|
|
+ name: '产品结构表',
|
|
|
+ tableName: 'product_structure',
|
|
|
+ description: '产品层级结构,自关联表,需先插入顶层节点。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 31,
|
|
|
+ fields: [
|
|
|
+ { name: 'level_no', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '当前层级编号(如 1、1-1、1-1-1)' },
|
|
|
+ { name: 'parent_id', type: 'VARCHAR(32)', constraint: 'FOREIGN KEY', description: '父类 ID(关联本表)' },
|
|
|
+ { name: 'parent_name', type: 'VARCHAR(100)', constraint: 'NOT NULL', description: '父类分类名称' },
|
|
|
+ { name: 'category_id', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '分类 ID' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'level_no', label: '层级编号', minWidth: 120 },
|
|
|
+ { prop: 'parent_id', label: '父级 ID', minWidth: 120 },
|
|
|
+ { prop: 'parent_name', label: '父级名称', minWidth: 140 },
|
|
|
+ { prop: 'category_id', label: '分类 ID', minWidth: 120 },
|
|
|
+ { prop: 'category_name', label: '分类名称', minWidth: 160 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { level_no: '1', parent_id: '-', parent_name: '根节点', category_id: 'CAT-ROOT', category_name: '产品目录' },
|
|
|
+ { level_no: '1-1', parent_id: '1', parent_name: '产品目录', category_id: 'CAT-SMART', category_name: '智能杯具' },
|
|
|
+ { level_no: '1-1-1', parent_id: '1-1', parent_name: '智能杯具', category_id: 'CAT-TRAVEL', category_name: '便携系列' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'store',
|
|
|
+ order: 6,
|
|
|
+ name: '店铺表',
|
|
|
+ tableName: 'store',
|
|
|
+ description: '店铺主数据,无外键依赖。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 9,
|
|
|
+ fields: [
|
|
|
+ { name: 'store_code', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '店铺编码(主键)' },
|
|
|
+ { name: 'store_name', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '店铺名称' },
|
|
|
+ { name: 'dept_name', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '事业部名称' },
|
|
|
+ { name: 'channel_name', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '渠道名称' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'store_code', label: '店铺编码', minWidth: 120 },
|
|
|
+ { prop: 'store_name', label: '店铺名称', minWidth: 180 },
|
|
|
+ { prop: 'dept_name', label: '事业部', minWidth: 120 },
|
|
|
+ { prop: 'channel_name', label: '渠道', minWidth: 120 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { store_code: 'ST-TM-01', store_name: '天猫旗舰店', dept_name: '电商事业部', channel_name: '天猫' },
|
|
|
+ { store_code: 'ST-JD-01', store_name: '京东自营店', dept_name: '电商事业部', channel_name: '京东' },
|
|
|
+ { store_code: 'ST-DY-01', store_name: '抖音直播店', dept_name: '新零售事业部', channel_name: '抖音' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'structure_rule_setting',
|
|
|
+ order: 7,
|
|
|
+ name: '结构规则设置表',
|
|
|
+ tableName: 'structure_rule_setting',
|
|
|
+ description: '结构编码规则配置,无外键依赖。',
|
|
|
+ dependencies: [],
|
|
|
+ recordCount: 6,
|
|
|
+ fields: [
|
|
|
+ { name: 'rule_id', type: 'BIGINT', constraint: 'PRIMARY KEY AUTO_INCREMENT', description: '规则 ID(主键)' },
|
|
|
+ { name: 'rule_type', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '规则对象:warehouse_location / product_structure' },
|
|
|
+ { name: 'parent_field', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '父编码字段(如 warehouse_code、parent_id)' },
|
|
|
+ { name: 'parent_length', type: 'INT', constraint: 'NOT NULL', description: '父编码占用位数' },
|
|
|
+ { name: 'current_level', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '当前层级名称(L1/L2/L3)' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'rule_type', label: '规则对象', minWidth: 180 },
|
|
|
+ { prop: 'parent_field', label: '父字段', minWidth: 140 },
|
|
|
+ { prop: 'parent_length', label: '父编码长度', minWidth: 110 },
|
|
|
+ { prop: 'current_level', label: '层级', minWidth: 100 },
|
|
|
+ { prop: 'current_length', label: '当前长度', minWidth: 100 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { rule_type: 'warehouse_location', parent_field: 'warehouse_code', parent_length: 8, current_level: 'L1', current_length: 4 },
|
|
|
+ { rule_type: 'warehouse_location', parent_field: 'warehouse_code', parent_length: 8, current_level: 'L2', current_length: 2 },
|
|
|
+ { rule_type: 'product_structure', parent_field: 'parent_id', parent_length: 6, current_level: 'L3', current_length: 3 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'location',
|
|
|
+ order: 8,
|
|
|
+ name: '库位表',
|
|
|
+ tableName: 'location',
|
|
|
+ description: '库位主数据,依赖 warehouse。',
|
|
|
+ dependencies: ['warehouse'],
|
|
|
+ recordCount: 64,
|
|
|
+ fields: [
|
|
|
+ { name: 'location_code', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '库位编码(主键)' },
|
|
|
+ { name: 'warehouse_code', type: 'VARCHAR(32)', constraint: 'FOREIGN KEY', description: '仓库编码(关联仓库表)' },
|
|
|
+ { name: 'location_type', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '库位类型(常温 / 冷藏 / 拣选区等)' },
|
|
|
+ { name: 'capacity_limit', type: 'INT', constraint: 'NOT NULL', description: '库位容量限制' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'location_code', label: '库位编码', minWidth: 140 },
|
|
|
+ { prop: 'warehouse_code', label: '仓库编码', minWidth: 120 },
|
|
|
+ { prop: 'location_type', label: '库位类型', minWidth: 120 },
|
|
|
+ { prop: 'capacity_limit', label: '容量上限', minWidth: 100 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { location_code: 'WH-SH-01-A01', warehouse_code: 'WH-SH-01', location_type: '拣选区', capacity_limit: 1200 },
|
|
|
+ { location_code: 'WH-SH-01-B12', warehouse_code: 'WH-SH-01', location_type: '常温区', capacity_limit: 2400 },
|
|
|
+ { location_code: 'WH-GZ-03-C02', warehouse_code: 'WH-GZ-03', location_type: '冷藏区', capacity_limit: 800 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'bom_list',
|
|
|
+ order: 9,
|
|
|
+ name: 'BOM 清单表',
|
|
|
+ tableName: 'bom_list',
|
|
|
+ description: '成品与半成品映射关系,依赖 product 和 semi_finished_product。',
|
|
|
+ dependencies: ['product', 'semi_finished_product'],
|
|
|
+ recordCount: 86,
|
|
|
+ fields: [
|
|
|
+ { name: 'id', type: 'BIGINT', constraint: 'PRIMARY KEY', description: '主键 ID(自增)' },
|
|
|
+ { name: 'finished_sku', type: 'VARCHAR(64)', constraint: 'FOREIGN KEY', description: '成品 SKU(关联产品表)' },
|
|
|
+ { name: 'semi_sku', type: 'VARCHAR(64)', constraint: 'FOREIGN KEY', description: '半成品 SKU(关联半成品表)' },
|
|
|
+ { name: 'quantity', type: 'INT', constraint: 'NOT NULL', description: '1 个成品所需该半成品的数量' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'finished_sku', label: '成品 SKU', minWidth: 160 },
|
|
|
+ { prop: 'semi_sku', label: '半成品 SKU', minWidth: 160 },
|
|
|
+ { prop: 'quantity', label: '用量', minWidth: 100 },
|
|
|
+ { prop: 'remark', label: '说明', minWidth: 180 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { finished_sku: 'SKU-P1001-BLK', semi_sku: 'SEMI-AX1001', quantity: 1, remark: '主控板组件' },
|
|
|
+ { finished_sku: 'SKU-P1001-BLK', semi_sku: 'SEMI-BX2040', quantity: 1, remark: '电源模块' },
|
|
|
+ { finished_sku: 'SKU-P2100-GRY', semi_sku: 'SEMI-CX3012', quantity: 2, remark: '外壳组件' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'purchase_receipt',
|
|
|
+ order: 10,
|
|
|
+ name: '采购入库单表',
|
|
|
+ tableName: 'purchase_receipt',
|
|
|
+ description: '采购入库业务数据,依赖 product 和 supplier。',
|
|
|
+ dependencies: ['product', 'supplier'],
|
|
|
+ recordCount: 124,
|
|
|
+ fields: [
|
|
|
+ { name: 'receipt_id', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '单据编号(主键)' },
|
|
|
+ { name: 'product_code', type: 'VARCHAR(32)', constraint: 'FOREIGN KEY', description: '产品代码(关联产品表)' },
|
|
|
+ { name: 'supplier_id', type: 'VARCHAR(32)', constraint: 'FOREIGN KEY', description: '供应商编号(关联供应商表)' },
|
|
|
+ { name: 'quantity', type: 'INT', constraint: 'NOT NULL', description: '采购入库数量' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'receipt_id', label: '入库单号', minWidth: 140 },
|
|
|
+ { prop: 'product_code', label: '产品代码', minWidth: 120 },
|
|
|
+ { prop: 'supplier_id', label: '供应商编号', minWidth: 120 },
|
|
|
+ { prop: 'quantity', label: '入库数量', minWidth: 100 },
|
|
|
+ { prop: 'receipt_date', label: '入库日期', minWidth: 120 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { receipt_id: 'PR-20260401-001', product_code: 'PD-1001', supplier_id: 'SUP-001', quantity: 500, receipt_date: '2026-04-01' },
|
|
|
+ { receipt_id: 'PR-20260403-008', product_code: 'PD-2100', supplier_id: 'SUP-007', quantity: 320, receipt_date: '2026-04-03' },
|
|
|
+ { receipt_id: 'PR-20260408-016', product_code: 'PD-1002', supplier_id: 'SUP-015', quantity: 260, receipt_date: '2026-04-08' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'order_main',
|
|
|
+ order: 11,
|
|
|
+ name: '订单表',
|
|
|
+ tableName: 'order_main',
|
|
|
+ description: '订单业务数据,依赖 product。',
|
|
|
+ dependencies: ['product'],
|
|
|
+ recordCount: 388,
|
|
|
+ fields: [
|
|
|
+ { name: 'order_id', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '订单编号(主键)' },
|
|
|
+ { name: 'order_date', type: 'DATETIME', constraint: 'NOT NULL', description: '下单时间' },
|
|
|
+ { name: 'product_code', type: 'VARCHAR(32)', constraint: 'FOREIGN KEY', description: '产品代码(关联产品表)' },
|
|
|
+ { name: 'quantity', type: 'INT', constraint: 'NOT NULL', description: '数量' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'order_id', label: '订单编号', minWidth: 160 },
|
|
|
+ { prop: 'order_date', label: '下单时间', minWidth: 160 },
|
|
|
+ { prop: 'product_code', label: '产品代码', minWidth: 120 },
|
|
|
+ { prop: 'quantity', label: '数量', minWidth: 90 },
|
|
|
+ { prop: 'store_code', label: '店铺编码', minWidth: 120 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { order_id: 'SO-20260410-0001', order_date: '2026-04-10 09:15:00', product_code: 'PD-1001', quantity: 2, store_code: 'ST-TM-01' },
|
|
|
+ { order_id: 'SO-20260410-0008', order_date: '2026-04-10 10:48:00', product_code: 'PD-1002', quantity: 1, store_code: 'ST-JD-01' },
|
|
|
+ { order_id: 'SO-20260410-0013', order_date: '2026-04-10 11:26:00', product_code: 'PD-2100', quantity: 3, store_code: 'ST-DY-01' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'assembly_record',
|
|
|
+ order: 12,
|
|
|
+ name: '组装记录表',
|
|
|
+ tableName: 'assembly_record',
|
|
|
+ description: '组装业务记录,依赖 product。',
|
|
|
+ dependencies: ['product'],
|
|
|
+ recordCount: 53,
|
|
|
+ fields: [
|
|
|
+ { name: 'id', type: 'BIGINT', constraint: 'PRIMARY KEY', description: '主键 ID(自增)' },
|
|
|
+ { name: 'product_code', type: 'VARCHAR(50)', constraint: 'FOREIGN KEY', description: '产品代码(关联产品表)' },
|
|
|
+ { name: 'assembly_date', type: 'DATE', constraint: 'NOT NULL', description: '组装日期' },
|
|
|
+ { name: 'quantity', type: 'INT', constraint: 'NOT NULL', description: '组装数量' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'id', label: '记录 ID', minWidth: 100 },
|
|
|
+ { prop: 'product_code', label: '产品代码', minWidth: 120 },
|
|
|
+ { prop: 'assembly_date', label: '组装日期', minWidth: 120 },
|
|
|
+ { prop: 'quantity', label: '组装数量', minWidth: 100 },
|
|
|
+ { prop: 'operator', label: '操作人', minWidth: 120 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { id: 9001, product_code: 'PD-1001', assembly_date: '2026-04-09', quantity: 180, operator: '张磊' },
|
|
|
+ { id: 9008, product_code: 'PD-1002', assembly_date: '2026-04-10', quantity: 120, operator: '陈辰' },
|
|
|
+ { id: 9015, product_code: 'PD-2100', assembly_date: '2026-04-11', quantity: 96, operator: '王瑶' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'outbound_order',
|
|
|
+ order: 13,
|
|
|
+ name: '出库单表',
|
|
|
+ tableName: 'outbound_order',
|
|
|
+ description: '出库业务数据,依赖 location 和 product。',
|
|
|
+ dependencies: ['location', 'product'],
|
|
|
+ recordCount: 117,
|
|
|
+ fields: [
|
|
|
+ { name: 'order_no', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY', description: '出库单据编号(主键)' },
|
|
|
+ { name: 'location_code', type: 'VARCHAR(32)', constraint: 'FOREIGN KEY', description: '库位编码(关联库位表)' },
|
|
|
+ { name: 'sku', type: 'VARCHAR(64)', constraint: 'FOREIGN KEY', description: '产品 SKU(关联产品表)' },
|
|
|
+ { name: 'outbound_date', type: 'DATE', constraint: 'NOT NULL', description: '出库日期' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'order_no', label: '出库单号', minWidth: 150 },
|
|
|
+ { prop: 'location_code', label: '库位编码', minWidth: 140 },
|
|
|
+ { prop: 'sku', label: 'SKU', minWidth: 160 },
|
|
|
+ { prop: 'outbound_date', label: '出库日期', minWidth: 120 },
|
|
|
+ { prop: 'quantity', label: '数量', minWidth: 90 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { order_no: 'OB-20260412-001', location_code: 'WH-SH-01-A01', sku: 'SKU-P1001-BLK', outbound_date: '2026-04-12', quantity: 42 },
|
|
|
+ { order_no: 'OB-20260412-003', location_code: 'WH-SH-01-B12', sku: 'SKU-P1001-WHT', outbound_date: '2026-04-12', quantity: 31 },
|
|
|
+ { order_no: 'OB-20260413-009', location_code: 'WH-GZ-03-C02', sku: 'SKU-P2100-GRY', outbound_date: '2026-04-13', quantity: 18 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'inventory_detail',
|
|
|
+ order: 14,
|
|
|
+ name: '库存明细表',
|
|
|
+ tableName: 'inventory_detail',
|
|
|
+ description: '库存明细数据,依赖 location 和 product。',
|
|
|
+ dependencies: ['location', 'product'],
|
|
|
+ recordCount: 142,
|
|
|
+ fields: [
|
|
|
+ { name: 'location_code', type: 'VARCHAR(32)', constraint: 'PRIMARY KEY, FOREIGN KEY', description: '库位编码(关联库位表)' },
|
|
|
+ { name: 'sku', type: 'VARCHAR(64)', constraint: 'PRIMARY KEY, FOREIGN KEY', description: '产品 SKU(关联产品表)' },
|
|
|
+ { name: 'stock_quantity', type: 'INT', constraint: 'NOT NULL DEFAULT 0', description: '可用库存数量' },
|
|
|
+ { name: 'lock_quantity', type: 'INT', constraint: 'NOT NULL DEFAULT 0', description: '锁定库存数量' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'location_code', label: '库位编码', minWidth: 140 },
|
|
|
+ { prop: 'sku', label: 'SKU', minWidth: 160 },
|
|
|
+ { prop: 'stock_quantity', label: '可用库存', minWidth: 100 },
|
|
|
+ { prop: 'lock_quantity', label: '锁定库存', minWidth: 100 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { location_code: 'WH-SH-01-A01', sku: 'SKU-P1001-BLK', stock_quantity: 860, lock_quantity: 28 },
|
|
|
+ { location_code: 'WH-SH-01-B12', sku: 'SKU-P1001-WHT', stock_quantity: 640, lock_quantity: 16 },
|
|
|
+ { location_code: 'WH-GZ-03-C02', sku: 'SKU-P2100-GRY', stock_quantity: 218, lock_quantity: 12 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'metrics',
|
|
|
+ order: 15,
|
|
|
+ name: '指标表',
|
|
|
+ tableName: 'metrics',
|
|
|
+ description: '业务指标聚合结果,依赖仓库、供应商、店铺、订单等业务数据。',
|
|
|
+ dependencies: ['warehouse', 'supplier', 'store', 'order_main'],
|
|
|
+ recordCount: 48,
|
|
|
+ fields: [
|
|
|
+ { name: 'metrics_id', type: 'BIGINT', constraint: 'PRIMARY KEY AUTO_INCREMENT', description: '指标 ID(主键自增)' },
|
|
|
+ { name: 'metrics_type', type: 'VARCHAR(32)', constraint: 'NOT NULL', description: '指标类型:inventory / supplier / store / order' },
|
|
|
+ { name: 'biz_code', type: 'VARCHAR(64)', constraint: 'NOT NULL', description: '业务编码:仓库编码 / 供应商 ID / 店铺编码 / 订单编号' },
|
|
|
+ { name: 'stat_start_date', type: 'DATE', constraint: 'NOT NULL', description: '统计开始日期' },
|
|
|
+ { name: 'stat_end_date', type: 'DATE', constraint: 'NOT NULL', description: '统计结束日期' }
|
|
|
+ ],
|
|
|
+ previewColumns: [
|
|
|
+ { prop: 'metrics_type', label: '指标类型', minWidth: 120 },
|
|
|
+ { prop: 'biz_code', label: '业务编码', minWidth: 160 },
|
|
|
+ { prop: 'stat_start_date', label: '开始日期', minWidth: 120 },
|
|
|
+ { prop: 'stat_end_date', label: '结束日期', minWidth: 120 },
|
|
|
+ { prop: 'metric_value', label: '指标值', minWidth: 120 }
|
|
|
+ ],
|
|
|
+ sampleRows: [
|
|
|
+ { metrics_type: 'inventory', biz_code: 'WH-SH-01', stat_start_date: '2026-04-01', stat_end_date: '2026-04-15', metric_value: '92.4' },
|
|
|
+ { metrics_type: 'supplier', biz_code: 'SUP-001', stat_start_date: '2026-04-01', stat_end_date: '2026-04-15', metric_value: '98.1' },
|
|
|
+ { metrics_type: 'store', biz_code: 'ST-TM-01', stat_start_date: '2026-04-01', stat_end_date: '2026-04-15', metric_value: '87.6' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+function createUploadTables() {
|
|
|
+ return JSON.parse(JSON.stringify(UPLOAD_TABLES)).map(item => ({
|
|
|
+ ...item,
|
|
|
+ uploaded: false,
|
|
|
+ fileName: '',
|
|
|
+ fileSize: '',
|
|
|
+ updatedAt: ''
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'UploadIndex',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ uploadTables: createUploadTables(),
|
|
|
+ activeTableKey: 'warehouse',
|
|
|
+ activePreviewTab: 'warehouse'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ orderedTables() {
|
|
|
+ return this.uploadTables.slice().sort((a, b) => a.order - b.order)
|
|
|
+ },
|
|
|
+ selectedTable() {
|
|
|
+ return this.uploadTables.find(item => item.key === this.activeTableKey) || this.uploadTables[0]
|
|
|
+ },
|
|
|
+ completedTableCount() {
|
|
|
+ return this.uploadTables.filter(item => item.uploaded).length
|
|
|
+ },
|
|
|
+ currentRecordTotal() {
|
|
|
+ return this.uploadTables.reduce((sum, item) => sum + item.recordCount, 0)
|
|
|
+ },
|
|
|
+ progressPercent() {
|
|
|
+ return Math.round((this.completedTableCount / this.uploadTables.length) * 100)
|
|
|
+ },
|
|
|
+ nextPendingTable() {
|
|
|
+ return this.orderedTables.find(item => !item.uploaded) || null
|
|
|
+ },
|
|
|
+ nextPendingLabel() {
|
|
|
+ return this.nextPendingTable ? `${this.nextPendingTable.order}. ${this.nextPendingTable.name}` : '全部完成'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ selectTable(key) {
|
|
|
+ this.activeTableKey = key
|
|
|
+ this.activePreviewTab = key
|
|
|
+ },
|
|
|
+ handlePreviewTabChange(tab) {
|
|
|
+ this.activeTableKey = tab.name
|
|
|
+ },
|
|
|
+ canUpload(table) {
|
|
|
+ return this.uploadTables
|
|
|
+ .filter(item => item.order < table.order)
|
|
|
+ .every(item => item.uploaded)
|
|
|
+ },
|
|
|
+ getBlockedReason(table) {
|
|
|
+ const blocked = this.uploadTables
|
|
|
+ .filter(item => item.order < table.order)
|
|
|
+ .sort((a, b) => a.order - b.order)
|
|
|
+ .find(item => !item.uploaded)
|
|
|
+
|
|
|
+ if (!blocked) {
|
|
|
+ return '当前步骤已解锁'
|
|
|
+ }
|
|
|
+
|
|
|
+ return `请先完成第 ${blocked.order} 步:${blocked.name}`
|
|
|
+ },
|
|
|
+ getStatusType(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return 'success'
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.canUpload(table) ? 'warning' : 'info'
|
|
|
+ },
|
|
|
+ getStatusText(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return '已上传'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (table.fileName && this.canUpload(table)) {
|
|
|
+ return '待提交'
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.canUpload(table) ? '可上传' : '等待前置'
|
|
|
+ },
|
|
|
+ formatDependencies(table) {
|
|
|
+ return table.dependencies.length ? table.dependencies.join(' / ') : '无外键依赖'
|
|
|
+ },
|
|
|
+ triggerFileSelect(table) {
|
|
|
+ if (!this.canUpload(table) && !table.uploaded) {
|
|
|
+ this.$message.warning(this.getBlockedReason(table))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const inputRef = this.$refs[`fileInput-${table.key}`]
|
|
|
+ const input = Array.isArray(inputRef) ? inputRef[0] : inputRef
|
|
|
+
|
|
|
+ if (input) {
|
|
|
+ input.click()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleFileChange(table, event) {
|
|
|
+ const file = event.target.files && event.target.files[0]
|
|
|
+ if (!file) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ table.fileName = file.name
|
|
|
+ table.fileSize = this.formatFileSize(file.size)
|
|
|
+ table.updatedAt = '已选择文件,待提交'
|
|
|
+
|
|
|
+ event.target.value = ''
|
|
|
+ },
|
|
|
+ submitTable(table) {
|
|
|
+ if (!this.canUpload(table) && !table.uploaded) {
|
|
|
+ this.$message.warning(this.getBlockedReason(table))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!table.fileName) {
|
|
|
+ this.$message.warning(`请先为${table.name}选择上传文件`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ table.uploaded = true
|
|
|
+ table.updatedAt = `${this.getCurrentTime()} 已模拟上传`
|
|
|
+ this.$message.success(`${table.name}上传成功(前端演示)`)
|
|
|
+
|
|
|
+ const nextTable = this.orderedTables.find(item => item.order === table.order + 1)
|
|
|
+ if (nextTable) {
|
|
|
+ this.selectTable(nextTable.key)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetFromTable(table) {
|
|
|
+ this.uploadTables.forEach(item => {
|
|
|
+ if (item.order >= table.order) {
|
|
|
+ item.uploaded = false
|
|
|
+ item.fileName = ''
|
|
|
+ item.fileSize = ''
|
|
|
+ item.updatedAt = ''
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ this.selectTable(table.key)
|
|
|
+ this.$message.info(`已清空 ${table.name} 及其后续步骤`)
|
|
|
+ },
|
|
|
+ resetAllProgress() {
|
|
|
+ this.uploadTables = createUploadTables()
|
|
|
+ this.activeTableKey = 'warehouse'
|
|
|
+ this.activePreviewTab = 'warehouse'
|
|
|
+ this.$message.success('上传进度已重置')
|
|
|
+ },
|
|
|
+ focusNextPending() {
|
|
|
+ if (this.nextPendingTable) {
|
|
|
+ this.selectTable(this.nextPendingTable.key)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$message.success('全部上传步骤已完成')
|
|
|
+ },
|
|
|
+ formatFileSize(size) {
|
|
|
+ if (!size) {
|
|
|
+ return '0 B'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (size < 1024) {
|
|
|
+ return `${size} B`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (size < 1024 * 1024) {
|
|
|
+ return `${(size / 1024).toFixed(1)} KB`
|
|
|
+ }
|
|
|
+
|
|
|
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
|
|
+ },
|
|
|
+ getCurrentTime() {
|
|
|
+ const now = new Date()
|
|
|
+ const year = now.getFullYear()
|
|
|
+ const month = `${now.getMonth() + 1}`.padStart(2, '0')
|
|
|
+ const day = `${now.getDate()}`.padStart(2, '0')
|
|
|
+ const hour = `${now.getHours()}`.padStart(2, '0')
|
|
|
+ const minute = `${now.getMinutes()}`.padStart(2, '0')
|
|
|
+
|
|
|
+ return `${year}-${month}-${day} ${hour}:${minute}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.upload-page {
|
|
|
+ background: #f5f7fb;
|
|
|
+}
|
|
|
+
|
|
|
+.page-header {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ h2 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+
|
|
|
+ i {
|
|
|
+ margin-right: 8px;
|
|
|
+ color: #409eff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-desc {
|
|
|
+ margin: 0;
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.metrics-row,
|
|
|
+.content-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card {
|
|
|
+ border: none;
|
|
|
+
|
|
|
+ ::v-deep .el-card__body {
|
|
|
+ padding: 22px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.metric-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-icon {
|
|
|
+ width: 52px;
|
|
|
+ height: 52px;
|
|
|
+ border-radius: 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16px;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-blue {
|
|
|
+ background: linear-gradient(135deg, #409eff, #63b3ff);
|
|
|
+}
|
|
|
+
|
|
|
+.icon-green {
|
|
|
+ background: linear-gradient(135deg, #67c23a, #85ce61);
|
|
|
+}
|
|
|
+
|
|
|
+.icon-orange {
|
|
|
+ background: linear-gradient(135deg, #e6a23c, #f3c36b);
|
|
|
+}
|
|
|
+
|
|
|
+.icon-red {
|
|
|
+ background: linear-gradient(135deg, #f56c6c, #ff8f8f);
|
|
|
+}
|
|
|
+
|
|
|
+.metric-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-value {
|
|
|
+ font-size: 26px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-sub {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.next-step {
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-card,
|
|
|
+.order-card,
|
|
|
+.detail-card,
|
|
|
+.snapshot-card {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-top {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-desc {
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.7;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-progress {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ span {
|
|
|
+ flex-shrink: 0;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.order-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-item {
|
|
|
+ display: flex;
|
|
|
+ gap: 14px;
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #c6e2ff;
|
|
|
+ box-shadow: 0 8px 18px rgba(64, 158, 255, 0.08);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #f5faff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.uploaded {
|
|
|
+ border-color: #d9f2d0;
|
|
|
+ background: #f6ffed;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.locked {
|
|
|
+ background: #fafafa;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.order-index {
|
|
|
+ width: 34px;
|
|
|
+ height: 34px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-weight: 700;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.order-main {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.order-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.order-table {
|
|
|
+ margin-top: 2px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-desc {
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.6;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.order-footer {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-actions {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-alert {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-descriptions {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.selected-file {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f4f9ff;
|
|
|
+ color: #409eff;
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ .file-size {
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.section-with-gap {
|
|
|
+ margin-top: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.snapshot-meta {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.snapshot-desc {
|
|
|
+ color: #606266;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.hidden-inputs {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .overview-top {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-actions {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .metric-value {
|
|
|
+ font-size: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-title-row,
|
|
|
+ .card-header {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-progress {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|