Quellcode durchsuchen

refactor: 优化 ImportData.vue 和 CommonAnalysis.vue 组件,调整样式和结构,提升用户体验。修复了一些代码中的格式问题,确保一致性和可读性。

master
lizhuang vor 1 Woche
Ursprung
Commit
09ddc2b4a1

+ 204
- 200
src/views/sales-analysis/analysis-data/ImportData.vue Datei anzeigen

@@ -7,8 +7,8 @@
align-center
class="steps-container"
>
<el-step title="选择配置" description="选择分类"></el-step>
<el-step title="上传文件" description="上传Excel数据文件"></el-step>
<el-step title="选择配置" description="选择分类" />
<el-step title="上传文件" description="上传Excel数据文件" />
</el-steps>

<!-- 第一步:选择分类 -->
@@ -26,7 +26,7 @@
:closable="false"
/>

<div class="category-list" v-loading="categoriesLoading">
<div v-loading="categoriesLoading" class="category-list">
<div
v-for="category in categoriesList"
:key="category.id"
@@ -36,9 +36,9 @@
>
<div class="category-name">{{ category.name }}</div>
<div class="category-desc">
{{ category.description || "暂无描述" }}
{{ category.description || '暂无描述' }}
</div>
<div class="category-fields" v-if="category.fieldConfig">
<div v-if="category.fieldConfig" class="category-fields">
<el-tag
v-for="field in parseFieldConfig(category.fieldConfig)"
:key="field.fieldName"
@@ -76,8 +76,7 @@
type="warning"
show-icon
:closable="false"
>
</el-alert>
/>

<el-upload
ref="upload"
@@ -94,13 +93,14 @@
drag
class="upload-area"
>
<i class="el-icon-upload"></i>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
<div slot="tip" class="el-upload__tip">
支持多选Excel文件(.xlsx/.xls),单文件不超过10MB,文件数量不超过40个
<el-button @click="clearAllFiles" type="text"
>清空文件列表</el-button
>
<el-button
type="text"
@click="clearAllFiles"
>清空文件列表</el-button>
</div>
</el-upload>

@@ -108,9 +108,9 @@
<el-button @click="prevStep">上一步</el-button>
<el-button
type="primary"
@click="saveData"
:loading="saveLoading"
:disabled="!fileList.length"
@click="saveData"
>
保存数据
</el-button>
@@ -121,11 +121,11 @@
</template>

<script>
import { getCategoriesSimple, saveImportedData } from "@/api/sales-analysis";
import * as XLSX from "xlsx";
import { getCategoriesSimple, saveImportedData } from '@/api/sales-analysis'
import * as XLSX from 'xlsx'

