|
|
@@ -1,137 +1,237 @@
|
|
|
<template>
|
|
|
<div class="app-container">
|
|
|
- <!-- 操作按钮区 -->
|
|
|
- <div class="button-group">
|
|
|
- <el-button
|
|
|
- :icon="Plus"
|
|
|
- type="primary"
|
|
|
- @click="openDialog('add')"
|
|
|
- class="custom-button add-button"
|
|
|
- >新增记录</el-button
|
|
|
- >
|
|
|
- <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-upload :before-upload="importDataAction" accept=".xlsx, .csv">
|
|
|
+ <div class="button-section">
|
|
|
+ <div class="button-group">
|
|
|
<el-button
|
|
|
- :icon="Upload"
|
|
|
+ :icon="Plus"
|
|
|
type="primary"
|
|
|
- class="custom-button import-button"
|
|
|
- >导入数据</el-button
|
|
|
+ @click="openDialog('add')"
|
|
|
+ class="custom-button add-button"
|
|
|
>
|
|
|
- </el-upload>
|
|
|
- </div>
|
|
|
+ 新增记录
|
|
|
+ </el-button>
|
|
|
|
|
|
- <!-- 数据表格 -->
|
|
|
- <el-table
|
|
|
- :data="pagedTableDataWithIndex"
|
|
|
- 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"
|
|
|
- ></el-table-column>
|
|
|
- <el-table-column
|
|
|
- v-for="col in columns.filter((col) => col.key !== 'id')"
|
|
|
- :key="col.key"
|
|
|
- :prop="col.dataKey"
|
|
|
- :label="col.title"
|
|
|
- :min-width="col.title.length * 20 + 40"
|
|
|
- :formatter="formatNumber"
|
|
|
- align="center"
|
|
|
- header-align="center"
|
|
|
- ></el-table-column>
|
|
|
- <el-table-column label="操作" width="120" align="center">
|
|
|
- <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"
|
|
|
+ <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
|
|
|
- circle
|
|
|
- :icon="DeleteFilled"
|
|
|
- @click.stop="deleteItem(scope.row)"
|
|
|
- class="action-button delete-button"
|
|
|
- ></el-button>
|
|
|
- </el-tooltip>
|
|
|
- </span>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
-
|
|
|
- <!-- 分页控制 -->
|
|
|
- <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"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 新增/编辑对话框 -->
|
|
|
- <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
|
|
|
- <el-form :model="formData" label-width="150px">
|
|
|
- <el-form-item
|
|
|
- v-for="col in editableColumns"
|
|
|
+ 导入降酸数据
|
|
|
+ </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'"
|
|
|
>
|
|
|
- <el-input
|
|
|
- v-model="formData[col.dataKey]"
|
|
|
- :type="col.inputType || 'text'"
|
|
|
- class="custom-input"
|
|
|
- ></el-input>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="dialogVisible = false" class="custom-cancel-button"
|
|
|
- >取消</el-button
|
|
|
- >
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- @click="submitForm"
|
|
|
- class="custom-submit-button"
|
|
|
- >{{ dialogSubmitButtonText }}</el-button
|
|
|
+ <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>
|
|
|
@@ -139,81 +239,189 @@
|
|
|
|
|
|
<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 } from "element-plus";
|
|
|
+import {
|
|
|
+ ElMessage,
|
|
|
+ ElForm,
|
|
|
+ ElMessageBox,
|
|
|
+ ElNotification,
|
|
|
+ FormRules,
|
|
|
+ FormItemRule,
|
|
|
+} from "element-plus";
|
|
|
import {
|
|
|
table,
|
|
|
updateItem,
|
|
|
addItem,
|
|
|
deleteItemApi,
|
|
|
- downloadTemplate,
|
|
|
exportData,
|
|
|
importData,
|
|
|
-} from "@/API/menus";
|
|
|
+ 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: 150 },
|
|
|
- { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 150 },
|
|
|
- { key: "CEC", dataKey: "CEC", title: "阳离子交换量", width: 150 },
|
|
|
- { key: "H_plus", dataKey: "H_plus", title: "交换性氢", width: 150 },
|
|
|
- { key: "N", dataKey: "N", title: "水解氮", width: 150 },
|
|
|
- { key: "Al3_plus", dataKey: "Al3_plus", title: "交换性铝", width: 150 },
|
|
|
- { key: "Delta_pH", dataKey: "Delta_pH", title: "ΔpH", width: 170 },
|
|
|
+ {
|
|
|
+ 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 showEmptyCellErrors = (errors: EmptyCellError[]) => {
|
|
|
+ emptyCellErrors.value = errors;
|
|
|
+ emptyCellDialogVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
const tableData = ref<any[]>([]);
|
|
|
const selectedRow = ref<any | null>(null);
|
|
|
-const dialogVisible = ref(false);
|
|
|
-const formData = reactive<any>({});
|
|
|
-const dialogMode = ref<"add" | "edit">("add"); // 新增还是编辑模式
|
|
|
+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 pagedTableData = computed(() => {
|
|
|
- const start = (currentPage4.value - 1) * pageSize4.value;
|
|
|
- const end = start + pageSize4.value;
|
|
|
- return tableData.value.slice(start, end);
|
|
|
+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(() => {
|
|
|
- return pagedTableData.value.map((item, index) => ({
|
|
|
- ...item,
|
|
|
- displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
|
|
|
- }));
|
|
|
+ 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 loading = ref(false);
|
|
|
-const currentTableName = "current_reflux";
|
|
|
-
|
|
|
const fetchTable = async () => {
|
|
|
- console.log("正在获取表格数据...");
|
|
|
try {
|
|
|
loading.value = true;
|
|
|
- const response = await table({ table: currentTableName });
|
|
|
- console.log("获取到的数据:", response);
|
|
|
- tableData.value = response.data.rows;
|
|
|
+ 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);
|
|
|
- ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
|
|
|
+ showMessage("获取数据失败,请检查网络连接或服务器状态", "error");
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
@@ -225,23 +433,35 @@ onMounted(() => {
|
|
|
|
|
|
const handleRowClick = (row: any) => {
|
|
|
selectedRow.value = row;
|
|
|
- Object.assign(formData, 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) => {
|
|
|
- console.log(`${mode === "add" ? "打开新增记录" : "打开编辑记录"}对话框`);
|
|
|
+ dialogMode.value = mode;
|
|
|
+ dialogVisible.value = true;
|
|
|
+
|
|
|
if (mode === "add") {
|
|
|
selectedRow.value = null;
|
|
|
editableColumns.forEach((col) => {
|
|
|
- formData[col.dataKey] = "";
|
|
|
+ const dataKey = col.dataKey;
|
|
|
+ formData[dataKey] = "";
|
|
|
});
|
|
|
} else if (row) {
|
|
|
- console.log("编辑记录:", row);
|
|
|
selectedRow.value = row;
|
|
|
- Object.assign(formData, row);
|
|
|
+ Object.assign(
|
|
|
+ formData,
|
|
|
+ Object.entries(row).reduce((acc: any, [key, val]) => {
|
|
|
+ acc[key] = normalizeValue(val);
|
|
|
+ return acc;
|
|
|
+ }, {})
|
|
|
+ );
|
|
|
}
|
|
|
- dialogMode.value = mode;
|
|
|
- dialogVisible.value = true;
|
|
|
};
|
|
|
|
|
|
function prepareFormData(
|
|
|
@@ -252,301 +472,1017 @@ function prepareFormData(
|
|
|
for (const key in formData) {
|
|
|
if (!excludeKeys.includes(key)) {
|
|
|
let value = formData[key];
|
|
|
- if (typeof value === "string" && value.startsWith("displayIndex-")) {
|
|
|
- value = value.replace("displayIndex-", "");
|
|
|
+ if (value === "") {
|
|
|
+ result[key] = undefined;
|
|
|
+ } else {
|
|
|
+ result[key] = value;
|
|
|
}
|
|
|
- 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 () => {
|
|
|
- console.log("开始提交表单...");
|
|
|
+ if (!formRef.value) return;
|
|
|
+
|
|
|
try {
|
|
|
- const isValid = validateFormData(formData);
|
|
|
+ const isValid = await formRef.value.validate();
|
|
|
if (!isValid) {
|
|
|
- console.error("表单验证失败,请检查输入的数据");
|
|
|
- alert("请检查输入的数据");
|
|
|
+ console.log("表单验证未通过");
|
|
|
+ showMessage("表单验证未通过,请检查输入", "warning");
|
|
|
return;
|
|
|
}
|
|
|
- const dataToSubmit = prepareFormData(formData);
|
|
|
- if (!dataToSubmit.id && dialogMode.value !== "add") {
|
|
|
- console.error("无法找到记录ID,请联系管理员");
|
|
|
- alert("无法找到记录ID,请联系管理员");
|
|
|
- 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}`;
|
|
|
+ }
|
|
|
}
|
|
|
- let response;
|
|
|
+ showMessage(messageToShow, "error");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const dataToSubmit = prepareFormData(formData);
|
|
|
+ let response: { status?: number; data?: any };
|
|
|
+
|
|
|
if (dialogMode.value === "add") {
|
|
|
- console.log("正在添加新记录...");
|
|
|
- response = await addItem({ table: currentTableName, item: dataToSubmit });
|
|
|
- } else {
|
|
|
- console.log("正在更新现有记录...");
|
|
|
- response = await updateItem({
|
|
|
+ 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;
|
|
|
}
|
|
|
- console.log(
|
|
|
- dialogMode.value === "add" ? "添加响应:" : "更新响应:",
|
|
|
- response
|
|
|
- );
|
|
|
- dialogVisible.value = false;
|
|
|
- fetchTable();
|
|
|
- alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
|
|
|
- } catch (error) {
|
|
|
+ } catch (error: any) {
|
|
|
console.error("提交表单时发生错误:", error);
|
|
|
- let errorMessage = "未知错误";
|
|
|
- if (error && typeof error === "object" && "response" in error) {
|
|
|
- const response = (error as { response?: { data?: { message?: string } } })
|
|
|
- .response;
|
|
|
- if (
|
|
|
- response &&
|
|
|
- response.data &&
|
|
|
- typeof response.data.message === "string"
|
|
|
- ) {
|
|
|
- errorMessage = response.data.message;
|
|
|
+
|
|
|
+ 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}`;
|
|
|
}
|
|
|
- console.error(`提交失败原因: ${errorMessage}`);
|
|
|
- alert(`提交失败: ${errorMessage}`);
|
|
|
+
|
|
|
+ showMessage(errorMessage, "error");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-function validateFormData(data: { [x: string]: undefined }) {
|
|
|
- for (let key in data) {
|
|
|
- if (data[key] === "" || data[key] === undefined) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- return true;
|
|
|
-}
|
|
|
+const deleteItem = async (rowId: number) => {
|
|
|
+ const index = tableData.value.findIndex((item) => item.id === rowId);
|
|
|
|
|
|
-const deleteItem = async (row: any) => {
|
|
|
- console.log("准备删除记录:", row);
|
|
|
- if (!row) {
|
|
|
- ElMessage.warning("请先选择一行记录");
|
|
|
+ if (index === -1) {
|
|
|
+ showMessage("无法找到要删除的记录或记录ID无效", "error");
|
|
|
+ console.error("DeleteItem: Row ID not found:", rowId);
|
|
|
return;
|
|
|
}
|
|
|
+
|
|
|
try {
|
|
|
- const condition = { id: row.id };
|
|
|
- await deleteItemApi({ table: "current_reflux", condition });
|
|
|
- const index = tableData.value.findIndex((item) => item.id === row.id);
|
|
|
- if (index > -1) {
|
|
|
- tableData.value.splice(index, 1);
|
|
|
+ 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;
|
|
|
}
|
|
|
- fetchTable();
|
|
|
- console.log("记录删除成功");
|
|
|
- } catch (error) {
|
|
|
console.error("删除记录时发生错误:", error);
|
|
|
+ showMessage("删除失败,请重试", "error");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const downloadTemplateAction = async () => {
|
|
|
try {
|
|
|
- await downloadTemplate("current_reflux");
|
|
|
+ await downloadTemplate(currentTableName);
|
|
|
+ showMessage("模板下载成功", "success");
|
|
|
} catch (error) {
|
|
|
console.error("下载模板时发生错误:", error);
|
|
|
+ showMessage("下载模板失败,请重试", "error");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const exportDataAction = async () => {
|
|
|
try {
|
|
|
- await exportData("current_reflux");
|
|
|
+ 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);
|
|
|
+};
|
|
|
+
|
|
|
+interface AxiosError extends Error {
|
|
|
+ response?: {
|
|
|
+ data?: {
|
|
|
+ detail?: any;
|
|
|
+ message?: string;
|
|
|
+ [key: string]: any;
|
|
|
+ };
|
|
|
+ status?: number;
|
|
|
+ statusText?: string;
|
|
|
+ headers?: any;
|
|
|
+ config?: any;
|
|
|
+ };
|
|
|
+ request?: any;
|
|
|
+ config?: any;
|
|
|
+}
|
|
|
+
|
|
|
+interface ErrorResponse {
|
|
|
+ error?: string;
|
|
|
+ message?: string;
|
|
|
+ [key: string]: any;
|
|
|
+}
|
|
|
+
|
|
|
+// 定义详细信息展示函数的类型
|
|
|
+interface ShowDetailedInfo {
|
|
|
+ (title: string, content: string): void;
|
|
|
+}
|
|
|
+
|
|
|
+// 优化后的数据导入函数
|
|
|
const importDataAction = async (file: File) => {
|
|
|
try {
|
|
|
- const response = await importData("reduce", file); // 传递 dataset_type 的值(如 'reduce')
|
|
|
- if (response && response.data) {
|
|
|
- const { total_data, new_data, duplicate_data, message } = response.data;
|
|
|
- ElMessage({
|
|
|
- message: `导入结果: ${message} 新增总数:${total_data}, 成功新增:${new_data}, 数据重复:${duplicate_data}`,
|
|
|
- type: "success",
|
|
|
+ // 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}列为空`);
|
|
|
+ }
|
|
|
});
|
|
|
- fetchTable(); // 假设存在 fetchTable 方法刷新表格
|
|
|
+ });
|
|
|
+
|
|
|
+ if (emptyErrors.length > 0) {
|
|
|
+ showMessage(`❌ 必填列为空:\n${emptyErrors.slice(0, 5).join('\n')}`, "error");
|
|
|
+ return;
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- let errorMessage = "数据导入失败";
|
|
|
- if (error && typeof error === "object" && "response" in error) {
|
|
|
- const response = (error as { response?: { data?: { message?: string } } })
|
|
|
- .response;
|
|
|
- if (response && response.data && typeof response.data.message === "string") {
|
|
|
- errorMessage += `: ${response.data.message}`;
|
|
|
+
|
|
|
+ // 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");
|
|
|
}
|
|
|
- ElMessage.error(errorMessage);
|
|
|
+ } 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 handleFileChange = (event: Event) => {
|
|
|
- const target = event.target as HTMLInputElement;
|
|
|
- if (target.files && target.files.length > 0) {
|
|
|
- importDataAction(target.files[0]);
|
|
|
- }
|
|
|
+// 定义详细信息展示函数,带有明确的参数类型
|
|
|
+const showDetailedInfo: ShowDetailedInfo = (title: string, content: string) => {
|
|
|
+ // 这里可以实现一个模态框或侧边栏来展示详细内容
|
|
|
+ console.log(`=== ${title} ===`);
|
|
|
+ console.log(content);
|
|
|
+ console.log('====================================');
|
|
|
};
|
|
|
|
|
|
-const handleSizeChange = (val: number) => {};
|
|
|
-const handleCurrentChange = (val: number) => {};
|
|
|
+const handleSizeChange = (val: number) => {
|
|
|
+ pageSize4.value = val;
|
|
|
+ currentPage4.value = 1;
|
|
|
+};
|
|
|
|
|
|
-const formatNumber = (row: any, column: any, cellValue: any) => {
|
|
|
- if (typeof cellValue === "number") {
|
|
|
- return cellValue.toFixed(3);
|
|
|
- }
|
|
|
- return cellValue;
|
|
|
+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" ? "新增记录" : "编辑记录";
|
|
|
+ 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 showNotification = (
|
|
|
+ title: string,
|
|
|
+ message: string,
|
|
|
+ type: "success" | "warning" | "info" | "error" = "info",
|
|
|
+ duration: number = 4500
|
|
|
+) => {
|
|
|
+ ElNotification({
|
|
|
+ title,
|
|
|
+ message,
|
|
|
+ type,
|
|
|
+ duration,
|
|
|
+ customClass: `unified-notification notification-${type}`,
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 详情弹窗相关
|
|
|
+const detailsVisible = ref(false);
|
|
|
+const detailsContent = ref('');
|
|
|
</script>
|
|
|
|
|
|
-<style scoped>
|
|
|
+<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: 20px 40px;
|
|
|
+ padding: 10px;
|
|
|
min-height: 100vh;
|
|
|
- background-color: #f0f2f5;
|
|
|
+ background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
|
|
|
box-sizing: border-box;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
- align-items: center;
|
|
|
}
|
|
|
+
|
|
|
+/* 统一消息样式 */
|
|
|
+: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: 12px;
|
|
|
- justify-content: flex-end;
|
|
|
+ gap: 15px;
|
|
|
+ justify-content: space-between;
|
|
|
width: 100%;
|
|
|
- margin-bottom: 20px;
|
|
|
}
|
|
|
+
|
|
|
+.button-group-right {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
.custom-button {
|
|
|
color: #fff;
|
|
|
border: none;
|
|
|
- border-radius: 8px;
|
|
|
+ border-radius: 6px;
|
|
|
font-size: 14px;
|
|
|
- padding: 10px 18px;
|
|
|
- transition: transform 0.3s ease, background-color 0.3s ease;
|
|
|
- min-width: 130px;
|
|
|
+ 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;
|
|
|
}
|
|
|
-.download-button,
|
|
|
-.export-button,
|
|
|
-.add-button,
|
|
|
-.import-button {
|
|
|
- background-color: #67c23a;
|
|
|
+
|
|
|
+.custom-button:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
}
|
|
|
-.download-button:hover,
|
|
|
-.export-button:hover,
|
|
|
-.import-button:hover,
|
|
|
+
|
|
|
+.custom-button:active {
|
|
|
+ transform: translateY(0);
|
|
|
+}
|
|
|
+
|
|
|
+.add-button {
|
|
|
+ background: linear-gradient(135deg, $success-color, #4ebc4e);
|
|
|
+}
|
|
|
+
|
|
|
.add-button:hover {
|
|
|
- background-color: #85ce61;
|
|
|
- transform: scale(1.05);
|
|
|
+ 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.1);
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
|
background-color: #fff;
|
|
|
- margin-top: 10px;
|
|
|
}
|
|
|
-:deep( .el-table th) {
|
|
|
- background: linear-gradient(180deg, #61e054, #4db944);
|
|
|
+
|
|
|
+:deep(.el-table th) {
|
|
|
color: #fff;
|
|
|
font-weight: bold;
|
|
|
text-align: center;
|
|
|
- padding: 14px 0;
|
|
|
+ padding: 12px 0;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
-:deep( .el-table__row:nth-child(odd)) {
|
|
|
- background-color: #E4FBE5;
|
|
|
+
|
|
|
+: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(even)) {
|
|
|
+
|
|
|
+: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 10px;
|
|
|
+
|
|
|
+: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: transform 0.3s ease, background-color 0.3s ease;
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
+
|
|
|
.edit-button {
|
|
|
- background-color: #409eff;
|
|
|
+ background-color: $primary-color;
|
|
|
color: #fff;
|
|
|
+ border: none;
|
|
|
}
|
|
|
+
|
|
|
.edit-button:hover {
|
|
|
background-color: #66b1ff;
|
|
|
transform: scale(1.1);
|
|
|
}
|
|
|
+
|
|
|
.delete-button {
|
|
|
- background-color: #f56c6c;
|
|
|
+ background-color: $error-color;
|
|
|
color: #fff;
|
|
|
+ border: none;
|
|
|
}
|
|
|
+
|
|
|
.delete-button:hover {
|
|
|
background-color: #f78989;
|
|
|
transform: scale(1.1);
|
|
|
}
|
|
|
-.el-form-item__label {
|
|
|
- width: 150px;
|
|
|
- font-weight: bold;
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
-.el-form-item__content {
|
|
|
- margin-left: 150px;
|
|
|
+
|
|
|
+.pagination-wrapper {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ width: 100%;
|
|
|
+ margin: 25px 0 10px 0;
|
|
|
}
|
|
|
-.custom-input .el-input__inner {
|
|
|
- border-radius: 6px;
|
|
|
- border: 1px solid #dcdcdc;
|
|
|
- transition: border-color 0.3s ease;
|
|
|
- padding: 10px;
|
|
|
+
|
|
|
+.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;
|
|
|
}
|
|
|
-.custom-input .el-input__inner:focus {
|
|
|
- border-color: #409eff;
|
|
|
+
|
|
|
+.dialog-footer {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 15px 40px 25px;
|
|
|
}
|
|
|
+
|
|
|
.custom-cancel-button,
|
|
|
.custom-submit-button {
|
|
|
- border: none;
|
|
|
+ min-width: 120px;
|
|
|
+ height: 40px;
|
|
|
border-radius: 6px;
|
|
|
- font-size: 14px;
|
|
|
- padding: 10px 20px;
|
|
|
- transition: transform 0.3s ease, background-color 0.3s ease;
|
|
|
- min-width: 100px;
|
|
|
+ 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-color: #f4f4f5;
|
|
|
- color: #606266;
|
|
|
+ background: linear-gradient(145deg, #f7fafc, #edf2f7);
|
|
|
+ color: #4a5568;
|
|
|
}
|
|
|
+
|
|
|
.custom-cancel-button:hover {
|
|
|
- background-color: #e4e7ed;
|
|
|
- transform: scale(1.05);
|
|
|
+ background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
|
}
|
|
|
+
|
|
|
.custom-submit-button {
|
|
|
- background-color: #409eff;
|
|
|
- color: #fff;
|
|
|
+ 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 {
|
|
|
- background-color: #66b1ff;
|
|
|
- transform: scale(1.05);
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
}
|
|
|
-.pagination-container {
|
|
|
- margin-top: 30px;
|
|
|
- text-align: center;
|
|
|
+
|
|
|
+:deep(.import-upload .el-upload-list) {
|
|
|
+ display: none !important;
|
|
|
}
|
|
|
-.action-buttons {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
+
|
|
|
+: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>
|
|
|
+</style>
|