soilAcidReductionData.vue 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435
  1. <template>
  2. <div class="app-container">
  3. <div class="button-section">
  4. <div class="button-group">
  5. <el-button
  6. :icon="Plus"
  7. type="primary"
  8. @click="openDialog('add')"
  9. class="custom-button add-button"
  10. >
  11. 新增记录
  12. </el-button>
  13. <div class="button-group-right">
  14. <el-upload
  15. :auto-upload="false"
  16. :on-change="handleFileSelect"
  17. :show-file-list="false"
  18. accept=".xlsx, .csv"
  19. class="import-upload"
  20. >
  21. <el-button
  22. :icon="Upload"
  23. type="primary"
  24. class="custom-button import-button"
  25. >
  26. 导入降酸数据
  27. </el-button>
  28. </el-upload>
  29. <el-button
  30. :icon="Download"
  31. type="primary"
  32. @click="downloadTemplateAction"
  33. class="custom-button download-button"
  34. >
  35. 下载模板
  36. </el-button>
  37. <el-button
  38. :icon="Download"
  39. type="primary"
  40. @click="exportDataAction"
  41. class="custom-button export-button"
  42. >
  43. 导出数据
  44. </el-button>
  45. <el-button
  46. :icon="Refresh"
  47. type="primary"
  48. @click="refreshData"
  49. class="custom-button refresh-button"
  50. >
  51. 刷新
  52. </el-button>
  53. </div>
  54. </div>
  55. </div>
  56. <div class="table-section">
  57. <el-table
  58. :data="pagedTableDataWithIndex"
  59. height="615"
  60. fit
  61. style="width: 100%"
  62. @row-click="handleRowClick"
  63. highlight-current-row
  64. class="custom-table"
  65. v-loading="loading"
  66. table-layout="auto"
  67. >
  68. <el-table-column
  69. key="displayIndex"
  70. prop="displayIndex"
  71. label="序号"
  72. width="80"
  73. align="center"
  74. fixed
  75. :header-class-name="'fixed-column-header'"
  76. ></el-table-column>
  77. <el-table-column
  78. v-for="col in filteredColumns"
  79. :key="col.key"
  80. :prop="col.dataKey"
  81. :label="col.title"
  82. :width="col.width"
  83. :formatter="formatTableValue"
  84. align="center"
  85. :show-overflow-tooltip="true"
  86. ></el-table-column>
  87. <el-table-column
  88. label="操作"
  89. width="120"
  90. align="center"
  91. fixed="right"
  92. :header-class-name="'fixed-column-header'"
  93. >
  94. <template #default="scope">
  95. <span class="action-buttons">
  96. <el-tooltip
  97. class="item"
  98. effect="dark"
  99. content="编辑"
  100. placement="top"
  101. >
  102. <el-button
  103. circle
  104. :icon="EditPen"
  105. @click.stop="openDialog('edit', scope.row)"
  106. class="action-button edit-button"
  107. ></el-button>
  108. </el-tooltip>
  109. <el-tooltip
  110. class="item"
  111. effect="dark"
  112. content="删除"
  113. placement="top"
  114. >
  115. <el-button
  116. circle
  117. :icon="DeleteFilled"
  118. @click.stop="deleteItem(scope.row.id)"
  119. class="action-button delete-button"
  120. >
  121. </el-button>
  122. </el-tooltip>
  123. </span>
  124. </template>
  125. </el-table-column>
  126. </el-table>
  127. <div class="pagination-wrapper">
  128. <PaginationComponent
  129. :total="tableData.length"
  130. :currentPage="currentPage4"
  131. :pageSize="pageSize4"
  132. @update:currentPage="currentPage4 = $event"
  133. @update:pageSize="pageSize4 = $event"
  134. @size-change="handleSizeChange"
  135. @current-change="handleCurrentChange"
  136. class="pagination-container"
  137. />
  138. </div>
  139. </div>
  140. <el-dialog
  141. :title="dialogTitle"
  142. v-model="dialogVisible"
  143. width="60%"
  144. :custom-class="['custom-dialog']"
  145. :close-on-click-modal="false"
  146. center
  147. >
  148. <div class="dialog-header">
  149. <h2>{{ dialogTitle }}</h2>
  150. <div class="header-decoration"></div>
  151. </div>
  152. <el-scrollbar max-height="calc(90vh - 300px)" style="width: 90%">
  153. <el-form
  154. ref="formRef"
  155. :model="formData"
  156. label-position="top"
  157. :rules="formRules"
  158. class="custom-dialog-form"
  159. >
  160. <el-form-item
  161. v-for="col in editableColumns"
  162. :key="col.key"
  163. :label="col.title"
  164. :prop="col.dataKey"
  165. class="custom-form-item"
  166. >
  167. <el-input
  168. v-model="formData[col.dataKey]"
  169. :type="col.inputType || 'text'"
  170. :placeholder="`请输入${col.title}`"
  171. />
  172. </el-form-item>
  173. </el-form>
  174. </el-scrollbar>
  175. <template #footer>
  176. <div class="dialog-footer">
  177. <el-button @click="handleCancel" class="custom-cancel-button"
  178. >取消</el-button
  179. >
  180. <el-button
  181. type="primary"
  182. @click="submitForm"
  183. class="custom-submit-button"
  184. >{{ dialogSubmitButtonText }}</el-button
  185. >
  186. </div>
  187. </template>
  188. </el-dialog>
  189. <!-- 详情弹窗 -->
  190. <el-dialog
  191. title="导入成功详情"
  192. v-model="detailsVisible"
  193. width="50%"
  194. :close-on-click-modal="false"
  195. center
  196. >
  197. <div class="details-content" v-html="detailsContent"></div>
  198. <template #footer>
  199. <div class="dialog-footer">
  200. <el-button @click="detailsVisible = false" class="custom-cancel-button">关闭</el-button>
  201. </div>
  202. </template>
  203. </el-dialog>
  204. <!-- 空值错误提示弹窗 -->
  205. <el-dialog
  206. title="空值错误"
  207. v-model="emptyCellDialogVisible"
  208. width="40%"
  209. :close-on-click-modal="false"
  210. center
  211. >
  212. <div class="error-content">
  213. <p>以下单元格存在空值,请检查:</p>
  214. <ul>
  215. <li v-for="(error, index) in emptyCellErrors" :key="index">
  216. 第{{ error.row + 1 }}行,{{ error.column }}列
  217. </li>
  218. </ul>
  219. </div>
  220. <template #footer>
  221. <div class="dialog-footer">
  222. <el-button @click="emptyCellDialogVisible = false" class="custom-cancel-button">关闭</el-button>
  223. </div>
  224. </template>
  225. </el-dialog>
  226. </div>
  227. </template>
  228. <script lang="ts" setup>
  229. import { ref, reactive, computed, onMounted } from "vue";
  230. import * as XLSX from "xlsx";
  231. import {
  232. DeleteFilled,
  233. Download,
  234. Upload,
  235. Plus,
  236. EditPen,
  237. Refresh,
  238. } from "@element-plus/icons-vue";
  239. import {
  240. ElMessage,
  241. ElForm,
  242. ElMessageBox,
  243. FormRules,
  244. FormItemRule,
  245. } from "element-plus";
  246. import {
  247. table,
  248. updateItem,
  249. addItem,
  250. deleteItemApi,
  251. exportData,
  252. importData,
  253. downloadTemplate,
  254. } from "@/API/admin";
  255. import PaginationComponent from "@/components/PaginationComponent.vue";
  256. interface EmptyCellError {
  257. row: number; // Excel 行号
  258. column: string; // 缺失列名
  259. }
  260. interface Column {
  261. key: string;
  262. dataKey: string;
  263. title: string;
  264. width: number;
  265. inputType?: string;
  266. step?: string;
  267. precision?: number;
  268. options?: Array<{ label: string; value: string | number }>;
  269. }
  270. // 表格列定义
  271. const columns: Column[] = [
  272. { key: "id", dataKey: "id", title: "ID", width: 100 },
  273. {
  274. key: "OM",
  275. dataKey: "OM",
  276. title: "有机质含量",
  277. width: 130,
  278. },
  279. {
  280. key: "CL",
  281. dataKey: "CL",
  282. title: "土壤粘粒",
  283. width: 130,
  284. },
  285. {
  286. key: "CEC",
  287. dataKey: "CEC",
  288. title: "阳离子交换量",
  289. width: 130,
  290. },
  291. {
  292. key: "H_plus",
  293. dataKey: "H_plus",
  294. title: "交换性氢",
  295. width: 130,
  296. },
  297. {
  298. key: "N",
  299. dataKey: "N",
  300. title: "水解氮",
  301. width: 130,
  302. },
  303. {
  304. key: "Al3_plus",
  305. dataKey: "Al3_plus",
  306. title: "交换性铝",
  307. width: 130,
  308. },
  309. {
  310. key: "Delta_pH",
  311. dataKey: "Delta_pH",
  312. title: "ΔpH",
  313. width: 130,
  314. },
  315. ];
  316. const editableColumns = columns.filter((col) => col.key !== "id");
  317. type TableName = "current_reduce" | "current_reflux";
  318. const currentTableName: TableName = "current_reflux";
  319. const emptyCellErrors = ref<EmptyCellError[]>([]);
  320. const emptyCellDialogVisible = ref(false);
  321. const tableData = ref<any[]>([]);
  322. const selectedRow = ref<any | null>(null);
  323. const loading = ref(false);
  324. const formRef = ref<InstanceType<typeof ElForm> | null>(null);
  325. const filteredColumns = computed(() =>
  326. columns.filter((col) => col.key !== "id")
  327. );
  328. const currentPage4 = ref(1);
  329. const pageSize4 = ref(10);
  330. const dialogVisible = ref(false);
  331. const formData = reactive<any>({});
  332. const dialogMode = ref<"add" | "edit">("add");
  333. // 定义支持的列名映射,包括中英文
  334. const REQUIRED_COLUMNS = {
  335. "OM": "有机质含量",
  336. "CL": "土壤粘粒",
  337. "CEC": "阳离子交换量",
  338. "H_plus": "交换性氢",
  339. "N": "水解氮",
  340. "Al3_plus": "交换性铝",
  341. "Delta_pH": "ΔpH"
  342. };
  343. // 反向映射,用于将中文列名转换为英文列名
  344. const COLUMN_NAME_MAPPING: Record<string, string> = {};
  345. Object.entries(REQUIRED_COLUMNS).forEach(([enKey, zhValue]) => {
  346. COLUMN_NAME_MAPPING[enKey] = enKey; // 英文映射
  347. COLUMN_NAME_MAPPING[zhValue] = enKey; // 中文映射
  348. });
  349. const normalizeValue = (val: any): any => {
  350. if (val === undefined) {
  351. return "";
  352. }
  353. if (typeof val === "number" && isNaN(val)) {
  354. return "";
  355. }
  356. if (typeof val === "string" && val.toLowerCase() === "nan") {
  357. return "";
  358. }
  359. return val;
  360. };
  361. const pagedTableDataWithIndex = computed(() => {
  362. const start = (currentPage4.value - 1) * pageSize4.value;
  363. const end = start + pageSize4.value;
  364. return tableData.value
  365. .slice(start, end)
  366. .map((row: { [s: string]: unknown } | ArrayLike<unknown>, idx: number) => {
  367. const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
  368. acc[key] = normalizeValue(val);
  369. return acc;
  370. }, {} as any);
  371. return {
  372. ...processedRow,
  373. displayIndex: start + idx + 1,
  374. };
  375. });
  376. });
  377. const fetchTable = async () => {
  378. try {
  379. loading.value = true;
  380. const response: any = await table({ table: currentTableName });
  381. const rawData = Array.isArray(response.data) ? response.data : [];
  382. tableData.value = rawData.map((row: any) =>
  383. Object.entries(row).reduce((acc: any, [key, val]) => {
  384. acc[key] = normalizeValue(val);
  385. return acc;
  386. }, {})
  387. );
  388. } catch (error) {
  389. console.error("获取数据时出错:", error);
  390. showMessage("获取数据失败,请检查网络连接或服务器状态", "error");
  391. } finally {
  392. loading.value = false;
  393. }
  394. };
  395. onMounted(() => {
  396. fetchTable();
  397. });
  398. const handleRowClick = (row: any) => {
  399. selectedRow.value = row;
  400. Object.assign(
  401. formData,
  402. Object.entries(row).reduce((acc: any, [key, val]) => {
  403. acc[key] = normalizeValue(val);
  404. return acc;
  405. }, {})
  406. );
  407. };
  408. const openDialog = (mode: "add" | "edit", row?: any) => {
  409. dialogMode.value = mode;
  410. dialogVisible.value = true;
  411. if (mode === "add") {
  412. selectedRow.value = null;
  413. editableColumns.forEach((col) => {
  414. const dataKey = col.dataKey;
  415. formData[dataKey] = "";
  416. });
  417. } else if (row) {
  418. selectedRow.value = row;
  419. Object.assign(
  420. formData,
  421. Object.entries(row).reduce((acc: any, [key, val]) => {
  422. acc[key] = normalizeValue(val);
  423. return acc;
  424. }, {})
  425. );
  426. }
  427. };
  428. function prepareFormData(
  429. formData: { [x: string]: any },
  430. excludeKeys = ["displayIndex"]
  431. ): { [x: string]: any } {
  432. const result: { [x: string]: any } = {};
  433. for (const key in formData) {
  434. if (!excludeKeys.includes(key)) {
  435. let value = formData[key];
  436. if (value === "") {
  437. result[key] = undefined;
  438. } else {
  439. result[key] = value;
  440. }
  441. }
  442. }
  443. return result;
  444. }
  445. const formRules = computed<FormRules>(() => {
  446. const rules: FormRules = {};
  447. editableColumns.forEach((col) => {
  448. const fieldKey = col.dataKey;
  449. const fieldRules: FormItemRule[] = [];
  450. fieldRules.push({
  451. required: true,
  452. message: `请输入${col.title}`,
  453. trigger: "blur",
  454. });
  455. rules[fieldKey] = fieldRules;
  456. });
  457. return rules;
  458. });
  459. const submitForm = async () => {
  460. if (!formRef.value) return;
  461. try {
  462. const isValid = await formRef.value.validate();
  463. if (!isValid) {
  464. console.log("表单验证未通过");
  465. showMessage("表单验证未通过,请检查输入", "warning");
  466. return;
  467. }
  468. } catch (validationError: any) {
  469. console.error("表单验证失败:", validationError);
  470. let messageToShow = "表单验证失败,请检查输入";
  471. if (
  472. validationError &&
  473. typeof validationError === "object" &&
  474. !Array.isArray(validationError)
  475. ) {
  476. const firstFieldErrors = Object.values(validationError)[0];
  477. if (
  478. Array.isArray(firstFieldErrors) &&
  479. firstFieldErrors.length > 0 &&
  480. firstFieldErrors[0].message
  481. ) {
  482. messageToShow = firstFieldErrors[0].message;
  483. }
  484. } else if (Array.isArray(validationError)) {
  485. const firstError = validationError[0];
  486. if (firstError && firstError.message) {
  487. messageToShow = `表单验证失败: ${firstError.message}`;
  488. }
  489. }
  490. showMessage(messageToShow, "error");
  491. return;
  492. }
  493. try {
  494. const dataToSubmit = prepareFormData(formData);
  495. let response: { status?: number; data?: any };
  496. if (dialogMode.value === "add") {
  497. response = (await addItem({
  498. table: currentTableName,
  499. item: dataToSubmit,
  500. })) as { status?: number; data?: any };
  501. // 修改成功判断逻辑:只检查状态码,不再检查 data.success
  502. if (response && (response.status === 200 || response.status === 201)) {
  503. showMessage("添加成功", "success");
  504. dialogVisible.value = false;
  505. await fetchTable();
  506. } else {
  507. console.warn("addItem 响应结构异常或状态码非200/201:", response);
  508. // 根据实际返回内容决定提示信息
  509. if (response && response.data) {
  510. const serverMsg =
  511. response.data.detail ||
  512. response.data.message ||
  513. response.data.error ||
  514. "未知错误";
  515. showMessage(`添加操作完成,但服务器返回: ${serverMsg}`, "warning");
  516. } else {
  517. showMessage("添加操作完成,但响应数据异常。", "warning");
  518. }
  519. }
  520. } else {
  521. console.log("编辑模式 - selectedRow:", selectedRow.value);
  522. if (!selectedRow.value) {
  523. showMessage("未选中任何记录,请先选择要编辑的记录", "error");
  524. return;
  525. }
  526. response = (await updateItem({
  527. table: currentTableName,
  528. id: selectedRow.value.id,
  529. update_data: dataToSubmit,
  530. })) as { status?: number; data?: any };
  531. const index = tableData.value.findIndex(
  532. (item: { id: any }) => item.id === selectedRow.value!.id
  533. );
  534. if (index > -1) {
  535. tableData.value[index] = {
  536. ...tableData.value[index],
  537. ...dataToSubmit,
  538. };
  539. showMessage("修改成功", "success");
  540. } else {
  541. console.warn("本地未找到对应记录,重新获取数据");
  542. await fetchTable();
  543. showMessage("修改成功,数据已刷新", "success");
  544. }
  545. dialogVisible.value = false;
  546. }
  547. } catch (error: any) {
  548. console.error("提交表单时发生错误:", error);
  549. let errorMessage = "提交失败";
  550. if (error.response) {
  551. console.log("服务器响应错误:", error.response);
  552. const status = error.response.status;
  553. const data = error.response.data;
  554. const errorDetail = data?.error || data?.detail || "";
  555. if (typeof errorDetail === "string" && errorDetail.includes("已存在")) {
  556. errorMessage = "提交失败:数据已存在,请勿重复添加。";
  557. } else if (status === 409) {
  558. errorMessage = "提交失败:数据已存在,请勿重复添加。";
  559. } else if (status === 400) {
  560. errorMessage = "提交失败:请求参数有误。";
  561. } else if (status === 500) {
  562. if (data && data.error) {
  563. errorMessage = `服务器内部错误: ${data.error}`;
  564. } else {
  565. errorMessage = "提交失败: 服务器内部错误。";
  566. }
  567. } else {
  568. errorMessage = `提交失败: 服务器错误 (${status})`;
  569. if (data && data.error) {
  570. errorMessage += ` - ${data.error}`;
  571. } else if (data && data.message) {
  572. errorMessage += ` - ${data.message}`;
  573. }
  574. }
  575. } else if (error.request) {
  576. console.error("网络错误或请求无响应:", error.request);
  577. errorMessage = "提交失败: 网络连接问题或服务器无响应。";
  578. } else {
  579. console.error("请求配置错误:", error.message);
  580. errorMessage = `提交失败: ${error.message}`;
  581. }
  582. showMessage(errorMessage, "error");
  583. }
  584. };
  585. const deleteItem = async (rowId: number) => {
  586. const index = tableData.value.findIndex((item) => item.id === rowId);
  587. if (index === -1) {
  588. showMessage("无法找到要删除的记录或记录ID无效", "error");
  589. console.error("DeleteItem: Row ID not found:", rowId);
  590. return;
  591. }
  592. try {
  593. await ElMessageBox.confirm(
  594. `确定要删除该记录吗?此操作不可恢复。`,
  595. "删除确认",
  596. {
  597. confirmButtonText: "确定",
  598. cancelButtonText: "取消",
  599. type: "warning",
  600. customClass: "unified-confirm-dialog",
  601. }
  602. );
  603. await deleteItemApi({
  604. table: currentTableName,
  605. id: rowId,
  606. });
  607. tableData.value.splice(index, 1);
  608. showMessage("记录删除成功", "success");
  609. } catch (error: any) {
  610. if (error === "cancel") {
  611. console.log("用户取消删除");
  612. return;
  613. }
  614. console.error("删除记录时发生错误:", error);
  615. showMessage("删除失败,请重试", "error");
  616. }
  617. };
  618. const downloadTemplateAction = async () => {
  619. try {
  620. await downloadTemplate(currentTableName);
  621. showMessage("模板下载成功", "success");
  622. } catch (error) {
  623. console.error("下载模板时发生错误:", error);
  624. showMessage("下载模板失败,请重试", "error");
  625. }
  626. };
  627. const exportDataAction = async () => {
  628. try {
  629. await exportData(currentTableName);
  630. showMessage("数据导出成功", "success");
  631. } catch (error) {
  632. console.error("导出数据时发生错误:", error);
  633. showMessage("导出数据失败,请重试", "error");
  634. }
  635. };
  636. const handleFileSelect = async (uploadFile: any) => {
  637. const file = uploadFile.raw;
  638. if (!file) {
  639. showMessage("请选择有效的 .xlsx 或 .csv 文件", "warning");
  640. return;
  641. }
  642. await importDataAction(file);
  643. };
  644. // 优化后的数据导入函数
  645. const importDataAction = async (file: File) => {
  646. try {
  647. // 1. 读取文件
  648. const dataArrayBuffer = await file.arrayBuffer();
  649. const workbook = XLSX.read(dataArrayBuffer, { type: "array" });
  650. const firstSheetName = workbook.SheetNames[0];
  651. const worksheet = workbook.Sheets[firstSheetName];
  652. const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
  653. // 2. 检查是否为空文件
  654. const hasData = jsonData.some(row =>
  655. Array.isArray(row) && row.some(cell =>
  656. cell !== null && cell !== undefined && String(cell).trim() !== ""
  657. )
  658. );
  659. if (!hasData) {
  660. showMessage("❌ 文件为空", "error");
  661. return;
  662. }
  663. // 3. 查找表头行
  664. const headerRowIndex = jsonData.findIndex(row =>
  665. Array.isArray(row) && row.some(cell =>
  666. cell !== null && cell !== undefined && String(cell).trim() !== ""
  667. )
  668. );
  669. if (headerRowIndex === -1) {
  670. showMessage("❌ 未找到表头", "error");
  671. return;
  672. }
  673. // 4. 获取列名并映射
  674. const headerRow = jsonData[headerRowIndex];
  675. const rawColumns = (headerRow as string[]).map(col => String(col).trim());
  676. const columns = rawColumns.map(col => COLUMN_NAME_MAPPING[col] || col);
  677. // 5. 检查必要列
  678. const requiredCols = Object.keys(REQUIRED_COLUMNS);
  679. const missingCols = requiredCols.filter(col => !columns.includes(col));
  680. if (missingCols.length > 0) {
  681. showMessage(`❌ 缺少列: ${missingCols.join(", ")}`, "error");
  682. return;
  683. }
  684. // 6. 检查数据行
  685. const dataRows = jsonData.slice(headerRowIndex + 1);
  686. const validRows = dataRows.filter(row =>
  687. requiredCols.some(col => {
  688. const idx = columns.indexOf(col);
  689. return idx !== -1 && row?.[idx] != null && String(row[idx]).trim() !== "";
  690. })
  691. );
  692. if (validRows.length === 0) {
  693. showMessage("⚠️ 无有效数据", "error");
  694. return;
  695. }
  696. // 7. 检查必填列是否为空
  697. const emptyErrors: string[] = [];
  698. validRows.forEach((row, rowIdx) => {
  699. requiredCols.forEach(col => {
  700. const colIndex = columns.indexOf(col);
  701. if (colIndex !== -1 && (row?.[colIndex] == null || String(row[colIndex]).trim() === "")) {
  702. emptyErrors.push(`第${rowIdx + 2}行,${col}列为空`);
  703. }
  704. });
  705. });
  706. if (emptyErrors.length > 0) {
  707. showMessage(`❌ 必填列为空:\n${emptyErrors.slice(0, 5).join('\n')}`, "error");
  708. return;
  709. }
  710. // 8. 转换数据格式
  711. const transformedData = validRows.map(row => {
  712. const newRow: any = {};
  713. rawColumns.forEach((origCol, idx) => {
  714. const engCol = COLUMN_NAME_MAPPING[origCol] || origCol;
  715. if (requiredCols.includes(engCol)) {
  716. newRow[engCol] = row?.[idx];
  717. }
  718. });
  719. return newRow;
  720. });
  721. // 9. 创建新工作簿
  722. const newWorkbook = XLSX.utils.book_new();
  723. const newWorksheet = XLSX.utils.json_to_sheet(transformedData);
  724. XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, "Sheet1");
  725. const outputArrayBuffer = XLSX.write(newWorkbook, { bookType: "xlsx", type: "array" });
  726. const newFile = new File([outputArrayBuffer], file.name, {
  727. type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  728. });
  729. // 10. 构造表单数据
  730. const formData = new FormData();
  731. formData.append("file", newFile);
  732. formData.append("dataset_name", file.name.replace(/\.[^/.]+$/, "") || "Imported Dataset");
  733. formData.append("dataset_description", `通过前端上传的 ${file.name} 数据集`);
  734. // 11. 上传数据
  735. const response: any = await importData(currentTableName, formData);
  736. // 12. 处理响应
  737. if ((response.status === 200 || response.status === 201) && response.data) {
  738. const data = response.data;
  739. const stats = data.data_stats || {};
  740. // 主要成功信息
  741. let mainMessage = `🎉 数据集上传成功!\n\n`;
  742. mainMessage += `📌 数据集名称: ${data.dataset_name || data.dataset_id || "未知"}\n`;
  743. mainMessage += `📌 唯一ID: ${data.dataset_id}\n`;
  744. // 详细统计信息(可折叠或单独显示)
  745. let detailMessage = `\n📊 数据统计详情:\n`;
  746. detailMessage += ` • 原始行数: ${stats.original_count || 0}\n`;
  747. detailMessage += ` • 文件内重复: ${stats.duplicates_in_file || 0}\n`;
  748. detailMessage += ` • 与现有数据重复: ${stats.duplicates_with_existing || 0}\n`;
  749. detailMessage += ` • 与测试集冲突: ${stats.test_overlap_count || 0}\n`;
  750. detailMessage += ` • 最终入库: ${stats.final_count || 0}\n`;
  751. // 附加信息
  752. let additionalInfo = '';
  753. if (data.training_triggered) {
  754. additionalInfo += `\n🚀 自动训练已触发,任务ID: ${data.task_id}`;
  755. }
  756. if (data.message) {
  757. additionalInfo += `\n\n💡 系统提示: ${data.message.replace("✅", "").replace("🎉", "").trim()}`;
  758. }
  759. // 分步骤显示消息
  760. showMessage(mainMessage, "success");
  761. // 在控制台显示详细信息
  762. console.log(detailMessage + additionalInfo);
  763. // 显示详细信息弹窗
  764. detailsContent.value = (mainMessage + detailMessage + additionalInfo).replace(/\n/g, '<br/>');
  765. detailsVisible.value = true;
  766. await fetchTable(); // 刷新表格
  767. } else {
  768. const errorMsg = response.data?.error || "服务器未返回预期数据";
  769. showMessage(`❌ 导入失败\n服务器返回错误:${errorMsg}\n状态码:${response.status}\n\n请检查网络或联系管理员。`, "error");
  770. }
  771. } catch (error: any) {
  772. let errorMsg = "导入失败";
  773. let errorDetail = "未知错误";
  774. if (error.name === "AbortError") {
  775. errorMsg = "请求已取消";
  776. errorDetail = "网络请求被中断,请检查连接后重试。";
  777. } else if (error.message.includes("Invalid data")) {
  778. errorMsg = "文件数据格式错误";
  779. errorDetail = "Excel 文件中包含不支持的数据类型(如公式、图片、合并单元格),请使用纯数据表格。";
  780. } else if (error.message.includes("Unable to parse") || error.message.includes("Bad file")) {
  781. errorMsg = "无法解析文件";
  782. errorDetail = "文件可能已损坏或不是有效的 .xlsx 格式,请重新保存或另存为 Excel 文件。";
  783. } else if (error.message.includes("Network Error")) {
  784. errorMsg = "网络错误";
  785. errorDetail = "无法连接到服务器,请检查网络连接。";
  786. } else {
  787. errorDetail = error.message || "请检查文件格式和内容是否符合要求。";
  788. }
  789. showMessage(`❌ ${errorMsg}\n${errorDetail}`, "error");
  790. console.error("导入文件出错:", error);
  791. }
  792. };
  793. // 定义详细信息展示函数,带有明确的参数类型
  794. const handleSizeChange = (val: number) => {
  795. pageSize4.value = val;
  796. currentPage4.value = 1;
  797. };
  798. const handleCurrentChange = (val: number) => {
  799. currentPage4.value = val;
  800. };
  801. const formatTableValue = (_row: any, _column: any, cellValue: any) => {
  802. return normalizeValue(cellValue);
  803. };
  804. const dialogTitle = computed(() => {
  805. return dialogMode.value === "add" ? `新增记录` : "编辑记录";
  806. });
  807. const dialogSubmitButtonText = computed(() => {
  808. return dialogMode.value === "add" ? "添加" : "保存";
  809. });
  810. const handleCancel = () => {
  811. if (formRef.value) {
  812. formRef.value.resetFields();
  813. console.log("表单已重置");
  814. } else {
  815. console.warn("表单引用无效,无法重置表单");
  816. }
  817. dialogVisible.value = false;
  818. console.log("对话框已关闭");
  819. };
  820. // 新增刷新数据功能
  821. const refreshData = async () => {
  822. try {
  823. loading.value = true;
  824. await fetchTable();
  825. showMessage("数据刷新成功", "success");
  826. } catch (error) {
  827. console.error("刷新数据失败:", error);
  828. showMessage("数据刷新失败,请重试", "error");
  829. } finally {
  830. loading.value = false;
  831. }
  832. };
  833. // 统一提示消息函数
  834. const showMessage = (
  835. message: string,
  836. type: "success" | "warning" | "info" | "error" = "info",
  837. duration: number = 3000
  838. ) => {
  839. ElMessage.closeAll();
  840. ElMessage({
  841. message,
  842. type,
  843. duration,
  844. customClass: "unified-message",
  845. showClose: true,
  846. });
  847. };
  848. // 详情弹窗相关
  849. const detailsVisible = ref(false);
  850. const detailsContent = ref('');
  851. </script>
  852. <style scoped lang="scss">
  853. $primary-color: #409eff;
  854. $success-color: #67c23a;
  855. $warning-color: #e6a23c;
  856. $error-color: #f56c6c;
  857. $info-color: #909399;
  858. $alert-success-bg: #f0f9eb;
  859. $alert-success-border: #e1f3d8;
  860. $alert-success-text: #67c23a;
  861. $alert-error-bg: #fef0f0;
  862. $alert-error-border: #fbc4c4;
  863. $alert-error-text: #f56c6c;
  864. $alert-warning-bg: #fdf6ec;
  865. $alert-warning-border: #f5dab1;
  866. $alert-warning-text: #e6a23c;
  867. $alert-info-bg: #f4f4f5;
  868. $alert-info-border: #e9e9eb;
  869. $alert-info-text: #909399;
  870. .app-container {
  871. padding: 10px;
  872. min-height: 100vh;
  873. background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
  874. box-sizing: border-box;
  875. display: flex;
  876. flex-direction: column;
  877. }
  878. /* 统一消息样式 */
  879. :global(.unified-message) {
  880. border-radius: 8px !important;
  881. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
  882. padding: 12px 20px !important;
  883. font-size: 14px !important;
  884. line-height: 1.5 !important;
  885. border: 1px solid transparent !important;
  886. text-align: center;
  887. font-weight: 500;
  888. }
  889. :global(.unified-message.el-message--success) {
  890. background-color: $alert-success-bg !important;
  891. border-color: $alert-success-border !important;
  892. color: $alert-success-text !important;
  893. }
  894. :global(.unified-message.el-message--error) {
  895. background-color: $alert-error-bg !important;
  896. border-color: $alert-error-border !important;
  897. color: $alert-error-text !important;
  898. }
  899. :global(.unified-message.el-message--warning) {
  900. background-color: $alert-warning-bg !important;
  901. border-color: $alert-warning-border !important;
  902. color: $alert-warning-text !important;
  903. }
  904. :global(.unified-message.el-message--info) {
  905. background-color: $alert-info-bg !important;
  906. border-color: $alert-info-border !important;
  907. color: $alert-info-text !important;
  908. }
  909. /* 修改后的统一通知样式 - 居中显示 */
  910. :global(.unified-notification) {
  911. border-radius: 8px !important;
  912. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
  913. padding: 16px 20px !important;
  914. font-size: 14px !important;
  915. line-height: 1.5 !important;
  916. border: 1px solid transparent !important;
  917. text-align: center !important;
  918. /* Element Plus 的通知默认是居中的,所以移除之前的 left/right/top/transform 定位 */
  919. max-width: 80vw;
  920. /* 可选:添加一些内边距或边框来美化 */
  921. /* background-color: #fff !important; */ /* 如果需要纯白背景 */
  922. }
  923. /* 为不同类型的 notification 添加特定样式 */
  924. :global(.unified-notification.notification-success) {
  925. background-color: $alert-success-bg !important;
  926. border-color: $alert-success-border !important;
  927. color: $alert-success-text !important;
  928. }
  929. :global(.unified-notification.notification-error) {
  930. background-color: $alert-error-bg !important;
  931. border-color: $alert-error-border !important;
  932. color: $alert-error-text !important;
  933. }
  934. :global(.unified-notification.notification-warning) {
  935. background-color: $alert-warning-bg !important;
  936. border-color: $alert-warning-border !important;
  937. color: $alert-warning-text !important;
  938. }
  939. :global(.unified-notification.notification-info) {
  940. background-color: $alert-info-bg !important;
  941. border-color: $alert-info-border !important;
  942. color: $alert-info-text !important;
  943. }
  944. :global(.unified-notification .el-notification__title) {
  945. font-weight: bold !important;
  946. text-align: center !important;
  947. margin-bottom: 8px !important; /* 标题和内容之间增加间距 */
  948. }
  949. :global(.unified-notification .el-notification__content) {
  950. text-align: center !important;
  951. word-wrap: break-word !important;
  952. }
  953. .content-wrapper {
  954. width: 100%;
  955. box-sizing: border-box;
  956. display: flex;
  957. flex-direction: column;
  958. gap: 20px;
  959. }
  960. .button-section {
  961. background: #ffffff;
  962. padding: 20px;
  963. border-radius: 8px;
  964. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  965. display: flex;
  966. flex-direction: column;
  967. gap: 15px;
  968. }
  969. .table-section {
  970. background: #ffffff;
  971. padding: 20px;
  972. border-radius: 8px;
  973. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  974. overflow: hidden;
  975. }
  976. .button-group {
  977. display: flex;
  978. gap: 15px;
  979. justify-content: space-between;
  980. width: 100%;
  981. }
  982. .button-group-right {
  983. display: flex;
  984. gap: 15px;
  985. }
  986. .custom-button {
  987. color: #fff;
  988. border: none;
  989. border-radius: 6px;
  990. font-size: 14px;
  991. padding: 10px 20px;
  992. transition: all 0.3s ease;
  993. min-width: 110px;
  994. display: flex;
  995. align-items: center;
  996. justify-content: center;
  997. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  998. font-weight: 500;
  999. }
  1000. .custom-button:hover {
  1001. transform: translateY(-2px);
  1002. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1003. }
  1004. .custom-button:active {
  1005. transform: translateY(0);
  1006. }
  1007. .add-button {
  1008. background: linear-gradient(135deg, $success-color, #4ebc4e);
  1009. }
  1010. .add-button:hover {
  1011. background: linear-gradient(135deg, #85ce61, $success-color);
  1012. }
  1013. .download-button {
  1014. background: linear-gradient(135deg, $primary-color, #2d8cf0);
  1015. }
  1016. .download-button:hover {
  1017. background: linear-gradient(135deg, #66b1ff, $primary-color);
  1018. }
  1019. .export-button {
  1020. background: linear-gradient(135deg, $warning-color, #d6902d);
  1021. }
  1022. .export-button:hover {
  1023. background: linear-gradient(135deg, #ebb563, $warning-color);
  1024. }
  1025. .import-button {
  1026. background: linear-gradient(135deg, $info-color, #7d8086);
  1027. }
  1028. .import-button:hover {
  1029. background: linear-gradient(135deg, #a6a9ad, $info-color);
  1030. }
  1031. .refresh-button {
  1032. background: linear-gradient(135deg, #409eff, #2d8cf0);
  1033. }
  1034. .refresh-button:hover {
  1035. background: linear-gradient(135deg, #66b1ff, #409eff);
  1036. }
  1037. .custom-table {
  1038. width: 100%;
  1039. border-radius: 8px;
  1040. overflow: hidden;
  1041. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
  1042. background-color: #fff;
  1043. }
  1044. :deep(.el-table th) {
  1045. color: #fff;
  1046. font-weight: bold;
  1047. text-align: center;
  1048. padding: 12px 0;
  1049. font-size: 14px;
  1050. }
  1051. :deep(.fixed-column-header) {
  1052. background-color: $primary-color !important;
  1053. }
  1054. :deep(.el-table th:not(.fixed-column-header)) {
  1055. background: linear-gradient(180deg, $primary-color, #2d8cf0);
  1056. }
  1057. :deep(.el-table__row:nth-child(odd)) {
  1058. background-color: #f9fbfd;
  1059. }
  1060. :deep(.el-table__row:nth-child(even)) {
  1061. background-color: #ffffff;
  1062. }
  1063. :deep(.el-table td) {
  1064. padding: 12px 8px;
  1065. text-align: center;
  1066. border-bottom: 1px solid #ebeef5;
  1067. white-space: nowrap;
  1068. overflow: hidden;
  1069. text-overflow: ellipsis;
  1070. }
  1071. .action-button {
  1072. font-size: 16px;
  1073. margin-right: 5px;
  1074. border-radius: 50%;
  1075. transition: all 0.3s ease;
  1076. }
  1077. .edit-button {
  1078. background-color: $primary-color;
  1079. color: #fff;
  1080. border: none;
  1081. }
  1082. .edit-button:hover {
  1083. background-color: #66b1ff;
  1084. transform: scale(1.1);
  1085. }
  1086. .delete-button {
  1087. background-color: $error-color;
  1088. color: #fff;
  1089. border: none;
  1090. }
  1091. .delete-button:hover {
  1092. background-color: #f78989;
  1093. transform: scale(1.1);
  1094. }
  1095. .action-buttons {
  1096. display: flex;
  1097. justify-content: center;
  1098. }
  1099. .pagination-wrapper {
  1100. display: flex;
  1101. justify-content: center;
  1102. width: 100%;
  1103. margin: 25px 0 10px 0;
  1104. }
  1105. .pagination-container {
  1106. display: flex;
  1107. justify-content: center;
  1108. }
  1109. :deep(.el-dialog) {
  1110. border-radius: 12px !important;
  1111. overflow: hidden;
  1112. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
  1113. background: linear-gradient(145deg, #ffffff, #f5f9ff);
  1114. border: 1px solid #e0e7ff;
  1115. margin-top: calc(var(--el-dialog-margin-top, 15vh) + 30px) !important;
  1116. }
  1117. .dialog-header {
  1118. position: relative;
  1119. padding: 20px 24px 10px;
  1120. text-align: center;
  1121. background: linear-gradient(90deg, $primary-color, #2d8cf0);
  1122. }
  1123. .dialog-header h2 {
  1124. margin: 0;
  1125. font-size: 22px;
  1126. font-weight: 600;
  1127. color: #fff;
  1128. text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  1129. }
  1130. .header-decoration {
  1131. height: 4px;
  1132. background: linear-gradient(90deg, #2d8cf0, $primary-color);
  1133. border-radius: 2px;
  1134. margin-top: 12px;
  1135. width: 60%;
  1136. margin-left: auto;
  1137. margin-right: auto;
  1138. }
  1139. .custom-dialog-form {
  1140. padding: 25px 10px 15px 180px;
  1141. display: grid;
  1142. grid-template-columns: repeat(2, 1fr);
  1143. gap: 20px;
  1144. max-height: calc(100vh - 300px);
  1145. overflow-y: auto;
  1146. scrollbar-width: thin;
  1147. scrollbar-color: #c0c4cc #f1f3f4;
  1148. }
  1149. .custom-dialog-form::-webkit-scrollbar {
  1150. width: 6px;
  1151. }
  1152. .custom-dialog-form::-webkit-scrollbar-track {
  1153. background: #f1f3f4;
  1154. border-radius: 3px;
  1155. }
  1156. .custom-dialog-form::-webkit-scrollbar-thumb {
  1157. background: #c0c4cc;
  1158. border-radius: 3px;
  1159. }
  1160. .custom-dialog-form::-webkit-scrollbar-thumb:hover {
  1161. background: #a8abb3;
  1162. }
  1163. @media (max-width: 1200px) {
  1164. .custom-dialog-form {
  1165. grid-template-columns: 1fr;
  1166. padding: 25px 30px 15px 30px;
  1167. }
  1168. }
  1169. .custom-form-item {
  1170. margin-bottom: 0 !important;
  1171. }
  1172. :deep(.el-form-item__label) {
  1173. display: block;
  1174. text-align: left;
  1175. margin-bottom: 6px !important;
  1176. font-size: 14px;
  1177. font-weight: 500;
  1178. color: #2d3748;
  1179. padding: 0 !important;
  1180. line-height: 1.5;
  1181. }
  1182. .dialog-footer {
  1183. display: flex;
  1184. gap: 16px;
  1185. justify-content: center;
  1186. padding: 15px 40px 25px;
  1187. }
  1188. .custom-cancel-button,
  1189. .custom-submit-button {
  1190. min-width: 120px;
  1191. height: 40px;
  1192. border-radius: 6px;
  1193. font-size: 15px;
  1194. font-weight: 500;
  1195. transition: all 0.3s ease;
  1196. letter-spacing: 0.5px;
  1197. border: none;
  1198. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  1199. }
  1200. .custom-cancel-button {
  1201. background: linear-gradient(145deg, #f7fafc, #edf2f7);
  1202. color: #4a5568;
  1203. }
  1204. .custom-cancel-button:hover {
  1205. background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
  1206. transform: translateY(-1px);
  1207. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  1208. }
  1209. .custom-submit-button {
  1210. background: linear-gradient(145deg, $primary-color, #2d8cf0);
  1211. color: white;
  1212. position: relative;
  1213. overflow: hidden;
  1214. }
  1215. .custom-submit-button::before {
  1216. content: "";
  1217. position: absolute;
  1218. top: 0;
  1219. left: -100%;
  1220. width: 100%;
  1221. height: 100%;
  1222. background: linear-gradient(
  1223. 90deg,
  1224. transparent,
  1225. rgba(255, 255, 255, 0.3),
  1226. transparent
  1227. );
  1228. transition: 0.5s;
  1229. }
  1230. .custom-submit-button:hover::before {
  1231. left: 100%;
  1232. }
  1233. .custom-submit-button:hover {
  1234. transform: translateY(-1px);
  1235. box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  1236. }
  1237. :deep(.import-upload .el-upload-list) {
  1238. display: none !important;
  1239. }
  1240. :deep(.el-table__row:hover) {
  1241. background-color: #e6f7ff !important;
  1242. transition: background-color 0.3s;
  1243. }
  1244. :deep(.el-table) {
  1245. border: 1px solid #ebeef5;
  1246. border-radius: 8px;
  1247. overflow: hidden;
  1248. }
  1249. :deep(.el-table__header-wrapper) {
  1250. border-bottom: 1px solid #ebeef5;
  1251. }
  1252. :deep(.el-table td) {
  1253. border-right: 1px solid #ebeef5;
  1254. }
  1255. :deep(.el-table td:last-child) {
  1256. border-right: none;
  1257. }
  1258. .details-content {
  1259. line-height: 1.8;
  1260. font-size: 14px;
  1261. color: #333;
  1262. }
  1263. .error-content {
  1264. ul {
  1265. list-style-type: disc;
  1266. padding-left: 20px;
  1267. margin-top: 10px;
  1268. li {
  1269. margin-bottom: 8px;
  1270. }
  1271. }
  1272. }
  1273. </style>