export default {
name: "ImportData",
name: 'ImportData',
data() {
return {
currentStep: 0,
@@ -143,120 +143,120 @@ export default {
fileList: [],
processingFiles: false,
errorData: [],
errorDataVisible: false,
};
errorDataVisible: false
}
},
computed: {
uploadUrl() {
if (this.selectedDataSource === "fba") {
return process.env.VUE_APP_BASE_API + "/analysis-data/import/fba";
if (this.selectedDataSource === 'fba') {
return process.env.VUE_APP_BASE_API + '/analysis-data/import/fba'
}
return process.env.VUE_APP_BASE_API + "/analysis-data/import";
return process.env.VUE_APP_BASE_API + '/analysis-data/import'
},
uploadData() {
if (this.selectedDataSource === "fba") {
if (this.selectedDataSource === 'fba') {
return {
shopName: this.selectedShopName,
};
shopName: this.selectedShopName
}
}
return {
categoryId: this.selectedCategoryId,
};
},
categoryId: this.selectedCategoryId
}
}
},
mounted() {
this.fetchCategoriesList();
this.fetchCategoriesList()
},
methods: {
// 解析字段配置字符串为 JSON 对象
parseFieldConfig(fieldConfig) {
try {
if (!fieldConfig || typeof fieldConfig === "object") {
return fieldConfig || [];
if (!fieldConfig || typeof fieldConfig === 'object') {
return fieldConfig || []
}
return JSON.parse(fieldConfig);
return JSON.parse(fieldConfig)
} catch (error) {
console.warn("解析字段配置失败:", error);
return [];
console.warn('解析字段配置失败:', error)
return []
}
},

showErrorData() {
this.errorDataVisible = true;
this.errorData = this.processResult.errorDetails;
this.errorDataVisible = true
this.errorData = this.processResult.errorDetails
},
// 选择数据来源
selectDataSource(source) {
this.selectedDataSource = source;
this.selectedDataSource = source
// 清空之前的选择
this.selectedCategoryId = null;
this.selectedCategory = null;
this.selectedShopName = null;
this.selectedCategoryId = null
this.selectedCategory = null
this.selectedShopName = null
},

// 获取分类列表
async fetchCategoriesList() {
try {
this.categoriesLoading = true;
const response = await getCategoriesSimple();
this.categoriesLoading = true
const response = await getCategoriesSimple()

if (response.code === 200) {
this.categoriesList = response.data || [];
this.categoriesList = response.data || []
} else {
this.$message.error(response.message || "获取分类列表失败");
this.$message.error(response.message || '获取分类列表失败')
}
} catch (error) {
console.error("获取分类列表失败:", error);
this.$message.error("获取分类列表失败");
console.error('获取分类列表失败:', error)
this.$message.error('获取分类列表失败')
} finally {
this.categoriesLoading = false;
this.categoriesLoading = false
}
},

// 选择分类
selectCategory(category) {
this.selectedCategoryId = category.id;
this.selectedCategory = category;
this.selectedCategoryId = category.id
this.selectedCategory = category
},

// 下一步
nextStep() {
this.currentStep++;
this.currentStep++
},

// 上一步
prevStep() {
this.currentStep--;
this.currentStep--
},

// 上传前验证
beforeUpload(file) {
console.log("上传前验证:", {
console.log('上传前验证:', {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified,
});
lastModified: file.lastModified
})

const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase());
const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase())
if (!isExcel) {
console.log("文件类型验证失败:", file.name);
this.$message.error("只能上传Excel文件!");
return false;
console.log('文件类型验证失败:', file.name)
this.$message.error('只能上传Excel文件!')
return false
}

const isLt10M = file.size / 1024 / 1024 < 10;
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
console.log("文件大小验证失败:", file.size / 1024 / 1024 + "MB");
this.$message.error("上传文件大小不能超过10MB!");
return false;
console.log('文件大小验证失败:', file.size / 1024 / 1024 + 'MB')
this.$message.error('上传文件大小不能超过10MB!')
return false
}

console.log("前端验证通过,准备上传");
console.log('前端验证通过,准备上传')

// 对于多文件上传,我们先不让它自动上传,而是添加到文件列表
this.addToFileList(file);
return false; // 阻止自动上传
this.addToFileList(file)
return false // 阻止自动上传
},

// 添加文件到列表
@@ -264,262 +264,266 @@ export default {
const fileItem = {
name: file.name,
size: file.size,
status: "ready",
status: 'ready',
file: file,
uid: Date.now() + Math.random(),
};
this.fileList.push(fileItem);
uid: Date.now() + Math.random()
}
this.fileList.push(fileItem)
},

// 移除文件
onFileRemove(file) {
const index = this.fileList.findIndex((item) => item.uid === file.uid);
const index = this.fileList.findIndex((item) => item.uid === file.uid)
if (index > -1) {
this.fileList.splice(index, 1);
this.fileList.splice(index, 1)
}
},

// 清空文件列表
clearAllFiles() {
this.fileList = [];
this.fileList = []
},

// 上传成功
onUploadSuccess(response) {
console.log("上传成功回调:", response);
console.log('上传成功回调:', response)

if (response.code === 200) {
this.processResult = response.data || {};
this.$message.success(response.message || "文件处理完成");
this.currentStep = 3;
this.processResult = response.data || {}
this.$message.success(response.message || '文件处理完成')
this.currentStep = 3
} else {
console.error("处理失败:", response);
this.$message.error(response.message || "文件处理失败");
console.error('处理失败:', response)
this.$message.error(response.message || '文件处理失败')
}
},

