Commit f3883e76 by renyizhao

商品导入

parent 04340bf7
......@@ -71,3 +71,28 @@ export const ResourceSpuApi = {
return await request.download({ url: `/compute/resource-spu/export-excel`, params })
}
}
// ========== 阶段 8 - 批量导入(弹窗两步式:模板下载 + 上传导入) ==========
/** 算力资源 SPU 导入 - 失败行 */
export interface ResourceSpuImportRowVO {
rowIndex?: number
name?: string
message?: string
}
/** 算力资源 SPU 导入 - 响应 */
export interface ResourceSpuImportRespVO {
createNames?: string[]
failureRows?: ResourceSpuImportRowVO[]
}
// 下载算力资源 SPU 导入模板
export const getResourceSpuImportTemplate = () => {
return request.download({ url: '/compute/resource-spu/get-import-template' })
}
// 上传 Excel 导入算力资源 SPU
export const importResourceSpu = async (formData: FormData) => {
return await request.upload({ url: `/compute/resource-spu/import`, data: formData })
}
......@@ -265,7 +265,6 @@ const formRules = reactive({
initUsername: [{required: true, message: '初始用户名不能为空', trigger: 'blur'}],
initPassword: [{required: true, message: '初始密码不能为空', trigger: 'blur'}],
categoryId: [{required: true, message: '算力资源分类编号不能为空', trigger: 'blur'}],
picUrl: [{required: true, message: '商品封面图不能为空', trigger: 'blur'}],
sales: [{required: true, message: '商品销量不能为空', trigger: 'blur'}],
status: [{required: true, message: '状态(0 下架,1 上架,2 回收)不能为空', trigger: 'blur'}]
})
......
......@@ -124,6 +124,14 @@
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="openImportDialog"
v-hasPermi="['compute:resource-spu:import']"
>
<Icon icon="ep:upload" class="mr-5px" /> 批量导入
</el-button>
<!-- <el-button
type="success"
plain
......@@ -244,16 +252,117 @@
@pagination="getList"
/>
</ContentWrap>
<!-- 批量导入 - 两步式弹窗(阶段 8) -->
<Dialog
v-model="importDialogVisible"
title="批量导入算力资源 SPU"
width="720"
:close-on-click-modal="false"
>
<el-steps :active="importStep" align-center finish-status="success" class="mb-20px">
<el-step title="上传文件" description="下载模板并填写商品信息" />
<el-step title="导入结果" description="查看成功/失败明细" />
</el-steps>
<!-- Step 1:下载模板 + 上传文件 -->
<div v-show="importStep === 0">
<el-alert
title="操作说明"
type="info"
:closable="false"
show-icon
class="mb-12px"
>
<template #default>
<div>1. 点击「下载 Excel 模板」,按模板列填写商品信息;规格列名必须与字典「算力资源配置类别」一致(如 CPU / GPU / RAM)</div>
<div>2. 商品名称、分类、封面图为必填;至少需要 1 个规格;同名商品会报错,不支持 update</div>
<div>3. 分类填写分类 ID(数字),可在分类管理中查看</div>
</template>
</el-alert>
<el-button :loading="templateLoading" @click="downloadImportTemplate">
<Icon icon="ep:download" class="mr-5px" /> 下载 Excel 模板
</el-button>
<el-upload
ref="importUploadRef"
v-model:file-list="importFileList"
:auto-upload="false"
:limit="1"
:on-exceed="handleImportExceed"
accept=".xlsx, .xls"
action="none"
drag
class="mt-12px"
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入 xls、xlsx 格式文件。</span>
</div>
</template>
</el-upload>
</div>
<!-- Step 2:结果展示 -->
<div v-show="importStep === 1">
<el-alert
:title="`成功创建 ${importResult.createNames?.length || 0} 个,失败 ${importResult.failureRows?.length || 0} 个`"
:type="(importResult.failureRows?.length || 0) > 0 ? 'warning' : 'success'"
:closable="false"
show-icon
class="mb-12px"
/>
<el-table
v-if="(importResult.failureRows?.length || 0) > 0"
:data="importResult.failureRows"
border
max-height="320"
class="mb-12px"
>
<el-table-column label="Excel 行号" prop="rowIndex" width="100" align="center" />
<el-table-column label="商品名称" prop="name" min-width="180" />
<el-table-column label="失败原因" prop="message" min-width="280" />
</el-table>
<div v-if="(importResult.failureRows?.length || 0) > 0">
<el-link type="primary" :underline="false" @click="downloadFailureExcel">
<Icon icon="ep:download" class="mr-5px" />下载失败行 Excel
</el-link>
</div>
</div>
<template #footer>
<el-button v-if="importStep === 0" @click="importDialogVisible = false">取 消</el-button>
<el-button
v-if="importStep === 0"
type="primary"
:loading="importLoading"
:disabled="importFileList.length === 0"
@click="submitImport"
>
开始导入
</el-button>
<el-button v-if="importStep === 1" @click="resetImportDialog">重新导入</el-button>
<el-button v-if="importStep === 1" type="primary" @click="importDialogVisible = false">关 闭</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onActivated } from 'vue'
import { ref, reactive, onMounted, onActivated, nextTick } from 'vue'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { createImageViewer } from '@/components/ImageViewer'
import { ResourceSpuApi, ResourceSpu } from '@/api/compute/resourcespu'
import {
ResourceSpuApi,
ResourceSpu,
getResourceSpuImportTemplate,
importResourceSpu,
ResourceSpuImportRespVO
} from '@/api/compute/resourcespu'
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import type { UploadUserFile, UploadRawFile } from 'element-plus'
const { push } = useRouter()
......@@ -372,6 +481,95 @@ const handleExport = async () => {
}
}
// ========== 阶段 8 - 批量导入(弹窗两步式) ==========
const importDialogVisible = ref(false) // 导入弹窗是否显示
const importStep = ref(0) // 0=上传,1=结果
const importFileList = ref<UploadUserFile[]>([]) // 上传文件列表
const importLoading = ref(false) // 导入中
const templateLoading = ref(false) // 下载模板中
const importUploadRef = ref() // upload 组件 ref
const importResult = ref<ResourceSpuImportRespVO>({
createNames: [],
failureRows: []
})
/** 打开导入弹窗 */
const openImportDialog = () => {
resetImportDialog()
importDialogVisible.value = true
}
/** 重置导入弹窗状态 */
const resetImportDialog = async () => {
importStep.value = 0
importFileList.value = []
importResult.value = { createNames: [], failureRows: [] }
await nextTick()
importUploadRef.value?.clearFiles()
}
/** 下载导入模板 */
const downloadImportTemplate = async () => {
templateLoading.value = true
try {
const data = await getResourceSpuImportTemplate()
download.excel(data, '算力资源SPU导入模板.xls')
} catch {
} finally {
templateLoading.value = false
}
}
/** 文件超出限制提示 */
const handleImportExceed = (files: UploadUserFile[]) => {
message.error('最多只能上传一个文件!')
}
/** 提交导入 */
const submitImport = async () => {
if (importFileList.value.length === 0) {
message.error('请上传文件')
return
}
importLoading.value = true
try {
const formData = new FormData()
const raw = importFileList.value[0].raw as UploadRawFile
formData.append('file', raw)
const res: any = await importResourceSpu(formData)
if (res && res.data) {
importResult.value = res.data
} else {
importResult.value = { createNames: [], failureRows: [] }
}
importStep.value = 1
// 有成功就刷新列表
if ((importResult.value.createNames?.length || 0) > 0) {
await getList()
}
} catch (e: any) {
message.error('导入失败:' + (e?.message || '请稍后重试'))
} finally {
importLoading.value = false
}
}
/** 下载失败行 Excel(占位实现:纯前端导出 .xls 文本,后续可改为后端生成) */
const downloadFailureExcel = () => {
const rows = importResult.value.failureRows || []
const head = ['行号', '商品名称', '失败原因']
const lines: string[] = []
lines.push(head.join('\t'))
rows.forEach((r) => {
lines.push([r.rowIndex, r.name || '', r.message || ''].join('\t'))
})
const content = lines.join('\n')
// 用 Excel 可识别的 BOM 头
const blob = new Blob(['\ufeff' + content], { type: 'application/vnd.ms-excel;charset=utf-8' })
download.excel(blob, '算力资源SPU导入失败行.xls')
}
/** 初始化 **/
onMounted(async () => {
try {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment