index.vue 16 KB

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