index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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 } = 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 = 7; // 将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 * 3,
  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. async function handlePageChange(newPage: number, newSize: number) {
  204. currentPage.value = newPage;
  205. pageSize.value = newSize;
  206. await fetchTableData();
  207. }
  208. </script>
  209. <template>
  210. <div class="mx-3">
  211. <el-divider>
  212. <div class="font-bold text-xl">
  213. <el-icon style="top: 3px">
  214. <DataAnalysis/>
  215. </el-icon>
  216. Top Search Term - Table
  217. </div>
  218. </el-divider>
  219. </div>
  220. <el-card v-loading="tableLoading" class="mx-3 mb-2.5" style="border: none;">
  221. <!-- table筛选栏 -->
  222. <div class="flex justify-between">
  223. <div class="flex gap-5 flex-wrap">
  224. <div>
  225. <span class="font-medium mr-0.5">市场 </span>
  226. <el-select v-model="marketplaceSelect" style="width: 90px" @change="handleSelectChange">
  227. <el-option
  228. v-for="item in marketplaceOptions"
  229. :key="item.value"
  230. :disabled="item.disabled"
  231. :label="item.label"
  232. :value="item.value"/>
  233. </el-select>
  234. </div>
  235. <div>
  236. <span class="font-medium mr-0.5">报告类型 </span>
  237. <el-select v-model="reportTypeSelect" style="width: 90px" @change="handleSelectChange">
  238. <el-option label="周度" value="weekly"/>
  239. <el-option label="月度" value="monthly"/>
  240. </el-select>
  241. </div>
  242. <div>
  243. <span class="font-medium mr-0.5">搜索词 </span>
  244. <el-input
  245. v-model="searchTermInp"
  246. :prefix-icon="Search"
  247. clearable
  248. placeholder="输入后回车查询"
  249. style="width: 240px"
  250. @clear="handleSelectChange"
  251. @keyup.enter="handleQueryChange"/>
  252. </div>
  253. <div>
  254. <span class="font-medium mr-0.5">ASIN </span>
  255. <el-input
  256. v-model="asinInp"
  257. :prefix-icon="Search"
  258. clearable
  259. placeholder="输入后回车查询"
  260. style="width: 180px"
  261. @clear="handleSelectChange"
  262. @keyup.enter="handleQueryChange"/>
  263. </div>
  264. <div>
  265. <span class="font-medium mr-0.5">报告日期 </span>
  266. <el-config-provider v-if="reportTypeSelect === 'weekly'" :locale="enLocale">
  267. <el-date-picker
  268. v-model="dateDimension"
  269. :clearable="false"
  270. :disabled-date="(time: Date) => time > new Date()"
  271. :format="`${date[0]} To ${date[1]}`"
  272. :popper-options="{ placement: 'bottom-end' }"
  273. type="week"
  274. value-format="YYYY-MM-DD"/>
  275. </el-config-provider>
  276. <el-date-picker
  277. v-else
  278. v-model="dateDimension"
  279. :clearable="false"
  280. :disabled-date="(time: Date) => time > new Date()"
  281. :format="`${date[0]} To ${date[1]}`"
  282. :popper-options="{ placement: 'bottom-end' }"
  283. type="month"
  284. value-format="YYYY-MM">
  285. <!--<template #default> 123</template>-->
  286. </el-date-picker>
  287. </div>
  288. </div>
  289. <div class="flex">
  290. <el-button :icon="TopRight" plain type="primary" @click="handleJump">搜索词管理</el-button>
  291. <el-button
  292. :disabled="!tableData.length"
  293. :icon="Download"
  294. :loading="downloadLoading"
  295. plain
  296. round
  297. type="success"
  298. @click="handleDownload"
  299. >下载表格
  300. </el-button>
  301. <el-button :icon="Refresh" circle @click="refreshTable"></el-button>
  302. </div>
  303. </div>
  304. <div class="mt-3.5">
  305. <span class="font-medium" style="color: #606266">当前搜索词总数: </span>
  306. <span class="font-medium">{{ total }}</span>
  307. </div>
  308. <!-- table -->
  309. <el-card class="mt-5" shadow="never">
  310. <div style="height: 100%; overflow: auto">
  311. <el-table :data="tableData" :span-method="arraySpanMethod" height="920" stripe style="width: 100%">
  312. <!-- 保持索引是1, 2, 3的顺序 不会收到合并单元格的影响 -->
  313. <el-table-column label="No." :index="(index) => Math.floor(index / 3) + (currentPage - 1) * pageSize + 1" fixed type="index" width="50"/>
  314. <el-table-column label="搜索词" prop="searchTerm" width="245">
  315. <template #header>
  316. <el-icon style="top: 2px; margin-right: 3px">
  317. <Key/>
  318. </el-icon>
  319. <span>搜索词</span>
  320. </template>
  321. <template #default="{ row }">
  322. <el-link :underline="false" style="color: #5a6fc0" target="_blank"
  323. >{{ row.searchTerm }}
  324. </el-link>
  325. </template>
  326. </el-table-column>
  327. <el-table-column align="center" label="搜索词搜索排名" prop="searchFrequencyRank" width="140">
  328. <template #header>
  329. <el-icon style="top: 2px; margin-right: 4px">
  330. <Rank/>
  331. </el-icon>
  332. <span>搜索词搜索排名</span>
  333. </template>
  334. <template #default="{ row }">
  335. <span class="font-medium">{{ row.searchFrequencyRank }}</span>
  336. </template>
  337. </el-table-column>
  338. <el-table-column align="center" label="点击分享率(SUM)" prop="clickShareSummary" width="140">
  339. <template #header>
  340. <el-icon style="top: 2px; margin-right: 4px">
  341. <Star/>
  342. </el-icon>
  343. <span>点击分享率汇总</span>
  344. </template>
  345. <template #default="{ row }">
  346. <span class="font-medium">{{ row.clickShareSummary }}</span>
  347. </template>
  348. </el-table-column>
  349. <el-table-column align="center" label="转化分享率(SUM)" prop="conversionShareSummary" width="140">
  350. <template #header>
  351. <el-icon style="top: 2px; margin-right: 4px">
  352. <Star/>
  353. </el-icon>
  354. <span>转化分享率汇总</span>
  355. </template>
  356. <template #default="{ row }">
  357. <span class="font-medium">{{ row.conversionShareSummary }}</span>
  358. </template>
  359. </el-table-column>
  360. <el-table-column align="center" label="Asin" prop="clickedAsin" width="160">
  361. <template #header>
  362. <el-icon style="top: 2px; margin-right: 5px">
  363. <Goods/>
  364. </el-icon>
  365. <span>Asin</span>
  366. </template>
  367. <template #default="{ row }">
  368. <div class="font-medium" style="color: black">{{ row.clickedAsin }}</div>
  369. <!--<div class="text-sm text-left">-->
  370. <!-- <el-tooltip class="box-item" effect="dark" :content="row.clickedItemName" placement="top-start" :show-after="500">-->
  371. <!-- <div class="tooltip-text">-->
  372. <!-- <span class="font-medium mr-1">Title:</span>-->
  373. <!-- {{ row.clickedItemName }}-->
  374. <!-- </div>-->
  375. <!-- </el-tooltip>-->
  376. <!--</div>-->
  377. </template>
  378. </el-table-column>
  379. <el-table-column label="标 题" prop="clickedItemName" width="160">
  380. <template #header>
  381. <el-icon style="top: 2px; margin-right: 5px">
  382. <Reading/>
  383. </el-icon>
  384. <span>标 题</span>
  385. </template>
  386. <template #default="{ row }">
  387. <div class="text-sm text-left">
  388. <el-tooltip :content="row.clickedItemName" :show-after="500" class="box-item" effect="dark"
  389. placement="top">
  390. <div class="tooltip-text">
  391. {{ row.clickedItemName }}
  392. </div>
  393. </el-tooltip>
  394. </div>
  395. </template>
  396. </el-table-column>
  397. <el-table-column align="center" label="点击分享率排名" prop="clickShareRank" width="140">
  398. <template #header>
  399. <el-icon style="top: 2px; margin-right: 4px">
  400. <Medal/>
  401. </el-icon>
  402. <span>点击分享率排名</span>
  403. </template>
  404. <template #default="{ row }">
  405. <el-tag :style="getTagStyle(row.clickShareRank)">
  406. {{ row.clickShareRank }}
  407. </el-tag>
  408. </template>
  409. </el-table-column>
  410. <el-table-column align="center" label="点击分享率" prop="clickShare">
  411. <template #header>
  412. <el-icon style="top: 2px; margin-right: 4px">
  413. <Pointer/>
  414. </el-icon>
  415. <span>点击分享率</span>
  416. </template>
  417. <template #default="{ row }">
  418. <span class="font-semibold">{{ row.clickShare }}</span>
  419. </template>
  420. </el-table-column>
  421. <el-table-column align="center" label="转化分享率" prop="conversionShare">
  422. <template #header>
  423. <el-icon style="top: 2px; margin-right: 5px">
  424. <Switch/>
  425. </el-icon>
  426. <span>转化分享率</span>
  427. </template>
  428. <template #default="{ row }">
  429. <span class="font-semibold">{{ row.conversionShare }}</span>
  430. </template>
  431. </el-table-column>
  432. </el-table>
  433. </div>
  434. <div class="mt-3.5 flex justify-end">
  435. <el-pagination
  436. v-model:current-page="currentPage"
  437. v-model:page-size="pageSize"
  438. :page-sizes="[7, 14, 21, 28, 35]"
  439. :total="total / 3"
  440. layout="prev, pager, next, sizes, total"
  441. @change="handlePageChange"/>
  442. </div>
  443. </el-card>
  444. </el-card>
  445. </template>
  446. <style scoped>
  447. /* 修改 el-divider 的背景颜色 */
  448. :deep(.el-divider__text.is-center.el-divider__text) {
  449. background-color: #f8f8f8;
  450. }
  451. .tooltip-text {
  452. display: -webkit-box;
  453. -webkit-box-orient: vertical;
  454. -webkit-line-clamp: 1;
  455. overflow: hidden;
  456. text-overflow: ellipsis;
  457. white-space: normal;
  458. line-height: 1.2em;
  459. max-height: 2.4em;
  460. }
  461. </style>