Kaynağa Gözat

✨ feat<谷歌关键词>:谷歌关键词页面添加

xinyan 7 ay önce
ebeveyn
işleme
2deaf744bb

+ 2 - 2
.env.development

@@ -4,8 +4,8 @@ ENV='development'
 
 # 本地环境接口地址
 # VITE_API_URL = 'http://127.0.0.1:8000'
-VITE_API_URL='http://192.168.1.225/'
-#VITE_API_URL='http://192.168.1.25:8080/'
+# VITE_API_URL='http://192.168.1.225/'
+VITE_API_URL='http://192.168.1.25:8080/'
 # VITE_API_URL = 'http://amzads.zositechc.cn'
 
 # 是否启用按钮权限

+ 46 - 0
src/views/googleTrends/api.ts

@@ -0,0 +1,46 @@
+import { request } from '/@/utils/service';
+
+
+const apiPrefix = '/api/google_trends/';
+
+export function getKeyword(query: any) {
+  return request({
+    url: apiPrefix + 'wordmanage/',
+    method: 'GET',
+    params: query,
+  })
+}
+
+export function postCreateKeyword(query: any) {
+  return request({
+    url: apiPrefix + 'wordmanage/',
+    method: 'PUT',
+    params: query,
+  });
+}
+
+export function deleteKeyword(query: any) {
+  return request({
+    url: apiPrefix + 'wordmanage/',
+    method: 'DELETE',
+    params: query,
+  })
+}
+
+export function UpdateKeyword(query: any) {
+  return request({
+    url: apiPrefix + 'wordmanage/',
+    method: 'POST',
+    params: query,
+  });
+}
+
+export function getTrends(query: any) {
+  return request({
+    url: apiPrefix + 'chart/',
+    method: 'GET',
+    params: query,
+  })
+}
+
+

+ 281 - 0
src/views/googleTrends/components/createKeyword.vue

