index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <script lang="ts" setup>
  2. /**
  3. * @Name: index.vue
  4. * @Description: 搜索词-TopSearchTerm Table
  5. * @Author: Cheney
  6. */
  7. import { nextTick, onBeforeMount, ref, watch } from 'vue';
  8. import { usePagination } from '/@/utils/usePagination';
  9. import { getTopSearchTermTable, postDownload } from './api';
  10. import { marketplaceIdEnum } from '/@/utils/marketplaceIdEnum';
  11. import { Download, Goods, Key, Medal, Pointer, Rank, Refresh, Search, Switch, TopRight } from '@element-plus/icons-vue';
  12. import { useRouter } from 'vue-router';
  13. import { ElMessage } from 'element-plus';
  14. import dayjs from 'dayjs';
  15. import enLocale from 'element-plus/es/locale/lang/en';
  16. const router = useRouter();
  17. const { tableData, total, currentPage, pageSize, handlePageChange } = usePagination(fetchTableData);
  18. const marketplaceSelect = ref(marketplaceIdEnum[0].value); // 当前只有美国区 默认第一个为美国
  19. const marketplaceOptions = marketplaceIdEnum;
  20. const reportTypeSelect = ref('weekly');
  21. const searchTermInp = ref('');
  22. const asinInp = ref('');
  23. const tableLoading = ref(false);
  24. const downloadLoading = ref(false);
  25. const date = ref(calculateLastWeek()); // 设置默认日期为上周的周日到周六
  26. const dateDimension = ref(date.value[0]);
  27. onBeforeMount(() => {
  28. pageSize.value = 21; // 将usePagination中的pageSize默认修改每页显示21条
  29. fetchTableData();
  30. });
  31. watch(dateDimension, () => {
  32. calculateDate();
  33. // console.log('==Date==', date.value[0], date.value[1]);
  34. fetchTableData();
  35. });
  36. /**
  37. * 判断当前时间维度 并 计算起始和结束日期
  38. */
  39. function calculateDate() {
  40. if (reportTypeSelect.value === 'weekly') {
  41. date.value[0] = dateDimension.value;
  42. date.value[1] = calculateEndDate(dateDimension.value);
  43. } else if (reportTypeSelect.value === 'monthly') {
  44. const selectedMonth = dayjs(dateDimension.value);
  45. date.value[0] = selectedMonth.startOf('month').format('YYYY-MM-DD');
  46. date.value[1] = selectedMonth.endOf('month').format('YYYY-MM-DD');
  47. }
  48. }
  49. /**
  50. * 计算上周的周日到周六的日期范围
  51. */
  52. function calculateLastWeek() {
  53. const today = dayjs();
  54. const lastSaturday = today.subtract(today.day() + 1, 'day'); // 上周六
  55. const lastSunday = lastSaturday.subtract(6, 'day'); // 上周日
  56. return [ lastSunday.format('YYYY-MM-DD'), lastSaturday.format('YYYY-MM-DD') ];
  57. }
  58. /**
  59. * 计算结束日期
  60. * @param startDate el-date-picker组件的value
  61. */
  62. function calculateEndDate(startDate: string) {
  63. return dayjs(startDate).add(6, 'day').format('YYYY-MM-DD');
  64. }
  65. async function refreshTable() {
  66. currentPage.value = 1;
  67. pageSize.value = 21;
  68. asinInp.value = '';
  69. searchTermInp.value = '';
  70. reportTypeSelect.value = 'weekly';
  71. marketplaceSelect.value = marketplaceIdEnum[0].value;
  72. await fetchTableData();
  73. }
  74. async function fetchTableData() {
  75. tableLoading.value = true;
  76. const query = {
  77. page: currentPage.value,
  78. limit: pageSize.value,
  79. asin: asinInp.value,
  80. search_term: searchTermInp.value,
  81. report_type: reportTypeSelect.value,
  82. marketplace_Ids: marketplaceSelect.value,
  83. date_start: date.value[0],
  84. date_end: date.value[1]
  85. };
  86. try {
  87. const response = await getTopSearchTermTable(query);
  88. total.value = response.total;
  89. tableData.value = response.data;
  90. } catch (error) {
  91. console.error('==Error==:', error);
  92. } finally {
  93. tableLoading.value = false;
  94. await nextTick();
  95. // 触发窗口 resize 事件
  96. window.dispatchEvent(new Event('resize'));
  97. }
  98. }
  99. /**
  100. * 下拉框值改变和input清空事件触发
  101. */
  102. async function handleSelectChange() {
  103. calculateDate();
  104. await fetchTableData();
  105. }
  106. /**
  107. * 输入框按下回车后触发
  108. */
  109. async function handleQueryChange() {
  110. if (!validateSearchTermInput(searchTermInp.value)) {
  111. if (searchTermInp.value.length == 0) {
  112. return;
  113. } else {
  114. ElMessage.warning({ message: '搜索词只能输入数字和英文字母', plain: true });
  115. return;
  116. }
  117. }
  118. if (asinInp.value.length > 0 && !validateAsinInput(asinInp.value)) {
  119. ElMessage.warning({ message: '不符合匹配规范', plain: true });
  120. return;
  121. }
  122. await fetchTableData();
  123. }
  124. /**
  125. * 校验SearchTerm输入是否合法
  126. * @param input 输入的字符串
  127. */
  128. function validateSearchTermInput(input: string) {
  129. const regex = /^[a-zA-Z0-9\s]*$/;
  130. return regex.test(input);
  131. }
  132. /**
  133. * 校验Asin输入是否合法
  134. * @param input 输入的字符串
  135. */
  136. function validateAsinInput(input: string) {
  137. const regex = /^[Bb]0[A-Za-z0-9\s]*$/i;
  138. return regex.test(input);
  139. }
  140. function handleJump() {
  141. // console.log('All defined routes:', router.getRoutes());
  142. router.push({ path: '/searchTerm/rootWordManage' });
  143. }
  144. async function handleDownload() {
  145. downloadLoading.value = true;
  146. try {
  147. const body = {
  148. asin: asinInp.value,
  149. date_start: date.value[0],
  150. date_end: date.value[1],
  151. search_term: searchTermInp.value,
  152. marketplace_Ids: marketplaceSelect.value,
  153. report_type: reportTypeSelect.value
  154. };
  155. const response = await postDownload(body);
  156. const blob = new Blob([ response.data ], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  157. // 创建一个临时 URL
  158. const url = window.URL.createObjectURL(blob);
  159. // 创建一个临时的 <a> 元素并触发下载
  160. const link = document.createElement('a');
  161. link.href = url;
  162. // 设置文件名
  163. const currentTime = dayjs().format('YYYY-MM-DD_HH_mm_ss');
  164. const filename = `TopSearchTerm_${ currentTime }.xlsx`;
  165. link.setAttribute('download', filename);
  166. // 添加到 body, 触发点击, 然后移除
  167. document.body.appendChild(link);
  168. link.click();
  169. document.body.removeChild(link);
  170. // 释放 URL 对象
  171. window.URL.revokeObjectURL(url);
  172. ElMessage.success('文件下载成功');
  173. } catch (error) {
  174. console.error('==Error==:', error);
  175. ElMessage.error('文件下载失败,请重试');
  176. } finally {
  177. downloadLoading.value = false;
  178. }
  179. }
  180. function getTagStyle(clickShareRank: number): Record<string, string> {
  181. switch (clickShareRank) {
  182. case 1:
  183. return { backgroundColor: '#fbbf24', color: '#fff', border: '1px solid #fbbf24' }; // 金色
  184. case 2:
  185. return { backgroundColor: '#C0C0C0', color: '#fff', border: '1px solid #C0C0C0' }; // 银色
  186. case 3:
  187. return { backgroundColor: '#CD7F32', color: '#fff', border: '1px solid #CD7F32' }; // 铜色
  188. default:
  189. return { backgroundColor: '#f0f0f0', color: '#000', border: '1px solid #e0e0e0' };
  190. }
  191. }
  192. function arraySpanMethod({ row, column, rowIndex, columnIndex }) {
  193. // 每三个合并为一个单元格
  194. if (columnIndex >= 0 && columnIndex <= 4) {
  195. if (rowIndex % 3 === 0) {
  196. return [ 3, 1 ]; // 跨越三行
  197. } else {
  198. return [ 0, 0 ]; // 被合并的单元格
  199. }
  200. }
  201. return [ 1, 1 ];
  202. }
  203. </script>
  204. <template>
  205. <div class="mx-3">
  206. <el-divider>
  207. <div class="font-bold text-xl">
  208. <el-icon style="top: 3px">
  209. <DataAnalysis/>
  210. </el-icon>
  211. Top Search Term - Table
  212. </div>
  213. </el-divider>
  214. </div>
  215. <el-card v-loading="tableLoading" class="mx-3 mb-2.5" style="border: none;">
  216. <!-- table筛选栏 -->
  217. <div class="flex justify-between">
  218. <div class="flex gap-5 flex-wrap">
  219. <div>
  220. <span class="font-medium mr-0.5">市场 </span>
  221. <el-select v-model="marketplaceSelect" style="width: 90px" @change="handleSelectChange">
  222. <el-option
  223. v-for="item in marketplaceOptions"
  224. :key="item.value"
  225. :disabled="item.disabled"
  226. :label="item.label"
  227. :value="item.value"/>
  228. </el-select>
  229. </div>
  230. <div>
  231. <span class="font-medium mr-0.5">报告类型 </span>
  232. <el-select v-model="reportTypeSelect" style="width: 90px" @change="handleSelectChange">
  233. <el-option label="周度" value="weekly"/>
  234. <el-option label="月度" value="monthly"/>
  235. </el-select>
  236. </div>
  237. <div>
  238. <span class="font-medium mr-0.5">搜索词 </span>
  239. <el-input
  240. v-model="searchTermInp"
  241. :prefix-icon="Search"
  242. clearable
  243. placeholder="输入后回车查询"
  244. style="width: 240px"
  245. @clear="handleSelectChange"
  246. @keyup.enter="handleQueryChange"/>
  247. </div>
  248. <div>
  249. <span class="font-medium mr-0.5">ASIN </span>
  250. <el-input
  251. v-model="asinInp"
  252. :prefix-icon="Search"
  253. clearable
  254. placeholder="输入后回车查询"
  255. style="width: 180px"
  256. @clear="handleSelectChange"
  257. @keyup.enter="handleQueryChange"/>
  258. </div>
  259. <div>
  260. <span class="font-medium mr-0.5">报告日期 </span>
  261. <el-config-provider v-if="reportTypeSelect === 'weekly'" :locale="enLocale">
  262. <el-date-picker
  263. v-model="dateDimension"
  264. :clearable="false"
  265. :disabled-date="(time: Date) => time > new Date()"
  266. :format="`${date[0]} To ${date[1]}`"
  267. :popper-options="{ placement: 'bottom-end' }"
  268. type="week"
  269. value-format="YYYY-MM-DD"/>
  270. </el-config-provider>
  271. <el-date-picker
  272. v-else
  273. v-model="dateDimension"
  274. :clearable="false"
  275. :disabled-date="(time: Date) => time > new Date()"
  276. :format="`${date[0]} To ${date[1]}`"
  277. :popper-options="{ placement: 'bottom-end' }"
  278. type="month"
  279. value-format="YYYY-MM">
  280. <!--<template #default> 123</template>-->
  281. </el-date-picker>
  282. </div>
  283. </div>
  284. <div class="flex">
  285. <el-button :icon="TopRight" plain type="primary" @click="handleJump">搜索词管理</el-button>
  286. <el-button
  287. :disabled="!tableData.length"
  288. :icon="Download"
  289. :loading="downloadLoading"
  290. plain
  291. round
  292. type="success"
  293. @click="handleDownload"
  294. >下载表格
  295. </el-button>
  296. <el-button :icon="Refresh" circle @click="refreshTable"></el-button>
  297. </div>
  298. </div>
  299. <!-- table -->
  300. <el-card class="mt-5" shadow="never">
  301. <div style="height: 100%; overflow: auto">
  302. <el-table :data="tableData" :span-method="arraySpanMethod" height="920" stripe style="width: 100%">
  303. <!-- 保持索引是1, 2, 3的顺序 不会收到合并单元格的影响 -->
  304. <el-table-column :index="(index) => Math.floor(index / 3) + 1" fixed type="index" width="50"/>
  305. <el-table-column label="搜索词" prop="searchTerm" width="260">
  306. <template #header>
  307. <el-icon style="top: 2px; margin-right: 3px">
  308. <Key/>
  309. </el-icon>
  310. <span>搜索词</span>
  311. </template>
  312. <template #default="{ row }">
  313. <el-link :underline="false" style="color: #5a6fc0" target="_blank"
  314. >{{ row.searchTerm }}
  315. </el-link>
  316. </template>
  317. </el-table-column>
  318. <el-table-column align="center" label="搜索词搜索排名" prop="searchFrequencyRank" width="150">
  319. <template #header>
  320. <el-icon style="top: 2px; margin-right: 4px">
  321. <Rank/>
  322. </el-icon>
  323. <span>搜索词搜索排名</span>
  324. </template>
  325. <template #default="{ row }">
  326. <span class="font-medium">{{ row.searchFrequencyRank }}</span>
  327. </template>
  328. </el-table-column>
  329. <el-table-column align="center" label="点击分享率(SUM)" prop="clickShareSummary" width="150">
  330. <template #header>
  331. <el-icon style="top: 2px; margin-right: 4px">
  332. <Star/>
  333. </el-icon>
  334. <span>点击分享率汇总</span>
  335. </template>
  336. <template #default="{ row }">
  337. <span class="font-medium">{{ row.clickShareSummary }}</span>
  338. </template>
  339. </el-table-column>
  340. <el-table-column align="center" label="转化分享率(SUM)" prop="conversionShareSummary" width="150">
  341. <template #header>
  342. <el-icon style="top: 2px; margin-right: 4px">
  343. <Star/>
  344. </el-icon>
  345. <span>转化分享率汇总</span>
  346. </template>
  347. <template #default="{ row }">
  348. <span class="font-medium">{{ row.conversionShareSummary }}</span>
  349. </template>
  350. </el-table-column>
  351. <el-table-column align="center" label="Asin" prop="clickedAsin">
  352. <template #header>
  353. <el-icon style="top: 2px; margin-right: 5px">
  354. <Goods/>
  355. </el-icon>
  356. <span>Asin</span>
  357. </template>
  358. <template #default="{ row }">
  359. <div class="font-medium" style="color: black">{{ row.clickedAsin }}</div>
  360. <!--<div class="text-sm text-left">-->
  361. <!-- <el-tooltip class="box-item" effect="dark" :content="row.clickedItemName" placement="top-start" :show-after="500">-->
  362. <!-- <div class="tooltip-text">-->
  363. <!-- <span class="font-medium mr-1">Title:</span>-->
  364. <!-- {{ row.clickedItemName }}-->
  365. <!-- </div>-->
  366. <!-- </el-tooltip>-->
  367. <!--</div>-->
  368. </template>
  369. </el-table-column>
  370. <el-table-column label="标题" prop="clickedItemName">
  371. <template #header>
  372. <el-icon style="top: 2px; margin-right: 5px">
  373. <Reading/>
  374. </el-icon>
  375. <span>标题</span>
  376. </template>
  377. <template #default="{ row }">
  378. <div class="text-sm text-left">
  379. <el-tooltip :content="row.clickedItemName" :show-after="500" class="box-item" effect="dark"
  380. placement="top">
  381. <div class="tooltip-text">
  382. {{ row.clickedItemName }}
  383. </div>
  384. </el-tooltip>
  385. </div>
  386. </template>
  387. </el-table-column>
  388. <el-table-column align="center" label="点击分享率排名" prop="clickShareRank" width="150">
  389. <template #header>
  390. <el-icon style="top: 2px; margin-right: 4px">
  391. <Medal/>
  392. </el-icon>
  393. <span>点击分享率排名</span>
  394. </template>
  395. <template #default="{ row }">
  396. <el-tag :style="getTagStyle(row.clickShareRank)">
  397. {{ row.clickShareRank }}
  398. </el-tag>
  399. </template>
  400. </el-table-column>
  401. <el-table-column align="center" label="点击分享率" prop="clickShare">
  402. <template #header>
  403. <el-icon style="top: 2px; margin-right: 4px">
  404. <Pointer/>
  405. </el-icon>
  406. <span>点击分享率</span>
  407. </template>
  408. <template #default="{ row }">
  409. <span class="font-semibold">{{ row.clickShare }}</span>
  410. </template>
  411. </el-table-column>
  412. <el-table-column align="center" label="转化分享率" prop="conversionShare">
  413. <template #header>
  414. <el-icon style="top: 2px; margin-right: 5px">
  415. <Switch/>
  416. </el-icon>
  417. <span>转化分享率</span>
  418. </template>
  419. <template #default="{ row }">
  420. <span class="font-semibold">{{ row.conversionShare }}</span>
  421. </template>
  422. </el-table-column>
  423. </el-table>
  424. </div>
  425. <div class="mt-3.5 flex justify-end">
  426. <el-pagination
  427. v-model:current-page="currentPage"
  428. v-model:page-size="pageSize"
  429. :page-sizes="[21, 42, 72, 102, 132, 162]"
  430. :total="total"
  431. layout="sizes, prev, pager, next, total"
  432. @change="handlePageChange"/>
  433. </div>
  434. </el-card>
  435. </el-card>
  436. </template>
  437. <style scoped>
  438. /* 修改 el-divider 的背景颜色 */
  439. :deep(.el-divider__text.is-center.el-divider__text) {
  440. background-color: #f8f8f8;
  441. }
  442. .tooltip-text {
  443. display: -webkit-box;
  444. -webkit-box-orient: vertical;
  445. -webkit-line-clamp: 1;
  446. overflow: hidden;
  447. text-overflow: ellipsis;
  448. white-space: normal;
  449. line-height: 1.2em;
  450. max-height: 2.4em;
  451. }
  452. </style>