Visualization.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <template>
  2. <div class="app-container">
  3. <!-- 操作按钮区 -->
  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. >新增记录</el-button
  11. >
  12. <el-button
  13. :icon="Download"
  14. type="primary"
  15. @click="downloadTemplateAction"
  16. class="custom-button download-button"
  17. >下载模板</el-button
  18. >
  19. <el-button
  20. :icon="Download"
  21. type="primary"
  22. @click="exportDataAction"
  23. class="custom-button export-button"
  24. >导出数据</el-button
  25. >
  26. <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
  27. <el-button
  28. :icon="Upload"
  29. type="primary"
  30. class="custom-button import-button"
  31. >导入数据</el-button
  32. >
  33. </el-upload>
  34. </div>
  35. <!-- 数据表格 -->
  36. <el-table
  37. :data="pagedTableDataWithIndex"
  38. fit
  39. style="width: 100%"
  40. @row-click="handleRowClick"
  41. highlight-current-row
  42. class="custom-table"
  43. v-loading="loading"
  44. table-layout="auto"
  45. >
  46. <el-table-column
  47. key="displayIndex"
  48. prop="displayIndex"
  49. label="序号"
  50. width="80"
  51. align="center"
  52. ></el-table-column>
  53. <el-table-column
  54. v-for="col in columns.filter((col) => col.key !== 'id')"
  55. :key="col.key"
  56. :prop="col.dataKey"
  57. :label="col.title"
  58. :min-width="col.title.length * 20 + 40"
  59. :formatter="formatNumber"
  60. align="center"
  61. header-align="center"
  62. ></el-table-column>
  63. <el-table-column label="操作" width="120" align="center">
  64. <template #default="scope">
  65. <span class="action-buttons">
  66. <el-tooltip
  67. class="item"
  68. effect="dark"
  69. content="编辑"
  70. placement="top"
  71. >
  72. <el-button
  73. circle
  74. :icon="EditPen"
  75. @click.stop="openDialog('edit', scope.row)"
  76. class="action-button edit-button"
  77. ></el-button>
  78. </el-tooltip>
  79. <el-tooltip
  80. class="item"
  81. effect="dark"
  82. content="删除"
  83. placement="top"
  84. >
  85. <el-button
  86. circle
  87. :icon="DeleteFilled"
  88. @click.stop="deleteItem(scope.row)"
  89. class="action-button delete-button"
  90. ></el-button>
  91. </el-tooltip>
  92. </span>
  93. </template>
  94. </el-table-column>
  95. </el-table>
  96. <!-- 分页控制 -->
  97. <PaginationComponent
  98. :total="tableData.length"
  99. :currentPage="currentPage4"
  100. :pageSize="pageSize4"
  101. @update:currentPage="currentPage4 = $event"
  102. @update:pageSize="pageSize4 = $event"
  103. @size-change="handleSizeChange"
  104. @current-change="handleCurrentChange"
  105. class="pagination-container"
  106. />
  107. <!-- 新增/编辑对话框 -->
  108. <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
  109. <el-form :model="formData" label-width="120px">
  110. <el-form-item
  111. v-for="col in editableColumns"
  112. :key="col.key"
  113. :label="col.title"
  114. >
  115. <el-input
  116. v-model="formData[col.dataKey]"
  117. :type="col.inputType || 'text'"
  118. class="custom-input"
  119. ></el-input>
  120. </el-form-item>
  121. </el-form>
  122. <template #footer>
  123. <el-button @click="dialogVisible = false" class="custom-cancel-button">
  124. 取消
  125. </el-button>
  126. <el-button
  127. type="primary"
  128. @click="submitForm"
  129. class="custom-submit-button"
  130. >
  131. {{ dialogSubmitButtonText }}
  132. </el-button>
  133. </template>
  134. </el-dialog>
  135. </div>
  136. </template>
  137. <script lang="ts" setup>
  138. import { ref, reactive, computed, onMounted } from "vue";
  139. import {
  140. DeleteFilled,
  141. Download,
  142. Upload,
  143. Plus,
  144. EditPen,
  145. } from "@element-plus/icons-vue";
  146. import { ElMessage } from "element-plus";
  147. import {
  148. table,
  149. updateItem,
  150. addItem,
  151. deleteItemApi,
  152. downloadTemplate,
  153. exportData,
  154. importData,
  155. } from "@/API/menus";
  156. import PaginationComponent from "@/components/PaginationComponent.vue";
  157. interface Column {
  158. key: string;
  159. dataKey: string;
  160. title: string;
  161. width: number;
  162. inputType?: string;
  163. }
  164. const columns: Column[] = [
  165. { key: "id", dataKey: "id", title: "序号", width: 100 },
  166. { key: "Q_over_b", dataKey: "Q_over_b", title: "Q/ΔpH", width: 180 },
  167. { key: "pH", dataKey: "pH", title: "初始pH", width: 180 },
  168. { key: "OM", dataKey: "OM", title: "有机质含量", width: 180 },
  169. { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 180 },
  170. { key: "H", dataKey: "H", title: "交换性氢", width: 180 },
  171. { key: "Al", dataKey: "Al", title: "交换性铝", width: 180 },
  172. ];
  173. const editableColumns = columns.filter((col) => col.key !== "id");
  174. const tableData = ref<any[]>([]);
  175. const selectedRow = ref<any | null>(null);
  176. const dialogVisible = ref(false);
  177. const formData = reactive<any>({});
  178. const dialogMode = ref<"add" | "edit">("add"); // 新增还是编辑模式
  179. const currentPage4 = ref(1);
  180. const pageSize4 = ref(10);
  181. const pagedTableData = computed(() => {
  182. const start = (currentPage4.value - 1) * pageSize4.value;
  183. const end = start + pageSize4.value;
  184. return tableData.value.slice(start, end);
  185. });
  186. const pagedTableDataWithIndex = computed(() => {
  187. return pagedTableData.value.map((item, index) => ({
  188. ...item,
  189. displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
  190. }));
  191. });
  192. const loading = ref(false);
  193. const currentTableName = "current_reduce";
  194. const fetchTable = async () => {
  195. try {
  196. loading.value = true;
  197. const response = await table({ table: currentTableName });
  198. tableData.value = response.data.rows;
  199. } catch (error) {
  200. console.error("获取数据时出错:", error);
  201. ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
  202. } finally {
  203. loading.value = false;
  204. }
  205. };
  206. onMounted(() => {
  207. fetchTable();
  208. });
  209. const handleRowClick = (row: any) => {
  210. selectedRow.value = row;
  211. Object.assign(formData, row);
  212. };
  213. const openDialog = (mode: "add" | "edit", row?: any) => {
  214. if (mode === "add") {
  215. selectedRow.value = null;
  216. editableColumns.forEach((col) => {
  217. formData[col.dataKey] = "";
  218. });
  219. } else if (row) {
  220. selectedRow.value = row;
  221. Object.assign(formData, row);
  222. }
  223. dialogMode.value = mode;
  224. dialogVisible.value = true;
  225. };
  226. function prepareFormData(
  227. formData: { [x: string]: any },
  228. excludeKeys = ["displayIndex"]
  229. ): { [x: string]: any } {
  230. const result: { [x: string]: any } = {};
  231. for (const key in formData) {
  232. if (!excludeKeys.includes(key)) {
  233. let value = formData[key];
  234. if (typeof value === "string" && value.startsWith("displayIndex-")) {
  235. value = value.replace("displayIndex-", "");
  236. }
  237. result[key] = value;
  238. }
  239. }
  240. return result;
  241. }
  242. const submitForm = async () => {
  243. try {
  244. const isValid = validateFormData(formData);
  245. if (!isValid) {
  246. alert("请检查输入的数据");
  247. return;
  248. }
  249. const dataToSubmit = prepareFormData(formData);
  250. if (!dataToSubmit.id && dialogMode.value !== "add") {
  251. alert("无法找到记录ID,请联系管理员");
  252. return;
  253. }
  254. let response;
  255. if (dialogMode.value === "add") {
  256. response = await addItem({ table: currentTableName, item: dataToSubmit });
  257. } else {
  258. response = await updateItem({
  259. table: currentTableName,
  260. item: dataToSubmit,
  261. });
  262. }
  263. dialogVisible.value = false;
  264. fetchTable();
  265. alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
  266. } catch (error) {
  267. let errorMessage = "未知错误";
  268. if (error && typeof error === "object" && "response" in error) {
  269. const response = (error as { response?: { data?: { message?: string } } })
  270. .response;
  271. if (
  272. response &&
  273. response.data &&
  274. typeof response.data.message === "string"
  275. ) {
  276. errorMessage = response.data.message;
  277. }
  278. }
  279. alert(`提交失败: ${errorMessage}`);
  280. }
  281. };
  282. function validateFormData(data: { [x: string]: undefined }) {
  283. for (let key in data) {
  284. if (data[key] === "" || data[key] === undefined) {
  285. return false;
  286. }
  287. }
  288. return true;
  289. }
  290. const deleteItem = async (row: any) => {
  291. if (!row) {
  292. ElMessage.warning("请先选择一行记录");
  293. return;
  294. }
  295. try {
  296. const condition = { id: row.id };
  297. await deleteItemApi({ table: "current_reduce", condition });
  298. const index = tableData.value.findIndex((item) => item.id === row.id);
  299. if (index > -1) {
  300. tableData.value.splice(index, 1);
  301. }
  302. fetchTable();
  303. } catch (error) {
  304. console.error("删除记录时发生错误:", error);
  305. }
  306. };
  307. const downloadTemplateAction = async () => {
  308. try {
  309. await downloadTemplate("current_reduce");
  310. } catch (error) {
  311. console.error("下载模板时发生错误:", error);
  312. }
  313. };
  314. const exportDataAction = async () => {
  315. try {
  316. await exportData("current_reduce");
  317. } catch (error) {
  318. console.error("导出数据时发生错误:", error);
  319. }
  320. };
  321. // 导入
  322. const importDataAction = async (file: File) => {
  323. try {
  324. const response = await importData("reduce", file); // 传递 dataset_type 的值(如 'reduce')
  325. if (response && response.data) {
  326. const { total_data, new_data, duplicate_data, message } = response.data;
  327. ElMessage({
  328. message: `导入结果: ${message} 新增总数:${total_data}, 成功新增:${new_data}, 数据重复:${duplicate_data}`,
  329. type: "success",
  330. });
  331. fetchTable(); // 假设存在 fetchTable 方法刷新表格
  332. }
  333. } catch (error) {
  334. let errorMessage = "数据导入失败";
  335. if (error && typeof error === "object" && "response" in error) {
  336. const response = (error as { response?: { data?: { message?: string } } })
  337. .response;
  338. if (response && response.data && typeof response.data.message === "string") {
  339. errorMessage += `: ${response.data.message}`;
  340. }
  341. }
  342. ElMessage.error(errorMessage);
  343. }
  344. };
  345. const handleFileChange = (event: Event) => {
  346. const target = event.target as HTMLInputElement;
  347. if (target.files && target.files.length > 0) {
  348. importDataAction(target.files[0]);
  349. }
  350. };
  351. const handleSizeChange = (val: number) => {};
  352. const handleCurrentChange = (val: number) => {};
  353. const formatNumber = (row: any, column: any, cellValue: any) => {
  354. if (typeof cellValue === "number") {
  355. return cellValue.toFixed(3);
  356. }
  357. return cellValue;
  358. };
  359. const dialogTitle = computed(() => {
  360. return dialogMode.value === "add" ? "新增记录" : "编辑记录";
  361. });
  362. const dialogSubmitButtonText = computed(() => {
  363. return dialogMode.value === "add" ? "添加" : "保存";
  364. });
  365. </script>
  366. <style scoped>
  367. .app-container {
  368. padding: 15px 40px;
  369. min-height: 100vh;
  370. background-color: #f0f2f5;
  371. display: flex;
  372. flex-direction: column;
  373. align-items: center;
  374. }
  375. .button-group {
  376. display: flex;
  377. gap: 12px;
  378. justify-content: flex-end;
  379. width: 100%;
  380. margin-bottom: 20px;
  381. }
  382. .custom-button {
  383. color: #fff;
  384. border: none;
  385. border-radius: 8px;
  386. font-size: 14px;
  387. padding: 10px 18px;
  388. transition: transform 0.3s ease, background-color 0.3s ease;
  389. min-width: 130px;
  390. display: flex;
  391. align-items: center;
  392. justify-content: center;
  393. }
  394. .download-button,
  395. .export-button,
  396. .add-button,
  397. .import-button {
  398. background-color: #67c23a;
  399. }
  400. .download-button:hover,
  401. .export-button:hover,
  402. .import-button:hover,
  403. .add-button:hover {
  404. background-color: #85ce61;
  405. transform: scale(1.05);
  406. }
  407. .custom-table {
  408. width: 100%;
  409. border-radius: 8px;
  410. overflow: hidden;
  411. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  412. background-color: #fff;
  413. margin-top: 10px;
  414. }
  415. :deep( .el-table th) {
  416. background: linear-gradient(180deg, #61e054, #4db944);
  417. color: #fff;
  418. font-weight: bold;
  419. text-align: center;
  420. padding: 14px 0;
  421. font-size: 14px;
  422. }
  423. :deep( .el-table__row:nth-child(odd)) {
  424. background-color: #E4FBE5;
  425. }
  426. :deep( .el-table__row:nth-child(even)) {
  427. background-color: #ffffff;
  428. }
  429. :deep( .el-table td) {
  430. padding: 12px 10px;
  431. text-align: center;
  432. border-bottom: 1px solid #ebeef5;
  433. }
  434. .action-button {
  435. font-size: 16px;
  436. margin-right: 5px;
  437. border-radius: 50%;
  438. transition: transform 0.3s ease, background-color 0.3s ease;
  439. }
  440. .edit-button {
  441. background-color: #409eff;
  442. color: #fff;
  443. }
  444. .edit-button:hover {
  445. background-color: #66b1ff;
  446. transform: scale(1.1);
  447. }
  448. .delete-button {
  449. background-color: #f56c6c;
  450. color: #fff;
  451. }
  452. .delete-button:hover {
  453. background-color: #f78989;
  454. transform: scale(1.1);
  455. }
  456. .el-form-item__label {
  457. width: 150px;
  458. font-weight: bold;
  459. }
  460. .el-form-item__content {
  461. margin-left: 150px;
  462. }
  463. .custom-input .el-input__inner {
  464. border-radius: 6px;
  465. border: 1px solid #dcdcdc;
  466. transition: border-color 0.3s ease;
  467. padding: 10px;
  468. font-size: 14px;
  469. }
  470. .custom-input .el-input__inner:focus {
  471. border-color: #409eff;
  472. }
  473. .custom-cancel-button,
  474. .custom-submit-button {
  475. border: none;
  476. border-radius: 6px;
  477. font-size: 14px;
  478. padding: 10px 20px;
  479. transition: transform 0.3s ease, background-color 0.3s ease;
  480. min-width: 100px;
  481. }
  482. .custom-cancel-button {
  483. background-color: #f4f4f5;
  484. color: #606266;
  485. }
  486. .custom-cancel-button:hover {
  487. background-color: #e4e7ed;
  488. transform: scale(1.05);
  489. }
  490. .custom-submit-button {
  491. background-color: #409eff;
  492. color: #fff;
  493. }
  494. .custom-submit-button:hover {
  495. background-color: #66b1ff;
  496. transform: scale(1.05);
  497. }
  498. .pagination-container {
  499. margin-top: 30px;
  500. text-align: center;
  501. }
  502. .action-buttons {
  503. display: flex;
  504. justify-content: center;
  505. }
  506. </style>