// 上传失败
onUploadError(error) {
console.error("上传失败:", error);
let errorMessage = "文件上传失败";
console.error('上传失败:', error)
let errorMessage = '文件上传失败'

// 尝试从错误中提取详细信息
if (error && error.message) {
errorMessage = error.message;
} else if (typeof error === "string") {
errorMessage = error;
errorMessage = error.message
} else if (typeof error === 'string') {
errorMessage = error
}

this.$message.error(errorMessage);
this.$message.error(errorMessage)
},

// 保存数据
async saveData() {
console.log(this.selectedCategory);
console.log(this.selectedCategory)

try {
this.saveLoading = true;
this.saveLoading = true

// 存储所有文件的解析数据
const allParsedData = [];
const allParsedData = []

// 遍历文件列表
for (const fileItem of this.fileList) {
try {
// 读取文件内容
const reader = new FileReader();
const reader = new FileReader()
const fileData = await new Promise((resolve, reject) => {
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsArrayBuffer(fileItem.file);
});
reader.onload = (e) => resolve(e.target.result)
reader.onerror = (e) => reject(e)
reader.readAsArrayBuffer(fileItem.file)
})

// 解析Excel文件
const workbook = XLSX.read(fileData, { type: "array" });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const workbook = XLSX.read(fileData, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]

// 将工作表转换为JSON数据
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
header: 1,
raw: false, // 将所有值转换为字符串
defval: "", // 空单元格的默认值
});
defval: '' // 空单元格的默认值
})

// 验证数据格式
if (jsonData.length < 2) {
throw new Error(`文件 ${fileItem.name} 数据行数不足`);
throw new Error(`文件 ${fileItem.name} 数据行数不足`)
}

// 获取表头
const headers = jsonData[0];
const headers = jsonData[0]

// 验证字段配置
const fieldConfig = this.parseFieldConfig(
this.selectedCategory.fieldConfig
);
)
const requiredFields = fieldConfig
.filter((field) => field.required)
.map((field) => field.fieldName);
.map((field) => field.fieldName)

// 检查必填字段是否存在
const missingFields = requiredFields.filter(
(field) => !headers.includes(field)
);
)
if (missingFields.length > 0) {
throw new Error(
`文件 ${fileItem.name} 缺少必填字段: ${missingFields.join(
", "
', '
)}`
);
)
}

// 处理数据行
const parsedRows = [];
const parsedRows = []
for (let i = 1; i < jsonData.length; i++) {
const row = jsonData[i];
if (row.length === 0 || row.every((cell) => !cell)) continue; // 跳过空行
const row = jsonData[i]
if (row.length === 0 || row.every((cell) => !cell)) continue // 跳过空行

// 创建符合目标结构的数据对象
const rowData = {
id: null,
date: "", // 默认当前日期,可以从Excel中获取
shopName: "",
productCode: "",
productName: "",
customerName: "",
date: '', // 默认当前日期,可以从Excel中获取
shopName: '',
productCode: '',
productName: '',
customerName: '',
category: this.selectedCategory.name,
categorySpecs: "",
categorySpecs: '',
quantity: 0,
totalAmount: "",
source: "",
totalAmount: '',
source: '',
status: 1,
deliveryType: "",
destination: "",
remarks: "",
orderNumber: "",
deliveryType: '',
destination: '',
remarks: '',
orderNumber: '',
rowNumber: i + 1,
brand: "",
brand: '',
createdAt: null,
updatedAt: null,
};
updatedAt: null
}

const specs = {};
const specs = {}

