Compare commits
2 Commits
a6cd73c5b2
...
fe1ebce332
| Author | SHA1 | Date | |
|---|---|---|---|
| fe1ebce332 | |||
| 93cb47deaf |
@@ -1,76 +1,34 @@
|
||||
<template>
|
||||
<div class="trade-operation-analysis-product">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>商品数据统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 商品类目分布 -->
|
||||
<template #header><div class="card-header"><span>商品数据统计</span></div></template>
|
||||
<div class="chart-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">商品类目分布</div>
|
||||
</template>
|
||||
<template #header><div class="card-header">商品类目分布</div></template>
|
||||
<div ref="categoryChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品类目">
|
||||
<el-select v-model="searchParams.categoryId" placeholder="选择商品类目">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option v-for="category in categories" :key="category.id" :label="category.name" :value="category.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- SKU销售排行 -->
|
||||
<div class="sku-ranking">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">SKU销售排行</div>
|
||||
</template>
|
||||
<el-table :data="skuList" style="width: 100%">
|
||||
<template #header><div class="card-header">SKU销售排行</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围"><el-date-picker v-model="searchParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="商品类目"><el-select v-model="selectedCategoryId" placeholder="选择商品类目"><el-option label="全部" value="" /><el-option v-for="category in categories" :key="category.id" :label="category.name" :value="String(category.id)" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedSkuList" style="width: 100%">
|
||||
<el-table-column prop="rank" label="排名" width="80" />
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="sku" label="SKU" />
|
||||
<el-table-column prop="category" label="类目" />
|
||||
<el-table-column prop="sales" label="销售额" />
|
||||
<el-table-column prop="salesVolume" label="销量" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'hot' ? 'success' : 'warning'" size="small">{{
|
||||
scope.row.status === 'hot' ? '热销' : '滞销'
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态"><template #default="scope"><el-tag :type="scope.row.status === 'hot' ? 'success' : 'warning'" size="small">{{ scope.row.status === 'hot' ? '热销' : '滞销' }}</el-tag></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -79,191 +37,71 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
categoryId: '',
|
||||
});
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SkuItem {
|
||||
rank: number;
|
||||
productName: string;
|
||||
sku: string;
|
||||
category: string;
|
||||
sales: number;
|
||||
salesVolume: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const searchParams = reactive({ dateRange: [], categoryId: '' });
|
||||
interface Category { id: string; name: string }
|
||||
interface SkuItem { rank: number; productName: string; sku: string; category: string; sales: number; salesVolume: number; status: string }
|
||||
const categories = ref<Category[]>([]);
|
||||
const skuList = ref<SkuItem[]>([]);
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
const selectedCategoryId = computed({
|
||||
get: () => searchParams.categoryId,
|
||||
set: (value: string) => {
|
||||
searchParams.categoryId = value;
|
||||
},
|
||||
});
|
||||
|
||||
const skuList = ref<SkuItem[]>([]);
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const pagedSkuList = computed(() => skuList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize));
|
||||
const categoryChartRef = ref();
|
||||
let categoryChart: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟商品类目数据
|
||||
const getMockCategories = () => {
|
||||
return [
|
||||
{ id: 1, name: '电子产品' },
|
||||
{ id: 2, name: '服装鞋帽' },
|
||||
{ id: 3, name: '食品饮料' },
|
||||
{ id: 4, name: '家居用品' },
|
||||
{ id: 5, name: '美妆护肤' },
|
||||
];
|
||||
};
|
||||
const getMockCategories = () => [{ id: '1', name: '电子产品' }, { id: '2', name: '服装鞋帽' }, { id: '3', name: '食品饮料' }, { id: '4', name: '家居用品' }, { id: '5', name: '美妆护肤' }];
|
||||
const getMockSkuList = () => [
|
||||
{ rank: 1, productName: '智能手机', sku: 'SKU001', category: '电子产品', sales: 890000, salesVolume: 2500, status: 'hot' },
|
||||
{ rank: 2, productName: '运动跑鞋', sku: 'SKU002', category: '服装鞋帽', sales: 650000, salesVolume: 3200, status: 'hot' },
|
||||
{ rank: 3, productName: '高端耳机', sku: 'SKU003', category: '电子产品', sales: 420000, salesVolume: 1800, status: 'hot' },
|
||||
{ rank: 4, productName: '有机蔬菜', sku: 'SKU004', category: '食品饮料', sales: 380000, salesVolume: 5000, status: 'hot' },
|
||||
{ rank: 5, productName: '护肤套装', sku: 'SKU005', category: '美妆护肤', sales: 320000, salesVolume: 1200, status: 'hot' },
|
||||
{ rank: 6, productName: '办公椅', sku: 'SKU006', category: '家居用品', sales: 280000, salesVolume: 800, status: 'hot' },
|
||||
{ rank: 7, productName: 'T恤衫', sku: 'SKU007', category: '服装鞋帽', sales: 250000, salesVolume: 4500, status: 'hot' },
|
||||
{ rank: 8, productName: '厨房电器', sku: 'SKU008', category: '家居用品', sales: 180000, salesVolume: 600, status: 'warning' },
|
||||
{ rank: 9, productName: '零食礼包', sku: 'SKU009', category: '食品饮料', sales: 150000, salesVolume: 3000, status: 'warning' },
|
||||
{ rank: 10, productName: '化妆品', sku: 'SKU010', category: '美妆护肤', sales: 120000, salesVolume: 900, status: 'warning' },
|
||||
];
|
||||
const getMockCategoryDistribution = () => (searchParams.categoryId ? [{ categoryName: '电子产品', sales: 1310000 }] : [{ categoryName: '电子产品', sales: 1310000 }, { categoryName: '服装鞋帽', sales: 900000 }, { categoryName: '食品饮料', sales: 530000 }, { categoryName: '家居用品', sales: 460000 }, { categoryName: '美妆护肤', sales: 440000 }]);
|
||||
|
||||
// 模拟SKU销售排行数据
|
||||
const getMockSkuList = () => {
|
||||
const skus: SkuItem[] = [
|
||||
{ rank: 1, productName: '智能手机', sku: 'SKU001', category: '电子产品', sales: 890000, salesVolume: 2500, status: 'hot' },
|
||||
{ rank: 2, productName: '运动跑鞋', sku: 'SKU002', category: '服装鞋帽', sales: 650000, salesVolume: 3200, status: 'hot' },
|
||||
{ rank: 3, productName: '高端耳机', sku: 'SKU003', category: '电子产品', sales: 420000, salesVolume: 1800, status: 'hot' },
|
||||
{ rank: 4, productName: '有机蔬菜', sku: 'SKU004', category: '食品饮料', sales: 380000, salesVolume: 5000, status: 'hot' },
|
||||
{ rank: 5, productName: '护肤套装', sku: 'SKU005', category: '美妆护肤', sales: 320000, salesVolume: 1200, status: 'hot' },
|
||||
{ rank: 6, productName: '办公椅', sku: 'SKU006', category: '家居用品', sales: 280000, salesVolume: 800, status: 'hot' },
|
||||
{ rank: 7, productName: 'T恤衫', sku: 'SKU007', category: '服装鞋帽', sales: 250000, salesVolume: 4500, status: 'hot' },
|
||||
{ rank: 8, productName: '厨房电器', sku: 'SKU008', category: '家居用品', sales: 180000, salesVolume: 600, status: 'warning' },
|
||||
{ rank: 9, productName: '零食礼包', sku: 'SKU009', category: '食品饮料', sales: 150000, salesVolume: 3000, status: 'warning' },
|
||||
{ rank: 10, productName: '化妆品', sku: 'SKU010', category: '美妆护肤', sales: 120000, salesVolume: 900, status: 'warning' },
|
||||
];
|
||||
return skus;
|
||||
};
|
||||
|
||||
// 模拟类目分布数据
|
||||
const getMockCategoryDistribution = () => {
|
||||
return [
|
||||
{ categoryName: '电子产品', sales: 1310000 },
|
||||
{ categoryName: '服装鞋帽', sales: 900000 },
|
||||
{ categoryName: '食品饮料', sales: 530000 },
|
||||
{ categoryName: '家居用品', sales: 460000 },
|
||||
{ categoryName: '美妆护肤', sales: 440000 },
|
||||
];
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
categories.value = getMockCategories();
|
||||
skuList.value = getMockSkuList();
|
||||
pagination.total = 50;
|
||||
initCategoryChart(getMockCategoryDistribution());
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.categoryId = '';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
handleSearch();
|
||||
};
|
||||
const handleSearch = () => { categories.value = getMockCategories(); skuList.value = getMockSkuList(); pagination.total = skuList.value.length; pagination.currentPage = 1; initCategoryChart(getMockCategoryDistribution()); };
|
||||
const handleReset = () => { searchParams.dateRange = []; searchParams.categoryId = ''; pagination.currentPage = 1; pagination.pageSize = 10; handleSearch(); };
|
||||
const handleSizeChange = (size: number) => { pagination.pageSize = size; pagination.currentPage = 1; };
|
||||
const handleCurrentChange = (current: number) => { pagination.currentPage = current; };
|
||||
|
||||
const initCategoryChart = (categoryDistribution: any[]) => {
|
||||
if (!categoryChartRef.value) return;
|
||||
|
||||
if (categoryChart) {
|
||||
categoryChart.dispose();
|
||||
}
|
||||
|
||||
if (categoryChart) categoryChart.dispose();
|
||||
categoryChart = echarts.init(categoryChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: categoryDistribution.map((item) => item.categoryName),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '商品类目',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: categoryDistribution.map((item) => ({
|
||||
value: item.sales,
|
||||
name: item.categoryName,
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
categoryChart.setOption(option);
|
||||
categoryChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left', data: categoryDistribution.map((item) => item.categoryName) },
|
||||
series: [{ name: '商品类目', type: 'pie', radius: categoryDistribution.length <= 1 ? '35%' : '50%', data: categoryDistribution.map((item) => ({ value: item.sales, name: item.categoryName })), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } }],
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
categoryChart?.resize();
|
||||
});
|
||||
});
|
||||
onMounted(() => { handleSearch(); window.addEventListener('resize', () => categoryChart?.resize()); });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-analysis-product {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.sku-ranking {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.trade-operation-analysis-product { padding: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; }
|
||||
.search-container { margin-bottom: 16px; }
|
||||
.search-form { display: flex; align-items: center; flex-wrap: wrap; }
|
||||
.search-form :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }
|
||||
.trade-operation-analysis-product :deep(.el-select) { width: 180px; }
|
||||
.trade-operation-analysis-product :deep(.el-select__wrapper),
|
||||
.trade-operation-analysis-product :deep(.el-select__selected-item),
|
||||
.trade-operation-analysis-product :deep(.el-select__placeholder) { color: #303133; }
|
||||
.chart-container { margin: 0 0 20px; }
|
||||
.chart { width: 100%; height: 400px; }
|
||||
.sku-ranking { margin-top: 20px; }
|
||||
.pagination-container { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,24 @@
|
||||
<template>
|
||||
<div class="trade-operation-analysis-region">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>地域分布分析</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 地域分布图表 -->
|
||||
<template #header><div class="card-header"><span>地域分布分析</span></div></template>
|
||||
<div class="chart-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">地域销售分布</div>
|
||||
</template>
|
||||
<template #header><div class="card-header">地域销售分布</div></template>
|
||||
<div ref="regionChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="区域级别">
|
||||
<el-select v-model="searchParams.regionLevel" placeholder="选择区域级别">
|
||||
<el-option label="省份" value="province" />
|
||||
<el-option label="城市" value="city" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 地域销售排行 -->
|
||||
<div class="region-ranking">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">地域销售排行</div>
|
||||
</template>
|
||||
<el-table :data="regionList" style="width: 100%">
|
||||
<template #header><div class="card-header">地域销售排行</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围"><el-date-picker v-model="searchParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="区域级别"><el-select v-model="selectedRegionLevel" placeholder="选择区域级别"><el-option v-for="option in regionLevelOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedRegionList" style="width: 100%">
|
||||
<el-table-column prop="rank" label="排名" width="80" />
|
||||
<el-table-column prop="regionName" label="地区名称" />
|
||||
<el-table-column prop="sales" label="销售额" />
|
||||
@@ -55,15 +27,7 @@
|
||||
<el-table-column prop="poiCount" label="店铺数量" />
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -72,167 +36,67 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
regionLevel: 'province',
|
||||
const searchParams = reactive({ dateRange: [], regionLevel: 'province' });
|
||||
const regionLevelOptions = [
|
||||
{ label: '省份', value: 'province' },
|
||||
{ label: '城市', value: 'city' },
|
||||
];
|
||||
const selectedRegionLevel = computed({
|
||||
get: () => searchParams.regionLevel,
|
||||
set: (value: string) => {
|
||||
searchParams.regionLevel = value;
|
||||
},
|
||||
});
|
||||
|
||||
const regionList = ref([]);
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const regionList = ref<any[]>([]);
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const pagedRegionList = computed(() => regionList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize));
|
||||
const regionChartRef = ref();
|
||||
let regionChart: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟地域分布数据
|
||||
const getMockRegionDistribution = () => {
|
||||
const regions = [
|
||||
{ regionName: '北京', sales: 1200000 },
|
||||
{ regionName: '上海', sales: 1500000 },
|
||||
{ regionName: '广州', sales: 980000 },
|
||||
{ regionName: '深圳', sales: 1100000 },
|
||||
{ regionName: '杭州', sales: 850000 },
|
||||
{ regionName: '成都', sales: 780000 },
|
||||
{ regionName: '武汉', sales: 650000 },
|
||||
{ regionName: '西安', sales: 520000 },
|
||||
];
|
||||
return regions;
|
||||
};
|
||||
const getMockRegionDistribution = () => (searchParams.regionLevel === 'city' ? [{ regionName: '上海', sales: 1500000 }] : [{ regionName: '北京', sales: 1200000 }, { regionName: '上海', sales: 1500000 }, { regionName: '广州', sales: 980000 }, { regionName: '深圳', sales: 1100000 }]);
|
||||
const getMockRegionList = () => [
|
||||
{ rank: 1, regionName: '上海', sales: 1500000, orderCount: 3200, avgOrderValue: 468.75, poiCount: 25 },
|
||||
{ rank: 2, regionName: '北京', sales: 1200000, orderCount: 2800, avgOrderValue: 428.57, poiCount: 20 },
|
||||
{ rank: 3, regionName: '深圳', sales: 1100000, orderCount: 2600, avgOrderValue: 423.08, poiCount: 18 },
|
||||
{ rank: 4, regionName: '广州', sales: 980000, orderCount: 2300, avgOrderValue: 426.09, poiCount: 15 },
|
||||
{ rank: 5, regionName: '杭州', sales: 850000, orderCount: 2100, avgOrderValue: 404.76, poiCount: 12 },
|
||||
{ rank: 6, regionName: '成都', sales: 780000, orderCount: 1900, avgOrderValue: 410.53, poiCount: 10 },
|
||||
];
|
||||
|
||||
// 模拟地域销售排行数据
|
||||
const getMockRegionList = () => {
|
||||
const regions = [
|
||||
{ rank: 1, regionName: '上海', sales: 1500000, orderCount: 3200, avgOrderValue: 468.75, poiCount: 25 },
|
||||
{ rank: 2, regionName: '北京', sales: 1200000, orderCount: 2800, avgOrderValue: 428.57, poiCount: 20 },
|
||||
{ rank: 3, regionName: '深圳', sales: 1100000, orderCount: 2600, avgOrderValue: 423.08, poiCount: 18 },
|
||||
{ rank: 4, regionName: '广州', sales: 980000, orderCount: 2300, avgOrderValue: 426.09, poiCount: 15 },
|
||||
{ rank: 5, regionName: '杭州', sales: 850000, orderCount: 2100, avgOrderValue: 404.76, poiCount: 12 },
|
||||
{ rank: 6, regionName: '成都', sales: 780000, orderCount: 1900, avgOrderValue: 410.53, poiCount: 10 },
|
||||
{ rank: 7, regionName: '武汉', sales: 650000, orderCount: 1600, avgOrderValue: 406.25, poiCount: 8 },
|
||||
{ rank: 8, regionName: '西安', sales: 520000, orderCount: 1300, avgOrderValue: 400.0, poiCount: 6 },
|
||||
{ rank: 9, regionName: '南京', sales: 480000, orderCount: 1200, avgOrderValue: 400.0, poiCount: 5 },
|
||||
{ rank: 10, regionName: '重庆', sales: 450000, orderCount: 1100, avgOrderValue: 409.09, poiCount: 7 },
|
||||
];
|
||||
return regions;
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
regionList.value = getMockRegionList();
|
||||
pagination.total = 50;
|
||||
initRegionChart(getMockRegionDistribution());
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.regionLevel = 'province';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
handleSearch();
|
||||
};
|
||||
const handleSearch = () => { regionList.value = getMockRegionList(); pagination.total = regionList.value.length; pagination.currentPage = 1; initRegionChart(getMockRegionDistribution()); };
|
||||
const handleReset = () => { searchParams.dateRange = []; searchParams.regionLevel = 'province'; pagination.currentPage = 1; pagination.pageSize = 10; handleSearch(); };
|
||||
const handleSizeChange = (size: number) => { pagination.pageSize = size; pagination.currentPage = 1; };
|
||||
const handleCurrentChange = (current: number) => { pagination.currentPage = current; };
|
||||
|
||||
const initRegionChart = (regionDistribution: any[]) => {
|
||||
if (!regionChartRef.value) return;
|
||||
|
||||
if (regionChart) {
|
||||
regionChart.dispose();
|
||||
}
|
||||
|
||||
if (regionChart) regionChart.dispose();
|
||||
regionChart = echarts.init(regionChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: regionDistribution.map((item) => item.regionName),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '地域销售',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: regionDistribution.map((item) => ({
|
||||
value: item.sales,
|
||||
name: item.regionName,
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
regionChart.setOption(option);
|
||||
regionChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left', data: regionDistribution.map((item) => item.regionName) },
|
||||
series: [{ name: '地域销售', type: 'pie', radius: regionDistribution.length <= 1 ? '35%' : '50%', data: regionDistribution.map((item) => ({ value: item.sales, name: item.regionName })), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } }],
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
regionChart?.resize();
|
||||
});
|
||||
});
|
||||
onMounted(() => { handleSearch(); window.addEventListener('resize', () => regionChart?.resize()); });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-analysis-region {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.region-ranking {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.trade-operation-analysis-region { padding: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; }
|
||||
.search-container { margin-bottom: 16px; }
|
||||
.search-form { display: flex; align-items: center; flex-wrap: wrap; }
|
||||
.search-form :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }
|
||||
.trade-operation-analysis-region :deep(.el-select) { width: 180px; }
|
||||
.trade-operation-analysis-region :deep(.el-select__wrapper),
|
||||
.trade-operation-analysis-region :deep(.el-select__selected-item),
|
||||
.trade-operation-analysis-region :deep(.el-select__placeholder) { color: #303133; }
|
||||
.chart-container { margin: 0 0 20px; }
|
||||
.chart { width: 100%; height: 400px; }
|
||||
.region-ranking { margin-top: 20px; }
|
||||
.pagination-container { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
@@ -1,89 +1,58 @@
|
||||
<template>
|
||||
<div class="trade-operation-analysis-shop">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>店铺评分监控</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 评分趋势 -->
|
||||
<template #header><div class="card-header"><span>店铺评分监控</span></div></template>
|
||||
<div class="chart-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">评分趋势</div>
|
||||
</template>
|
||||
<template #header><div class="card-header">评分趋势 - {{ selectedShopName }}</div></template>
|
||||
<div ref="scoreChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间粒度">
|
||||
<el-select v-model="searchParams.granularity" placeholder="选择时间粒度">
|
||||
<el-option label="日" value="day" />
|
||||
<el-option label="周" value="week" />
|
||||
<el-option label="月" value="month" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 评分设置 -->
|
||||
<div class="score-setting">
|
||||
<div class="shop-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">评分预警设置</div>
|
||||
</template>
|
||||
<el-form :model="scoreSettings" label-width="120px">
|
||||
<el-form-item label="口碑分预警阈值">
|
||||
<el-input-number v-model="scoreSettings.reputationThreshold" :min="0" :max="5" :step="0.1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="体验分预警阈值">
|
||||
<el-input-number v-model="scoreSettings.experienceThreshold" :min="0" :max="5" :step="0.1" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveSettings">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #header><div class="card-header">店铺列表</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="店铺搜索">
|
||||
<el-input
|
||||
v-model="searchParams.shopKeyword"
|
||||
placeholder="请输入店铺名称或店铺ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围"><el-date-picker v-model="searchParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" :shortcuts="dateShortcuts" /></el-form-item>
|
||||
<el-form-item label="时间粒度">
|
||||
<el-radio-group v-model="searchParams.granularity" class="granularity-group">
|
||||
<el-radio-button v-for="option in granularityOptions" :key="option.value" :label="option.value">
|
||||
{{ option.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedShopList" style="width: 100%" highlight-current-row @row-click="handleShopClick">
|
||||
<el-table-column prop="id" label="店铺ID" width="100" />
|
||||
<el-table-column prop="name" label="店铺名称" />
|
||||
<el-table-column prop="type" label="店铺类型"><template #default="scope"><el-tag size="small">{{ scope.row.type === 'physical' ? '线下店铺' : '线上店铺' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="status" label="状态"><template #default="scope"><el-tag size="small" :type="scope.row.status === 'active' ? 'success' : 'danger'">{{ scope.row.status === 'active' ? '营业中' : '已关闭' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="120"><template #default="scope"><el-button type="primary" size="small" @click.stop="handleShopSelect(scope.row.id)">查看监控</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container"><el-pagination v-model:current-page="shopPagination.currentPage" v-model:page-size="shopPagination.pageSize" :page-sizes="[5, 10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" :total="shopPagination.total" @size-change="handleShopPageSizeChange" @current-change="handleShopPageChange" /></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 评分详情 -->
|
||||
<div class="score-setting"><el-card><template #header><div class="card-header">评分预警设置 - {{ selectedShopName }}</div></template><el-form :model="scoreSettings" label-width="120px"><el-form-item label="口碑分预警阈值"><el-input-number v-model="scoreSettings.reputationThreshold" :min="0" :max="5" :step="0.1" /></el-form-item><el-form-item label="体验分预警阈值"><el-input-number v-model="scoreSettings.experienceThreshold" :min="0" :max="5" :step="0.1" /></el-form-item><el-form-item><el-button type="primary" @click="handleSaveSettings">保存设置</el-button></el-form-item></el-form></el-card></div>
|
||||
<div class="score-detail">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">评分详情</div>
|
||||
</template>
|
||||
<el-table :data="scoreDetail" style="width: 100%">
|
||||
<template #header><div class="card-header">评分详情 - {{ selectedShopName }}</div></template>
|
||||
<el-table :data="pagedScoreDetail" style="width: 100%">
|
||||
<el-table-column prop="date" label="日期" />
|
||||
<el-table-column prop="reputationScore" label="口碑分">
|
||||
<template #default="scope">
|
||||
<div class="score-item" :class="{ warning: scope.row.reputationScore < scoreSettings.reputationThreshold }">
|
||||
{{ scope.row.reputationScore }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="experienceScore" label="体验分">
|
||||
<template #default="scope">
|
||||
<div class="score-item" :class="{ warning: scope.row.experienceScore < scoreSettings.experienceThreshold }">
|
||||
{{ scope.row.experienceScore }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reputationScore" label="口碑分"><template #default="scope"><div class="score-item" :class="{ warning: scope.row.reputationScore < scoreSettings.reputationThreshold }">{{ scope.row.reputationScore }}</div></template></el-table-column>
|
||||
<el-table-column prop="experienceScore" label="体验分"><template #default="scope"><div class="score-item" :class="{ warning: scope.row.experienceScore < scoreSettings.experienceThreshold }">{{ scope.row.experienceScore }}</div></template></el-table-column>
|
||||
<el-table-column prop="commentCount" label="评价数量" />
|
||||
</el-table>
|
||||
<div class="pagination-container"><el-pagination v-model:current-page="detailPagination.currentPage" v-model:page-size="detailPagination.pageSize" :page-sizes="[5, 10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" :total="detailPagination.total" @size-change="handleDetailPageSizeChange" @current-change="handleDetailPageChange" /></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -91,179 +60,195 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
granularity: 'day',
|
||||
});
|
||||
|
||||
const scoreSettings = reactive({
|
||||
reputationThreshold: 4.0,
|
||||
experienceThreshold: 4.2,
|
||||
});
|
||||
|
||||
interface ScoreDetail {
|
||||
date: string;
|
||||
reputationScore: string;
|
||||
experienceScore: string;
|
||||
commentCount: number;
|
||||
}
|
||||
|
||||
interface Shop { id: string; name: string; type: string; status: string }
|
||||
interface ScoreDetail { date: string; reputationScore: number; experienceScore: number; commentCount: number }
|
||||
const granularityOptions = [
|
||||
{ label: '小时', value: 'hour' },
|
||||
{ label: '日', value: 'day' },
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '季度', value: 'quarter' },
|
||||
{ label: '年', value: 'year' },
|
||||
];
|
||||
const searchParams = reactive({ shopId: 'all', shopKeyword: '', dateRange: [], granularity: 'day' });
|
||||
const dateShortcuts = [{ text: '最近7天', value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); return [start, end]; } }, { text: '最近30天', value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); return [start, end]; } }, { text: '最近90天', value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); return [start, end]; } }, { text: '今年', value: () => { const end = new Date(); const start = new Date(new Date().getFullYear(), 0, 1); return [start, end]; } }, { text: '去年', value: () => { const end = new Date(new Date().getFullYear() - 1, 11, 31); const start = new Date(new Date().getFullYear() - 1, 0, 1); return [start, end]; } }];
|
||||
const scoreSettings = reactive({ reputationThreshold: 4.0, experienceThreshold: 4.2 });
|
||||
const shopList = ref<Shop[]>([]);
|
||||
const scoreDetail = ref<ScoreDetail[]>([]);
|
||||
|
||||
const filteredShopList = computed(() => {
|
||||
const keyword = searchParams.shopKeyword.trim().toLowerCase();
|
||||
if (!keyword) return shopList.value;
|
||||
return shopList.value.filter((shop) => shop.name.toLowerCase().includes(keyword) || String(shop.id).toLowerCase().includes(keyword));
|
||||
});
|
||||
const shopPagination = reactive({ currentPage: 1, pageSize: 5, total: 0 });
|
||||
const detailPagination = reactive({ currentPage: 1, pageSize: 5, total: 0 });
|
||||
const pagedShopList = computed(() =>
|
||||
filteredShopList.value.slice((shopPagination.currentPage - 1) * shopPagination.pageSize, shopPagination.currentPage * shopPagination.pageSize)
|
||||
);
|
||||
const pagedScoreDetail = computed(() => scoreDetail.value.slice((detailPagination.currentPage - 1) * detailPagination.pageSize, detailPagination.currentPage * detailPagination.pageSize));
|
||||
const scoreChartRef = ref();
|
||||
let scoreChart: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟评分趋势数据
|
||||
const getMockScoreTrend = () => {
|
||||
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
return dates.map((date) => ({
|
||||
const getMockShopList = () => [{ id: '1', name: '旗舰店', type: 'online', status: 'active' }, { id: '2', name: '华东直营网点', type: 'physical', status: 'active' }, { id: '3', name: '华南直营店', type: 'physical', status: 'active' }, { id: '4', name: '品牌商城', type: 'online', status: 'active' }, { id: '5', name: '北区体验店', type: 'physical', status: 'closed' }];
|
||||
const selectedShopName = computed(() => { if (searchParams.shopId === 'all') return '全部店铺'; const shop = shopList.value.find((item) => String(item.id) === searchParams.shopId); return shop?.name || '未知店铺'; });
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const toDate = (value?: string | Date) => {
|
||||
if (!value) return new Date();
|
||||
return value instanceof Date ? new Date(value.getTime()) : new Date(`${value}T00:00:00`);
|
||||
};
|
||||
const formatDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${date.getDate()}`.padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
const getDateRange = () => {
|
||||
if (Array.isArray(searchParams.dateRange) && searchParams.dateRange.length === 2) {
|
||||
const [start, end] = searchParams.dateRange as Array<string | Date>;
|
||||
return { start: toDate(start), end: toDate(end) };
|
||||
}
|
||||
const end = new Date();
|
||||
const start = new Date(end.getTime() - 6 * DAY_IN_MS);
|
||||
return { start, end };
|
||||
};
|
||||
const getWeekLabel = (date: Date) => {
|
||||
const firstDay = new Date(date.getFullYear(), 0, 1);
|
||||
const dayOffset = Math.floor((date.getTime() - firstDay.getTime()) / DAY_IN_MS);
|
||||
return `${date.getFullYear()}年第${Math.floor(dayOffset / 7) + 1}周`;
|
||||
};
|
||||
const getQuarterLabel = (date: Date) => `${date.getFullYear()}年Q${Math.floor(date.getMonth() / 3) + 1}`;
|
||||
const buildTrendLabels = () => {
|
||||
const { start, end } = getDateRange();
|
||||
const labels: string[] = [];
|
||||
const cursor = new Date(start.getTime());
|
||||
if (searchParams.granularity === 'hour') {
|
||||
return Array.from({ length: 24 }, (_, index) => `${index}:00`);
|
||||
}
|
||||
if (searchParams.granularity === 'day') {
|
||||
while (cursor <= end) {
|
||||
labels.push(formatDate(cursor));
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
if (searchParams.granularity === 'week') {
|
||||
while (cursor <= end) {
|
||||
labels.push(getWeekLabel(cursor));
|
||||
cursor.setDate(cursor.getDate() + 7);
|
||||
}
|
||||
return [...new Set(labels)];
|
||||
}
|
||||
if (searchParams.granularity === 'month') {
|
||||
cursor.setDate(1);
|
||||
while (cursor <= end) {
|
||||
labels.push(`${cursor.getFullYear()}-${`${cursor.getMonth() + 1}`.padStart(2, '0')}`);
|
||||
cursor.setMonth(cursor.getMonth() + 1);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
if (searchParams.granularity === 'quarter') {
|
||||
cursor.setMonth(Math.floor(cursor.getMonth() / 3) * 3, 1);
|
||||
while (cursor <= end) {
|
||||
labels.push(getQuarterLabel(cursor));
|
||||
cursor.setMonth(cursor.getMonth() + 3);
|
||||
}
|
||||
return [...new Set(labels)];
|
||||
}
|
||||
cursor.setMonth(0, 1);
|
||||
while (cursor <= end) {
|
||||
labels.push(`${cursor.getFullYear()}年`);
|
||||
cursor.setFullYear(cursor.getFullYear() + 1);
|
||||
}
|
||||
return [...new Set(labels)];
|
||||
};
|
||||
const getShopSeed = (shopId: string) => (shopId && shopId !== 'all' ? Number(shopId) : 0);
|
||||
const getMockScoreTrend = (shopId: string) => {
|
||||
const labels = buildTrendLabels();
|
||||
const seed = getShopSeed(shopId);
|
||||
const baseReputation = 4.55 - seed * 0.08;
|
||||
const baseExperience = 4.62 - seed * 0.06;
|
||||
return labels.map((date, index) => ({
|
||||
date,
|
||||
reputationScore: 3.8 + Math.random() * 0.7,
|
||||
experienceScore: 4.0 + Math.random() * 0.5,
|
||||
reputationScore: Number(Math.min(5, Math.max(3.2, baseReputation + Math.sin(index + seed) * 0.18 + index * 0.01)).toFixed(2)),
|
||||
experienceScore: Number(Math.min(5, Math.max(3.3, baseExperience + Math.cos(index + seed) * 0.16 + index * 0.008)).toFixed(2)),
|
||||
}));
|
||||
};
|
||||
|
||||
// 模拟评分详情数据
|
||||
const getMockScoreDetail = () => {
|
||||
const dates = ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05'];
|
||||
return dates.map((date) => ({
|
||||
date,
|
||||
reputationScore: (3.8 + Math.random() * 0.7).toFixed(1),
|
||||
experienceScore: (4.0 + Math.random() * 0.5).toFixed(1),
|
||||
commentCount: 100 + Math.floor(Math.random() * 200),
|
||||
const getMockScoreDetail = (shopId: string) => {
|
||||
const trend = getMockScoreTrend(shopId);
|
||||
return trend.map((item, index) => ({
|
||||
date: item.date,
|
||||
reputationScore: item.reputationScore,
|
||||
experienceScore: item.experienceScore,
|
||||
commentCount: 120 + getShopSeed(shopId) * 18 + index * 15,
|
||||
}));
|
||||
};
|
||||
|
||||
const syncSelectedShop = () => {
|
||||
if (searchParams.shopId === 'all') return;
|
||||
const hasSelectedShop = filteredShopList.value.some((shop) => String(shop.id) === searchParams.shopId);
|
||||
if (!hasSelectedShop) {
|
||||
searchParams.shopId = 'all';
|
||||
}
|
||||
};
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
scoreDetail.value = getMockScoreDetail();
|
||||
initScoreChart(getMockScoreTrend());
|
||||
shopPagination.currentPage = 1;
|
||||
detailPagination.currentPage = 1;
|
||||
syncSelectedShop();
|
||||
shopPagination.total = filteredShopList.value.length;
|
||||
scoreDetail.value = getMockScoreDetail(searchParams.shopId);
|
||||
detailPagination.total = scoreDetail.value.length;
|
||||
initScoreChart(getMockScoreTrend(searchParams.shopId));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.shopId = 'all';
|
||||
searchParams.shopKeyword = '';
|
||||
searchParams.dateRange = [];
|
||||
searchParams.granularity = 'day';
|
||||
};
|
||||
|
||||
const handleSaveSettings = () => {
|
||||
// 保存评分预警设置
|
||||
// 这里可以调用API保存设置
|
||||
};
|
||||
|
||||
const initScoreChart = (scoreTrend: any[]) => {
|
||||
if (!scoreChartRef.value) return;
|
||||
|
||||
if (scoreChart) {
|
||||
scoreChart.dispose();
|
||||
}
|
||||
|
||||
scoreChart = echarts.init(scoreChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['口碑分', '体验分'],
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: scoreTrend.map((item) => item.date),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '评分',
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '口碑分',
|
||||
type: 'line',
|
||||
data: scoreTrend.map((item) => item.reputationScore),
|
||||
smooth: true,
|
||||
},
|
||||
{
|
||||
name: '体验分',
|
||||
type: 'line',
|
||||
data: scoreTrend.map((item) => item.experienceScore),
|
||||
smooth: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
scoreChart.setOption(option);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
shopPagination.currentPage = 1;
|
||||
detailPagination.currentPage = 1;
|
||||
handleSearch();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
scoreChart?.resize();
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleShopClick = (shop: Shop) => {
|
||||
searchParams.shopId = String(shop.id);
|
||||
handleSearch();
|
||||
};
|
||||
const handleShopSelect = (shopId: string) => {
|
||||
searchParams.shopId = String(shopId);
|
||||
handleSearch();
|
||||
};
|
||||
const handleShopPageSizeChange = (size: number) => { shopPagination.pageSize = size; shopPagination.currentPage = 1; };
|
||||
const handleShopPageChange = (page: number) => { shopPagination.currentPage = page; };
|
||||
const handleDetailPageSizeChange = (size: number) => { detailPagination.pageSize = size; detailPagination.currentPage = 1; };
|
||||
const handleDetailPageChange = (page: number) => { detailPagination.currentPage = page; };
|
||||
const handleSaveSettings = () => {};
|
||||
const initScoreChart = (scoreTrend: Array<{ date: string; reputationScore: number; experienceScore: number }>) => { if (!scoreChartRef.value) return; if (scoreChart) scoreChart.dispose(); scoreChart = echarts.init(scoreChartRef.value); const isSingle = scoreTrend.length <= 1; scoreChart.setOption({ tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } }, legend: { data: ['口碑分', '体验分'] }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', boundaryGap: isSingle, data: scoreTrend.map((item) => item.date) }, yAxis: { type: 'value', name: '评分', min: 0, max: 5 }, series: [{ name: '口碑分', type: 'line', data: scoreTrend.map((item) => item.reputationScore), smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 }, { name: '体验分', type: 'line', data: scoreTrend.map((item) => item.experienceScore), smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 }] }); };
|
||||
watch(
|
||||
() => filteredShopList.value.length,
|
||||
(length) => {
|
||||
shopPagination.total = length;
|
||||
const maxPage = Math.max(1, Math.ceil(length / shopPagination.pageSize));
|
||||
if (shopPagination.currentPage > maxPage) {
|
||||
shopPagination.currentPage = maxPage;
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(() => searchParams.granularity, () => handleSearch());
|
||||
onMounted(() => { shopList.value = getMockShopList(); shopPagination.total = shopList.value.length; handleSearch(); window.addEventListener('resize', () => scoreChart?.resize()); });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-analysis-shop {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.score-setting {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.score-detail {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.score-item.warning {
|
||||
background-color: #fde2e2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.trade-operation-analysis-shop { padding: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; }
|
||||
.search-container { margin-bottom: 16px; }
|
||||
.search-form { display: flex; align-items: center; flex-wrap: wrap; }
|
||||
.search-form :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }
|
||||
.granularity-group { flex-wrap: wrap; }
|
||||
.shop-list { margin-bottom: 20px; }
|
||||
.score-setting { margin-bottom: 20px; }
|
||||
.chart-container { margin: 0 0 20px; }
|
||||
.chart { width: 100%; height: 400px; }
|
||||
.score-detail { margin-top: 20px; }
|
||||
.score-item { padding: 2px 8px; border-radius: 10px; display: inline-block; }
|
||||
.score-item.warning { background-color: #fde2e2; color: #f56c6c; }
|
||||
.pagination-container { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
@@ -6,29 +6,6 @@
|
||||
<span>分销效果核算</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="达人名称">
|
||||
<el-input v-model="searchParams.anchorName" placeholder="请输入达人名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 分销效果趋势 -->
|
||||
<div class="chart-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
@@ -37,32 +14,25 @@
|
||||
<div ref="effectChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 核心指标 -->
|
||||
<div class="stats-cards">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">总销售额</div>
|
||||
<div class="stats-card-value">{{ statsData.totalSales || 0 }}</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">总佣金金额</div>
|
||||
<div class="stats-card-value">{{ statsData.totalCommission || 0 }}</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">平均佣金率</div>
|
||||
<div class="stats-card-value">{{ statsData.avgCommissionRate || 0 }}%</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">达人数量</div>
|
||||
<div class="stats-card-value">{{ statsData.anchorCount || 0 }}</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card"><div class="stats-card-title">总销售额</div><div class="stats-card-value">{{ statsData.totalSales || 0 }}</div></el-card>
|
||||
<el-card class="stats-card"><div class="stats-card-title">总佣金金额</div><div class="stats-card-value">{{ statsData.totalCommission || 0 }}</div></el-card>
|
||||
<el-card class="stats-card"><div class="stats-card-title">平均佣金率</div><div class="stats-card-value">{{ statsData.avgCommissionRate || 0 }}%</div></el-card>
|
||||
<el-card class="stats-card"><div class="stats-card-title">达人数量</div><div class="stats-card-value">{{ statsData.anchorCount || 0 }}</div></el-card>
|
||||
</div>
|
||||
<!-- 达人推广效果 -->
|
||||
<div class="anchor-effect">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">达人推广效果</div>
|
||||
</template>
|
||||
<el-table :data="anchorList" style="width: 100%">
|
||||
<template #header><div class="card-header">达人推广效果</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker v-model="searchParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="达人名称"><el-input v-model="searchParams.anchorName" placeholder="请输入达人名称" clearable /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedAnchorList" style="width: 100%">
|
||||
<el-table-column prop="rank" label="排名" width="80" />
|
||||
<el-table-column prop="anchorName" label="达人名称" />
|
||||
<el-table-column prop="sales" label="销售额" />
|
||||
@@ -72,15 +42,7 @@
|
||||
<el-table-column prop="conversionRate" label="转化率" />
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -89,223 +51,74 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
anchorName: '',
|
||||
});
|
||||
|
||||
const statsData = reactive({
|
||||
totalSales: 0,
|
||||
totalCommission: 0,
|
||||
avgCommissionRate: 0,
|
||||
anchorCount: 0,
|
||||
});
|
||||
|
||||
interface AnchorItem {
|
||||
rank: number;
|
||||
anchorName: string;
|
||||
sales: number;
|
||||
commission: number;
|
||||
commissionRate: number;
|
||||
orderCount: number;
|
||||
conversionRate: number;
|
||||
}
|
||||
|
||||
const searchParams = reactive({ dateRange: [], anchorName: '' });
|
||||
const statsData = reactive({ totalSales: 0, totalCommission: 0, avgCommissionRate: 0, anchorCount: 0 });
|
||||
interface AnchorItem { rank: number; anchorName: string; sales: number; commission: number; commissionRate: number; orderCount: number; conversionRate: number }
|
||||
const anchorList = ref<AnchorItem[]>([]);
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const pagedAnchorList = computed(() => anchorList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize));
|
||||
const effectChartRef = ref();
|
||||
let effectChart: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟分销效果趋势数据
|
||||
const getMockEffectTrend = () => {
|
||||
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
return dates.map((date) => ({
|
||||
date,
|
||||
sales: 500000 + Math.random() * 1000000,
|
||||
commission: 50000 + Math.random() * 200000,
|
||||
}));
|
||||
};
|
||||
|
||||
// 模拟达人推广效果数据
|
||||
const getMockAnchorList = () => {
|
||||
const anchors: AnchorItem[] = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
anchors.push({
|
||||
rank: i,
|
||||
anchorName: `达人${i}`,
|
||||
sales: 100000 + Math.random() * 900000,
|
||||
commission: 10000 + Math.random() * 180000,
|
||||
commissionRate: 10 + Math.random() * 10,
|
||||
orderCount: 100 + Math.floor(Math.random() * 900),
|
||||
conversionRate: parseFloat((1 + Math.random() * 9).toFixed(2)),
|
||||
});
|
||||
}
|
||||
return anchors;
|
||||
const dates = searchParams.anchorName ? ['本期'] : ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
return dates.map((date) => ({ date, sales: 500000 + Math.random() * 1000000, commission: 50000 + Math.random() * 200000 }));
|
||||
};
|
||||
const getMockAnchorList = () => Array.from({ length: 24 }, (_, i) => ({ rank: i + 1, anchorName: `达人${i + 1}`, sales: 100000 + Math.random() * 900000, commission: 10000 + Math.random() * 180000, commissionRate: +(10 + Math.random() * 10).toFixed(2), orderCount: 100 + Math.floor(Math.random() * 900), conversionRate: +(1 + Math.random() * 9).toFixed(2) }));
|
||||
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
statsData.totalSales = 5000000 + Math.random() * 5000000;
|
||||
statsData.totalCommission = 500000 + Math.random() * 500000;
|
||||
statsData.avgCommissionRate = 15 + Math.random() * 5;
|
||||
statsData.avgCommissionRate = +(15 + Math.random() * 5).toFixed(2);
|
||||
statsData.anchorCount = 50 + Math.floor(Math.random() * 50);
|
||||
anchorList.value = getMockAnchorList();
|
||||
pagination.total = 20;
|
||||
pagination.total = anchorList.value.length;
|
||||
pagination.currentPage = 1;
|
||||
initEffectChart(getMockEffectTrend());
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.anchorName = '';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
handleSearch();
|
||||
};
|
||||
const handleReset = () => { searchParams.dateRange = []; searchParams.anchorName = ''; pagination.currentPage = 1; pagination.pageSize = 10; handleSearch(); };
|
||||
const handleSizeChange = (size: number) => { pagination.pageSize = size; pagination.currentPage = 1; };
|
||||
const handleCurrentChange = (current: number) => { pagination.currentPage = current; };
|
||||
|
||||
const initEffectChart = (effectTrend: any[]) => {
|
||||
if (!effectChartRef.value) return;
|
||||
|
||||
if (effectChart) {
|
||||
effectChart.dispose();
|
||||
}
|
||||
|
||||
if (effectChart) effectChart.dispose();
|
||||
effectChart = echarts.init(effectChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['销售额', '佣金金额'],
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: effectTrend.map((item) => item.date),
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
position: 'left',
|
||||
},
|
||||
],
|
||||
const isSingle = effectTrend.length <= 1;
|
||||
effectChart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } },
|
||||
legend: { data: ['销售额', '佣金金额'] },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: isSingle, data: effectTrend.map((item) => item.date) },
|
||||
yAxis: [{ type: 'value', name: '金额', position: 'left' }],
|
||||
series: [
|
||||
{
|
||||
name: '销售额',
|
||||
type: 'line',
|
||||
data: effectTrend.map((item) => item.sales),
|
||||
smooth: true,
|
||||
},
|
||||
{
|
||||
name: '佣金金额',
|
||||
type: 'line',
|
||||
data: effectTrend.map((item) => item.commission),
|
||||
smooth: true,
|
||||
},
|
||||
{ name: '销售额', type: 'line', data: effectTrend.map((item) => item.sales), smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 },
|
||||
{ name: '佣金金额', type: 'line', data: effectTrend.map((item) => item.commission), smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 },
|
||||
],
|
||||
};
|
||||
|
||||
effectChart.setOption(option);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
effectChart?.resize();
|
||||
});
|
||||
window.addEventListener('resize', () => effectChart?.resize());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-distribution-effect {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-card-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats-card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.anchor-effect {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
.trade-operation-distribution-effect { padding: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; }
|
||||
.search-container { margin-bottom: 16px; }
|
||||
.search-form { display: flex; align-items: center; flex-wrap: wrap; }
|
||||
.search-form :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }
|
||||
.stats-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
|
||||
.stats-card { text-align: center; padding: 20px; }
|
||||
.stats-card-title { font-size: 14px; color: #606266; margin-bottom: 10px; }
|
||||
.stats-card-value { font-size: 24px; font-weight: bold; }
|
||||
.anchor-effect { margin-bottom: 20px; }
|
||||
.pagination-container { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
.chart-container { margin: 0 0 20px; }
|
||||
.chart { width: 100%; height: 400px; }
|
||||
</style>
|
||||
|
||||
@@ -1,85 +1,37 @@
|
||||
<template>
|
||||
<div class="trade-operation-distribution-order">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>分销订单查询</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="订单类型">
|
||||
<el-select v-model="searchParams.orderType" placeholder="选择订单类型">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="快分销二创订单" value="quick_distribution" />
|
||||
<el-option label="分销达人推广订单" value="anchor_promotion" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="searchParams.orderNo" placeholder="请输入订单号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="达人名称">
|
||||
<el-input v-model="searchParams.anchorName" placeholder="请输入达人名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 订单列表 -->
|
||||
<template #header><div class="card-header"><span>分销订单查询</span></div></template>
|
||||
<div class="order-list">
|
||||
<el-table :data="orderList" style="width: 100%">
|
||||
<el-table-column prop="orderNo" label="订单号" />
|
||||
<el-table-column prop="orderType" label="订单类型">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">
|
||||
{{ scope.row.orderType === 'quick_distribution' ? '快分销二创' : '达人推广' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="anchorName" label="达人名称" />
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="amount" label="订单金额" />
|
||||
<el-table-column prop="commission" label="佣金金额" />
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="handleOrderDetail(scope.row.id)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
<el-card>
|
||||
<template #header><div class="card-header">订单列表</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="订单类型"><el-select v-model="selectedOrderType" placeholder="选择订单类型"><el-option v-for="option in orderTypeOptions" :key="option.value || 'all'" :label="option.label" :value="option.value" /></el-select></el-form-item>
|
||||
<el-form-item label="订单号"><el-input v-model="searchParams.orderNo" placeholder="请输入订单号" clearable /></el-form-item>
|
||||
<el-form-item label="达人名称"><el-input v-model="searchParams.anchorName" placeholder="请输入达人名称" clearable /></el-form-item>
|
||||
<el-form-item label="时间范围"><el-date-picker v-model="searchParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedOrderList" style="width: 100%">
|
||||
<el-table-column prop="orderNo" label="订单号" />
|
||||
<el-table-column prop="orderType" label="订单类型"><template #default="scope"><el-tag size="small">{{ scope.row.orderType === 'quick_distribution' ? '快分销二创' : '达人推广' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="anchorName" label="达人名称" />
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="amount" label="订单金额" />
|
||||
<el-table-column prop="commission" label="佣金金额" />
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column label="操作"><template #default="scope"><el-button type="primary" size="small" @click="handleOrderDetail(scope.row.id)">查看详情</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container"><el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="订单详情" width="80%">
|
||||
<div v-if="orderDetail" class="order-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="订单号">{{ orderDetail.orderNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单类型">{{
|
||||
orderDetail.orderType === 'quick_distribution' ? '快分销二创' : '达人推广'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单类型">{{ orderDetail.orderType === 'quick_distribution' ? '快分销二创' : '达人推广' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="达人名称">{{ orderDetail.anchorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">{{ orderDetail.amount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="佣金金额">{{ orderDetail.commission }}</el-descriptions-item>
|
||||
@@ -87,27 +39,8 @@
|
||||
<el-descriptions-item label="创建时间">{{ orderDetail.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ orderDetail.status }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 商品信息 -->
|
||||
<div class="detail-section">
|
||||
<h3>商品信息</h3>
|
||||
<el-table :data="orderDetail.products" style="width: 100%">
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="sku" label="SKU" />
|
||||
<el-table-column prop="quantity" label="数量" />
|
||||
<el-table-column prop="price" label="单价" />
|
||||
<el-table-column prop="commission" label="佣金" />
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 推广信息 -->
|
||||
<div class="detail-section">
|
||||
<h3>推广信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="推广渠道">{{ orderDetail.promotion.channel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推广时间">{{ orderDetail.promotion.time }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推广链接">{{ orderDetail.promotion.link }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推广效果">{{ orderDetail.promotion.effect }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="detail-section"><h3>商品信息</h3><el-table :data="orderDetail.products" style="width: 100%"><el-table-column prop="productName" label="商品名称" /><el-table-column prop="sku" label="SKU" /><el-table-column prop="quantity" label="数量" /><el-table-column prop="price" label="单价" /><el-table-column prop="commission" label="佣金" /></el-table></div>
|
||||
<div class="detail-section"><h3>推广信息</h3><el-descriptions :column="2" border><el-descriptions-item label="推广渠道">{{ orderDetail.promotion.channel }}</el-descriptions-item><el-descriptions-item label="推广时间">{{ orderDetail.promotion.time }}</el-descriptions-item><el-descriptions-item label="推广链接">{{ orderDetail.promotion.link }}</el-descriptions-item><el-descriptions-item label="推广效果">{{ orderDetail.promotion.effect }}</el-descriptions-item></el-descriptions></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
@@ -115,193 +48,49 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
|
||||
const searchParams = reactive({
|
||||
orderType: '',
|
||||
orderNo: '',
|
||||
anchorName: '',
|
||||
dateRange: [],
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
const searchParams = reactive({ orderType: '', orderNo: '', anchorName: '', dateRange: [] });
|
||||
const orderTypeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '快分销二创订单', value: 'quick_distribution' },
|
||||
{ label: '分销达人推广订单', value: 'anchor_promotion' },
|
||||
];
|
||||
const selectedOrderType = computed({
|
||||
get: () => searchParams.orderType,
|
||||
set: (value: string) => {
|
||||
searchParams.orderType = value;
|
||||
},
|
||||
});
|
||||
|
||||
interface OrderItem {
|
||||
id: number;
|
||||
orderNo: string;
|
||||
orderType: string;
|
||||
anchorName: string;
|
||||
productName: string;
|
||||
amount: number;
|
||||
commission: number;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
interface OrderItem { id: number; orderNo: string; orderType: string; anchorName: string; productName: string; amount: number; commission: number; createTime: string }
|
||||
const orderList = ref<OrderItem[]>([]);
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
interface OrderDetail {
|
||||
orderNo: string;
|
||||
orderType: string;
|
||||
anchorName: string;
|
||||
amount: number;
|
||||
commission: number;
|
||||
commissionRate: number;
|
||||
createTime: string;
|
||||
status: string;
|
||||
products: {
|
||||
productName: string;
|
||||
sku: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
commission: number;
|
||||
}[];
|
||||
promotion: {
|
||||
channel: string;
|
||||
time: string;
|
||||
link: string;
|
||||
effect: string;
|
||||
};
|
||||
}
|
||||
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const pagedOrderList = computed(() => orderList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize));
|
||||
interface OrderDetail { orderNo: string; orderType: string; anchorName: string; amount: number; commission: number; commissionRate: number; createTime: string; status: string; products: { productName: string; sku: string; quantity: number; price: number; commission: number }[]; promotion: { channel: string; time: string; link: string; effect: string } }
|
||||
const dialogVisible = ref(false);
|
||||
const orderDetail = ref<OrderDetail | null>(null);
|
||||
|
||||
// 模拟分销订单列表数据
|
||||
const getMockOrderList = () => {
|
||||
const orderTypes = ['quick_distribution', 'anchor_promotion'];
|
||||
const orders: OrderItem[] = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
orders.push({
|
||||
id: i,
|
||||
orderNo: `DO${Date.now() + i}`,
|
||||
orderType: orderTypes[Math.floor(Math.random() * orderTypes.length)],
|
||||
anchorName: `达人${i}`,
|
||||
productName: `商品${i}`,
|
||||
amount: 1000 + Math.random() * 9000,
|
||||
commission: 100 + Math.random() * 900,
|
||||
createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
});
|
||||
}
|
||||
return orders;
|
||||
};
|
||||
|
||||
// 模拟分销订单详情数据
|
||||
const getMockOrderDetail = (id: number) => {
|
||||
return {
|
||||
orderNo: `DO${Date.now() + id}`,
|
||||
orderType: Math.random() > 0.5 ? 'quick_distribution' : 'anchor_promotion',
|
||||
anchorName: `达人${id}`,
|
||||
amount: 5000 + Math.random() * 5000,
|
||||
commission: 500 + Math.random() * 500,
|
||||
commissionRate: 10 + Math.random() * 10,
|
||||
createTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: '已完成',
|
||||
products: [
|
||||
{
|
||||
productName: '商品1',
|
||||
sku: 'SKU001',
|
||||
quantity: 2,
|
||||
price: 1500,
|
||||
commission: 300,
|
||||
},
|
||||
{
|
||||
productName: '商品2',
|
||||
sku: 'SKU002',
|
||||
quantity: 1,
|
||||
price: 2500,
|
||||
commission: 500,
|
||||
},
|
||||
],
|
||||
promotion: {
|
||||
channel: '抖音',
|
||||
time: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
link: `https://example.com/promotion/${id}`,
|
||||
effect: '良好',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
orderList.value = getMockOrderList();
|
||||
pagination.total = 20;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.orderType = '';
|
||||
searchParams.orderNo = '';
|
||||
searchParams.anchorName = '';
|
||||
searchParams.dateRange = [];
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleOrderDetail = (id: number) => {
|
||||
// 使用模拟数据
|
||||
orderDetail.value = getMockOrderDetail(id);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
const getMockOrderList = () => Array.from({ length: 24 }, (_, i) => ({ id: i + 1, orderNo: `DO${Date.now() + i}`, orderType: Math.random() > 0.5 ? 'quick_distribution' : 'anchor_promotion', anchorName: `达人${i + 1}`, productName: `商品${i + 1}`, amount: 1000 + Math.random() * 9000, commission: 100 + Math.random() * 900, createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString() }));
|
||||
const getMockOrderDetail = (id: number) => ({ orderNo: `DO${Date.now() + id}`, orderType: Math.random() > 0.5 ? 'quick_distribution' : 'anchor_promotion', anchorName: `达人${id}`, amount: 5000 + Math.random() * 5000, commission: 500 + Math.random() * 500, commissionRate: +(10 + Math.random() * 10).toFixed(2), createTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), status: '已完成', products: [{ productName: '商品1', sku: 'SKU001', quantity: 2, price: 1500, commission: 300 }, { productName: '商品2', sku: 'SKU002', quantity: 1, price: 2500, commission: 500 }], promotion: { channel: '抖音', time: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000).toISOString(), link: `https://example.com/promotion/${id}`, effect: '良好' } });
|
||||
const handleSearch = () => { orderList.value = getMockOrderList(); pagination.total = orderList.value.length; pagination.currentPage = 1; };
|
||||
const handleReset = () => { searchParams.orderType = ''; searchParams.orderNo = ''; searchParams.anchorName = ''; searchParams.dateRange = []; pagination.currentPage = 1; pagination.pageSize = 10; handleSearch(); };
|
||||
const handleSizeChange = (size: number) => { pagination.pageSize = size; pagination.currentPage = 1; };
|
||||
const handleCurrentChange = (current: number) => { pagination.currentPage = current; };
|
||||
const handleOrderDetail = (id: number) => { orderDetail.value = getMockOrderDetail(id); dialogVisible.value = true; };
|
||||
onMounted(() => handleSearch());
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-distribution-order {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.order-detail {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
color: #606266;
|
||||
}
|
||||
.trade-operation-distribution-order { padding: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; }
|
||||
.search-container { margin-bottom: 16px; }
|
||||
.search-form { display: flex; align-items: center; flex-wrap: wrap; }
|
||||
.search-form :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }
|
||||
.trade-operation-distribution-order :deep(.el-select) { width: 180px; }
|
||||
.trade-operation-distribution-order :deep(.el-select__wrapper),
|
||||
.trade-operation-distribution-order :deep(.el-select__selected-item),
|
||||
.trade-operation-distribution-order :deep(.el-select__placeholder) { color: #303133; }
|
||||
.order-list { margin-top: 20px; }
|
||||
.pagination-container { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
.order-detail { padding: 20px 0; }
|
||||
.detail-section { margin-top: 30px; }
|
||||
.detail-section h3 { font-size: 14px; font-weight: bold; margin-bottom: 15px; color: #606266; }
|
||||
</style>
|
||||
|
||||
@@ -1,76 +1,63 @@
|
||||
<template>
|
||||
<div class="trade-operation-order">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>订单管理</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="searchParams.orderNo" placeholder="请输入订单号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品名称">
|
||||
<el-input v-model="searchParams.productName" placeholder="请输入商品名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单状态">
|
||||
<el-select v-model="searchParams.status" placeholder="选择订单状态">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待发货" value="pending_shipping" />
|
||||
<el-option label="已发货" value="shipped" />
|
||||
<el-option label="已签收" value="signed" />
|
||||
<el-option label="已退款" value="refunded" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 订单列表 -->
|
||||
<template #header
|
||||
><div class="card-header"><span>订单管理</span></div></template
|
||||
>
|
||||
<div class="order-list">
|
||||
<el-table :data="orderList" style="width: 100%">
|
||||
<el-table-column prop="orderNo" label="订单号" />
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="amount" label="订单金额" />
|
||||
<el-table-column prop="status" label="订单状态">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.status)" size="small">{{ getStatusText(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="handleOrderDetail(scope.row.id)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
<el-card>
|
||||
<template #header><div class="card-header">订单列表</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="订单号"><el-input v-model="searchParams.orderNo" placeholder="请输入订单号" clearable /></el-form-item>
|
||||
<el-form-item label="商品名称"><el-input v-model="searchParams.productName" placeholder="请输入商品名称" clearable /></el-form-item>
|
||||
<el-form-item label="订单状态"
|
||||
><el-select v-model="searchParams.status" placeholder="选择订单状态"
|
||||
><el-option v-for="option in statusOptions" :key="option.value || 'all'" :label="option.label" :value="option.value" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="时间范围"
|
||||
><el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedOrderList" style="width: 100%">
|
||||
<el-table-column prop="orderNo" label="订单号" />
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="amount" label="订单金额" />
|
||||
<el-table-column prop="status" label="订单状态"
|
||||
><template #default="scope"
|
||||
><el-tag :type="getStatusTagType(scope.row.status)" size="small">{{ getStatusText(scope.row.status) }}</el-tag></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
<el-table-column label="操作"
|
||||
><template #default="scope"
|
||||
><el-button type="primary" size="small" @click="handleOrderDetail(scope.row.id)">查看详情</el-button></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog v-model="dialogVisible" title="订单详情" width="80%">
|
||||
<div v-if="orderDetail" class="order-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
@@ -83,51 +70,31 @@
|
||||
<el-descriptions-item label="签收时间">{{ orderDetail.signTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="退款时间">{{ orderDetail.refundTime || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 商品信息 -->
|
||||
<div class="detail-section">
|
||||
<h3>商品信息</h3>
|
||||
<el-table :data="orderDetail.products" style="width: 100%">
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="sku" label="SKU" />
|
||||
<el-table-column prop="quantity" label="数量" />
|
||||
<el-table-column prop="price" label="单价" />
|
||||
<el-table-column prop="subtotal" label="小计" />
|
||||
</el-table>
|
||||
<el-table :data="orderDetail.products" style="width: 100%"
|
||||
><el-table-column prop="productName" label="商品名称" /><el-table-column prop="sku" label="SKU" /><el-table-column
|
||||
prop="quantity"
|
||||
label="数量" /><el-table-column prop="price" label="单价" /><el-table-column prop="subtotal" label="小计"
|
||||
/></el-table>
|
||||
</div>
|
||||
<!-- 物流信息 -->
|
||||
<div class="detail-section">
|
||||
<h3>物流信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="快递公司">{{ orderDetail.logistics?.company || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="物流单号">{{ orderDetail.logistics?.trackingNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发货地址">{{ orderDetail.logistics?.senderAddress || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址">{{ orderDetail.logistics?.receiverAddress || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="2" border
|
||||
><el-descriptions-item label="快递公司">{{ orderDetail.logistics?.company || '-' }}</el-descriptions-item
|
||||
><el-descriptions-item label="物流单号">{{ orderDetail.logistics?.trackingNo || '-' }}</el-descriptions-item
|
||||
><el-descriptions-item label="发货地址">{{ orderDetail.logistics?.senderAddress || '-' }}</el-descriptions-item
|
||||
><el-descriptions-item label="收货地址">{{ orderDetail.logistics?.receiverAddress || '-' }}</el-descriptions-item></el-descriptions
|
||||
>
|
||||
</div>
|
||||
<!-- 分销信息 -->
|
||||
<div class="detail-section" v-if="orderDetail.distribution">
|
||||
<h3>分销信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分销达人">{{ orderDetail.distribution.anchorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="佣金金额">{{ orderDetail.distribution.commission }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分销类型">{{ orderDetail.distribution.type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推广时间">{{ orderDetail.distribution.promotionTime }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<!-- 订单进度 -->
|
||||
<div class="detail-section">
|
||||
<h3>订单进度</h3>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(item, index) in orderDetail.progress"
|
||||
:key="index"
|
||||
:timestamp="item.time"
|
||||
:type="item.status === 'completed' ? 'success' : 'primary'"
|
||||
:icon="item.status === 'completed' ? 'el-icon-check' : 'el-icon-loading'"
|
||||
>
|
||||
{{ item.description }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-descriptions :column="2" border
|
||||
><el-descriptions-item label="分销达人">{{ orderDetail.distribution.anchorName }}</el-descriptions-item
|
||||
><el-descriptions-item label="佣金金额">{{ orderDetail.distribution.commission }}</el-descriptions-item
|
||||
><el-descriptions-item label="分销类型">{{ orderDetail.distribution.type }}</el-descriptions-item
|
||||
><el-descriptions-item label="推广时间">{{ orderDetail.distribution.promotionTime }}</el-descriptions-item></el-descriptions
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -136,15 +103,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
|
||||
const searchParams = reactive({
|
||||
orderNo: '',
|
||||
productName: '',
|
||||
status: '',
|
||||
dateRange: [],
|
||||
});
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
const searchParams = reactive({ orderNo: '', productName: '', status: '', dateRange: [] });
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待发货', value: 'pending_shipping' },
|
||||
{ label: '已发货', value: 'shipped' },
|
||||
{ label: '已签收', value: 'signed' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
];
|
||||
interface OrderItem {
|
||||
id: number;
|
||||
orderNo: string;
|
||||
@@ -153,15 +120,11 @@ interface OrderItem {
|
||||
status: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
const orderList = ref<OrderItem[]>([]);
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const pagedOrderList = computed(() =>
|
||||
orderList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize)
|
||||
);
|
||||
interface OrderDetail {
|
||||
orderNo: string;
|
||||
amount: number;
|
||||
@@ -171,142 +134,50 @@ interface OrderDetail {
|
||||
shipTime: string;
|
||||
signTime: string;
|
||||
refundTime: string;
|
||||
products: {
|
||||
productName: string;
|
||||
sku: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
subtotal: number;
|
||||
}[];
|
||||
logistics: {
|
||||
company: string;
|
||||
trackingNo: string;
|
||||
senderAddress: string;
|
||||
receiverAddress: string;
|
||||
};
|
||||
distribution: {
|
||||
anchorName: string;
|
||||
commission: number;
|
||||
type: string;
|
||||
promotionTime: string;
|
||||
};
|
||||
progress: {
|
||||
time: string;
|
||||
description: string;
|
||||
status: string;
|
||||
}[];
|
||||
products: { productName: string; sku: string; quantity: number; price: number; subtotal: number }[];
|
||||
logistics: { company: string; trackingNo: string; senderAddress: string; receiverAddress: string };
|
||||
distribution: { anchorName: string; commission: number; type: string; promotionTime: string };
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const orderDetail = ref<OrderDetail | null>(null);
|
||||
|
||||
// 模拟订单列表数据
|
||||
const getMockOrderList = () => {
|
||||
const statuses = ['pending_shipping', 'shipped', 'signed', 'refunded'];
|
||||
const orders: OrderItem[] = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
orders.push({
|
||||
id: i,
|
||||
orderNo: `ORDER${Date.now() + i}`,
|
||||
productName: `商品${i}`,
|
||||
amount: 1000 + Math.random() * 9000,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
});
|
||||
}
|
||||
return orders;
|
||||
};
|
||||
|
||||
// 模拟订单详情数据
|
||||
const getMockOrderDetail = (id: number) => {
|
||||
return {
|
||||
orderNo: `ORDER${Date.now() + id}`,
|
||||
amount: 5000 + Math.random() * 5000,
|
||||
status: 'signed',
|
||||
createTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
payTime: new Date(Date.now() - 9.5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
shipTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
signTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
refundTime: '',
|
||||
products: [
|
||||
{
|
||||
productName: '商品1',
|
||||
sku: 'SKU001',
|
||||
quantity: 2,
|
||||
price: 1500,
|
||||
subtotal: 3000,
|
||||
},
|
||||
{
|
||||
productName: '商品2',
|
||||
sku: 'SKU002',
|
||||
quantity: 1,
|
||||
price: 2500,
|
||||
subtotal: 2500,
|
||||
},
|
||||
],
|
||||
logistics: {
|
||||
company: '顺丰速运',
|
||||
trackingNo: `SF${Math.floor(Math.random() * 10000000000)}`,
|
||||
senderAddress: '上海市浦东新区',
|
||||
receiverAddress: '北京市朝阳区',
|
||||
},
|
||||
distribution: {
|
||||
anchorName: '主播A',
|
||||
commission: 500,
|
||||
type: '二创',
|
||||
promotionTime: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
progress: [
|
||||
{
|
||||
time: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
description: '订单创建',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
time: new Date(Date.now() - 9.5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
description: '订单支付',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
time: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
description: '订单发货',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
description: '订单签收',
|
||||
status: 'completed',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending_shipping: '待发货',
|
||||
shipped: '已发货',
|
||||
signed: '已签收',
|
||||
refunded: '已退款',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
pending_shipping: 'info',
|
||||
shipped: 'primary',
|
||||
signed: 'success',
|
||||
refunded: 'warning',
|
||||
};
|
||||
return typeMap[status] || 'info';
|
||||
};
|
||||
|
||||
const getMockOrderList = () =>
|
||||
Array.from({ length: 24 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
orderNo: `ORDER${Date.now() + i}`,
|
||||
productName: `商品${i + 1}`,
|
||||
amount: 1000 + Math.random() * 9000,
|
||||
status: ['pending_shipping', 'shipped', 'signed', 'refunded'][Math.floor(Math.random() * 4)],
|
||||
createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
}));
|
||||
const getMockOrderDetail = (id: number) => ({
|
||||
orderNo: `ORDER${Date.now() + id}`,
|
||||
amount: 5000 + Math.random() * 5000,
|
||||
status: 'signed',
|
||||
createTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
payTime: new Date(Date.now() - 9.5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
shipTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
signTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
refundTime: '',
|
||||
products: [
|
||||
{ productName: '商品1', sku: 'SKU001', quantity: 2, price: 1500, subtotal: 3000 },
|
||||
{ productName: '商品2', sku: 'SKU002', quantity: 1, price: 2500, subtotal: 2500 },
|
||||
],
|
||||
logistics: {
|
||||
company: '顺丰速运',
|
||||
trackingNo: `SF${Math.floor(Math.random() * 10000000000)}`,
|
||||
senderAddress: '上海市浦东新区',
|
||||
receiverAddress: '北京市朝阳区',
|
||||
},
|
||||
distribution: { anchorName: '主播A', commission: 500, type: '二创', promotionTime: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000).toISOString() },
|
||||
});
|
||||
const getStatusText = (status: string) => ({ pending_shipping: '待发货', shipped: '已发货', signed: '已签收', refunded: '已退款' })[status] || status;
|
||||
const getStatusTagType = (status: string) =>
|
||||
({ pending_shipping: 'info', shipped: 'primary', signed: 'success', refunded: 'warning' })[status] || 'info';
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
orderList.value = getMockOrderList();
|
||||
pagination.total = 20;
|
||||
pagination.total = orderList.value.length;
|
||||
pagination.currentPage = 1;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.orderNo = '';
|
||||
searchParams.productName = '';
|
||||
@@ -314,66 +185,67 @@ const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
handleSearch();
|
||||
pagination.currentPage = 1;
|
||||
};
|
||||
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleOrderDetail = (id: number) => {
|
||||
// 使用模拟数据
|
||||
orderDetail.value = getMockOrderDetail(id);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
onMounted(() => handleSearch());
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-order {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.trade-operation-order :deep(.el-select) {
|
||||
width: 180px;
|
||||
}
|
||||
.trade-operation-order :deep(.el-select__wrapper),
|
||||
.trade-operation-order :deep(.el-select__selected-item),
|
||||
.trade-operation-order :deep(.el-select__placeholder) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.order-detail {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -1,66 +1,38 @@
|
||||
<template>
|
||||
<div class="trade-operation-stats-anchor">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>主播维度统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 主播销售趋势 -->
|
||||
<template #header><div class="card-header"><span>主播维度统计</span></div></template>
|
||||
<div class="chart-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">主播销售趋势</div>
|
||||
</template>
|
||||
<template #header><div class="card-header">主播销售趋势 - {{ currentAnchorName }}</div></template>
|
||||
<div ref="salesChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="主播">
|
||||
<el-input v-model="searchParams.anchorName" placeholder="请输入主播名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 主播业绩排行 -->
|
||||
<div class="anchor-ranking">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">主播业绩排行</div>
|
||||
</template>
|
||||
<el-table :data="anchorList" style="width: 100%">
|
||||
<template #header><div class="card-header">主播业绩排行</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="时间范围"><el-date-picker v-model="searchParams.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="主播"><el-input v-model="searchParams.anchorName" placeholder="请输入主播名称" clearable @keyup.enter="handleSearch" /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table ref="anchorTableRef" :data="pagedAnchorList" style="width: 100%" row-key="anchorName" highlight-current-row @row-click="handleAnchorClick">
|
||||
<el-table-column prop="rank" label="排名" width="80" />
|
||||
<el-table-column prop="anchorName" label="主播名称" />
|
||||
<el-table-column prop="sales" label="销售额" />
|
||||
<el-table-column prop="orderCount" label="订单数" />
|
||||
<el-table-column prop="signRate" label="签收率" />
|
||||
<el-table-column prop="repurchaseRate" label="复购率" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click.stop="handleAnchorSelect(scope.row.anchorName)">查看趋势</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -69,169 +41,195 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
anchorName: '',
|
||||
interface AnchorItem {
|
||||
rank: number;
|
||||
anchorName: string;
|
||||
sales: number;
|
||||
orderCount: number;
|
||||
signRate: string;
|
||||
repurchaseRate: string;
|
||||
}
|
||||
|
||||
interface AnchorTrend {
|
||||
anchorName: string;
|
||||
dates: string[];
|
||||
sales: number[];
|
||||
orders: number[];
|
||||
}
|
||||
|
||||
const searchParams = reactive({ dateRange: [], anchorName: '' });
|
||||
const anchorList = ref<AnchorItem[]>([]);
|
||||
const anchorTableRef = ref();
|
||||
const selectedAnchorName = ref('');
|
||||
const filteredAnchorList = computed(() => {
|
||||
const keyword = searchParams.anchorName.trim().toLowerCase();
|
||||
if (!keyword) return anchorList.value;
|
||||
return anchorList.value.filter((item) => item.anchorName.toLowerCase().includes(keyword));
|
||||
});
|
||||
|
||||
const anchorList = ref([]);
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
|
||||
const pagedAnchorList = computed(() =>
|
||||
filteredAnchorList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize)
|
||||
);
|
||||
const currentAnchorName = computed(() => selectedAnchorName.value || filteredAnchorList.value[0]?.anchorName || '暂无主播');
|
||||
const salesChartRef = ref();
|
||||
let salesChart: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟主播列表数据
|
||||
const getMockAnchorList = () => {
|
||||
const anchors = [
|
||||
{ rank: 1, anchorName: '主播A', sales: 528000, orderCount: 1250, signRate: '98.5%', repurchaseRate: '32.8%' },
|
||||
{ rank: 2, anchorName: '主播B', sales: 465000, orderCount: 1080, signRate: '97.2%', repurchaseRate: '28.5%' },
|
||||
{ rank: 3, anchorName: '主播C', sales: 389000, orderCount: 920, signRate: '99.1%', repurchaseRate: '35.2%' },
|
||||
{ rank: 4, anchorName: '主播D', sales: 320000, orderCount: 780, signRate: '96.8%', repurchaseRate: '25.3%' },
|
||||
{ rank: 5, anchorName: '主播E', sales: 285000, orderCount: 690, signRate: '98.2%', repurchaseRate: '30.1%' },
|
||||
{ rank: 6, anchorName: '主播F', sales: 256000, orderCount: 620, signRate: '97.5%', repurchaseRate: '27.8%' },
|
||||
{ rank: 7, anchorName: '主播G', sales: 210000, orderCount: 510, signRate: '96.3%', repurchaseRate: '24.5%' },
|
||||
{ rank: 8, anchorName: '主播H', sales: 185000, orderCount: 450, signRate: '98.8%', repurchaseRate: '31.2%' },
|
||||
{ rank: 9, anchorName: '主播I', sales: 162000, orderCount: 390, signRate: '97.9%', repurchaseRate: '29.5%' },
|
||||
{ rank: 10, anchorName: '主播J', sales: 145000, orderCount: 350, signRate: '96.7%', repurchaseRate: '26.8%' },
|
||||
];
|
||||
return anchors;
|
||||
const getMockAnchorList = () => [
|
||||
{ rank: 1, anchorName: '主播A', sales: 528000, orderCount: 1250, signRate: '98.5%', repurchaseRate: '32.8%' },
|
||||
{ rank: 2, anchorName: '主播B', sales: 465000, orderCount: 1080, signRate: '97.2%', repurchaseRate: '28.5%' },
|
||||
{ rank: 3, anchorName: '主播C', sales: 389000, orderCount: 920, signRate: '99.1%', repurchaseRate: '35.2%' },
|
||||
{ rank: 4, anchorName: '主播D', sales: 320000, orderCount: 780, signRate: '96.8%', repurchaseRate: '25.3%' },
|
||||
{ rank: 5, anchorName: '主播E', sales: 285000, orderCount: 690, signRate: '98.2%', repurchaseRate: '30.1%' },
|
||||
{ rank: 6, anchorName: '主播F', sales: 256000, orderCount: 620, signRate: '97.5%', repurchaseRate: '27.8%' },
|
||||
{ rank: 7, anchorName: '主播G', sales: 210000, orderCount: 510, signRate: '96.3%', repurchaseRate: '24.5%' },
|
||||
{ rank: 8, anchorName: '主播H', sales: 185000, orderCount: 450, signRate: '98.8%', repurchaseRate: '31.2%' },
|
||||
{ rank: 9, anchorName: '主播I', sales: 162000, orderCount: 390, signRate: '97.9%', repurchaseRate: '29.5%' },
|
||||
{ rank: 10, anchorName: '主播J', sales: 145000, orderCount: 350, signRate: '96.7%', repurchaseRate: '26.8%' },
|
||||
];
|
||||
const getTrendLabels = () => {
|
||||
if (Array.isArray(searchParams.dateRange) && searchParams.dateRange.length === 2) {
|
||||
const [startText, endText] = searchParams.dateRange as string[];
|
||||
const start = new Date(`${startText}T00:00:00`);
|
||||
const end = new Date(`${endText}T00:00:00`);
|
||||
const labels: string[] = [];
|
||||
const cursor = new Date(start.getTime());
|
||||
while (cursor <= end && labels.length < 31) {
|
||||
labels.push(`${cursor.getMonth() + 1}/${cursor.getDate()}`);
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
return ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
};
|
||||
|
||||
// 模拟主播销售趋势数据
|
||||
const getMockSalesTrend = () => {
|
||||
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
const anchors = ['主播A', '主播B', '主播C'];
|
||||
return anchors.map((anchorName) => ({
|
||||
const getMockSalesTrend = (anchorName: string): AnchorTrend => {
|
||||
const labels = getTrendLabels();
|
||||
const anchorIndex = Math.max(anchorList.value.findIndex((item) => item.anchorName === anchorName), 0);
|
||||
const baseSales = 120000 + anchorIndex * 26000;
|
||||
const baseOrders = 260 + anchorIndex * 36;
|
||||
return {
|
||||
anchorName,
|
||||
dates,
|
||||
sales: dates.map(() => 100000 + Math.random() * 300000),
|
||||
}));
|
||||
dates: labels,
|
||||
sales: labels.map((_, index) => {
|
||||
const trendFactor = 1 + index * 0.06;
|
||||
const fluctuation = 0.9 + ((index + anchorIndex) % 4) * 0.08;
|
||||
return Math.round(baseSales * trendFactor * fluctuation);
|
||||
}),
|
||||
orders: labels.map((_, index) => {
|
||||
const trendFactor = 1 + index * 0.045;
|
||||
const fluctuation = 0.92 + ((index + anchorIndex + 1) % 5) * 0.05;
|
||||
return Math.round(baseOrders * trendFactor * fluctuation);
|
||||
}),
|
||||
};
|
||||
};
|
||||
const setCurrentAnchorRow = async () => {
|
||||
await nextTick();
|
||||
const currentRow = pagedAnchorList.value.find((item) => item.anchorName === selectedAnchorName.value) || null;
|
||||
anchorTableRef.value?.setCurrentRow(currentRow);
|
||||
};
|
||||
const syncSelectedAnchor = () => {
|
||||
const hasSelectedAnchor = filteredAnchorList.value.some((item) => item.anchorName === selectedAnchorName.value);
|
||||
if (!hasSelectedAnchor) {
|
||||
selectedAnchorName.value = filteredAnchorList.value[0]?.anchorName || '';
|
||||
}
|
||||
};
|
||||
const updateChartByAnchor = (anchorName: string) => {
|
||||
selectedAnchorName.value = anchorName;
|
||||
initSalesChart(getMockSalesTrend(anchorName));
|
||||
setCurrentAnchorRow();
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
anchorList.value = getMockAnchorList();
|
||||
pagination.total = 100;
|
||||
initSalesChart(getMockSalesTrend());
|
||||
if (!anchorList.value.length) {
|
||||
anchorList.value = getMockAnchorList();
|
||||
}
|
||||
pagination.currentPage = 1;
|
||||
pagination.total = filteredAnchorList.value.length;
|
||||
syncSelectedAnchor();
|
||||
if (selectedAnchorName.value) {
|
||||
updateChartByAnchor(selectedAnchorName.value);
|
||||
} else {
|
||||
initSalesChart(null);
|
||||
setCurrentAnchorRow();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.anchorName = '';
|
||||
selectedAnchorName.value = anchorList.value[0]?.anchorName || '';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
handleSearch();
|
||||
};
|
||||
const handleAnchorClick = (row: AnchorItem) => {
|
||||
updateChartByAnchor(row.anchorName);
|
||||
};
|
||||
const handleAnchorSelect = (anchorName: string) => {
|
||||
updateChartByAnchor(anchorName);
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
handleSearch();
|
||||
pagination.currentPage = 1;
|
||||
setCurrentAnchorRow();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
handleSearch();
|
||||
setCurrentAnchorRow();
|
||||
};
|
||||
|
||||
const initSalesChart = (salesTrend: any[]) => {
|
||||
const initSalesChart = (salesTrend: AnchorTrend | null) => {
|
||||
if (!salesChartRef.value) return;
|
||||
|
||||
if (salesChart) {
|
||||
salesChart.dispose();
|
||||
}
|
||||
|
||||
if (salesChart) salesChart.dispose();
|
||||
salesChart = echarts.init(salesChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: salesTrend.map((item) => item.anchorName),
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: salesTrend[0]?.dates || [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '销售额',
|
||||
},
|
||||
series: salesTrend.map((item) => ({
|
||||
name: item.anchorName,
|
||||
type: 'line',
|
||||
data: item.sales,
|
||||
smooth: true,
|
||||
})),
|
||||
};
|
||||
|
||||
salesChart.setOption(option);
|
||||
const xAxisData = salesTrend?.dates || [];
|
||||
const isSingle = xAxisData.length <= 1;
|
||||
salesChart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } },
|
||||
legend: { data: salesTrend ? ['销售额', '订单数'] : [] },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: isSingle, data: xAxisData },
|
||||
yAxis: [
|
||||
{ type: 'value', name: '销售额', position: 'left' },
|
||||
{ type: 'value', name: '订单数', position: 'right' },
|
||||
],
|
||||
series: salesTrend
|
||||
? [
|
||||
{ name: '销售额', type: 'line', data: salesTrend.sales, smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 },
|
||||
{ name: '订单数', type: 'line', yAxisIndex: 1, data: salesTrend.orders, smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 },
|
||||
]
|
||||
: [],
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => filteredAnchorList.value.length,
|
||||
(length) => {
|
||||
pagination.total = length;
|
||||
const maxPage = Math.max(1, Math.ceil(length / pagination.pageSize));
|
||||
if (pagination.currentPage > maxPage) {
|
||||
pagination.currentPage = maxPage;
|
||||
}
|
||||
setCurrentAnchorRow();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
anchorList.value = getMockAnchorList();
|
||||
selectedAnchorName.value = anchorList.value[0]?.anchorName || '';
|
||||
handleSearch();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
salesChart?.resize();
|
||||
});
|
||||
window.addEventListener('resize', () => salesChart?.resize());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-operation-stats-anchor {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.anchor-ranking {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
.trade-operation-stats-anchor { padding: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; }
|
||||
.search-container { margin-bottom: 16px; }
|
||||
.search-form { display: flex; align-items: center; flex-wrap: wrap; }
|
||||
.search-form :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }
|
||||
.anchor-ranking { margin-bottom: 20px; }
|
||||
.pagination-container { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
.chart-container { margin: 0 0 20px; }
|
||||
.chart { width: 100%; height: 400px; }
|
||||
</style>
|
||||
|
||||
@@ -1,158 +1,114 @@
|
||||
<template>
|
||||
<div class="trade-operation-stats-shop">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>店铺维度统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="店铺">
|
||||
<el-select v-model="searchParams.shopId" placeholder="选择店铺">
|
||||
<el-option label="全部店铺" value="" />
|
||||
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.name" :value="shop.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:shortcuts="[
|
||||
{
|
||||
text: '最近7天',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近30天',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近90天',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '今年',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date(new Date().getFullYear(), 0, 1);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '去年',
|
||||
value: () => {
|
||||
const end = new Date(new Date().getFullYear() - 1, 11, 31);
|
||||
const start = new Date(new Date().getFullYear() - 1, 0, 1);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间粒度">
|
||||
<el-select v-model="searchParams.granularity" placeholder="选择时间粒度">
|
||||
<el-option label="小时" value="hour" />
|
||||
<el-option label="日" value="day" />
|
||||
<el-option label="周" value="week" />
|
||||
<el-option label="月" value="month" />
|
||||
<el-option label="季度" value="quarter" />
|
||||
<el-option label="年" value="year" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 销售趋势图表 -->
|
||||
<template #header
|
||||
><div class="card-header"><span>店铺维度统计</span></div></template
|
||||
>
|
||||
<div class="chart-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">销售趋势 - {{ selectedShopName }}</div>
|
||||
</template>
|
||||
<template #header
|
||||
><div class="card-header">销售趋势 - {{ selectedShopName }}</div></template
|
||||
>
|
||||
<div ref="salesChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 核心指标 -->
|
||||
<div class="stats-cards">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">总销售额</div>
|
||||
<el-card class="stats-card"
|
||||
><div class="stats-card-title">总销售额</div>
|
||||
<div class="stats-card-value">{{ statsData.totalSales || 0 }}</div>
|
||||
<div class="stats-card-change" :class="{ positive: statsData.salesGrowth > 0, negative: statsData.salesGrowth < 0 }">
|
||||
{{ statsData.salesGrowth > 0 ? '+' : '' }}{{ statsData.salesGrowth || 0 }}%
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">订单量</div>
|
||||
</div></el-card
|
||||
>
|
||||
<el-card class="stats-card"
|
||||
><div class="stats-card-title">订单量</div>
|
||||
<div class="stats-card-value">{{ statsData.orderCount || 0 }}</div>
|
||||
<div class="stats-card-change" :class="{ positive: statsData.orderGrowth > 0, negative: statsData.orderGrowth < 0 }">
|
||||
{{ statsData.orderGrowth > 0 ? '+' : '' }}{{ statsData.orderGrowth || 0 }}%
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">退款率</div>
|
||||
</div></el-card
|
||||
>
|
||||
<el-card class="stats-card"
|
||||
><div class="stats-card-title">退款率</div>
|
||||
<div class="stats-card-value">{{ statsData.refundRate || 0 }}%</div>
|
||||
<div class="stats-card-change" :class="{ positive: statsData.refundRateChange < 0, negative: statsData.refundRateChange > 0 }">
|
||||
{{ statsData.refundRateChange > 0 ? '+' : '' }}{{ statsData.refundRateChange || 0 }}%
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-card-title">物流履约率</div>
|
||||
</div></el-card
|
||||
>
|
||||
<el-card class="stats-card"
|
||||
><div class="stats-card-title">物流履约率</div>
|
||||
<div class="stats-card-value">{{ statsData.logisticsRate || 0 }}%</div>
|
||||
<div class="stats-card-change" :class="{ positive: statsData.logisticsRateGrowth > 0, negative: statsData.logisticsRateGrowth < 0 }">
|
||||
{{ statsData.logisticsRateGrowth > 0 ? '+' : '' }}{{ statsData.logisticsRateGrowth || 0 }}%
|
||||
</div>
|
||||
</el-card>
|
||||
</div></el-card
|
||||
>
|
||||
</div>
|
||||
<!-- 店铺列表 -->
|
||||
<div class="shop-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">店铺列表</div>
|
||||
</template>
|
||||
<el-table :data="shopList" style="width: 100%" @row-click="handleShopClick">
|
||||
<template #header><div class="card-header">店铺列表</div></template>
|
||||
<div class="search-container">
|
||||
<el-form :model="searchParams" :inline="true" class="search-form">
|
||||
<el-form-item label="店铺搜索">
|
||||
<el-input
|
||||
v-model="searchParams.shopKeyword"
|
||||
placeholder="请输入店铺名称或店铺ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围"
|
||||
><el-date-picker
|
||||
v-model="searchParams.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:shortcuts="dateShortcuts"
|
||||
/></el-form-item>
|
||||
<el-form-item label="时间粒度">
|
||||
<el-radio-group v-model="searchParams.granularity" class="granularity-group">
|
||||
<el-radio-button v-for="option in granularityOptions" :key="option.value" :label="option.value">
|
||||
{{ option.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
><el-button type="primary" @click="handleSearch">查询</el-button><el-button @click="handleReset">重置</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="pagedShopList" style="width: 100%" @row-click="handleShopClick">
|
||||
<el-table-column prop="id" label="店铺ID" width="100" />
|
||||
<el-table-column prop="name" label="店铺名称" />
|
||||
<el-table-column prop="type" label="店铺类型">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ scope.row.type === 'physical' ? '线下店铺' : '线上店铺' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" :type="scope.row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 'active' ? '营业中' : '已关闭' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="handleShopSelect(scope.row.id)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="店铺类型"
|
||||
><template #default="scope"
|
||||
><el-tag size="small">{{ scope.row.type === 'physical' ? '线下店铺' : '线上店铺' }}</el-tag></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="status" label="状态"
|
||||
><template #default="scope"
|
||||
><el-tag size="small" :type="scope.row.status === 'active' ? 'success' : 'danger'">{{
|
||||
scope.row.status === 'active' ? '营业中' : '已关闭'
|
||||
}}</el-tag></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="操作"
|
||||
><template #default="scope"
|
||||
><el-button type="primary" size="small" @click.stop="handleShopSelect(scope.row.id)">查看详情</el-button></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -160,51 +116,78 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const searchParams = reactive({
|
||||
shopId: '',
|
||||
dateRange: [],
|
||||
granularity: 'day',
|
||||
});
|
||||
|
||||
// 监听店铺选择变化,自动更新数据
|
||||
watch(
|
||||
() => searchParams.shopId,
|
||||
() => {
|
||||
handleSearch();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听时间粒度变化,自动更新数据
|
||||
watch(
|
||||
() => searchParams.granularity,
|
||||
() => {
|
||||
handleSearch();
|
||||
}
|
||||
);
|
||||
|
||||
const granularityOptions = [
|
||||
{ label: '小时', value: 'hour' },
|
||||
{ label: '日', value: 'day' },
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '季度', value: 'quarter' },
|
||||
{ label: '年', value: 'year' },
|
||||
];
|
||||
const searchParams = reactive({ shopId: 'all', shopKeyword: '', dateRange: [], granularity: 'day' });
|
||||
const dateShortcuts = [
|
||||
{
|
||||
text: '最近7天',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近30天',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近90天',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '今年',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date(new Date().getFullYear(), 0, 1);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '去年',
|
||||
value: () => {
|
||||
const end = new Date(new Date().getFullYear() - 1, 11, 31);
|
||||
const start = new Date(new Date().getFullYear() - 1, 0, 1);
|
||||
return [start, end];
|
||||
},
|
||||
},
|
||||
];
|
||||
interface Shop {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const shopList = ref<Shop[]>([]);
|
||||
|
||||
// 模拟店铺列表数据
|
||||
const getMockShopList = () => {
|
||||
return [
|
||||
{ id: '1', name: '店铺A', type: 'physical', status: 'active' },
|
||||
{ id: '2', name: '店铺B', type: 'online', status: 'active' },
|
||||
{ id: '3', name: '店铺C', type: 'physical', status: 'active' },
|
||||
{ id: '4', name: '店铺D', type: 'online', status: 'active' },
|
||||
{ id: '5', name: '店铺E', type: 'physical', status: 'active' },
|
||||
];
|
||||
};
|
||||
|
||||
const filteredShopList = computed(() => {
|
||||
const keyword = searchParams.shopKeyword.trim().toLowerCase();
|
||||
if (!keyword) return shopList.value;
|
||||
return shopList.value.filter((shop) => shop.name.toLowerCase().includes(keyword) || String(shop.id).toLowerCase().includes(keyword));
|
||||
});
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 5, total: 0 });
|
||||
const pagedShopList = computed(() =>
|
||||
filteredShopList.value.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize)
|
||||
);
|
||||
const statsData = reactive({
|
||||
totalSales: 1258000,
|
||||
orderCount: 5230,
|
||||
@@ -215,161 +198,202 @@ const statsData = reactive({
|
||||
refundRateChange: -0.5,
|
||||
logisticsRateGrowth: 1.2,
|
||||
});
|
||||
|
||||
const salesChartRef = ref();
|
||||
let salesChart: echarts.ECharts | null = null;
|
||||
|
||||
// 计算选中的店铺名称
|
||||
const getMockShopList = () => [
|
||||
{ id: '1', name: '店铺A', type: 'physical', status: 'active' },
|
||||
{ id: '2', name: '店铺B', type: 'online', status: 'active' },
|
||||
{ id: '3', name: '店铺C', type: 'physical', status: 'active' },
|
||||
{ id: '4', name: '店铺D', type: 'online', status: 'active' },
|
||||
{ id: '5', name: '店铺E', type: 'physical', status: 'active' },
|
||||
];
|
||||
const selectedShopName = computed(() => {
|
||||
if (!searchParams.shopId) return '全部店铺';
|
||||
const shop = shopList.value.find((s) => s.id === searchParams.shopId);
|
||||
if (searchParams.shopId === 'all') return '全部店铺';
|
||||
const shop = shopList.value.find((s) => String(s.id) === searchParams.shopId);
|
||||
return shop ? shop.name : '未知店铺';
|
||||
});
|
||||
|
||||
// 模拟销售趋势数据
|
||||
const getMockSalesTrend = (shopId: string) => {
|
||||
let dates: string[] = [];
|
||||
const baseSales = shopId ? 800000 + parseInt(shopId) * 100000 : 1000000;
|
||||
const baseOrders = shopId ? 3000 + parseInt(shopId) * 500 : 4000;
|
||||
|
||||
// 根据时间粒度生成不同的时间标签
|
||||
switch (searchParams.granularity) {
|
||||
case 'hour':
|
||||
dates = ['0时', '4时', '8时', '12时', '16时', '20时'];
|
||||
break;
|
||||
case 'day':
|
||||
dates = ['1日', '5日', '10日', '15日', '20日', '25日'];
|
||||
break;
|
||||
case 'week':
|
||||
dates = ['第1周', '第2周', '第3周', '第4周', '第5周', '第6周'];
|
||||
break;
|
||||
case 'month':
|
||||
dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
break;
|
||||
case 'quarter':
|
||||
dates = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
break;
|
||||
case 'year':
|
||||
dates = ['2022年', '2023年', '2024年', '2025年', '2026年'];
|
||||
break;
|
||||
default:
|
||||
dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const toDate = (value?: string | Date) => {
|
||||
if (!value) return new Date();
|
||||
return value instanceof Date ? new Date(value.getTime()) : new Date(`${value}T00:00:00`);
|
||||
};
|
||||
const formatDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${date.getDate()}`.padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
const getDateRange = () => {
|
||||
if (Array.isArray(searchParams.dateRange) && searchParams.dateRange.length === 2) {
|
||||
const [start, end] = searchParams.dateRange as Array<string | Date>;
|
||||
return { start: toDate(start), end: toDate(end) };
|
||||
}
|
||||
const end = new Date();
|
||||
const start = new Date(end.getTime() - 6 * DAY_IN_MS);
|
||||
return { start, end };
|
||||
};
|
||||
const getWeekLabel = (date: Date) => {
|
||||
const firstDay = new Date(date.getFullYear(), 0, 1);
|
||||
const dayOffset = Math.floor((date.getTime() - firstDay.getTime()) / DAY_IN_MS);
|
||||
return `${date.getFullYear()}年第${Math.floor(dayOffset / 7) + 1}周`;
|
||||
};
|
||||
const getQuarterLabel = (date: Date) => `${date.getFullYear()}年Q${Math.floor(date.getMonth() / 3) + 1}`;
|
||||
const buildTrendLabels = () => {
|
||||
const { start, end } = getDateRange();
|
||||
const labels: string[] = [];
|
||||
const cursor = new Date(start.getTime());
|
||||
if (searchParams.granularity === 'hour') {
|
||||
return Array.from({ length: 24 }, (_, index) => `${index}:00`);
|
||||
}
|
||||
if (searchParams.granularity === 'day') {
|
||||
while (cursor <= end) {
|
||||
labels.push(formatDate(cursor));
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
if (searchParams.granularity === 'week') {
|
||||
while (cursor <= end) {
|
||||
labels.push(getWeekLabel(cursor));
|
||||
cursor.setDate(cursor.getDate() + 7);
|
||||
}
|
||||
return [...new Set(labels)];
|
||||
}
|
||||
if (searchParams.granularity === 'month') {
|
||||
cursor.setDate(1);
|
||||
while (cursor <= end) {
|
||||
labels.push(`${cursor.getFullYear()}-${`${cursor.getMonth() + 1}`.padStart(2, '0')}`);
|
||||
cursor.setMonth(cursor.getMonth() + 1);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
if (searchParams.granularity === 'quarter') {
|
||||
cursor.setMonth(Math.floor(cursor.getMonth() / 3) * 3, 1);
|
||||
while (cursor <= end) {
|
||||
labels.push(getQuarterLabel(cursor));
|
||||
cursor.setMonth(cursor.getMonth() + 3);
|
||||
}
|
||||
return [...new Set(labels)];
|
||||
}
|
||||
cursor.setMonth(0, 1);
|
||||
while (cursor <= end) {
|
||||
labels.push(`${cursor.getFullYear()}年`);
|
||||
cursor.setFullYear(cursor.getFullYear() + 1);
|
||||
}
|
||||
return [...new Set(labels)];
|
||||
};
|
||||
const getMockSalesTrend = (shopId: string) => {
|
||||
const selectedShopId = shopId === 'all' ? 0 : parseInt(shopId, 10) || 0;
|
||||
const labels = buildTrendLabels();
|
||||
const baseSales = 880000 + selectedShopId * 120000;
|
||||
const baseOrders = 3200 + selectedShopId * 380;
|
||||
return labels.map((date, index) => {
|
||||
const trendFactor = 1 + index * 0.035;
|
||||
const seasonalFactor = 0.92 + ((index + selectedShopId) % 5) * 0.045;
|
||||
return {
|
||||
date,
|
||||
sales: Math.round(baseSales * trendFactor * seasonalFactor),
|
||||
orders: Math.round(baseOrders * trendFactor * (0.95 + (index % 4) * 0.03)),
|
||||
};
|
||||
});
|
||||
};
|
||||
const syncSelectedShop = () => {
|
||||
if (searchParams.shopId === 'all') return;
|
||||
const hasSelectedShop = filteredShopList.value.some((shop) => String(shop.id) === searchParams.shopId);
|
||||
if (!hasSelectedShop) {
|
||||
searchParams.shopId = 'all';
|
||||
}
|
||||
|
||||
return dates.map((date) => ({
|
||||
date,
|
||||
sales: baseSales + Math.random() * 500000,
|
||||
orders: baseOrders + Math.random() * 2000,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 使用模拟数据
|
||||
const mockData = {
|
||||
totalSales: 1258000 + Math.random() * 100000,
|
||||
orderCount: 5230 + Math.random() * 500,
|
||||
refundRate: 2.5 + Math.random() * 1,
|
||||
logisticsRate: 98.2 + Math.random() * 1,
|
||||
salesGrowth: 15.8 + Math.random() * 5,
|
||||
orderGrowth: 12.3 + Math.random() * 3,
|
||||
refundRateChange: -0.5 + Math.random() * 1,
|
||||
logisticsRateGrowth: 1.2 + Math.random() * 0.5,
|
||||
salesTrend: getMockSalesTrend(searchParams.shopId),
|
||||
};
|
||||
Object.assign(statsData, mockData);
|
||||
initSalesChart(mockData.salesTrend);
|
||||
pagination.currentPage = 1;
|
||||
syncSelectedShop();
|
||||
pagination.total = filteredShopList.value.length;
|
||||
const salesTrend = getMockSalesTrend(searchParams.shopId);
|
||||
const totalSales = salesTrend.reduce((sum, item) => sum + item.sales, 0);
|
||||
const totalOrders = salesTrend.reduce((sum, item) => sum + item.orders, 0);
|
||||
const selectedShopId = searchParams.shopId === 'all' ? 0 : parseInt(searchParams.shopId, 10) || 0;
|
||||
Object.assign(statsData, {
|
||||
totalSales,
|
||||
orderCount: totalOrders,
|
||||
refundRate: Number((2.1 + selectedShopId * 0.18).toFixed(2)),
|
||||
logisticsRate: Number((97.2 + selectedShopId * 0.25).toFixed(2)),
|
||||
salesGrowth: Number((8 + salesTrend.length * 0.6 + selectedShopId).toFixed(1)),
|
||||
orderGrowth: Number((6 + salesTrend.length * 0.45 + selectedShopId * 0.8).toFixed(1)),
|
||||
refundRateChange: Number((-0.8 + selectedShopId * 0.12).toFixed(1)),
|
||||
logisticsRateGrowth: Number((0.6 + salesTrend.length * 0.08).toFixed(1)),
|
||||
});
|
||||
initSalesChart(salesTrend);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.shopId = '';
|
||||
searchParams.shopId = 'all';
|
||||
searchParams.shopKeyword = '';
|
||||
searchParams.dateRange = [];
|
||||
searchParams.granularity = 'day';
|
||||
pagination.currentPage = 1;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleShopClick = (shop: Shop) => {
|
||||
searchParams.shopId = shop.id;
|
||||
searchParams.shopId = String(shop.id);
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleShopSelect = (shopId: string) => {
|
||||
searchParams.shopId = shopId;
|
||||
searchParams.shopId = String(shopId);
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
pagination.currentPage = 1;
|
||||
};
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.currentPage = current;
|
||||
};
|
||||
const initSalesChart = (salesTrend: any[]) => {
|
||||
if (!salesChartRef.value) return;
|
||||
|
||||
if (salesChart) {
|
||||
salesChart.dispose();
|
||||
}
|
||||
|
||||
if (salesChart) salesChart.dispose();
|
||||
salesChart = echarts.init(salesChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['销售额', '订单量'],
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: salesTrend.map((item) => item.date),
|
||||
},
|
||||
const isSingle = salesTrend.length <= 1;
|
||||
salesChart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } },
|
||||
legend: { data: ['销售额', '订单量'] },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: isSingle, data: salesTrend.map((item) => item.date) },
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '销售额',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '订单量',
|
||||
position: 'right',
|
||||
},
|
||||
{ type: 'value', name: '销售额', position: 'left' },
|
||||
{ type: 'value', name: '订单量', position: 'right' },
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '销售额',
|
||||
type: 'line',
|
||||
data: salesTrend.map((item) => item.sales),
|
||||
smooth: true,
|
||||
},
|
||||
{ name: '销售额', type: 'line', data: salesTrend.map((item) => item.sales), smooth: true, showSymbol: true, symbolSize: isSingle ? 10 : 6 },
|
||||
{
|
||||
name: '订单量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: salesTrend.map((item) => item.orders),
|
||||
smooth: true,
|
||||
showSymbol: true,
|
||||
symbolSize: isSingle ? 10 : 6,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
salesChart.setOption(option);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化店铺列表
|
||||
shopList.value = getMockShopList();
|
||||
// 初始化数据
|
||||
handleSearch();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
salesChart?.resize();
|
||||
});
|
||||
};
|
||||
watch(
|
||||
() => filteredShopList.value.length,
|
||||
(length) => {
|
||||
pagination.total = length;
|
||||
const maxPage = Math.max(1, Math.ceil(length / pagination.pageSize));
|
||||
if (pagination.currentPage > maxPage) {
|
||||
pagination.currentPage = maxPage;
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => searchParams.granularity,
|
||||
() => handleSearch()
|
||||
);
|
||||
onMounted(() => {
|
||||
shopList.value = getMockShopList();
|
||||
pagination.total = shopList.value.length;
|
||||
handleSearch();
|
||||
window.addEventListener('resize', () => salesChart?.resize());
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -377,67 +401,71 @@ onMounted(() => {
|
||||
.trade-operation-stats-shop {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.search-form :deep(.el-form-item) {
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.granularity-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-card-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats-card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stats-card-change {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 20px 0;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.shop-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user