Explorar o código

feat(score-statistics):添加了评分统计作为首页

xinyan hai 9 meses
pai
achega
d26317ab55

+ 1 - 1
src/utils/menu.ts

@@ -62,7 +62,7 @@ export const handleMenu = (menuData: Array<any>) => {
     })
     const dynamicRoutes = [
         {
-            path: '/home', name: 'home', component: '/system/home/index', meta: {
+            path: '/home', name: 'home', component: '/scoreStatistics/index', meta: {
                 title: 'message.router.home',
                 isLink: '',
                 isHide: false,

+ 22 - 0
src/views/scoreStatistics/Columns.ts

@@ -0,0 +1,22 @@
+interface FormatterParams {
+	cellValue: any; // 你可以根据具体情况替换成更具体的类型
+}
+export const scoreStatisticsColumns = [
+	{ field: 'asin', title: 'ASIN', minWidth: 'auto' },
+	{ field: 'sku', title: 'SKU', minWidth: 'auto' },
+	{ field: 'reviews1', title: '一星评分个数', align: 'center', minWidth: 'auto' },
+	{ field: 'reviews2', title: '二星评分个数', align: 'center', minWidth: 'auto' },
+	{ field: 'reviews3', title: '三星评分个数', align: 'center', minWidth: 'auto' },
+	{ field: 'reviews4', title: '四星评分个数', align: 'center', minWidth: 'auto' },
+	{ field: 'reviews5', title: '五星评分个数', align: 'center', minWidth: 'auto' },
+	{
+		field: 'is_new_sku',
+		title: '是否新品',
+		align: 'center',
+		formatter: ({ cellValue }: FormatterParams) => (cellValue === true ? '是' : '否'),
+		minWidth: 'auto',
+	},
+	{ field: 'review_date', title: '评论日期', minWidth: 'auto' },
+	{ field: 'launch_date', title: '发布日期', minWidth: 'auto' },
+	{ field: 'avg_score', title: '平均评分', align: 'center', minWidth: 'auto' },
+];

+ 45 - 0
src/views/scoreStatistics/api.ts

@@ -0,0 +1,45 @@
+import { request } from '/@/utils/service';
+
+export function getOverviewData (query: any) {
+  return request({
+    url: '/api/index/overview/',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getDownloadData(query: any) {
+  return request({
+    url: '/api/index/overview-download/',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
+
+export function getTableData (query: any) {
+  return request({
+    url: '/api/index/asin-average-score/',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getTableDownloadData(query: any) {
+  return request({
+    url: '/api/index/asin-average-score/export_data/',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
+
+export function getChartData (query: any) {
+  return request({
+    url: '/api/index/average-score-chart/',
+    method: 'get',
+    params: query
+  })
+}
+
+

+ 318 - 0
src/views/scoreStatistics/components/lineChart.vue

@@ -0,0 +1,318 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts';
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+import { getChartData } from '/@/views/scoreStatistics/api';
+
+let chartObj: any;
+const chartRef = ref(null);
+const loading = ref(false);
+
+const newAverageScores = ref([]);
+const oldAverageScores = ref([]);
+const newStarRatings = ref<number[][]>([]);
+const oldStarRatings = ref<number[][]>([]);
+
+// 初始化 ECharts 图表
+onMounted(() => {
+	addResize();
+	initLine();
+});
+
+// 组件卸载前清理
+onBeforeUnmount(() => {
+	if (chartObj) {
+		chartObj.dispose();
+		chartObj = null;
+	}
+	removeResize();
+});
+
+// ECharts 配置项
+const option: any = {
+	// dataset: {
+	// 	source: [],
+	// },
+	title: {
+		left: '0%',
+		text: '每月平均评分',
+		textStyle: {
+			fontSize: '16px',
+			fontWeight: '500',
+		},
+	},
+	tooltip: {
+		trigger: 'axis',
+	},
+	legend: {
+		top: '10%',
+		align: 'right',
+	},
+	grid: {
+		top: '30%',
+		left: '5%',
+		right: '5%',
+		bottom: '5%',
+		containLabel: true,
+	},
+	xAxis: {
+		type: 'category',
+		data: [],
+	},
+	yAxis: [
+		{
+			type: 'value',
+			name: '平均评分',
+			min: 2,
+			max: 5,
+			interval: 0.5,
+			position: 'left',
+			axisLine: {
+				show: true,
+				lineStyle: {
+					color: '#70b6e3',
+				},
+			},
+			axisLabel: {
+				formatter: '{value} 分',
+			},
+		},
+		{
+			type: 'value',
+			name: '评分数量',
+			min: 0,
+			position: 'right',
+			splitLine: {
+				show: false,
+			},
+			axisLine: {
+				show: true,
+				lineStyle: {
+					color: '#5470c6',
+				},
+			},
+			axisLabel: {
+				formatter: '{value} 个',
+			},
+		},
+	],
+	series: [
+		{
+			id: 0,
+			name: '新品平均评分',
+			type: 'line',
+			yAxisIndex: 0,
+			itemStyle: {
+				color: '#70b6e3', //改变折线点的颜色
+				lineStyle: {
+					color: '#70b6e3', //改变折线颜色
+				},
+			},
+			data: [],
+		},
+		{
+			id: 1,
+			name: '旧品平均评分',
+			type: 'line',
+			yAxisIndex: 0,
+			itemStyle: {
+				color: '#8170cc', //改变折线点的颜色
+				lineStyle: {
+					color: '#3f3da4', //改变折线颜色
+				},
+			},
+			data: [],
+		},
+
+		{
+			name: '新品一星',
+			type: 'bar',
+			stack: 'stack1',
+			yAxisIndex: 1,
+			barGap: 0,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: '#e7b3b3',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '旧品一星',
+			type: 'bar',
+			stack: 'stack1',
+			yAxisIndex: 1,
+			barGap: 0,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: 'rgba(231,179,179,0.73)',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '新品二星',
+			type: 'bar',
+			stack: 'stack2',
+			yAxisIndex: 1,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: '#fdba74',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '旧品二星',
+			type: 'bar',
+			stack: 'stack2',
+			yAxisIndex: 1,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: 'rgba(253,186,116,0.79)',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '新品三星',
+			type: 'bar',
+			stack: 'stack3',
+			yAxisIndex: 1,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: '#eae09e',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '旧品三星',
+			type: 'bar',
+			stack: 'stack3',
+			yAxisIndex: 1,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: 'rgba(234,224,158,0.76)',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '新品四星',
+			type: 'bar',
+			yAxisIndex: 1,
+			stack: 'stack4',
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: '#bae6fd',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '旧品四星',
+			type: 'bar',
+			yAxisIndex: 1,
+			stack: 'stack4',
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: 'rgba(186,230,253,0.76)',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '新品五星',
+			type: 'bar',
+			stack: 'stack5',
+			yAxisIndex: 1,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: '#a6e3a1',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+		{
+			name: '旧品五星',
+			type: 'bar',
+			stack: 'stack5',
+			yAxisIndex: 1,
+			barMaxWidth: '14px',
+			itemStyle: {
+				color: 'rgba(166,227,161,0.66)',
+				//borderRadius: 4,
+			},
+			data: [],
+		},
+	],
+};
+
+// 初始化 ECharts 图表的函数
+async function initLine() {
+	await loadData();
+	chartObj = echarts.init(chartRef.value);
+	chartObj.setOption(option, true);
+}
+
+// 加载数据
+async function loadData() {
+	try {
+		loading.value = true;
+		const query = {};
+		const resp = await getChartData(query);
+		const trendData = resp.data;
+		newAverageScores.value = trendData.new_avg_score.map((item) => item.avg_val);
+		oldAverageScores.value = trendData.old_avg_score.map((item) => item.avg_val);
+		option.xAxis.data = trendData.new_avg_score.map((item) => item.date);
+		option.series[0].data = newAverageScores.value;
+		option.series[1].data = oldAverageScores.value;
+		trendData.reviews_items.forEach((item) => {
+			const ratings = [item.reviews1_sum || 0, item.reviews2_sum || 0, item.reviews3_sum || 0, item.reviews4_sum || 0, item.reviews5_sum || 0];
+
+			if (item.is_new_sku) {
+				newStarRatings.value.push(ratings);
+			} else {
+				oldStarRatings.value.push(ratings);
+			}
+		});
+		const starRatings = [newStarRatings, oldStarRatings];
+		starRatings.forEach((ratings, i) => {
+			ratings.value.forEach((rating, j) => {
+				rating.forEach((value, k) => {
+					option.series[2 * k + i + 2].data = option.series[2 * k + i + 2].data || [];
+					option.series[2 * k + i + 2].data[j] = value;
+				});
+			});
+		});
+	} catch (e) {
+		ElMessage.error('加载数据失败,请稍后再试');
+	} finally {
+		loading.value = false;
+	}
+}
+
+// 处理窗口大小变化
+function resizeChart() {
+	chartObj.resize();
+}
+
+// 添加窗口大小变化事件监听
+function addResize() {
+	window.addEventListener('resize', resizeChart);
+}
+
+// 移除窗口大小变化事件监听
+function removeResize() {
+	window.removeEventListener('resize', resizeChart);
+}
+</script>
+
+<template>
+	<div v-loading="loading">
+		<!-- 图表区域 -->
+		<div ref="chartRef" style="width: 100%; height: 520px; background: #fff"></div>
+	</div>
+</template>
+
+<style scoped></style>

+ 182 - 0
src/views/scoreStatistics/components/monthlyRating.vue

@@ -0,0 +1,182 @@
+<template>
+	<div>
+		<el-card>
+			<span class="title">监控ASIN每月平均评分</span>
+			<div style="display: flex; align-items: baseline; justify-content: space-between">
+				<div style="display: flex; gap: 25px; margin-bottom: 10px">
+					<div>
+						<span style="font-size: 14px; padding-right: 4px; color: #95969a">ASIN:</span>
+						<el-input v-model="searchAsin" clearable style="width: 180px" @change="fetchData"></el-input>
+					</div>
+					<div>
+						<span style="font-size: 14px; padding-right: 4px; color: #95969a">SKU:</span>
+						<el-input v-model="searchSku" clearable style="width: 180px" @change="fetchData"></el-input>
+					</div>
+					<div>
+						<span style="font-size: 14px; color: #95969a">时间选择:</span>
+						<el-date-picker
+							v-model="asinMonthDate"
+							:clearable="false"
+							:disabled-date="disabledDate"
+							placeholder="选择月"
+							style="width: 170px"
+							type="month"
+						>
+						</el-date-picker>
+					</div>
+				</div>
+				<div>
+					<el-button circle :icon="Download" plain @click="handleDownload"></el-button>
+				</div>
+			</div>
+			<el-card body-style="padding: 0" shadow="never">
+				<vxe-grid :cell-style="cellStyle" :header-cell-style="headerStyle" v-bind="gridOptions" v-on="gridEvents" show-overflow="tooltip"></vxe-grid>
+			</el-card>
+		</el-card>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { getTableData, getTableDownloadData } from '/@/views/scoreStatistics/api';
+import { Download } from '@element-plus/icons-vue';
+import { scoreStatisticsColumns } from '/@/views/scoreStatistics/Columns';
+
+const gridOptions = reactive({
+	border: 'inner',
+	loading: false,
+	height: 700,
+	pagerConfig: {
+		enabled: true,
+		total: 20,
+		currentPage: 1,
+		pageSize: 20,
+		pageSizes: [10, 20, 30],
+	},
+	rowConfig: {
+		height: 38,
+	},
+	columns: scoreStatisticsColumns,
+	data: [],
+});
+
+const asinMonthDate = ref(dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD'))
+
+const disabledDate = (time: Date) => {
+	// 禁用当前月和当前月之后的日期
+	const today = new Date(); // 获取当前日期
+	const currentYear = today.getFullYear();
+	const currentMonth = today.getMonth() + 1; // 获取月份,注意要 +1
+	// 通过比较年份和月份来禁用
+	return time.getFullYear() > currentYear || (time.getFullYear() === currentYear && time.getMonth() + 2 > currentMonth);
+};
+
+const searchAsin = ref('');
+const searchSku = ref('');
+const averageData = ref([]);
+
+
+const gridEvents = {
+	pageChange({ pageSize, currentPage }) {
+		gridOptions.pagerConfig.currentPage = currentPage;
+		gridOptions.pagerConfig.pageSize = pageSize;
+		fetchData();
+	},
+};
+
+async function fetchData() {
+	gridOptions.loading = true; // 显示加载状态
+	try {
+		const params = {
+			limit: gridOptions.pagerConfig.pageSize,
+			page: gridOptions.pagerConfig.currentPage,
+			review_date: asinMonthDate.value,
+			asin : searchAsin.value,
+			sku : searchSku.value,
+		};
+
+		const resp = await getTableData(params);
+		gridOptions.data = resp.data;
+		console.log("=>(monthlyRating.vue:121) gridOptions.data", gridOptions.data);
+		gridOptions.pagerConfig.total = resp.total;
+	} catch (error) {
+		console.log(error);
+	} finally {
+		gridOptions.loading = false;
+	}
+}
+
+async function handleDownload() {
+	gridOptions.loading = true; // 显示加载状态
+	try {
+		const params = {
+			review_date: asinMonthDate.value,
+			asin : searchAsin.value,
+			sku : searchSku.value,
+		};
+
+		const resp = await getTableDownloadData(params);
+		const url = window.URL.createObjectURL(resp.data);
+		const link = document.createElement('a');
+		link.href = url;
+		link.setAttribute('download', asinMonthDate.slice(0, 7) + '平均评分.xlsx');
+		document.body.appendChild(link);
+		link.click();
+		document.body.removeChild(link);
+		window.URL.revokeObjectURL(url);
+		gridOptions.loading = false;
+		ElMessage.success('下载成功!');
+	} catch (error) {
+		ElMessage.error('下载失败!');
+	} finally {
+		gridOptions.loading = false;
+	}
+}
+
+function cellStyle() {
+	return {
+		fontSize: '13px',
+		fontWeight: '500',
+	};
+}
+
+function headerStyle() {
+	return {
+		fontSize: '13px',
+		backgroundColor: '#f0f1f3',
+		height: 10,
+		//color: '#b4b6ba',
+		//fontWeight: '500',
+	};
+}
+
+watch(asinMonthDate, (newVal) => {
+		const date = dayjs(newVal).startOf('month').format('YYYY-MM-DD');
+		console.log("=>(monthlyRating.vue:180) date", date);
+		if (date !== asinMonthDate.value) {
+			asinMonthDate.value = date;
+			console.log("=>(monthlyRating.vue:183) asinMonthDate.value", asinMonthDate.value);
+			fetchData();
+		}
+	}
+);
+
+onMounted(() => {
+	fetchData();
+});
+
+</script>
+
+<style scoped>
+.tool {
+	padding-right: 25px;
+}
+
+.title {
+	display: flex;
+	align-items: center; /* 垂直居中对齐 */
+	/* justify-content: space-between; !* 水平居中对齐 *! */
+	font-size: 16px;
+	font-weight: 500;
+	margin-bottom: 20px;
+}
+</style>

+ 154 - 0
src/views/scoreStatistics/components/overview.vue

@@ -0,0 +1,154 @@
+<template>
+	<el-card v-loading="loading" style="margin-bottom: 10px; height: 155px">
+		<div class="header-container">
+			<span style="font-size: 16px; font-weight: 500">统计概览</span>
+			<div>
+				<span style="font-size: 14px; color: #95969a">时间选择:</span>
+				<el-date-picker v-model="dateRange" :clearable="false" type="daterange"></el-date-picker>
+			</div>
+		</div>
+
+		<div class="stat-overview">
+			<div v-for="(item, index) in stats" :key="index" class="stat-item">
+				<div class="stat-label">{{ item.label }}</div>
+				<div class="stat-value">
+					{{ item.value }}
+					<el-tooltip content="下载" placement="top">
+						<el-button
+							v-if="item.label === '总评论数' || item.label === '新增好评数' || item.label === '新增中评数' || item.label === '新增差评数'"
+							:icon="Download"
+							:size="mini"
+							class="stat-button"
+							type="text"
+							@click.stop="handelDownload(item.label)"
+						>
+						</el-button>
+					</el-tooltip>
+				</div>
+			</div>
+		</div>
+	</el-card>
+</template>
+
+<script lang="ts" setup>
+import { getDownloadData, getOverviewData } from '/@/views/scoreStatistics/api';
+import dayjs from 'dayjs';
+import { Download } from '@element-plus/icons-vue';
+import { downloadFile } from '/@/utils/service';
+
+
+const loading = ref(false);
+const dateRange = ref([dayjs().startOf('month').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]);
+
+const stats = ref([]);
+const statsMapping = [
+	{ label: '监控商品数', field: 'monitor_goods_count' },
+	{ label: '监控竞品数', field: 'monitor_competitors_count' },
+	{ label: '总评论数', field: 'total_review_count' },
+	{ label: '新增好评数', field: 'good_review_count' },
+	{ label: '新增中评数', field: 'mid_review_count' },
+	{ label: '新增差评数', field: 'bad_review_count' },
+	{ label: '在售商品数', field: 'sale_goods_count' },
+	{ label: '停售商品数', field: 'discontinued_goods_count' },
+];
+
+async function fetchOverviewData() {
+	loading.value = true;
+	const query = {
+		query_start_date: dateRange.value[0],
+		query_end_date: dateRange.value[1],
+	};
+	try {
+		const { data } = await getOverviewData(query);
+		stats.value = statsMapping.map((stat) => ({
+			label: stat.label,
+			value: data[stat.field] || 0,
+		}));
+		loading.value = false;
+	} catch (e) {
+		console.error('获取数据时出错:', error);
+	}
+}
+
+async function handelDownload(label: string) {
+	loading.value = true;
+	const downloadType = {
+		'总评论数': 0,
+		'新增好评数': 1,
+		'新增中评数': 2,
+		'新增差评数': 3,
+	}[label];
+
+	try{
+		const query = {
+		download_type: downloadType,
+		query_start_date: dateRange.value[0],
+		query_end_date: dateRange.value[1],
+	};
+		const fileName = `${dateRange.value[0]}至${dateRange.value[1]}${label}.xlsx`;
+		const resp = await getDownloadData(query);
+		if(resp.code === 2000){
+			const url = window.URL.createObjectURL(resp.data);
+			const link = document.createElement('a');
+			link.href = url;
+			link.setAttribute('download', fileName);
+			document.body.appendChild(link);
+			link.click();
+			ElMessage.success('下载成功');
+		} else {
+			ElMessage.error('下载失败');
+		}
+		loading.value = false;
+	}catch (e) {
+		ElMessage.error('下载失败');
+	}
+}
+
+watch(dateRange, (newValue, oldValue) => {
+	if (newValue !== oldValue) {
+		dateRange.value[0]=dayjs(newValue[0]).format('YYYY-MM-DD');
+		dateRange.value[1]=dayjs(newValue[1]).format('YYYY-MM-DD');
+		fetchOverviewData();
+	}
+
+});
+
+onMounted(() => {
+	fetchOverviewData();
+});
+</script>
+
+<style scoped>
+.header-container {
+	display: flex;
+	align-items: center; /* 垂直居中对齐 */
+	justify-content: space-between; /* 水平居中对齐 */
+}
+
+.stat-overview {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 20px;
+	flex-wrap: wrap; /* 当屏幕宽度不够时换行 */
+}
+
+.stat-button {
+	height: 20px;
+}
+
+.stat-item {
+	text-align: center;
+}
+
+.stat-value {
+	font-size: 19px;
+	font-weight: bold;
+}
+
+.stat-label {
+	font-size: 14px;
+	color: #666;
+	font-weight: 500;
+}
+</style>

+ 23 - 0
src/views/scoreStatistics/index.vue

@@ -0,0 +1,23 @@
+<script lang="ts" setup>
+/**
+ * @Name: index.vue
+ * @Description:
+ * @Author: xinyan
+ */
+
+import LineChart from '/@/views/scoreStatistics/components/lineChart.vue';
+import MonthlyRating from '/@/views/scoreStatistics/components/monthlyRating.vue';
+import OverView from '/@/views/scoreStatistics/components/overView.vue';
+</script>
+
+<template>
+	<div class="flex-grow p-5">
+		<OverView></OverView>
+		<el-card style="margin: 10px 0">
+			<LineChart></LineChart>
+		</el-card>
+		<MonthlyRating></MonthlyRating>
+	</div>
+</template>
+
+<style scoped></style>