|
|
@@ -0,0 +1,1422 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container upload-page">
|
|
|
+ <div class="page-header">
|
|
|
+ <h2><i class="el-icon-upload2"></i> 资料上传</h2>
|
|
|
+ <p class="page-desc">请先处理页面上方标记为“可上传”的资料;暂未开放的资料会提示需要先上传哪些内容。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-card class="overview-card hero-card">
|
|
|
+ <div class="overview-top">
|
|
|
+ <div>
|
|
|
+ <div class="overview-title">选择文件后即可上传</div>
|
|
|
+ <div class="overview-desc">
|
|
|
+ 支持 CSV、XLS、XLSX 文件。页面已把可以直接上传的资料排在前面,暂不可上传的资料会在列表中给出原因。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="hero-summary">
|
|
|
+ <div class="summary-item">
|
|
|
+ <span>{{ readyUploadCount }}</span>
|
|
|
+ <label>可上传</label>
|
|
|
+ </div>
|
|
|
+ <div class="summary-item">
|
|
|
+ <span>{{ completedTableCount }}</span>
|
|
|
+ <label>已完成</label>
|
|
|
+ </div>
|
|
|
+ <div class="summary-item">
|
|
|
+ <span>{{ waitingUploadCount }}</span>
|
|
|
+ <label>待准备</label>
|
|
|
+ </div>
|
|
|
+ </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="success">{{ readyUploadCount }} 项可处理</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="order-list">
|
|
|
+ <div
|
|
|
+ v-for="table in displayTables"
|
|
|
+ :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">
|
|
|
+ <i :class="getItemIcon(table)"></i>
|
|
|
+ </div>
|
|
|
+ <div class="order-main">
|
|
|
+ <div class="order-title-row">
|
|
|
+ <div>
|
|
|
+ <div class="order-title">{{ getUploadName(table) }}</div>
|
|
|
+ <div class="order-table">{{ getUploadHint(table) }}</div>
|
|
|
+ </div>
|
|
|
+ <el-tag :type="getStatusType(table)" size="mini">
|
|
|
+ {{ getStatusText(table) }}
|
|
|
+ </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>{{ getUploadName(selectedTable) }}</span>
|
|
|
+ <el-tag size="mini" :type="getStatusType(selectedTable)">{{ getStatusText(selectedTable) }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="detail-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :disabled="selectedTable.uploaded || selectedTable.uploading || !isBackendSupported(selectedTable)"
|
|
|
+ @click="triggerFileSelect(selectedTable)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-folder-opened"></i> 选择文件
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ size="small"
|
|
|
+ :disabled="selectedTable.uploaded || selectedTable.uploading || selectedTable.previewLoading || !canUpload(selectedTable) || hasMissingFields(selectedTable)"
|
|
|
+ :loading="selectedTable.uploading"
|
|
|
+ @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 ? '这项资料已上传完成。' : '这项资料现在可以上传,请选择 CSV、XLS 或 XLSX 文件。'"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="upload-dropzone" :class="{ disabled: selectedTable.uploaded || !isBackendSupported(selectedTable) }" @click="triggerFileSelect(selectedTable)">
|
|
|
+ <i class="el-icon-upload"></i>
|
|
|
+ <div>
|
|
|
+ <strong>{{ selectedTable.fileName || '点击选择上传文件' }}</strong>
|
|
|
+ <p>{{ selectedTable.fileName ? selectedTable.fileSize : (isBackendSupported(selectedTable) ? '支持 CSV、XLS、XLSX,选择后再点击开始上传' : '这项资料暂未接入上传') }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-if="selectedTable.previewLoading"
|
|
|
+ :closable="false"
|
|
|
+ type="info"
|
|
|
+ show-icon
|
|
|
+ title="正在读取文件内容,请稍候"
|
|
|
+ class="detail-alert"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-else-if="hasMissingFields(selectedTable)"
|
|
|
+ :closable="false"
|
|
|
+ type="error"
|
|
|
+ show-icon
|
|
|
+ class="detail-alert"
|
|
|
+ >
|
|
|
+ <div slot="title">文件缺少必要内容,暂不能上传</div>
|
|
|
+ <div class="missing-fields">
|
|
|
+ <el-tag
|
|
|
+ v-for="field in selectedTable.missingFields"
|
|
|
+ :key="field.name"
|
|
|
+ size="mini"
|
|
|
+ type="danger"
|
|
|
+ effect="plain"
|
|
|
+ >
|
|
|
+ {{ field.label || field.name }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </el-alert>
|
|
|
+
|
|
|
+ <el-alert
|
|
|
+ v-else-if="selectedTable.previewError"
|
|
|
+ :closable="false"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :title="selectedTable.previewError"
|
|
|
+ class="detail-alert"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="section-title section-with-gap">
|
|
|
+ <span>内容预览</span>
|
|
|
+ <el-tag size="mini" :type="selectedTable.fileName ? 'success' : 'info'">{{ getPreviewCountText(selectedTable) }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ v-loading="selectedTable.previewLoading"
|
|
|
+ :data="selectedTable.sampleRows"
|
|
|
+ stripe
|
|
|
+ size="small"
|
|
|
+ class="data-table"
|
|
|
+ :empty-text="selectedTable.fileName ? '文件中没有可预览的数据' : '选择文件后展示真实数据'"
|
|
|
+ >
|
|
|
+ <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">可切换查看 {{ uploadTables.length }} 项</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">{{ getUploadName(table) }}</span>
|
|
|
+ <div class="snapshot-meta">
|
|
|
+ <el-tag size="mini" :type="getStatusType(table)">{{ getStatusText(table) }}</el-tag>
|
|
|
+ <el-tag size="mini" effect="plain" :type="table.fileName ? 'success' : 'info'">{{ getPreviewCountText(table) }}</el-tag>
|
|
|
+ <span class="snapshot-desc">{{ getUploadHint(table) }}</span>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ v-loading="table.previewLoading"
|
|
|
+ :data="table.sampleRows"
|
|
|
+ stripe
|
|
|
+ size="mini"
|
|
|
+ :empty-text="table.fileName ? '文件中没有可预览的数据' : '选择文件后展示真实数据'"
|
|
|
+ >
|
|
|
+ <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>
|
|
|
+import { previewPreparedData, uploadPreparedData } from '@/api/uploadData'
|
|
|
+
|
|
|
+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: '业务指标聚合结果,SQL 与设计文档未定义外键约束,可单独上传。',
|
|
|
+ dependencies: [],
|
|
|
+ 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' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const UPLOAD_NAMES = {
|
|
|
+ warehouse: '仓库资料',
|
|
|
+ supplier: '供应商资料',
|
|
|
+ semi_finished_product: '半成品资料',
|
|
|
+ product: '产品资料',
|
|
|
+ product_structure: '产品分类资料',
|
|
|
+ store: '店铺资料',
|
|
|
+ structure_rule_setting: '编码规则资料',
|
|
|
+ location: '库位资料',
|
|
|
+ bom_list: '组合清单资料',
|
|
|
+ purchase_receipt: '采购入库资料',
|
|
|
+ order_main: '订单资料',
|
|
|
+ assembly_record: '组装记录资料',
|
|
|
+ outbound_order: '出库资料',
|
|
|
+ inventory_detail: '库存资料',
|
|
|
+ metrics: '经营指标资料'
|
|
|
+}
|
|
|
+
|
|
|
+const BACKEND_UPLOAD_KEYS = [
|
|
|
+ 'supplier',
|
|
|
+ 'semi_finished_product',
|
|
|
+ 'product',
|
|
|
+ 'bom_list',
|
|
|
+ 'purchase_receipt',
|
|
|
+ 'order_main',
|
|
|
+ 'assembly_record',
|
|
|
+ 'store'
|
|
|
+]
|
|
|
+
|
|
|
+function createUploadTables() {
|
|
|
+ return JSON.parse(JSON.stringify(UPLOAD_TABLES)).map(item => ({
|
|
|
+ ...item,
|
|
|
+ defaultPreviewColumns: item.previewColumns,
|
|
|
+ defaultSampleRows: item.sampleRows,
|
|
|
+ recordCount: 0,
|
|
|
+ sampleRows: [],
|
|
|
+ uploaded: false,
|
|
|
+ fileName: '',
|
|
|
+ fileSize: '',
|
|
|
+ rawFile: null,
|
|
|
+ uploading: false,
|
|
|
+ previewLoading: false,
|
|
|
+ previewError: '',
|
|
|
+ missingFields: [],
|
|
|
+ updatedAt: ''
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'UploadIndex',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ uploadTables: createUploadTables(),
|
|
|
+ activeTableKey: 'supplier',
|
|
|
+ activePreviewTab: 'supplier'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ progressPercent() {
|
|
|
+ return Math.round((this.completedTableCount / this.uploadTables.length) * 100)
|
|
|
+ },
|
|
|
+ readyUploadCount() {
|
|
|
+ return this.uploadTables.filter(item => !item.uploaded && this.canUpload(item)).length
|
|
|
+ },
|
|
|
+ waitingUploadCount() {
|
|
|
+ return this.uploadTables.filter(item => !item.uploaded && !this.canUpload(item)).length
|
|
|
+ },
|
|
|
+ displayTables() {
|
|
|
+ return this.orderedTables.slice().sort((a, b) => {
|
|
|
+ const aRank = this.getDisplayRank(a)
|
|
|
+ const bRank = this.getDisplayRank(b)
|
|
|
+
|
|
|
+ if (aRank !== bRank) {
|
|
|
+ return aRank - bRank
|
|
|
+ }
|
|
|
+
|
|
|
+ return a.order - b.order
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ selectTable(key) {
|
|
|
+ this.activeTableKey = key
|
|
|
+ this.activePreviewTab = key
|
|
|
+ },
|
|
|
+ handlePreviewTabChange(tab) {
|
|
|
+ this.activeTableKey = tab.name
|
|
|
+ },
|
|
|
+ getTableByKey(key) {
|
|
|
+ return this.uploadTables.find(item => item.key === key)
|
|
|
+ },
|
|
|
+ getBlockedDependencies(table) {
|
|
|
+ return table.dependencies
|
|
|
+ .map(key => this.getTableByKey(key))
|
|
|
+ .filter(item => item && !item.uploaded)
|
|
|
+ },
|
|
|
+ getDisplayRank(table) {
|
|
|
+ if (!table.uploaded && this.canUpload(table)) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!table.uploaded && this.isBackendSupported(table)) {
|
|
|
+ return 1
|
|
|
+ }
|
|
|
+
|
|
|
+ return table.uploaded ? 2 : 3
|
|
|
+ },
|
|
|
+ isBackendSupported(table) {
|
|
|
+ return BACKEND_UPLOAD_KEYS.includes(table.key)
|
|
|
+ },
|
|
|
+ getUploadName(table) {
|
|
|
+ return UPLOAD_NAMES[table.key] || table.name.replace(/表$/, '资料')
|
|
|
+ },
|
|
|
+ getUploadHint(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return table.updatedAt || '已完成'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.isBackendSupported(table)) {
|
|
|
+ return '资料文件暂未整理完成,暂不支持上传'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.canUpload(table)) {
|
|
|
+ return '现在可以选择文件上传'
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.getBlockedReason(table)
|
|
|
+ },
|
|
|
+ getItemIcon(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return 'el-icon-check'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.isBackendSupported(table)) {
|
|
|
+ return 'el-icon-document'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.canUpload(table)) {
|
|
|
+ return 'el-icon-upload2'
|
|
|
+ }
|
|
|
+
|
|
|
+ return 'el-icon-lock'
|
|
|
+ },
|
|
|
+ getDependentKeys(table) {
|
|
|
+ const dependentKeys = new Set([table.key])
|
|
|
+ let changed = true
|
|
|
+
|
|
|
+ while (changed) {
|
|
|
+ changed = false
|
|
|
+ this.uploadTables.forEach(item => {
|
|
|
+ if (
|
|
|
+ !dependentKeys.has(item.key) &&
|
|
|
+ item.dependencies.some(dependency => dependentKeys.has(dependency))
|
|
|
+ ) {
|
|
|
+ dependentKeys.add(item.key)
|
|
|
+ changed = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return dependentKeys
|
|
|
+ },
|
|
|
+ canUpload(table) {
|
|
|
+ return this.isBackendSupported(table) && this.getBlockedDependencies(table).length === 0
|
|
|
+ },
|
|
|
+ getBlockedReason(table) {
|
|
|
+ if (!this.isBackendSupported(table)) {
|
|
|
+ return '这项资料暂未接入上传'
|
|
|
+ }
|
|
|
+
|
|
|
+ const blocked = this.getBlockedDependencies(table)
|
|
|
+
|
|
|
+ if (!blocked.length) {
|
|
|
+ return '现在可以上传'
|
|
|
+ }
|
|
|
+
|
|
|
+ return `请先上传:${blocked.map(item => this.getUploadName(item)).join('、')}`
|
|
|
+ },
|
|
|
+ getStatusType(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return 'success'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.hasMissingFields(table)) {
|
|
|
+ return 'danger'
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.canUpload(table) ? 'warning' : 'info'
|
|
|
+ },
|
|
|
+ getStatusText(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return '已上传'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.isBackendSupported(table)) {
|
|
|
+ return '暂未接入'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (table.uploading) {
|
|
|
+ return '上传中'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (table.previewLoading) {
|
|
|
+ return '读取中'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.hasMissingFields(table)) {
|
|
|
+ return '需调整'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (table.fileName && this.canUpload(table)) {
|
|
|
+ return '待提交'
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.canUpload(table) ? '可上传' : '待准备'
|
|
|
+ },
|
|
|
+ triggerFileSelect(table) {
|
|
|
+ if (table.uploaded) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.isBackendSupported(table)) {
|
|
|
+ this.$message.warning(this.getBlockedReason(table))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ 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()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleFileChange(table, event) {
|
|
|
+ const file = event.target.files && event.target.files[0]
|
|
|
+ if (!file) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const isAllowedFile = /\.(csv|xls|xlsx)$/i.test(file.name)
|
|
|
+ if (!isAllowedFile) {
|
|
|
+ this.$message.warning('仅支持 CSV、XLS、XLSX 格式文件')
|
|
|
+ event.target.value = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ table.fileName = file.name
|
|
|
+ table.fileSize = this.formatFileSize(file.size)
|
|
|
+ table.rawFile = file
|
|
|
+ table.updatedAt = '已选择文件,正在读取内容'
|
|
|
+ table.previewLoading = true
|
|
|
+ table.previewError = ''
|
|
|
+ table.missingFields = []
|
|
|
+ table.sampleRows = []
|
|
|
+ table.recordCount = 0
|
|
|
+
|
|
|
+ event.target.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await previewPreparedData(table.key, file)
|
|
|
+ const data = response.data || {}
|
|
|
+ table.previewColumns = data.columns && data.columns.length ? data.columns : table.defaultPreviewColumns
|
|
|
+ table.sampleRows = data.sampleRows || []
|
|
|
+ table.recordCount = data.totalRows || 0
|
|
|
+ table.missingFields = data.missingFields || []
|
|
|
+ table.updatedAt = this.hasMissingFields(table) ? '文件内容需要调整' : '已选择文件,待提交'
|
|
|
+
|
|
|
+ if (this.hasMissingFields(table)) {
|
|
|
+ this.$message.warning(`${this.getUploadName(table)}缺少必要内容:${this.formatMissingFields(table)}`)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ table.previewError = error && error.message ? error.message : '文件内容读取失败,请检查文件后重新选择'
|
|
|
+ table.updatedAt = '文件内容读取失败'
|
|
|
+ table.rawFile = null
|
|
|
+ } finally {
|
|
|
+ table.previewLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async submitTable(table) {
|
|
|
+ if (!this.isBackendSupported(table)) {
|
|
|
+ this.$message.warning(this.getBlockedReason(table))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.canUpload(table) && !table.uploaded) {
|
|
|
+ this.$message.warning(this.getBlockedReason(table))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!table.rawFile) {
|
|
|
+ this.$message.warning(`请先为${this.getUploadName(table)}选择上传文件`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.hasMissingFields(table)) {
|
|
|
+ this.$message.warning(`文件缺少必要内容:${this.formatMissingFields(table)}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ table.uploading = true
|
|
|
+ try {
|
|
|
+ const response = await uploadPreparedData(table.key, table.rawFile)
|
|
|
+ table.uploaded = true
|
|
|
+ table.updatedAt = `${this.getCurrentTime()} 已上传`
|
|
|
+ table.recordCount = (response.data && response.data.totalRows) || table.recordCount
|
|
|
+ this.$message.success((response.data && response.data.message) || `${this.getUploadName(table)}上传成功`)
|
|
|
+
|
|
|
+ const nextTable = this.displayTables.find(item => !item.uploaded && this.canUpload(item))
|
|
|
+ if (nextTable) {
|
|
|
+ this.selectTable(nextTable.key)
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ table.uploading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetFromTable(table) {
|
|
|
+ const dependentKeys = this.getDependentKeys(table)
|
|
|
+
|
|
|
+ this.uploadTables.forEach(item => {
|
|
|
+ if (dependentKeys.has(item.key)) {
|
|
|
+ item.uploaded = false
|
|
|
+ item.fileName = ''
|
|
|
+ item.fileSize = ''
|
|
|
+ item.rawFile = null
|
|
|
+ item.uploading = false
|
|
|
+ item.previewLoading = false
|
|
|
+ item.previewError = ''
|
|
|
+ item.missingFields = []
|
|
|
+ item.sampleRows = []
|
|
|
+ item.recordCount = 0
|
|
|
+ item.previewColumns = item.defaultPreviewColumns
|
|
|
+ item.updatedAt = ''
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ this.selectTable(table.key)
|
|
|
+ this.$message.info(`已重新打开 ${this.getUploadName(table)} 的上传`)
|
|
|
+ },
|
|
|
+ hasMissingFields(table) {
|
|
|
+ return table && table.missingFields && table.missingFields.length > 0
|
|
|
+ },
|
|
|
+ formatMissingFields(table) {
|
|
|
+ if (!this.hasMissingFields(table)) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ return table.missingFields.map(field => field.label || field.name).join('、')
|
|
|
+ },
|
|
|
+ getPreviewCountText(table) {
|
|
|
+ if (table.previewLoading) {
|
|
|
+ return '读取中'
|
|
|
+ }
|
|
|
+ return table.fileName ? `${table.recordCount || 0} 条真实数据` : '待选择文件'
|
|
|
+ },
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.content-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-card,
|
|
|
+.order-card,
|
|
|
+.detail-card,
|
|
|
+.snapshot-card {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-card {
|
|
|
+ background: linear-gradient(135deg, #ffffff 0%, #f3f8ff 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.overview-top {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24px;
|
|
|
+ margin-bottom: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-title {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.overview-desc {
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.7;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-summary {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-item {
|
|
|
+ min-width: 86px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ border: 1px solid #dcecff;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(255, 255, 255, 0.82);
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: block;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1.2;
|
|
|
+ color: #2563eb;
|
|
|
+ }
|
|
|
+
|
|
|
+ label {
|
|
|
+ display: block;
|
|
|
+ margin-top: 4px;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.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: 8px;
|
|
|
+ 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: 8px;
|
|
|
+ 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;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+.missing-fields {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-dropzone {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 24px;
|
|
|
+ border: 1px dashed #9dccff;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f7fbff;
|
|
|
+ color: #409eff;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #eef7ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ color: #909399;
|
|
|
+ background: #f7f7f7;
|
|
|
+ border-color: #dcdfe6;
|
|
|
+ }
|
|
|
+
|
|
|
+ > i {
|
|
|
+ font-size: 34px;
|
|
|
+ }
|
|
|
+
|
|
|
+ strong {
|
|
|
+ display: block;
|
|
|
+ color: #303133;
|
|
|
+ font-size: 16px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 0;
|
|
|
+ color: #909399;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .hero-summary {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-item {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .order-title-row,
|
|
|
+ .card-header {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-progress {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .hero-summary {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-dropzone {
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 18px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|