root-word-manage-table.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <script setup lang="ts">
  2. /**
  3. * @Name: root-word-manage-table.vue
  4. * @Description: 词根管理表格
  5. * @Author: Cheney
  6. */
  7. import { nextTick, onMounted, reactive, ref } from 'vue';
  8. import { Plus, Search, Upload } from '@element-plus/icons-vue';
  9. import * as api from '../api';
  10. import type { UploadInstance, UploadRawFile } from 'element-plus';
  11. import { ElMessage, FormInstance, FormRules, genFileId } from 'element-plus';
  12. import { SUCCESS_CODE, WARNING_CODE } from '/@/utils/requestCode';
  13. interface DataItem {
  14. id: number;
  15. modifier_name: string;
  16. creator_name: string;
  17. create_datetime: string;
  18. update_datetime: string;
  19. description: string | null;
  20. modifier: string;
  21. searchTerm: string;
  22. searchTerm_type: string;
  23. add_date: string;
  24. creator: number;
  25. isEditing: boolean; // 添加isEditing字段
  26. popConfirmVisible: boolean; // 添加popConfirmVisible字段
  27. tempSearchTermType: string; // 添加tempSearchTermType字段
  28. }
  29. const currentDate = new Date().toISOString().split('T')[0];
  30. const tableLoading = ref(false);
  31. const tableData = ref<DataItem[]>([]);
  32. const total = ref(0);
  33. const currentPage = ref(1);
  34. const pageSize = ref(10);
  35. const searchTermInpRef = ref();
  36. const dialogVisible = ref(false);
  37. const formSearchTermInpRef = ref();
  38. const ruleFormRef = ref<FormInstance>();
  39. const ruleForm = reactive({
  40. searchTerm: '',
  41. searchTermType: 'positive',
  42. });
  43. const rules = reactive<FormRules<typeof ruleForm>>({
  44. searchTerm: [{ required: true, validator: checkSearchTerm, trigger: 'blur' }],
  45. searchTermType: [{ required: true, validator: checkSearchTermType, trigger: 'blur' }],
  46. });
  47. const searchTermTypeFilter = ref('all');
  48. const searchTermFilter = ref('');
  49. const upload = ref<UploadInstance>();
  50. onMounted(() => {
  51. fetchSearchTermList();
  52. });
  53. /**
  54. * 添加关键词
  55. */
  56. async function addSearchTerm() {
  57. const body = {
  58. searchTerm: ruleForm.searchTerm,
  59. searchTerm_type: ruleForm.searchTermType,
  60. add_date: currentDate, // 使用当前日期代替硬编码日期
  61. };
  62. try {
  63. const response = await api.postCreateSearchTerm(body);
  64. handleResponse(response);
  65. if (response.code === SUCCESS_CODE) await fetchSearchTermList();
  66. } catch (error) {
  67. console.error('==Error==', error);
  68. }
  69. }
  70. /**
  71. * 删除行数据
  72. * @param row 行数据
  73. */
  74. async function handleDelete(row: any) {
  75. try {
  76. const response = await api.deleteSearchTerm(row.id);
  77. handleResponse(response);
  78. if (response.code === SUCCESS_CODE) {
  79. tableData.value = tableData.value.filter((item) => item.id !== row.id);
  80. await fetchSearchTermList();
  81. } else {
  82. ElMessage.error('删除失败');
  83. }
  84. } catch (error) {
  85. console.error('==Error==', error);
  86. }
  87. }
  88. /**
  89. * 获取关键词列表数据
  90. */
  91. async function fetchSearchTermList() {
  92. tableLoading.value = true;
  93. const query = {
  94. page: currentPage.value,
  95. limit: pageSize.value,
  96. searchTerm: searchTermFilter.value,
  97. searchTerm_type: searchTermTypeFilter.value,
  98. };
  99. try {
  100. const response = await api.getSearchTermList(query);
  101. total.value = response.total;
  102. const responseData: DataItem[] = response.data;
  103. tableData.value = responseData.map((item) => ({
  104. ...item,
  105. isEditing: false,
  106. popConfirmVisible: false,
  107. tempSearchTermType: item.searchTerm_type,
  108. }));
  109. } catch (error) {
  110. console.error('==Error==', error);
  111. } finally {
  112. tableLoading.value = false;
  113. }
  114. }
  115. /**
  116. * 更新关键词
  117. * @param row 修改行数据
  118. */
  119. async function updateSearchTerm(row: any) {
  120. row.isEditing = false;
  121. const data = {
  122. searchTerm: row.searchTerm,
  123. searchTerm_type: row.searchTerm_type,
  124. };
  125. try {
  126. const response = await api.postUpdateSearchTerm({ data, id: row.id });
  127. handleResponse(response);
  128. if (response.code === SUCCESS_CODE) {
  129. await fetchSearchTermList();
  130. } else {
  131. ElMessage.error('更新失败');
  132. }
  133. } catch (error) {
  134. console.error('==Error==', error);
  135. }
  136. }
  137. /**
  138. * 更新搜索词类型
  139. * @param row
  140. */
  141. async function updateSearchTermType(row: any) {
  142. const data = {
  143. searchTerm: row.searchTerm,
  144. searchTerm_type: row.searchTerm_type,
  145. };
  146. try {
  147. const response = await api.postUpdateSearchTerm({ data, id: row.id });
  148. handleResponse(response);
  149. if (response.code === SUCCESS_CODE) {
  150. await fetchSearchTermList();
  151. } else {
  152. ElMessage.error('更新失败');
  153. }
  154. } catch (error) {
  155. console.error('==Error==', error);
  156. } finally {
  157. row.popConfirmVisible = false;
  158. }
  159. }
  160. function showPopConfirm(row: any) {
  161. row.popConfirmVisible = true;
  162. }
  163. /**
  164. * 取消修改, 并恢复搜索词类型的值
  165. * @param row 行数据
  166. */
  167. function cancelUpdate(row: any) {
  168. row.popConfirmVisible = false;
  169. row.searchTerm_type = row.tempSearchTermType;
  170. ElMessage.warning({ message: '修改已取消', plain: true });
  171. }
  172. /**
  173. * 切换编辑状态, 并自动获取焦点
  174. * @param row 行数据
  175. */
  176. function handleClick(row: any) {
  177. row.isEditing = !row.isEditing;
  178. nextTick(() => {
  179. searchTermInpRef.value.focus();
  180. });
  181. }
  182. /**
  183. * 切换页码
  184. * @param newPage 新页码
  185. */
  186. async function handleCurrentChange(newPage: number) {
  187. currentPage.value = newPage;
  188. await fetchSearchTermList();
  189. }
  190. /**
  191. * 切换每页条数
  192. * @param newSize 新每页条数
  193. */
  194. async function handleSizeChange(newSize: number) {
  195. currentPage.value = 1;
  196. pageSize.value = newSize;
  197. await fetchSearchTermList();
  198. }
  199. /**
  200. * 关闭对话框时清空表单
  201. * @param done
  202. */
  203. function handleClose(done: Function) {
  204. if (ruleFormRef.value) ruleFormRef.value.resetFields();
  205. done();
  206. }
  207. /**
  208. * 打开对话框, 并自动获取焦点
  209. */
  210. function handleDialogVisible() {
  211. dialogVisible.value = true;
  212. setTimeout(() => {
  213. formSearchTermInpRef.value.focus();
  214. }, 100);
  215. }
  216. function checkSearchTerm(rule: any, value: any, callback: any) {
  217. if (!value) {
  218. return callback(new Error('请输入关键词'));
  219. } else {
  220. callback();
  221. }
  222. }
  223. function checkSearchTermType(rule: any, value: any, callback: any) {
  224. if (!value) {
  225. callback(new Error('请选择关键词类型'));
  226. } else {
  227. callback();
  228. }
  229. }
  230. function submitForm(formEl: FormInstance | undefined) {
  231. if (!formEl) return;
  232. formEl.validate((valid) => {
  233. if (valid) dialogVisible.value = false;
  234. addSearchTerm();
  235. formEl.resetFields();
  236. });
  237. }
  238. function resetForm(formEl: FormInstance | undefined) {
  239. if (!formEl) return;
  240. formEl.resetFields();
  241. dialogVisible.value = false;
  242. }
  243. /**
  244. * 上传文件
  245. * @param uploadRequest 上传请求
  246. */
  247. async function handleCustomUpload(uploadRequest: any) {
  248. try {
  249. const { file } = uploadRequest;
  250. const response = await api.uploadFile(file);
  251. handleResponse(response);
  252. uploadRequest.onSuccess(response); // 通知 el-upload 上传成功
  253. } catch (error) {
  254. console.log('==Error==', error);
  255. uploadRequest.onError(error);
  256. }
  257. }
  258. /**
  259. * @description 替换文件并上传
  260. * @param files 文件列表
  261. */
  262. function handleExceed(files: any) {
  263. upload.value!.clearFiles();
  264. const file = files[0] as UploadRawFile;
  265. file.uid = genFileId();
  266. upload.value!.handleStart(file);
  267. upload.value!.submit();
  268. }
  269. /**
  270. * 统一处理响应
  271. * @param response 后端返回的响应
  272. */
  273. function handleResponse(response: any) {
  274. if (response.code === SUCCESS_CODE) {
  275. ElMessage.success({ message: response.msg, plain: true });
  276. } else if (response.code === WARNING_CODE) {
  277. ElMessage.warning({ message: response.msg, plain: true });
  278. } else {
  279. ElMessage.error({ message: response.msg, plain: true });
  280. }
  281. }
  282. </script>
  283. <template>
  284. <div>
  285. <el-card v-loading="tableLoading" body-style="background-color: #f7f7f7;">
  286. <!-- 筛选 -->
  287. <el-card body-class="flex justify-between gap-3.5" shadow="hover" style="border: none; margin-bottom: 10px">
  288. <div class="flex gap-7">
  289. <div>
  290. <span class="font-bold mr-2" style="color: #303133">词根:</span>
  291. <el-input
  292. v-model="searchTermFilter"
  293. placeholder="请输入词根"
  294. clearable
  295. @change="fetchSearchTermList"
  296. :prefix-icon="Search"
  297. style="width: 240px"></el-input>
  298. </div>
  299. <div>
  300. <span class="font-bold mr-2" style="color: #303133">词根类型:</span>
  301. <el-select v-model="searchTermTypeFilter" style="width: 200px" @change="fetchSearchTermList">
  302. <el-option label="全部" value="all" />
  303. <el-option label="positive" value="positive" />
  304. <el-option label="negative" value="negative" />
  305. <el-option label="无词根类型" value="typeless" />
  306. </el-select>
  307. </div>
  308. </div>
  309. <div class="flex gap-3.5">
  310. <el-button plain type="primary" @click="handleDialogVisible">
  311. <el-icon>
  312. <Plus />
  313. </el-icon>
  314. 添加词根
  315. </el-button>
  316. <!-- 想要不页面不跳动可以加72的高度 -->
  317. <div>
  318. <el-upload
  319. ref="upload"
  320. action="#"
  321. :limit="1"
  322. :auto-upload="true"
  323. :on-exceed="handleExceed"
  324. :http-request="handleCustomUpload">
  325. <template #trigger>
  326. <el-button plain round type="warning" :icon="Upload">批量词根上传</el-button>
  327. </template>
  328. </el-upload>
  329. </div>
  330. </div>
  331. </el-card>
  332. <!-- 表格 -->
  333. <el-card shadow="hover" style="border: none">
  334. <div style="height: 535px; overflow: auto">
  335. <el-table :data="tableData" stripe style="width: 100%">
  336. <el-table-column fixed prop="add_date" label="添加日期" width="180" sortable>
  337. <template #header>
  338. <el-icon style="top: 2px; margin-right: 3px">
  339. <Calendar />
  340. </el-icon>
  341. <span>添加日期</span>
  342. </template>
  343. <template #default="{ row }">
  344. <span class="font-medium">{{ row.add_date }}</span>
  345. </template>
  346. </el-table-column>
  347. <el-table-column prop="searchTerm" label="词根" sortable>
  348. <template #header>
  349. <el-icon style="top: 2px; right: 2px">
  350. <Key />
  351. </el-icon>
  352. <span>词根</span>
  353. </template>
  354. <template #default="{ row }">
  355. <el-input ref="searchTermInpRef" v-if="row.isEditing" v-model="row.searchTerm" @change="updateSearchTerm(row)" />
  356. <span class="font-semibold" v-else>{{ row.searchTerm }}</span>
  357. </template>
  358. </el-table-column>
  359. <el-table-column prop="searchTerm_type" label="词根类型" sortable>
  360. <template #header>
  361. <el-icon style="top: 2px; right: 2px">
  362. <Coin />
  363. </el-icon>
  364. <span>词根类型</span>
  365. </template>
  366. <template #default="{ row }">
  367. <el-popconfirm
  368. title="确定修改吗?"
  369. @confirm="updateSearchTermType(row)"
  370. @cancel="cancelUpdate(row)"
  371. :visible="row.popConfirmVisible">
  372. <template #reference>
  373. <el-select v-model="row.searchTerm_type" @change="showPopConfirm(row)" style="width: 150px">
  374. <el-option label="positive" value="positive" />
  375. <el-option label="negative" value="negative" />
  376. </el-select>
  377. </template>
  378. </el-popconfirm>
  379. </template>
  380. </el-table-column>
  381. <el-table-column fixed="right" label="操作" width="120">
  382. <template #header>
  383. <el-icon style="top: 2px; margin-right: 5px">
  384. <EditPen />
  385. </el-icon>
  386. <span>操作</span>
  387. </template>
  388. <template #default="{ row }">
  389. <el-button link type="primary" size="small" @click="handleClick(row)" v-if="!row.isEditing"> 编辑</el-button>
  390. <el-button link type="primary" size="small" @click="handleClick(row)" v-else> 取消</el-button>
  391. <el-popconfirm title="确定删除吗?" @confirm="handleDelete(row)">
  392. <template #reference>
  393. <el-button link type="danger" size="small">删除</el-button>
  394. </template>
  395. </el-popconfirm>
  396. </template>
  397. </el-table-column>
  398. </el-table>
  399. </div>
  400. <div class="mt-3.5 flex justify-end">
  401. <el-pagination
  402. v-model:current-page="currentPage"
  403. v-model:page-size="pageSize"
  404. :page-sizes="[10, 20, 30, 50]"
  405. layout="sizes, prev, pager, next"
  406. :total="total"
  407. @size-change="handleSizeChange"
  408. @current-change="handleCurrentChange" />
  409. </div>
  410. </el-card>
  411. </el-card>
  412. </div>
  413. <!-- 添加词根弹窗 -->
  414. <el-dialog v-model="dialogVisible" title="添加关键词" width="500" :before-close="handleClose">
  415. <el-form ref="ruleFormRef" style="max-width: 600px" :model="ruleForm" status-icon :rules="rules" label-width="auto">
  416. <el-form-item label="关键词" prop="searchTerm">
  417. <el-input ref="formSearchTermInpRef" v-model="ruleForm.searchTerm" />
  418. </el-form-item>
  419. <el-form-item label="关键词类型" prop="searchTermType">
  420. <el-select v-model="ruleForm.searchTermType">
  421. <el-option label="positive" value="positive" />
  422. <el-option label="negative" value="negative" />
  423. </el-select>
  424. </el-form-item>
  425. </el-form>
  426. <template #footer>
  427. <div class="dialog-footer">
  428. <el-button @click="resetForm(ruleFormRef)">取消</el-button>
  429. <el-button type="primary" @click="submitForm(ruleFormRef)"> 确定</el-button>
  430. </div>
  431. </template>
  432. </el-dialog>
  433. </template>
  434. <style scoped></style>