|  | @@ -0,0 +1,328 @@
 | 
	
		
			
				|  |  | +<script setup lang="ts">
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * @Name: keyword-manage-table.vue
 | 
	
		
			
				|  |  | + * @Description: 关键词管理表格
 | 
	
		
			
				|  |  | + * @Author: Cheney
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import { nextTick, onMounted, reactive, ref } from 'vue';
 | 
	
		
			
				|  |  | +import { Plus } from '@element-plus/icons-vue';
 | 
	
		
			
				|  |  | +import * as api from '../api';
 | 
	
		
			
				|  |  | +import { ElMessage, FormInstance, FormRules } from 'element-plus';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +interface DataItem {
 | 
	
		
			
				|  |  | +  id: number;
 | 
	
		
			
				|  |  | +  modifier_name: string;
 | 
	
		
			
				|  |  | +  creator_name: string;
 | 
	
		
			
				|  |  | +  create_datetime: string;
 | 
	
		
			
				|  |  | +  update_datetime: string;
 | 
	
		
			
				|  |  | +  description: string | null;
 | 
	
		
			
				|  |  | +  modifier: string;
 | 
	
		
			
				|  |  | +  searchTerm: string;
 | 
	
		
			
				|  |  | +  searchTerm_type: string;
 | 
	
		
			
				|  |  | +  add_date: string;
 | 
	
		
			
				|  |  | +  creator: number;
 | 
	
		
			
				|  |  | +  isEditing: boolean; // 添加isEditing字段
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const SUCCESS_CODE = 2000;
 | 
	
		
			
				|  |  | +const currentDate = new Date().toISOString().split('T')[0];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const tableLoading = ref(false);
 | 
	
		
			
				|  |  | +const tableData = ref<DataItem[]>([]);
 | 
	
		
			
				|  |  | +const total = ref(0);
 | 
	
		
			
				|  |  | +const currentPage = ref(1);
 | 
	
		
			
				|  |  | +const pageSize = ref(10);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// const searchTermTypeSelect = ref('positive');
 | 
	
		
			
				|  |  | +const searchTermInpRef = ref();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const dialogVisible = ref(false);
 | 
	
		
			
				|  |  | +const formSearchTermInpRef = ref();
 | 
	
		
			
				|  |  | +const ruleFormRef = ref<FormInstance>();
 | 
	
		
			
				|  |  | +const ruleForm = reactive({
 | 
	
		
			
				|  |  | +  searchTerm: '',
 | 
	
		
			
				|  |  | +  searchTermType: 'positive',
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +const rules = reactive<FormRules<typeof ruleForm>>({
 | 
	
		
			
				|  |  | +  searchTerm: [{ required: true, validator: checkSearchTerm, trigger: 'blur' }],
 | 
	
		
			
				|  |  | +  searchTermType: [{ required: true, validator: checkSearchTermType, trigger: 'blur' }],
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +onMounted(() => {
 | 
	
		
			
				|  |  | +  fetchSearchTermList();
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 添加关键词
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +async function addSearchTerm() {
 | 
	
		
			
				|  |  | +  const body = {
 | 
	
		
			
				|  |  | +    searchTerm: ruleForm.searchTerm,
 | 
	
		
			
				|  |  | +    searchTerm_type: ruleForm.searchTermType,
 | 
	
		
			
				|  |  | +    add_date: currentDate, // 使用当前日期代替硬编码日期
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const response = await api.postCreateSearchTerm(body);
 | 
	
		
			
				|  |  | +    handleResponse(response);
 | 
	
		
			
				|  |  | +    if (response.code === SUCCESS_CODE) {
 | 
	
		
			
				|  |  | +      await fetchSearchTermList();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      ElMessage.error('添加失败');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.error('error:', error);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 删除行数据
 | 
	
		
			
				|  |  | + * @param row 行数据
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +async function handleDelete(row: any) {
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const response = await api.deleteSearchTerm(row.id);
 | 
	
		
			
				|  |  | +    handleResponse(response);
 | 
	
		
			
				|  |  | +    if (response.code === SUCCESS_CODE) {
 | 
	
		
			
				|  |  | +      tableData.value = tableData.value.filter((item) => item.id !== row.id);
 | 
	
		
			
				|  |  | +      await fetchSearchTermList();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      ElMessage.error('删除失败');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.log('error:', error);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 获取关键词列表数据
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +async function fetchSearchTermList() {
 | 
	
		
			
				|  |  | +  tableLoading.value = true;
 | 
	
		
			
				|  |  | +  const query = {
 | 
	
		
			
				|  |  | +    page: currentPage.value,
 | 
	
		
			
				|  |  | +    limit: pageSize.value,
 | 
	
		
			
				|  |  | +    searchTerm: '', // TODO: 暂时写死
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const response = await api.getSearchTermList(query);
 | 
	
		
			
				|  |  | +    total.value = response.total;
 | 
	
		
			
				|  |  | +    const responseData: DataItem[] = response.data;
 | 
	
		
			
				|  |  | +    tableData.value = responseData.map((item) => ({ ...item, isEditing: false }));
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.log('error:', error);
 | 
	
		
			
				|  |  | +  } finally {
 | 
	
		
			
				|  |  | +    tableLoading.value = false;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 更新关键词
 | 
	
		
			
				|  |  | + * @param row 修改行数据
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +async function updateSearchTerm(row: any) {
 | 
	
		
			
				|  |  | +  row.isEditing = false;
 | 
	
		
			
				|  |  | +  const data = {
 | 
	
		
			
				|  |  | +    searchTerm: row.searchTerm,
 | 
	
		
			
				|  |  | +    searchTerm_type: row.searchTerm_type,
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const response = await api.postUpdateSearchTerm({ data, id: row.id });
 | 
	
		
			
				|  |  | +    handleResponse(response);
 | 
	
		
			
				|  |  | +    if (response.code === SUCCESS_CODE) {
 | 
	
		
			
				|  |  | +      await fetchSearchTermList();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      ElMessage.error('更新失败');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.log('error:', error);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async function updateSearchTermType(row: any, newValue: string) {
 | 
	
		
			
				|  |  | +  const data = {
 | 
	
		
			
				|  |  | +    searchTerm: row.searchTerm,
 | 
	
		
			
				|  |  | +    searchTerm_type: newValue,
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +  try {
 | 
	
		
			
				|  |  | +    const response = await api.postUpdateSearchTerm({ data, id: row.id });
 | 
	
		
			
				|  |  | +    handleResponse(response);
 | 
	
		
			
				|  |  | +    if (response.code === SUCCESS_CODE) {
 | 
	
		
			
				|  |  | +      await fetchSearchTermList();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      ElMessage.error('更新失败');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  } catch (error) {
 | 
	
		
			
				|  |  | +    console.log('error:', error);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 切换编辑状态, 并自动获取焦点
 | 
	
		
			
				|  |  | + * @param row 行数据
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +function handleClick(row: any) {
 | 
	
		
			
				|  |  | +  row.isEditing = !row.isEditing;
 | 
	
		
			
				|  |  | +  nextTick(() => {
 | 
	
		
			
				|  |  | +    searchTermInpRef.value.focus();
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 切换页码
 | 
	
		
			
				|  |  | + * @param newPage 新页码
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +async function handleCurrentChange(newPage: number) {
 | 
	
		
			
				|  |  | +  currentPage.value = newPage;
 | 
	
		
			
				|  |  | +  await fetchSearchTermList();
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 切换每页条数
 | 
	
		
			
				|  |  | + * @param newSize 新每页条数
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +async function handleSizeChange(newSize: number) {
 | 
	
		
			
				|  |  | +  currentPage.value = 1;
 | 
	
		
			
				|  |  | +  pageSize.value = newSize;
 | 
	
		
			
				|  |  | +  await fetchSearchTermList();
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 关闭对话框时清空表单
 | 
	
		
			
				|  |  | + * @param done
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +function handleClose(done: Function) {
 | 
	
		
			
				|  |  | +  if (ruleFormRef.value) ruleFormRef.value.resetFields();
 | 
	
		
			
				|  |  | +  done();
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 打开对话框, 并自动获取焦点
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +function handleDialogVisible() {
 | 
	
		
			
				|  |  | +  dialogVisible.value = true;
 | 
	
		
			
				|  |  | +  setTimeout(() => {
 | 
	
		
			
				|  |  | +    formSearchTermInpRef.value.focus();
 | 
	
		
			
				|  |  | +  }, 100);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +function checkSearchTerm(rule: any, value: any, callback: any) {
 | 
	
		
			
				|  |  | +  if (!value) {
 | 
	
		
			
				|  |  | +    return callback(new Error('请输入关键词'));
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    callback();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +function checkSearchTermType(rule: any, value: any, callback: any) {
 | 
	
		
			
				|  |  | +  if (!value) {
 | 
	
		
			
				|  |  | +    callback(new Error('请选择关键词类型'));
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    callback();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +function submitForm(formEl: FormInstance | undefined) {
 | 
	
		
			
				|  |  | +  if (!formEl) return;
 | 
	
		
			
				|  |  | +  formEl.validate((valid) => {
 | 
	
		
			
				|  |  | +    if (valid) {
 | 
	
		
			
				|  |  | +      dialogVisible.value = false;
 | 
	
		
			
				|  |  | +      addSearchTerm();
 | 
	
		
			
				|  |  | +      formEl.resetFields();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      console.log('error submit!');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +function resetForm(formEl: FormInstance | undefined) {
 | 
	
		
			
				|  |  | +  if (!formEl) return;
 | 
	
		
			
				|  |  | +  formEl.resetFields();
 | 
	
		
			
				|  |  | +  dialogVisible.value = false;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 统一处理响应
 | 
	
		
			
				|  |  | + * @param response 后端返回的响应
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +function handleResponse(response: any) {
 | 
	
		
			
				|  |  | +  if (response.code === SUCCESS_CODE) {
 | 
	
		
			
				|  |  | +    ElMessage.success(response.msg);
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    ElMessage.error(response.msg || '请联系管理员');
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +</script>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<template>
 | 
	
		
			
				|  |  | +  <div>
 | 
	
		
			
				|  |  | +    <el-card v-loading="tableLoading">
 | 
	
		
			
				|  |  | +      <el-button plain type="primary" @click="handleDialogVisible" class="mb-2">
 | 
	
		
			
				|  |  | +        <el-icon>
 | 
	
		
			
				|  |  | +          <Plus />
 | 
	
		
			
				|  |  | +        </el-icon>
 | 
	
		
			
				|  |  | +        添加关键词
 | 
	
		
			
				|  |  | +      </el-button>
 | 
	
		
			
				|  |  | +      <el-table :data="tableData" stripe height="530" style="width: 100%">
 | 
	
		
			
				|  |  | +        <el-table-column fixed="left" prop="add_date" label="添加日期" width="180" />
 | 
	
		
			
				|  |  | +        <el-table-column prop="searchTerm" label="关键词">
 | 
	
		
			
				|  |  | +          <template #default="{ row }">
 | 
	
		
			
				|  |  | +            <el-input ref="searchTermInpRef" v-if="row.isEditing" v-model="row.searchTerm" @change="updateSearchTerm(row)" />
 | 
	
		
			
				|  |  | +            <span class="font-bold" v-else>{{ row.searchTerm }}</span>
 | 
	
		
			
				|  |  | +          </template>
 | 
	
		
			
				|  |  | +        </el-table-column>
 | 
	
		
			
				|  |  | +        <el-table-column prop="searchTerm_type" label="关键词类型">
 | 
	
		
			
				|  |  | +          <template #default="{ row }">
 | 
	
		
			
				|  |  | +            <el-select v-model="row.searchTerm_type" @change="(value) => updateSearchTermType(row, value)">
 | 
	
		
			
				|  |  | +              <el-option label="positive" value="positive" />
 | 
	
		
			
				|  |  | +              <el-option label="negative" value="negative" />
 | 
	
		
			
				|  |  | +            </el-select>
 | 
	
		
			
				|  |  | +          </template>
 | 
	
		
			
				|  |  | +        </el-table-column>
 | 
	
		
			
				|  |  | +        <el-table-column fixed="right" label="操作" width="120">
 | 
	
		
			
				|  |  | +          <template #default="{ row }">
 | 
	
		
			
				|  |  | +            <el-button link type="primary" size="small" @click="handleClick(row)" v-if="!row.isEditing"> 编辑</el-button>
 | 
	
		
			
				|  |  | +            <el-button link type="primary" size="small" @click="handleClick(row)" v-else> 取消</el-button>
 | 
	
		
			
				|  |  | +            <el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
 | 
	
		
			
				|  |  | +          </template>
 | 
	
		
			
				|  |  | +        </el-table-column>
 | 
	
		
			
				|  |  | +      </el-table>
 | 
	
		
			
				|  |  | +      <div class="mt-3.5 flex justify-end">
 | 
	
		
			
				|  |  | +        <el-pagination
 | 
	
		
			
				|  |  | +          v-model:current-page="currentPage"
 | 
	
		
			
				|  |  | +          v-model:page-size="pageSize"
 | 
	
		
			
				|  |  | +          :page-sizes="[10, 20, 30, 50]"
 | 
	
		
			
				|  |  | +          layout="sizes, prev, pager, next"
 | 
	
		
			
				|  |  | +          :total="total"
 | 
	
		
			
				|  |  | +          @size-change="handleSizeChange"
 | 
	
		
			
				|  |  | +          @current-change="handleCurrentChange" />
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  | +    </el-card>
 | 
	
		
			
				|  |  | +  </div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  <el-dialog v-model="dialogVisible" title="添加关键词" width="500" :before-close="handleClose">
 | 
	
		
			
				|  |  | +    <el-form ref="ruleFormRef" style="max-width: 600px" :model="ruleForm" status-icon :rules="rules" label-width="auto">
 | 
	
		
			
				|  |  | +      <el-form-item label="关键词" prop="searchTerm">
 | 
	
		
			
				|  |  | +        <el-input ref="formSearchTermInpRef" v-model="ruleForm.searchTerm" />
 | 
	
		
			
				|  |  | +      </el-form-item>
 | 
	
		
			
				|  |  | +      <el-form-item label="关键词类型" prop="searchTermType">
 | 
	
		
			
				|  |  | +        <el-select v-model="ruleForm.searchTermType">
 | 
	
		
			
				|  |  | +          <el-option label="positive" value="positive" />
 | 
	
		
			
				|  |  | +          <el-option label="negative" value="negative" />
 | 
	
		
			
				|  |  | +        </el-select>
 | 
	
		
			
				|  |  | +      </el-form-item>
 | 
	
		
			
				|  |  | +      <!--<el-form-item>-->
 | 
	
		
			
				|  |  | +      <!--  <el-button type="primary" @click="submitForm(ruleFormRef)"> Submit</el-button>-->
 | 
	
		
			
				|  |  | +      <!--  <el-button @click="resetForm(ruleFormRef)">Reset</el-button>-->
 | 
	
		
			
				|  |  | +      <!--</el-form-item>-->
 | 
	
		
			
				|  |  | +    </el-form>
 | 
	
		
			
				|  |  | +    <template #footer>
 | 
	
		
			
				|  |  | +      <div class="dialog-footer">
 | 
	
		
			
				|  |  | +        <el-button @click="resetForm(ruleFormRef)">取消</el-button>
 | 
	
		
			
				|  |  | +        <el-button type="primary" @click="submitForm(ruleFormRef)"> 确定</el-button>
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  | +    </template>
 | 
	
		
			
				|  |  | +  </el-dialog>
 | 
	
		
			
				|  |  | +</template>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<style scoped></style>
 |