// 根据表头映射数据
headers.forEach((header, index) => {
const value = row[index] || "";
switch (header.toLowerCase()) {
case "日付":
const value = row[index] || ''
const headerTrim = header.trim().replace(/[\r\n]+/g, '')
console.log(headerTrim == '販売数量')
switch (headerTrim) {
case '出库日期':
case '日付':
// 转换日期格式 从 2025/7/18 到 2025-07-18
if (value) {
const parts = value.split("/");
const parts = value.split('/')
if (parts.length === 3) {
const year = parts[0];
const month = parts[1].padStart(2, "0");
const day = parts[2].padStart(2, "0");
rowData.date = `${year}-${month}-${day}`;
const year = parts[0]
const month = parts[1].padStart(2, '0')
const day = parts[2].padStart(2, '0')
rowData.date = `${year}-${month}-${day}`
} else {
rowData.date = value;
rowData.date = value
}
}
break;
case "販売店舗":
rowData.shopName = value;
break;
case "商品コード":
rowData.productCode = value;
break;
case "商品名":
rowData.productName = value;
break;
case "客户名":
rowData.customerName = value;
break;
case "分类":
rowData.category = value;
break;
case "販売\n数量":
case "販売数量":
rowData.quantity = parseInt(value) || 0;
break;
case "販売金額\n合計":
case "販売金額合計":
rowData.totalAmount = value.toString();
break;
case "来源":
if (value) rowData.source = value;
break;
case "出库类型":
if (value) rowData.deliveryType = value;
break;
case "目的地":
rowData.destination = value;
break;
case "备注":
rowData.remarks = value;
break;
case "订单号":
rowData.orderNumber = value;
break;
case "品牌":
rowData.brand = value;
break;
break
case '店铺':
case '販売店舗':
rowData.shopName = value
break
case '商品コード':
rowData.productCode = value
break
case '商品名':
rowData.productName = value
break
case '客户名':
rowData.customerName = value
break
case '分类':
rowData.category = value
break
case '販売\n数量':
case '販売数量':
rowData.quantity = parseInt(value) || 0
break
case '販売金額\n合計':
case '販売金額合計':
rowData.totalAmount = value.toString()
break
case '来源':
if (value) rowData.source = value
break
case '出库类型':
if (value) rowData.deliveryType = value
break
case '目的地':
rowData.destination = value
break
case '备注':
rowData.remarks = value
break
case '订单号':
rowData.orderNumber = value
break
case '品牌':
rowData.brand = value
break
default:
fieldConfig.forEach((field) => {
const headerTrim = header.trim().replace(/\n+/g, "");
const headerTrim = header.trim().replace(/[\r\n]+/g, '')
if (headerTrim.includes(field.displayLabel)) {
specs[field.displayLabel] = value;
specs[field.displayLabel] = value
}
});
rowData.categorySpecs = JSON.stringify(specs);
break;
})
rowData.categorySpecs = JSON.stringify(specs)
break
}
});
})

parsedRows.push(rowData);
parsedRows.push(rowData)
}

allParsedData.push(...parsedRows);
allParsedData.push(...parsedRows)

// 更新文件状态
fileItem.status = "success";
fileItem.parsedCount = parsedRows.length;
fileItem.status = 'success'
fileItem.parsedCount = parsedRows.length
} catch (error) {
console.error(`解析文件 ${fileItem.name} 失败:`, error);
fileItem.status = "error";
fileItem.error = error.message;
console.error(`解析文件 ${fileItem.name} 失败:`, error)
fileItem.status = 'error'
fileItem.error = error.message
this.$message.error(
`解析文件 ${fileItem.name} 失败: ${error.message}`
);
)
}
}

if (allParsedData.length === 0) {
throw new Error("没有可用的数据可以保存");
throw new Error('没有可用的数据可以保存')
}

// 调用API保存数据
const response = await saveImportedData(allParsedData);
const response = await saveImportedData(allParsedData)

if (response.code === 200) {
this.saveResult = response.data;
this.$message.success(response.message || "数据保存成功");
this.currentStep = 0;
this.saveResult = response.data
this.$message.success(response.message || '数据保存成功')
this.currentStep = 0
// 清空文件列表
this.fileList = [];
this.fileList = []
} else {
throw new Error(response.message || "数据保存失败");
throw new Error(response.message || '数据保存失败')
}
} catch (error) {
console.error("保存数据失败:", error);
this.$message.error(error.message || "数据保存失败");
console.error('保存数据失败:', error)
this.$message.error(error.message || '数据保存失败')
} finally {
this.saveLoading = false;
this.saveLoading = false
}
},
},
};
}
}
}
</script>

<style scoped>

+ 902
- 203
src/views/sales-analysis/reports/CommonAnalysis.vue
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


Laden…
Abbrechen
Speichern