| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435 |
- <template>
- <div class="app-container">
- <div class="button-section">
- <div class="button-group">
- <el-button
- :icon="Plus"
- type="primary"
- @click="openDialog('add')"
- class="custom-button add-button"
- >
- 新增记录
- </el-button>
- <div class="button-group-right">
- <el-upload
- :auto-upload="false"
- :on-change="handleFileSelect"
- :show-file-list="false"
- accept=".xlsx, .csv"
- class="import-upload"
- >
- <el-button
- :icon="Upload"
- type="primary"
- class="custom-button import-button"
- >
- 导入降酸数据
- </el-button>
- </el-upload>
- <el-button
- :icon="Download"
- type="primary"
- @click="downloadTemplateAction"
- class="custom-button download-button"
- >
- 下载模板
- </el-button>
- <el-button
- :icon="Download"
- type="primary"
- @click="exportDataAction"
- class="custom-button export-button"
- >
- 导出数据
- </el-button>
- <el-button
- :icon="Refresh"
- type="primary"
- @click="refreshData"
- class="custom-button refresh-button"
- >
- 刷新
- </el-button>
- </div>
- </div>
- </div>
- <div class="table-section">
- <el-table
- :data="pagedTableDataWithIndex"
- height="615"
- fit
- style="width: 100%"
- @row-click="handleRowClick"
- highlight-current-row
- class="custom-table"
- v-loading="loading"
- table-layout="auto"
- >
- <el-table-column
- key="displayIndex"
- prop="displayIndex"
- label="序号"
- width="80"
- align="center"
- fixed
- :header-class-name="'fixed-column-header'"
- ></el-table-column>
- <el-table-column
- v-for="col in filteredColumns"
- :key="col.key"
- :prop="col.dataKey"
- :label="col.title"
- :width="col.width"
- :formatter="formatTableValue"
- align="center"
- :show-overflow-tooltip="true"
- ></el-table-column>
- <el-table-column
- label="操作"
- width="120"
- align="center"
- fixed="right"
- :header-class-name="'fixed-column-header'"
- >
- <template #default="scope">
- <span class="action-buttons">
- <el-tooltip
- class="item"
- effect="dark"
- content="编辑"
- placement="top"
- >
- <el-button
- circle
- :icon="EditPen"
- @click.stop="openDialog('edit', scope.row)"
- class="action-button edit-button"
- ></el-button>
- </el-tooltip>
- <el-tooltip
- class="item"
- effect="dark"
- content="删除"
- placement="top"
- >
- <el-button
- circle
- :icon="DeleteFilled"
- @click.stop="deleteItem(scope.row.id)"
- class="action-button delete-button"
- >
- </el-button>
- </el-tooltip>
- </span>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-wrapper">
- <PaginationComponent
- :total="tableData.length"
- :currentPage="currentPage4"
- :pageSize="pageSize4"
- @update:currentPage="currentPage4 = $event"
- @update:pageSize="pageSize4 = $event"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- class="pagination-container"
- />
- </div>
- </div>
- <el-dialog
- :title="dialogTitle"
- v-model="dialogVisible"
- width="60%"
- :custom-class="['custom-dialog']"
- :close-on-click-modal="false"
- center
- >
- <div class="dialog-header">
- <h2>{{ dialogTitle }}</h2>
- <div class="header-decoration"></div>
- </div>
- <el-scrollbar max-height="calc(90vh - 300px)" style="width: 90%">
- <el-form
- ref="formRef"
- :model="formData"
- label-position="top"
- :rules="formRules"
- class="custom-dialog-form"
- >
- <el-form-item
- v-for="col in editableColumns"
- :key="col.key"
- :label="col.title"
- :prop="col.dataKey"
- class="custom-form-item"
- >
- <el-input
- v-model="formData[col.dataKey]"
- :type="col.inputType || 'text'"
- :placeholder="`请输入${col.title}`"
- />
- </el-form-item>
- </el-form>
- </el-scrollbar>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="handleCancel" class="custom-cancel-button"
- >取消</el-button
- >
- <el-button
- type="primary"
- @click="submitForm"
- class="custom-submit-button"
- >{{ dialogSubmitButtonText }}</el-button
- >
- </div>
- </template>
- </el-dialog>
- <!-- 详情弹窗 -->
- <el-dialog
- title="导入成功详情"
- v-model="detailsVisible"
- width="50%"
- :close-on-click-modal="false"
- center
- >
- <div class="details-content" v-html="detailsContent"></div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="detailsVisible = false" class="custom-cancel-button">关闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 空值错误提示弹窗 -->
- <el-dialog
- title="空值错误"
- v-model="emptyCellDialogVisible"
- width="40%"
- :close-on-click-modal="false"
- center
- >
- <div class="error-content">
- <p>以下单元格存在空值,请检查:</p>
- <ul>
- <li v-for="(error, index) in emptyCellErrors" :key="index">
- 第{{ error.row + 1 }}行,{{ error.column }}列
- </li>
- </ul>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="emptyCellDialogVisible = false" class="custom-cancel-button">关闭</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref, reactive, computed, onMounted } from "vue";
- import * as XLSX from "xlsx";
- import {
- DeleteFilled,
- Download,
- Upload,
- Plus,
- EditPen,
- Refresh,
- } from "@element-plus/icons-vue";
- import {
- ElMessage,
- ElForm,
- ElMessageBox,
- FormRules,
- FormItemRule,
- } from "element-plus";
- import {
- table,
- updateItem,
- addItem,
- deleteItemApi,
- exportData,
- importData,
- downloadTemplate,
- } from "@/API/admin";
- import PaginationComponent from "@/components/PaginationComponent.vue";
- interface EmptyCellError {
- row: number; // Excel 行号
- column: string; // 缺失列名
- }
- interface Column {
- key: string;
- dataKey: string;
- title: string;
- width: number;
- inputType?: string;
- step?: string;
- precision?: number;
- options?: Array<{ label: string; value: string | number }>;
- }
- // 表格列定义
- const columns: Column[] = [
- { key: "id", dataKey: "id", title: "ID", width: 100 },
- {
- key: "OM",
- dataKey: "OM",
- title: "有机质含量",
- width: 130,
- },
- {
- key: "CL",
- dataKey: "CL",
- title: "土壤粘粒",
- width: 130,
- },
- {
- key: "CEC",
- dataKey: "CEC",
- title: "阳离子交换量",
- width: 130,
- },
- {
- key: "H_plus",
- dataKey: "H_plus",
- title: "交换性氢",
- width: 130,
- },
- {
- key: "N",
- dataKey: "N",
- title: "水解氮",
- width: 130,
- },
- {
- key: "Al3_plus",
- dataKey: "Al3_plus",
- title: "交换性铝",
- width: 130,
- },
- {
- key: "Delta_pH",
- dataKey: "Delta_pH",
- title: "ΔpH",
- width: 130,
- },
- ];
- const editableColumns = columns.filter((col) => col.key !== "id");
- type TableName = "current_reduce" | "current_reflux";
- const currentTableName: TableName = "current_reflux";
- const emptyCellErrors = ref<EmptyCellError[]>([]);
- const emptyCellDialogVisible = ref(false);
- const tableData = ref<any[]>([]);
- const selectedRow = ref<any | null>(null);
- const loading = ref(false);
- const formRef = ref<InstanceType<typeof ElForm> | null>(null);
- const filteredColumns = computed(() =>
- columns.filter((col) => col.key !== "id")
- );
- const currentPage4 = ref(1);
- const pageSize4 = ref(10);
- const dialogVisible = ref(false);
- const formData = reactive<any>({});
- const dialogMode = ref<"add" | "edit">("add");
- // 定义支持的列名映射,包括中英文
- const REQUIRED_COLUMNS = {
- "OM": "有机质含量",
- "CL": "土壤粘粒",
- "CEC": "阳离子交换量",
- "H_plus": "交换性氢",
- "N": "水解氮",
- "Al3_plus": "交换性铝",
- "Delta_pH": "ΔpH"
- };
- // 反向映射,用于将中文列名转换为英文列名
- const COLUMN_NAME_MAPPING: Record<string, string> = {};
- Object.entries(REQUIRED_COLUMNS).forEach(([enKey, zhValue]) => {
- COLUMN_NAME_MAPPING[enKey] = enKey; // 英文映射
- COLUMN_NAME_MAPPING[zhValue] = enKey; // 中文映射
- });
- const normalizeValue = (val: any): any => {
- if (val === undefined) {
- return "";
- }
- if (typeof val === "number" && isNaN(val)) {
- return "";
- }
- if (typeof val === "string" && val.toLowerCase() === "nan") {
- return "";
- }
- return val;
- };
- const pagedTableDataWithIndex = computed(() => {
- const start = (currentPage4.value - 1) * pageSize4.value;
- const end = start + pageSize4.value;
- return tableData.value
- .slice(start, end)
- .map((row: { [s: string]: unknown } | ArrayLike<unknown>, idx: number) => {
- const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
- acc[key] = normalizeValue(val);
- return acc;
- }, {} as any);
- return {
- ...processedRow,
- displayIndex: start + idx + 1,
- };
- });
- });
- const fetchTable = async () => {
- try {
- loading.value = true;
- const response: any = await table({ table: currentTableName });
- const rawData = Array.isArray(response.data) ? response.data : [];
- tableData.value = rawData.map((row: any) =>
- Object.entries(row).reduce((acc: any, [key, val]) => {
- acc[key] = normalizeValue(val);
- return acc;
- }, {})
- );
- } catch (error) {
- console.error("获取数据时出错:", error);
- showMessage("获取数据失败,请检查网络连接或服务器状态", "error");
- } finally {
- loading.value = false;
- }
- };
- onMounted(() => {
- fetchTable();
- });
- const handleRowClick = (row: any) => {
- selectedRow.value = row;
- Object.assign(
- formData,
- Object.entries(row).reduce((acc: any, [key, val]) => {
- acc[key] = normalizeValue(val);
- return acc;
- }, {})
- );
- };
- const openDialog = (mode: "add" | "edit", row?: any) => {
- dialogMode.value = mode;
- dialogVisible.value = true;
- if (mode === "add") {
- selectedRow.value = null;
- editableColumns.forEach((col) => {
- const dataKey = col.dataKey;
- formData[dataKey] = "";
- });
- } else if (row) {
- selectedRow.value = row;
- Object.assign(
- formData,
- Object.entries(row).reduce((acc: any, [key, val]) => {
- acc[key] = normalizeValue(val);
- return acc;
- }, {})
- );
- }
- };
- function prepareFormData(
- formData: { [x: string]: any },
- excludeKeys = ["displayIndex"]
- ): { [x: string]: any } {
- const result: { [x: string]: any } = {};
- for (const key in formData) {
- if (!excludeKeys.includes(key)) {
- let value = formData[key];
- if (value === "") {
- result[key] = undefined;
- } else {
- result[key] = value;
- }
- }
- }
- return result;
- }
- const formRules = computed<FormRules>(() => {
- const rules: FormRules = {};
- editableColumns.forEach((col) => {
- const fieldKey = col.dataKey;
- const fieldRules: FormItemRule[] = [];
- fieldRules.push({
- required: true,
- message: `请输入${col.title}`,
- trigger: "blur",
- });
- rules[fieldKey] = fieldRules;
- });
- return rules;
- });
- const submitForm = async () => {
- if (!formRef.value) return;
- try {
- const isValid = await formRef.value.validate();
- if (!isValid) {
- console.log("表单验证未通过");
- showMessage("表单验证未通过,请检查输入", "warning");
- return;
- }
- } catch (validationError: any) {
- console.error("表单验证失败:", validationError);
- let messageToShow = "表单验证失败,请检查输入";
- if (
- validationError &&
- typeof validationError === "object" &&
- !Array.isArray(validationError)
- ) {
- const firstFieldErrors = Object.values(validationError)[0];
- if (
- Array.isArray(firstFieldErrors) &&
- firstFieldErrors.length > 0 &&
- firstFieldErrors[0].message
- ) {
- messageToShow = firstFieldErrors[0].message;
- }
- } else if (Array.isArray(validationError)) {
- const firstError = validationError[0];
- if (firstError && firstError.message) {
- messageToShow = `表单验证失败: ${firstError.message}`;
- }
- }
- showMessage(messageToShow, "error");
- return;
- }
- try {
- const dataToSubmit = prepareFormData(formData);
- let response: { status?: number; data?: any };
- if (dialogMode.value === "add") {
- response = (await addItem({
- table: currentTableName,
- item: dataToSubmit,
- })) as { status?: number; data?: any };
- // 修改成功判断逻辑:只检查状态码,不再检查 data.success
- if (response && (response.status === 200 || response.status === 201)) {
- showMessage("添加成功", "success");
- dialogVisible.value = false;
- await fetchTable();
- } else {
- console.warn("addItem 响应结构异常或状态码非200/201:", response);
- // 根据实际返回内容决定提示信息
- if (response && response.data) {
- const serverMsg =
- response.data.detail ||
- response.data.message ||
- response.data.error ||
- "未知错误";
- showMessage(`添加操作完成,但服务器返回: ${serverMsg}`, "warning");
- } else {
- showMessage("添加操作完成,但响应数据异常。", "warning");
- }
- }
- } else {
- console.log("编辑模式 - selectedRow:", selectedRow.value);
- if (!selectedRow.value) {
- showMessage("未选中任何记录,请先选择要编辑的记录", "error");
- return;
- }
- response = (await updateItem({
- table: currentTableName,
- id: selectedRow.value.id,
- update_data: dataToSubmit,
- })) as { status?: number; data?: any };
- const index = tableData.value.findIndex(
- (item: { id: any }) => item.id === selectedRow.value!.id
- );
- if (index > -1) {
- tableData.value[index] = {
- ...tableData.value[index],
- ...dataToSubmit,
- };
- showMessage("修改成功", "success");
- } else {
- console.warn("本地未找到对应记录,重新获取数据");
- await fetchTable();
- showMessage("修改成功,数据已刷新", "success");
- }
- dialogVisible.value = false;
- }
- } catch (error: any) {
- console.error("提交表单时发生错误:", error);
- let errorMessage = "提交失败";
- if (error.response) {
- console.log("服务器响应错误:", error.response);
- const status = error.response.status;
- const data = error.response.data;
- const errorDetail = data?.error || data?.detail || "";
- if (typeof errorDetail === "string" && errorDetail.includes("已存在")) {
- errorMessage = "提交失败:数据已存在,请勿重复添加。";
- } else if (status === 409) {
- errorMessage = "提交失败:数据已存在,请勿重复添加。";
- } else if (status === 400) {
- errorMessage = "提交失败:请求参数有误。";
- } else if (status === 500) {
- if (data && data.error) {
- errorMessage = `服务器内部错误: ${data.error}`;
- } else {
- errorMessage = "提交失败: 服务器内部错误。";
- }
- } else {
- errorMessage = `提交失败: 服务器错误 (${status})`;
- if (data && data.error) {
- errorMessage += ` - ${data.error}`;
- } else if (data && data.message) {
- errorMessage += ` - ${data.message}`;
- }
- }
- } else if (error.request) {
- console.error("网络错误或请求无响应:", error.request);
- errorMessage = "提交失败: 网络连接问题或服务器无响应。";
- } else {
- console.error("请求配置错误:", error.message);
- errorMessage = `提交失败: ${error.message}`;
- }
- showMessage(errorMessage, "error");
- }
- };
- const deleteItem = async (rowId: number) => {
- const index = tableData.value.findIndex((item) => item.id === rowId);
- if (index === -1) {
- showMessage("无法找到要删除的记录或记录ID无效", "error");
- console.error("DeleteItem: Row ID not found:", rowId);
- return;
- }
- try {
- await ElMessageBox.confirm(
- `确定要删除该记录吗?此操作不可恢复。`,
- "删除确认",
- {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning",
- customClass: "unified-confirm-dialog",
- }
- );
- await deleteItemApi({
- table: currentTableName,
- id: rowId,
- });
- tableData.value.splice(index, 1);
- showMessage("记录删除成功", "success");
- } catch (error: any) {
- if (error === "cancel") {
- console.log("用户取消删除");
- return;
- }
- console.error("删除记录时发生错误:", error);
- showMessage("删除失败,请重试", "error");
- }
- };
- const downloadTemplateAction = async () => {
- try {
- await downloadTemplate(currentTableName);
- showMessage("模板下载成功", "success");
- } catch (error) {
- console.error("下载模板时发生错误:", error);
- showMessage("下载模板失败,请重试", "error");
- }
- };
- const exportDataAction = async () => {
- try {
- await exportData(currentTableName);
- showMessage("数据导出成功", "success");
- } catch (error) {
- console.error("导出数据时发生错误:", error);
- showMessage("导出数据失败,请重试", "error");
- }
- };
- const handleFileSelect = async (uploadFile: any) => {
- const file = uploadFile.raw;
- if (!file) {
- showMessage("请选择有效的 .xlsx 或 .csv 文件", "warning");
- return;
- }
- await importDataAction(file);
- };
- // 优化后的数据导入函数
- const importDataAction = async (file: File) => {
- try {
- // 1. 读取文件
- const dataArrayBuffer = await file.arrayBuffer();
- const workbook = XLSX.read(dataArrayBuffer, { type: "array" });
- const firstSheetName = workbook.SheetNames[0];
- const worksheet = workbook.Sheets[firstSheetName];
- const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
- // 2. 检查是否为空文件
- const hasData = jsonData.some(row =>
- Array.isArray(row) && row.some(cell =>
- cell !== null && cell !== undefined && String(cell).trim() !== ""
- )
- );
- if (!hasData) {
- showMessage("❌ 文件为空", "error");
- return;
- }
- // 3. 查找表头行
- const headerRowIndex = jsonData.findIndex(row =>
- Array.isArray(row) && row.some(cell =>
- cell !== null && cell !== undefined && String(cell).trim() !== ""
- )
- );
- if (headerRowIndex === -1) {
- showMessage("❌ 未找到表头", "error");
- return;
- }
- // 4. 获取列名并映射
- const headerRow = jsonData[headerRowIndex];
- const rawColumns = (headerRow as string[]).map(col => String(col).trim());
- const columns = rawColumns.map(col => COLUMN_NAME_MAPPING[col] || col);
- // 5. 检查必要列
- const requiredCols = Object.keys(REQUIRED_COLUMNS);
- const missingCols = requiredCols.filter(col => !columns.includes(col));
- if (missingCols.length > 0) {
- showMessage(`❌ 缺少列: ${missingCols.join(", ")}`, "error");
- return;
- }
- // 6. 检查数据行
- const dataRows = jsonData.slice(headerRowIndex + 1);
- const validRows = dataRows.filter(row =>
- requiredCols.some(col => {
- const idx = columns.indexOf(col);
- return idx !== -1 && row?.[idx] != null && String(row[idx]).trim() !== "";
- })
- );
- if (validRows.length === 0) {
- showMessage("⚠️ 无有效数据", "error");
- return;
- }
- // 7. 检查必填列是否为空
- const emptyErrors: string[] = [];
- validRows.forEach((row, rowIdx) => {
- requiredCols.forEach(col => {
- const colIndex = columns.indexOf(col);
- if (colIndex !== -1 && (row?.[colIndex] == null || String(row[colIndex]).trim() === "")) {
- emptyErrors.push(`第${rowIdx + 2}行,${col}列为空`);
- }
- });
- });
- if (emptyErrors.length > 0) {
- showMessage(`❌ 必填列为空:\n${emptyErrors.slice(0, 5).join('\n')}`, "error");
- return;
- }
- // 8. 转换数据格式
- const transformedData = validRows.map(row => {
- const newRow: any = {};
- rawColumns.forEach((origCol, idx) => {
- const engCol = COLUMN_NAME_MAPPING[origCol] || origCol;
- if (requiredCols.includes(engCol)) {
- newRow[engCol] = row?.[idx];
- }
- });
- return newRow;
- });
- // 9. 创建新工作簿
- const newWorkbook = XLSX.utils.book_new();
- const newWorksheet = XLSX.utils.json_to_sheet(transformedData);
- XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, "Sheet1");
- const outputArrayBuffer = XLSX.write(newWorkbook, { bookType: "xlsx", type: "array" });
- const newFile = new File([outputArrayBuffer], file.name, {
- type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- });
- // 10. 构造表单数据
- const formData = new FormData();
- formData.append("file", newFile);
- formData.append("dataset_name", file.name.replace(/\.[^/.]+$/, "") || "Imported Dataset");
- formData.append("dataset_description", `通过前端上传的 ${file.name} 数据集`);
- // 11. 上传数据
- const response: any = await importData(currentTableName, formData);
- // 12. 处理响应
- if ((response.status === 200 || response.status === 201) && response.data) {
- const data = response.data;
- const stats = data.data_stats || {};
- // 主要成功信息
- let mainMessage = `🎉 数据集上传成功!\n\n`;
- mainMessage += `📌 数据集名称: ${data.dataset_name || data.dataset_id || "未知"}\n`;
- mainMessage += `📌 唯一ID: ${data.dataset_id}\n`;
- // 详细统计信息(可折叠或单独显示)
- let detailMessage = `\n📊 数据统计详情:\n`;
- detailMessage += ` • 原始行数: ${stats.original_count || 0}\n`;
- detailMessage += ` • 文件内重复: ${stats.duplicates_in_file || 0}\n`;
- detailMessage += ` • 与现有数据重复: ${stats.duplicates_with_existing || 0}\n`;
- detailMessage += ` • 与测试集冲突: ${stats.test_overlap_count || 0}\n`;
- detailMessage += ` • 最终入库: ${stats.final_count || 0}\n`;
- // 附加信息
- let additionalInfo = '';
- if (data.training_triggered) {
- additionalInfo += `\n🚀 自动训练已触发,任务ID: ${data.task_id}`;
- }
- if (data.message) {
- additionalInfo += `\n\n💡 系统提示: ${data.message.replace("✅", "").replace("🎉", "").trim()}`;
- }
- // 分步骤显示消息
- showMessage(mainMessage, "success");
-
- // 在控制台显示详细信息
- console.log(detailMessage + additionalInfo);
-
- // 显示详细信息弹窗
- detailsContent.value = (mainMessage + detailMessage + additionalInfo).replace(/\n/g, '<br/>');
- detailsVisible.value = true;
-
- await fetchTable(); // 刷新表格
- } else {
- const errorMsg = response.data?.error || "服务器未返回预期数据";
- showMessage(`❌ 导入失败\n服务器返回错误:${errorMsg}\n状态码:${response.status}\n\n请检查网络或联系管理员。`, "error");
- }
- } catch (error: any) {
- let errorMsg = "导入失败";
- let errorDetail = "未知错误";
- if (error.name === "AbortError") {
- errorMsg = "请求已取消";
- errorDetail = "网络请求被中断,请检查连接后重试。";
- } else if (error.message.includes("Invalid data")) {
- errorMsg = "文件数据格式错误";
- errorDetail = "Excel 文件中包含不支持的数据类型(如公式、图片、合并单元格),请使用纯数据表格。";
- } else if (error.message.includes("Unable to parse") || error.message.includes("Bad file")) {
- errorMsg = "无法解析文件";
- errorDetail = "文件可能已损坏或不是有效的 .xlsx 格式,请重新保存或另存为 Excel 文件。";
- } else if (error.message.includes("Network Error")) {
- errorMsg = "网络错误";
- errorDetail = "无法连接到服务器,请检查网络连接。";
- } else {
- errorDetail = error.message || "请检查文件格式和内容是否符合要求。";
- }
- showMessage(`❌ ${errorMsg}\n${errorDetail}`, "error");
- console.error("导入文件出错:", error);
- }
- };
- // 定义详细信息展示函数,带有明确的参数类型
- const handleSizeChange = (val: number) => {
- pageSize4.value = val;
- currentPage4.value = 1;
- };
- const handleCurrentChange = (val: number) => {
- currentPage4.value = val;
- };
- const formatTableValue = (_row: any, _column: any, cellValue: any) => {
- return normalizeValue(cellValue);
- };
- const dialogTitle = computed(() => {
- return dialogMode.value === "add" ? `新增记录` : "编辑记录";
- });
- const dialogSubmitButtonText = computed(() => {
- return dialogMode.value === "add" ? "添加" : "保存";
- });
- const handleCancel = () => {
- if (formRef.value) {
- formRef.value.resetFields();
- console.log("表单已重置");
- } else {
- console.warn("表单引用无效,无法重置表单");
- }
- dialogVisible.value = false;
- console.log("对话框已关闭");
- };
- // 新增刷新数据功能
- const refreshData = async () => {
- try {
- loading.value = true;
- await fetchTable();
- showMessage("数据刷新成功", "success");
- } catch (error) {
- console.error("刷新数据失败:", error);
- showMessage("数据刷新失败,请重试", "error");
- } finally {
- loading.value = false;
- }
- };
- // 统一提示消息函数
- const showMessage = (
- message: string,
- type: "success" | "warning" | "info" | "error" = "info",
- duration: number = 3000
- ) => {
- ElMessage.closeAll();
- ElMessage({
- message,
- type,
- duration,
- customClass: "unified-message",
- showClose: true,
- });
- };
- // 详情弹窗相关
- const detailsVisible = ref(false);
- const detailsContent = ref('');
- </script>
- <style scoped lang="scss">
- $primary-color: #409eff;
- $success-color: #67c23a;
- $warning-color: #e6a23c;
- $error-color: #f56c6c;
- $info-color: #909399;
- $alert-success-bg: #f0f9eb;
- $alert-success-border: #e1f3d8;
- $alert-success-text: #67c23a;
- $alert-error-bg: #fef0f0;
- $alert-error-border: #fbc4c4;
- $alert-error-text: #f56c6c;
- $alert-warning-bg: #fdf6ec;
- $alert-warning-border: #f5dab1;
- $alert-warning-text: #e6a23c;
- $alert-info-bg: #f4f4f5;
- $alert-info-border: #e9e9eb;
- $alert-info-text: #909399;
- .app-container {
- padding: 10px;
- min-height: 100vh;
- background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- }
- /* 统一消息样式 */
- :global(.unified-message) {
- border-radius: 8px !important;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
- padding: 12px 20px !important;
- font-size: 14px !important;
- line-height: 1.5 !important;
- border: 1px solid transparent !important;
- text-align: center;
- font-weight: 500;
- }
- :global(.unified-message.el-message--success) {
- background-color: $alert-success-bg !important;
- border-color: $alert-success-border !important;
- color: $alert-success-text !important;
- }
- :global(.unified-message.el-message--error) {
- background-color: $alert-error-bg !important;
- border-color: $alert-error-border !important;
- color: $alert-error-text !important;
- }
- :global(.unified-message.el-message--warning) {
- background-color: $alert-warning-bg !important;
- border-color: $alert-warning-border !important;
- color: $alert-warning-text !important;
- }
- :global(.unified-message.el-message--info) {
- background-color: $alert-info-bg !important;
- border-color: $alert-info-border !important;
- color: $alert-info-text !important;
- }
- /* 修改后的统一通知样式 - 居中显示 */
- :global(.unified-notification) {
- border-radius: 8px !important;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
- padding: 16px 20px !important;
- font-size: 14px !important;
- line-height: 1.5 !important;
- border: 1px solid transparent !important;
- text-align: center !important;
- /* Element Plus 的通知默认是居中的,所以移除之前的 left/right/top/transform 定位 */
- max-width: 80vw;
- /* 可选:添加一些内边距或边框来美化 */
- /* background-color: #fff !important; */ /* 如果需要纯白背景 */
- }
- /* 为不同类型的 notification 添加特定样式 */
- :global(.unified-notification.notification-success) {
- background-color: $alert-success-bg !important;
- border-color: $alert-success-border !important;
- color: $alert-success-text !important;
- }
- :global(.unified-notification.notification-error) {
- background-color: $alert-error-bg !important;
- border-color: $alert-error-border !important;
- color: $alert-error-text !important;
- }
- :global(.unified-notification.notification-warning) {
- background-color: $alert-warning-bg !important;
- border-color: $alert-warning-border !important;
- color: $alert-warning-text !important;
- }
- :global(.unified-notification.notification-info) {
- background-color: $alert-info-bg !important;
- border-color: $alert-info-border !important;
- color: $alert-info-text !important;
- }
- :global(.unified-notification .el-notification__title) {
- font-weight: bold !important;
- text-align: center !important;
- margin-bottom: 8px !important; /* 标题和内容之间增加间距 */
- }
- :global(.unified-notification .el-notification__content) {
- text-align: center !important;
- word-wrap: break-word !important;
- }
- .content-wrapper {
- width: 100%;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .button-section {
- background: #ffffff;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- display: flex;
- flex-direction: column;
- gap: 15px;
- }
- .table-section {
- background: #ffffff;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- overflow: hidden;
- }
- .button-group {
- display: flex;
- gap: 15px;
- justify-content: space-between;
- width: 100%;
- }
- .button-group-right {
- display: flex;
- gap: 15px;
- }
- .custom-button {
- color: #fff;
- border: none;
- border-radius: 6px;
- font-size: 14px;
- padding: 10px 20px;
- transition: all 0.3s ease;
- min-width: 110px;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
- font-weight: 500;
- }
- .custom-button:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
- .custom-button:active {
- transform: translateY(0);
- }
- .add-button {
- background: linear-gradient(135deg, $success-color, #4ebc4e);
- }
- .add-button:hover {
- background: linear-gradient(135deg, #85ce61, $success-color);
- }
- .download-button {
- background: linear-gradient(135deg, $primary-color, #2d8cf0);
- }
- .download-button:hover {
- background: linear-gradient(135deg, #66b1ff, $primary-color);
- }
- .export-button {
- background: linear-gradient(135deg, $warning-color, #d6902d);
- }
- .export-button:hover {
- background: linear-gradient(135deg, #ebb563, $warning-color);
- }
- .import-button {
- background: linear-gradient(135deg, $info-color, #7d8086);
- }
- .import-button:hover {
- background: linear-gradient(135deg, #a6a9ad, $info-color);
- }
- .refresh-button {
- background: linear-gradient(135deg, #409eff, #2d8cf0);
- }
- .refresh-button:hover {
- background: linear-gradient(135deg, #66b1ff, #409eff);
- }
- .custom-table {
- width: 100%;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
- background-color: #fff;
- }
- :deep(.el-table th) {
- color: #fff;
- font-weight: bold;
- text-align: center;
- padding: 12px 0;
- font-size: 14px;
- }
- :deep(.fixed-column-header) {
- background-color: $primary-color !important;
- }
- :deep(.el-table th:not(.fixed-column-header)) {
- background: linear-gradient(180deg, $primary-color, #2d8cf0);
- }
- :deep(.el-table__row:nth-child(odd)) {
- background-color: #f9fbfd;
- }
- :deep(.el-table__row:nth-child(even)) {
- background-color: #ffffff;
- }
- :deep(.el-table td) {
- padding: 12px 8px;
- text-align: center;
- border-bottom: 1px solid #ebeef5;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .action-button {
- font-size: 16px;
- margin-right: 5px;
- border-radius: 50%;
- transition: all 0.3s ease;
- }
- .edit-button {
- background-color: $primary-color;
- color: #fff;
- border: none;
- }
- .edit-button:hover {
- background-color: #66b1ff;
- transform: scale(1.1);
- }
- .delete-button {
- background-color: $error-color;
- color: #fff;
- border: none;
- }
- .delete-button:hover {
- background-color: #f78989;
- transform: scale(1.1);
- }
- .action-buttons {
- display: flex;
- justify-content: center;
- }
- .pagination-wrapper {
- display: flex;
- justify-content: center;
- width: 100%;
- margin: 25px 0 10px 0;
- }
- .pagination-container {
- display: flex;
- justify-content: center;
- }
- :deep(.el-dialog) {
- border-radius: 12px !important;
- overflow: hidden;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
- background: linear-gradient(145deg, #ffffff, #f5f9ff);
- border: 1px solid #e0e7ff;
- margin-top: calc(var(--el-dialog-margin-top, 15vh) + 30px) !important;
- }
- .dialog-header {
- position: relative;
- padding: 20px 24px 10px;
- text-align: center;
- background: linear-gradient(90deg, $primary-color, #2d8cf0);
- }
- .dialog-header h2 {
- margin: 0;
- font-size: 22px;
- font-weight: 600;
- color: #fff;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
- }
- .header-decoration {
- height: 4px;
- background: linear-gradient(90deg, #2d8cf0, $primary-color);
- border-radius: 2px;
- margin-top: 12px;
- width: 60%;
- margin-left: auto;
- margin-right: auto;
- }
- .custom-dialog-form {
- padding: 25px 10px 15px 180px;
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 20px;
- max-height: calc(100vh - 300px);
- overflow-y: auto;
- scrollbar-width: thin;
- scrollbar-color: #c0c4cc #f1f3f4;
- }
- .custom-dialog-form::-webkit-scrollbar {
- width: 6px;
- }
- .custom-dialog-form::-webkit-scrollbar-track {
- background: #f1f3f4;
- border-radius: 3px;
- }
- .custom-dialog-form::-webkit-scrollbar-thumb {
- background: #c0c4cc;
- border-radius: 3px;
- }
- .custom-dialog-form::-webkit-scrollbar-thumb:hover {
- background: #a8abb3;
- }
- @media (max-width: 1200px) {
- .custom-dialog-form {
- grid-template-columns: 1fr;
- padding: 25px 30px 15px 30px;
- }
- }
- .custom-form-item {
- margin-bottom: 0 !important;
- }
- :deep(.el-form-item__label) {
- display: block;
- text-align: left;
- margin-bottom: 6px !important;
- font-size: 14px;
- font-weight: 500;
- color: #2d3748;
- padding: 0 !important;
- line-height: 1.5;
- }
- .dialog-footer {
- display: flex;
- gap: 16px;
- justify-content: center;
- padding: 15px 40px 25px;
- }
- .custom-cancel-button,
- .custom-submit-button {
- min-width: 120px;
- height: 40px;
- border-radius: 6px;
- font-size: 15px;
- font-weight: 500;
- transition: all 0.3s ease;
- letter-spacing: 0.5px;
- border: none;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
- }
- .custom-cancel-button {
- background: linear-gradient(145deg, #f7fafc, #edf2f7);
- color: #4a5568;
- }
- .custom-cancel-button:hover {
- background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
- transform: translateY(-1px);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
- }
- .custom-submit-button {
- background: linear-gradient(145deg, $primary-color, #2d8cf0);
- color: white;
- position: relative;
- overflow: hidden;
- }
- .custom-submit-button::before {
- content: "";
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(255, 255, 255, 0.3),
- transparent
- );
- transition: 0.5s;
- }
- .custom-submit-button:hover::before {
- left: 100%;
- }
- .custom-submit-button:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
- }
- :deep(.import-upload .el-upload-list) {
- display: none !important;
- }
- :deep(.el-table__row:hover) {
- background-color: #e6f7ff !important;
- transition: background-color 0.3s;
- }
- :deep(.el-table) {
- border: 1px solid #ebeef5;
- border-radius: 8px;
- overflow: hidden;
- }
- :deep(.el-table__header-wrapper) {
- border-bottom: 1px solid #ebeef5;
- }
- :deep(.el-table td) {
- border-right: 1px solid #ebeef5;
- }
- :deep(.el-table td:last-child) {
- border-right: none;
- }
- .details-content {
- line-height: 1.8;
- font-size: 14px;
- color: #333;
- }
- .error-content {
- ul {
- list-style-type: disc;
- padding-left: 20px;
- margin-top: 10px;
- li {
- margin-bottom: 8px;
- }
- }
- }
- </style>
|