|
@@ -6,20 +6,25 @@
|
|
|
<el-select
|
|
<el-select
|
|
|
v-model="searchType"
|
|
v-model="searchType"
|
|
|
placeholder="搜索词分类"
|
|
placeholder="搜索词分类"
|
|
|
- style="width: 120px; margin-right: 15px"
|
|
|
|
|
|
|
+ class="type-select"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @change="handleTypeChange"
|
|
|
>
|
|
>
|
|
|
- <el-option label="搜索词分类" value="category" />
|
|
|
|
|
|
|
+ <el-option label="ISBN" value="isbn" />
|
|
|
|
|
+ <el-option label="其他" value="other" />
|
|
|
</el-select>
|
|
</el-select>
|
|
|
|
|
|
|
|
- <div class="date-ranges">
|
|
|
|
|
- <el-button-group>
|
|
|
|
|
- <el-button type="primary">实时</el-button>
|
|
|
|
|
- <el-button>昨日</el-button>
|
|
|
|
|
- <el-button>7日</el-button>
|
|
|
|
|
- <el-button>15日</el-button>
|
|
|
|
|
- <el-button>30日</el-button>
|
|
|
|
|
- </el-button-group>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="filter-main">
|
|
|
|
|
+ <div class="date-ranges">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ v-for="range in dateRanges"
|
|
|
|
|
+ :key="range.value"
|
|
|
|
|
+ :type="activeRange === range.value ? 'primary' : ''"
|
|
|
|
|
+ @click="handleRangeChange(range.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ range.label }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
<div class="date-picker-wrapper">
|
|
<div class="date-picker-wrapper">
|
|
|
<el-date-picker
|
|
<el-date-picker
|
|
@@ -28,115 +33,292 @@
|
|
|
range-separator="-"
|
|
range-separator="-"
|
|
|
start-placeholder="选择时间"
|
|
start-placeholder="选择时间"
|
|
|
end-placeholder="选择时间"
|
|
end-placeholder="选择时间"
|
|
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
size="default"
|
|
size="default"
|
|
|
|
|
+ @change="handleDateChange"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <el-button type="primary">搜索</el-button>
|
|
|
|
|
- <el-button>重置</el-button>
|
|
|
|
|
|
|
+ <el-button type="primary" @click="handleSearch">搜索</el-button>
|
|
|
|
|
+ <el-button @click="handleReset">重置</el-button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- Overview Stats -->
|
|
|
|
|
- <search-overview />
|
|
|
|
|
|
|
+ <search-overview
|
|
|
|
|
+ v-loading="summaryLoading"
|
|
|
|
|
+ :summary="summary"
|
|
|
|
|
+ :date-text="dateText"
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
- <!-- Data Table -->
|
|
|
|
|
<search-table
|
|
<search-table
|
|
|
|
|
+ v-loading="tableLoading"
|
|
|
:table-data="tableData"
|
|
:table-data="tableData"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ :page-num="pageNum"
|
|
|
|
|
+ :page-size="pageSize"
|
|
|
|
|
+ @page-change="handlePageChange"
|
|
|
@view-trend="handleViewTrend"
|
|
@view-trend="handleViewTrend"
|
|
|
@view-effect="handleViewEffect"
|
|
@view-effect="handleViewEffect"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <!-- Trend Modal -->
|
|
|
|
|
<search-trend-modal
|
|
<search-trend-modal
|
|
|
v-model="trendModalVisible"
|
|
v-model="trendModalVisible"
|
|
|
:row-data="currentRow"
|
|
:row-data="currentRow"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <!-- Effect Modal -->
|
|
|
|
|
<search-effect-modal
|
|
<search-effect-modal
|
|
|
v-model="effectModalVisible"
|
|
v-model="effectModalVisible"
|
|
|
:row-data="currentRow"
|
|
:row-data="currentRow"
|
|
|
|
|
+ :start-date="queryDates.startDate"
|
|
|
|
|
+ :end-date="queryDates.endDate"
|
|
|
/>
|
|
/>
|
|
|
</ele-card>
|
|
</ele-card>
|
|
|
</ele-page>
|
|
</ele-page>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
- import { ref } from 'vue';
|
|
|
|
|
|
|
+ import { computed, onMounted, ref } from 'vue';
|
|
|
|
|
+ import dayjs from 'dayjs';
|
|
|
|
|
+ import { EleMessage } from 'ele-admin-plus/es';
|
|
|
import SearchOverview from './components/SearchOverview.vue';
|
|
import SearchOverview from './components/SearchOverview.vue';
|
|
|
import SearchTable from './components/SearchTable.vue';
|
|
import SearchTable from './components/SearchTable.vue';
|
|
|
import SearchTrendModal from './components/SearchTrendModal.vue';
|
|
import SearchTrendModal from './components/SearchTrendModal.vue';
|
|
|
import SearchEffectModal from './components/SearchEffectModal.vue';
|
|
import SearchEffectModal from './components/SearchEffectModal.vue';
|
|
|
|
|
+ import {
|
|
|
|
|
+ getSearchSummary,
|
|
|
|
|
+ getSearchWordPage
|
|
|
|
|
+ } from '@/api/statsAnalysis/search';
|
|
|
|
|
|
|
|
- const searchType = ref('category');
|
|
|
|
|
- const dateRange = ref([]);
|
|
|
|
|
|
|
+ const dateRanges = [
|
|
|
|
|
+ { label: '实时', value: 'realtime' },
|
|
|
|
|
+ { label: '昨日', value: 'yesterday' },
|
|
|
|
|
+ { label: '7日', value: '7days' },
|
|
|
|
|
+ { label: '15日', value: '15days' },
|
|
|
|
|
+ { label: '30日', value: '30days' }
|
|
|
|
|
+ ];
|
|
|
|
|
|
|
|
|
|
+ const activeRange = ref('realtime');
|
|
|
|
|
+ const dateRange = ref([]);
|
|
|
|
|
+ const searchType = ref('');
|
|
|
|
|
+ const pageNum = ref(1);
|
|
|
|
|
+ const pageSize = ref(10);
|
|
|
|
|
+ const total = ref(0);
|
|
|
|
|
+ const summary = ref({});
|
|
|
|
|
+ const summaryLoading = ref(false);
|
|
|
|
|
+ const tableLoading = ref(false);
|
|
|
const trendModalVisible = ref(false);
|
|
const trendModalVisible = ref(false);
|
|
|
const effectModalVisible = ref(false);
|
|
const effectModalVisible = ref(false);
|
|
|
const currentRow = ref(null);
|
|
const currentRow = ref(null);
|
|
|
|
|
|
|
|
- const tableData = ref([
|
|
|
|
|
- {
|
|
|
|
|
- term: '中医基础理论',
|
|
|
|
|
- searchCount: 23,
|
|
|
|
|
- cartCount: 5,
|
|
|
|
|
- favCount: 0,
|
|
|
|
|
- payCount: 2,
|
|
|
|
|
- conversionRate: '8.70%'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- term: '大学语文',
|
|
|
|
|
- searchCount: 6,
|
|
|
|
|
- cartCount: 3,
|
|
|
|
|
- favCount: 1,
|
|
|
|
|
- payCount: 0,
|
|
|
|
|
- conversionRate: '0.00%'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- term: '马克思主义发展史',
|
|
|
|
|
- searchCount: 6,
|
|
|
|
|
- cartCount: 3,
|
|
|
|
|
- favCount: 0,
|
|
|
|
|
- payCount: 3,
|
|
|
|
|
- conversionRate: '50.00%'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- term: '高等数学',
|
|
|
|
|
- searchCount: 15,
|
|
|
|
|
- cartCount: 8,
|
|
|
|
|
- favCount: 2,
|
|
|
|
|
- payCount: 5,
|
|
|
|
|
- conversionRate: '33.33%'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- term: 'Python编程',
|
|
|
|
|
- searchCount: 12,
|
|
|
|
|
- cartCount: 4,
|
|
|
|
|
- favCount: 3,
|
|
|
|
|
- payCount: 1,
|
|
|
|
|
- conversionRate: '8.33%'
|
|
|
|
|
|
|
+ const tableData = ref([]);
|
|
|
|
|
+ const filteredRows = ref([]);
|
|
|
|
|
+
|
|
|
|
|
+ const rangeDays = {
|
|
|
|
|
+ realtime: 0,
|
|
|
|
|
+ '7days': 6,
|
|
|
|
|
+ '15days': 14,
|
|
|
|
|
+ '30days': 29
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ function buildRange(value) {
|
|
|
|
|
+ const today = dayjs();
|
|
|
|
|
+ if (value === 'yesterday') {
|
|
|
|
|
+ const day = today.subtract(1, 'day').format('YYYY-MM-DD');
|
|
|
|
|
+ return [day, day];
|
|
|
}
|
|
}
|
|
|
- ]);
|
|
|
|
|
|
|
+ const offset = rangeDays[value] ?? 0;
|
|
|
|
|
+ return [
|
|
|
|
|
+ today.subtract(offset, 'day').format('YYYY-MM-DD'),
|
|
|
|
|
+ today.format('YYYY-MM-DD')
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const queryDates = computed(() => {
|
|
|
|
|
+ const [startDate, endDate] = dateRange.value || [];
|
|
|
|
|
+ return {
|
|
|
|
|
+ startDate: startDate || '',
|
|
|
|
|
+ endDate: endDate || ''
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const dateText = computed(() => {
|
|
|
|
|
+ const { startDate, endDate } = queryDates.value;
|
|
|
|
|
+ if (!startDate || !endDate) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${startDate}至${endDate}`;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ function listParams() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...queryDates.value,
|
|
|
|
|
+ pageNum: pageNum.value,
|
|
|
|
|
+ pageSize: pageSize.value
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadSummary() {
|
|
|
|
|
+ summaryLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ summary.value = await getSearchSummary(queryDates.value);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ summary.value = {};
|
|
|
|
|
+ EleMessage.error(error.message || '获取搜索汇总失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ summaryLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function isIsbn(word) {
|
|
|
|
|
+ const text = String(word || '').replace(/[-\s]/g, '');
|
|
|
|
|
+ return /^\d{13}$/.test(text) || /^\d{9}[\dXx]$/.test(text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function matchSearchType(row) {
|
|
|
|
|
+ if (!searchType.value) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ const isbn = isIsbn(row.searchWord);
|
|
|
|
|
+ return searchType.value === 'isbn' ? isbn : !isbn;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function sliceFiltered() {
|
|
|
|
|
+ const start = (pageNum.value - 1) * pageSize.value;
|
|
|
|
|
+ tableData.value = filteredRows.value.slice(
|
|
|
|
|
+ start,
|
|
|
|
|
+ start + pageSize.value
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadTable() {
|
|
|
|
|
+ tableLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!searchType.value) {
|
|
|
|
|
+ const res = await getSearchWordPage(listParams());
|
|
|
|
|
+ tableData.value = res.rows || [];
|
|
|
|
|
+ total.value = res.total || 0;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const matched = [];
|
|
|
|
|
+ let page = 1;
|
|
|
|
|
+ const size = 100;
|
|
|
|
|
+ let fetched = 0;
|
|
|
|
|
+ let serverTotal = Infinity;
|
|
|
|
|
+ while (fetched < serverTotal && page <= 30) {
|
|
|
|
|
+ const res = await getSearchWordPage({
|
|
|
|
|
+ ...queryDates.value,
|
|
|
|
|
+ pageNum: page,
|
|
|
|
|
+ pageSize: size
|
|
|
|
|
+ });
|
|
|
|
|
+ const rows = res.rows || [];
|
|
|
|
|
+ serverTotal = res.total || 0;
|
|
|
|
|
+ matched.push(...rows.filter(matchSearchType));
|
|
|
|
|
+ fetched += rows.length;
|
|
|
|
|
+ if (!rows.length || rows.length < size) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ page += 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ filteredRows.value = matched;
|
|
|
|
|
+ total.value = matched.length;
|
|
|
|
|
+ sliceFiltered();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ tableData.value = [];
|
|
|
|
|
+ filteredRows.value = [];
|
|
|
|
|
+ total.value = 0;
|
|
|
|
|
+ EleMessage.error(error.message || '获取搜索词列表失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ tableLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const handleViewTrend = (row) => {
|
|
|
|
|
|
|
+ function loadPage() {
|
|
|
|
|
+ loadSummary();
|
|
|
|
|
+ loadTable();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleRangeChange(value) {
|
|
|
|
|
+ activeRange.value = value;
|
|
|
|
|
+ dateRange.value = buildRange(value);
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+ loadPage();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleDateChange() {
|
|
|
|
|
+ activeRange.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleSearch() {
|
|
|
|
|
+ if (!dateRange.value?.length) {
|
|
|
|
|
+ dateRange.value = buildRange('realtime');
|
|
|
|
|
+ activeRange.value = 'realtime';
|
|
|
|
|
+ }
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+ loadPage();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleReset() {
|
|
|
|
|
+ searchType.value = '';
|
|
|
|
|
+ handleRangeChange('realtime');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleTypeChange() {
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+ loadTable();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handlePageChange(page) {
|
|
|
|
|
+ pageNum.value = page;
|
|
|
|
|
+ if (searchType.value) {
|
|
|
|
|
+ sliceFiltered();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ loadTable();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleViewTrend(row) {
|
|
|
currentRow.value = row;
|
|
currentRow.value = row;
|
|
|
trendModalVisible.value = true;
|
|
trendModalVisible.value = true;
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const handleViewEffect = (row) => {
|
|
|
|
|
|
|
+ function handleViewEffect(row) {
|
|
|
currentRow.value = row;
|
|
currentRow.value = row;
|
|
|
effectModalVisible.value = true;
|
|
effectModalVisible.value = true;
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(() => {
|
|
|
|
|
+ dateRange.value = buildRange('realtime');
|
|
|
|
|
+ loadPage();
|
|
|
|
|
+ });
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
.filter-container {
|
|
.filter-container {
|
|
|
|
|
+ position: relative;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
margin-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
|
|
|
+ min-height: 32px;
|
|
|
|
|
+
|
|
|
|
|
+ .type-select {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 140px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .filter-main {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
.date-ranges {
|
|
.date-ranges {
|
|
|
margin-right: 15px;
|
|
margin-right: 15px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ margin-left: 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.date-picker-wrapper {
|
|
.date-picker-wrapper {
|