index.vue 17 KB

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