@@ -0,0 +1,281 @@
+<script lang="ts" setup>/**
+ * @Name: createKeyword.vue
+ * @Description: 谷歌关键词添加
+ * @Author: xinyan
+ */
+import { deleteKeyword, getKeyword, postCreateKeyword, UpdateKeyword } from '/@/views/googleTrends/api';
+import { Delete, Plus } from '@element-plus/icons-vue';
+import { ref } from 'vue';
+import { VxeGridInstance } from 'vxe-table';
+
+const emit = defineEmits([ 'updateKeyword' ]);
+
+const keywordInput = ref('');
+const btnLoading = ref(false);
+
+const isCancel = ref(false); // 是否取消编辑
+const isEditing = ref(false); // 是否正在编辑
+const keywordList = ref([]); // 关键词列表
+const xGrid = ref<VxeGridInstance>();
+const gridOptions = reactive({
+  border: 'inner',
+  keepSource: true,
+  loading: false,
+  height: 555,
+  columnConfig: {
+    resizable: true
+  },
+  rowConfig: {
+    isHover: true,
+    height: 38,
+  },
+  editConfig: {
+    trigger: 'manual',
+    showIcon: false,
+    // autoFocus: true,
+    autoClear: false,
+  },
+  pagerConfig: {
+    enabled: true,
+    total: 20,
+    currentPage: 1,
+    pageSize: 20,
+    pageSizes: [10, 20, 30],
+  },
+  columns: [
+    { field: 'keyword', title: '关键词', editRender: { name: 'input' }, slots: { edit: 'keyword_edit' } },
+    { field: 'insert_time', title: '添加时间' },
+    { title: '操作', width: 120, slots: { default: 'operate' }, align: 'center' },
+  ],
+  data: []
+});
+
+const gridEvents = {
+  pageChange({ currentPage, pageSize }) {
+    if (gridOptions.pagerConfig) {
+      gridOptions.pagerConfig.currentPage = currentPage;
+      gridOptions.pagerConfig.pageSize = pageSize;
+    }
+    getList();
+  }
+};
+
+const hasActiveEditRow = (row) => {
+  const $grid = xGrid.value;
+  if ($grid) {
+    return $grid.isEditByRow(row);
+  }
+  return false;
+};
+
+function editRowEvent(row) {
+  isCancel.value = false;
+  isEditing.value = true;
+  const $grid = xGrid.value;
+  if ($grid) {
+    $grid.setEditRow(row);
+    row.original_keyword = row.keyword;
+  }
+}
+
+function handelEditClosed({ row }) {
+  isEditing.value = false;
+  if (isCancel.value)return;
+  if (row.original_keyword !== row.keyword){
+    updateKeyword(row);
+  }
+}
+
+async function getList() {
+  gridOptions.loading = true;
+  try {
+    const resp = await getKeyword({
+      page: gridOptions.pagerConfig.currentPage,
+      limit: gridOptions.pagerConfig.pageSize,
+    });
+    gridOptions.data = resp.data;
+    keywordList.value = resp.data.map(item => item.keyword);
+    emit('updateKeyword',keywordList.value);
+    gridOptions.pagerConfig.total = resp.total;
+    gridOptions.loading = false;
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+async function handleAdd() {
+  const keywordsInput = keywordInput.value.trim();
+  if (!keywordsInput) {
+    return;
+  }
+  // 按行拆分关键词,并过滤掉空行
+  const keywords = keywordsInput.split('\n').map(keyword => keyword.trim()).filter(keyword => keyword.length > 0);
+
+  try {
+    btnLoading.value = true;
+    const resp = await postCreateKeyword({ keyword: keywords.join(',') });
+    if (resp.code === 2000) {
+      ElMessage.success('关键词添加成功');
+      keywordInput.value = ''; // 清空输入框
+      await getList();
+    }
+  } catch (error) {
+    ElMessage.error('添加关键词失败,请重试!'); // 提示失败消息
+  } finally {
+    btnLoading.value = false;
+  }
+}
+
+async function updateKeyword(row) {
+  try {
+    gridOptions.loading = true;
+    const resp = await UpdateKeyword({
+      id: row.id,
+      new_keyword: row.keyword
+    });
+    if (resp.code === 2000) {
+      await getList();
+      ElMessage.success('关键词更新成功');
+    }
+  } catch (error) {
+    ElMessage.error('更新关键词失败,请重试!'); // 提示失败消息
+  } finally {
+    gridOptions.loading = false;
+  }
+}
+
+function cancel(row) {
+  isCancel.value = true;
+  const $grid = xGrid.value;
+  if ($grid) {
+    $grid.clearEdit().then(() => {
+      $grid.revertData(row)
+    })
+  }
+}
+
+async function handleDelete(row) {
+  try {
+    gridOptions.loading = true;
+    const resp = await deleteKeyword({ keyword: row.keyword });
+    if (resp.code === 2000) {
+      ElMessage.success('关键词删除成功');
+      await getList();
+    }
+  } catch (error) {
+    ElMessage.error('删除关键词失败,请重试!'); // 提示失败消息
+  } finally {
+    gridOptions.loading = false;
+  }
+}
+
+function handleClear() {
+  keywordInput.value = '';
+}
+
+const cellStyle = () => {
+  return {
+    fontSize: '13px',
+    fontWeight: '500',
+  };
+};
+
+const headerCellStyle = () => {
+  return {
+    fontSize: '13px',
+    backgroundColor: '#f0f1f3',
+    height: 10,
+  };
+};
+
+onMounted(() => {
+  getList();
+});
+
+</script>
+
+<template>
+  <el-card class="mx-2 my-5">
+    <div class="my-4 mx-1.5" style="font-size: 18px;font-weight: bold;color: #464646">谷歌关键词添加</div>
+    <div class="mx-2">
+      <el-row :gutter="20">
+        <el-col :span="8" class="input-section">
+          <div class="input-container">
+            <el-input
+                v-model="keywordInput"
+                :rows="26"
+                class="textarea"
+                placeholder="添加谷歌趋势关键词,一行一个关键词......(关键词添加上限200个)"
+                type="textarea"
+            />
+            <!-- 按钮组 -->
+            <div class="button-group">
+              <el-popconfirm
+                :icon="InfoFilled"
+                icon-color="#626AEF"
+                title="确认清空关键词?"
+                width="220"
+                @confirm="handleClear"
+            >
+              <template #reference>
+                <el-button :icon="Delete" bg size="small" text type="danger">清空</el-button>
+              </template>
+            </el-popconfirm>
+              <el-button :icon="Plus" :loading="btnLoading" bg size="small" text type="primary" @click="handleAdd">添加
+              </el-button>
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="16">
+          <el-card body-style="padding: 0" shadow="never">
+            <vxe-grid ref="xGrid" :cell-style="cellStyle" :header-cell-style="headerCellStyle" show-overflow
+                      v-bind="gridOptions" v-on="gridEvents" @edit-closed="handelEditClosed">
+              <template #operate="{ row }">
+                <template v-if="hasActiveEditRow(row)">
+                  <el-button link size="small" @click="cancel(row)">取消</el-button>
+                  <el-button link size="small" type="warning" @click="updateKeyword(row)">保存</el-button>
+                </template>
+                <template v-else>
+                  <el-button link size="small" type="primary" @click="editRowEvent(row)">编辑</el-button>
+                  <el-popconfirm
+                      :icon="InfoFilled"
+                      icon-color="#626AEF"
+                      title="确认删除关键词?"
+                      width="220"
+                      @confirm="handleDelete(row)"
+                  >
+                    <template #reference>
+                      <el-button :disabled="isEditing" link size="small" type="danger">删除</el-button>
+                    </template>
+                  </el-popconfirm>
+                </template>
+              </template>
+              <template #keyword_edit="{ row }">
+                <el-input v-model="row.keyword"/>
+              </template>
+            </vxe-grid>
+          </el-card>
+        </el-col>
+      </el-row>
+    </div>
+  </el-card>
+</template>
+
+<style scoped>
+.input-container {
+  position: relative;
+}
+
+.textarea {
+  width: 100%;
+  padding-bottom: 40px; /* 为按钮腾出空间 */
+}
+
+.button-group {
+  position: absolute;
+  bottom: 50px;
+  right: 10px;
+  /* display: flex; */
+  /* gap: 5px; */
+}
+</style>

+ 248 - 0
src/views/googleTrends/components/keywordTrendsChart.vue

@@ -0,0 +1,248 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts';
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+import { getTrends } from '/@/views/googleTrends/api';
+import DateRangePicker from '/@/components/DateRangePicker/index.vue';
+import dayjs from 'dayjs';
+
+
+const dateRange = ref([
+  dayjs().subtract(2, 'week').startOf('day').format('YYYY-MM-DD'),
+  dayjs().subtract(1, 'week').endOf('day').format('YYYY-MM-DD')
+]);
+
+const { keywordList } = defineProps<{ keywordList: any; }>();
+const defaultKeywords = ref<string[]>(keywordList.slice(0, 5));
+const colors = ['#0098ed', '#229b32', '#35e6ee', '#ff4a4c', '#f8cb4b'];
+
+let chartObj: any;
+const chartRef = ref(null);
+const loading = ref(false);
+const dataSet = ref([]);
+
+// 初始化 ECharts 图表
+onMounted(() => {
+  addResize();
+  initLine();
+});
+
+// 组件卸载前清理
+onBeforeUnmount(() => {
+  if (chartObj) {
+    chartObj.dispose();
+    chartObj = null;
+  }
+  removeResize();
+});
+
+// ECharts 配置项
+const option: any = {
+  dataset: {
+    source: [],
+  },
+  tooltip: {
+    trigger: 'axis'
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  // toolbox: {
+  //   feature: {
+  //     saveAsImage: {}
+  //   }
+  // },
+  xAxis: {
+    type: 'category',
+  },
+  yAxis: {
+    type: 'value',
+    // name: '搜索量',
+    show: true,
+    splitLine: {
+      show: true, // 设置显示分割线
+    },
+    axisLine: {
+      show: true,
+      lineStyle: {
+        color: '#0090e4'
+      }
+    },
+
+  },
+  series: [
+    {
+      id: 0,
+      name: '1',
+      type: 'line',
+      stack: 'Total',
+      data: []
+    },
+    {
+      id: 1,
+      name: '2',
+      type: 'line',
+      stack: 'Total',
+      data: []
+    },
+    {
+      id: 2,
+      name: '',
+      type: 'line',
+      stack: 'Total',
+      data: []
+    },
+    {
+      id: 3,
+      name: '',
+      type: 'line',
+      stack: 'Total',
+      data: []
+    },
+    {
+      id: 4,
+      name: '',
+      type: 'line',
+      stack: 'Total',
+      data: []
+    }
+  ]
+};
+
+// 初始化 ECharts 图表的函数
+async function initLine() {
+  await loadData();
+  chartObj = echarts.init(chartRef.value);
+  option.dataset.source = dataSet.value;
+  // option.series = defaultKeywords.value.map((keyword, index) => ({
+  //   id: index,
+  //   name: keyword,
+  //   type: 'line',
+  //   stack: 'Total',
+  //   itemStyle: {
+  //     color: colors[index]
+  //   }
+  // }));
+  chartObj.setOption(option, true);
+}
+
+// 加载数据
+async function loadData() {
+  try {
+    loading.value = true;
+    const query = {
+      keyword: defaultKeywords.value.join(','),
+      date_start: dateRange.value[0],
+      date_end: dateRange.value[1],
+    }
+    const resp = await getTrends(query);
+    const trendData = resp.data;
+    // dataSet.value = values; // 这里是 y 轴的数据
+    option.xAxis.data = trendData.date; // 这里是 x 轴的数据
+    option.series = defaultKeywords.value.map((keyword, index) => ({
+      id: index,
+      name: keyword,
+      type: 'line',
+      data: trendData[keyword],
+      itemStyle: {
+        color: colors[index]
+      }
+    }));
+    console.log("=>(keywordTrendsChart.vue:150) option.series", option.series);
+  }catch (e) {
+    console.log("=>(keywordTrendsChart.vue:153) e", e);
+    ElMessage.error('加载数据失败,请稍后再试');
+  }
+  finally {
+    loading.value = false;
+  }
+}
+
+
+function handleSelectChange(index: number) {
+  console.log("=>(keywordTrendsChart.vue:175) index", index);
+  // defaultKeywords.value[index] = keywordList[index];
+  console.log("=>(keywordTrendsChart.vue:177) defaultKeywords.value[index]", defaultKeywords.value);
+}
+
+// 处理窗口大小变化
+function resizeChart() {
+  chartObj.resize();
+}
+
+// 添加窗口大小变化事件监听
+function addResize() {
+  window.addEventListener('resize', resizeChart);
+}
+
+// 移除窗口大小变化事件监听
+function removeResize() {
+  window.removeEventListener('resize', resizeChart);
+}
+
+// 动态计算 el-select 的宽度
+function calculateSelectWidth(text) {
+  const baseWidth = 50; // 基础宽度
+  const padding = 15; // 额外的填充
+  const charWidth = 10; // 平均字符宽度,可根据需要调整
+  return baseWidth + (text.length * charWidth) + padding; // 计算宽度
+}
+</script>
+
+<template>
+  <div v-loading="loading">
+    <div class="flex justify-between items-center mb-4">
+      <span class="my-4 mx-1.5" style="font-size: 18px;font-weight: bold;color: #464646">谷歌关键词趋势</span>
+      <DateRangePicker v-model="dateRange" @change="initLine()"></DateRangePicker>
+    </div>
+    <div class="keyword-select">
+      <el-select
+          v-for="(keyword, index) in defaultKeywords"
+          :key="index"
+          v-model="defaultKeywords[index]"
+          @change="initLine()"
+          :style="{'width': calculateSelectWidth(keyword) + 'px', 'border': 'none', 'box-shadow': 'none'}"
+      >
+        <el-option
+            v-for="item in keywordList"
+            :key="item"
+            :disabled="defaultKeywords.includes(item)"
+            :label="item"
+            :value="item"
+        />
+        <template #label="{ label, value }">
+          <el-tag :color="colors[index]" size="small"></el-tag>
+          <span style="font-weight: bold;font-size:13px">{{ value }}</span>
+        </template>
+      </el-select>
+    </div>
+    <!-- 图表区域 -->
+    <div ref="chartRef" style="height: 350px"></div>
+  </div>
+</template>
+
+<style scoped>
+.keyword-select {
+  display: flex;
+  /* align-items: center; */
+  justify-content: space-between;
+  padding: 20px;
+  margin: 0 30px
+}
+
+:deep .el-select__wrapper {
+  box-shadow: none;
+}
+
+:deep .is-hovering {
+  box-shadow: none !important;
+}
+
+.el-tag {
+  border: none;
+  aspect-ratio: 1;
+  margin-right: 10px;
+}
+</style>

+ 33 - 0
src/views/googleTrends/index.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description:
+ * @Author: xinyan
+ */
+
+import CreateKeyword from '/@/views/googleTrends/components/createKeyword.vue';
+import KeywordTrendsChart from '/@/views/googleTrends/components/keywordTrendsChart.vue';
+
+const loading = ref(true);
+const keywordList = ref([]);
+
+function updateKeywordList(newKeywords) {
+  if (newKeywords.length > 0) {
+    keywordList.value = newKeywords; // 更新关键词列表
+    loading.value = false; // 有值时显示图表
+  }
+}
+</script>
+
+<template>
+  <el-card class="mt-5 mx-2" v-loading="loading" style="height: 600px;">
+    <KeywordTrendsChart v-if="keywordList.length > 0" :keywordList="keywordList"></KeywordTrendsChart>
+  </el-card>
+  <!--<el-card class="mt-5 mx-2">-->
+    <CreateKeyword @updateKeyword="updateKeywordList"></CreateKeyword>
+  <!--</el-card>-->
+</template>
+
+<style scoped>
+
+</style>