feat(交易运营): 新增交易运营模块相关功能

新增交易运营模块的统计、分析、分销和订单管理功能
- 添加主播维度统计和店铺维度统计页面
- 实现店铺评分监控、地域分布分析和商品数据统计功能
- 完成分销效果核算和分销订单查询功能
- 开发订单管理页面及相关接口
- 修复知识库文档列表和详情的部分问题
- 更新环境配置和API接口文件
This commit is contained in:
2026-04-07 17:06:56 +08:00
parent 08ae659a56
commit c610c6b327
20 changed files with 2447 additions and 12 deletions

View File

@@ -4,3 +4,5 @@ ENV = 'development'
# 统一后端服务地址前缀网关服务名admin-go
# 开发环境走本地代理,避免 CORS
VITE_API_URL = 'http://192.168.74.41:8000'
# VITE_API_URL = 'http://192.168.3.30:8000'

View File

@@ -3,23 +3,27 @@ import request from '/@/utils/request';
// 文档查询参数
export interface DocumentQueryParams {
keyword?: string;
title?: string;
datasetId?: string;
knowledgeId?: string;
fileType?: string;
status?: string;
pageNum: number;
pageSize: number;
}
// 创建文档参数
export interface CreateDocumentParams {
datasetId: string; // 必传
filePath: string; // 必传
fileSize: number; // 必传
format: string; // 必传
title: string; // 必传
datasetId: string;
filePath: string;
fileSize: number;
format: string;
title: string;
}
// 更新文档参数
export interface UpdateDocumentParams {
id: string; // 必传
id: string;
datasetId?: string;
filePath?: string;
fileSize?: number;
@@ -27,6 +31,13 @@ export interface UpdateDocumentParams {
title?: string;
}
// 文档分段查询参数
export interface DocumentChunkQueryParams {
documentId: string;
pageNum: number;
pageSize: number;
}
// 文档信息
export interface DocumentInfo {
id?: string;
@@ -102,6 +113,15 @@ export function uploadDocument(data: FormData) {
});
}
// 预览文档
export function previewDocument(id: string) {
return request({
url: '/rag-knowledge/document/previewDocument',
method: 'get',
params: { id },
});
}
// 删除文档
export function deleteDocument(id: string) {
return request({
@@ -111,6 +131,51 @@ export function deleteDocument(id: string) {
});
}
// 批量删除文档
export function batchDeleteDocuments(ids: string[]) {
return request({
url: '/rag-knowledge/document/batchDeleteDocument',
method: 'delete',
data: { ids },
});
}
// 重新处理文档
export function reprocessDocument(id: string) {
return request({
url: '/rag-knowledge/document/reprocessDocument',
method: 'post',
data: { id },
});
}
// 获取文档分段列表
export function listDocumentChunks(params: DocumentChunkQueryParams) {
return request({
url: '/rag-knowledge/document/listDocumentChunk',
method: 'get',
params,
});
}
// 更新文档分段
export function updateDocumentChunk(data: { id: string; content: string }) {
return request({
url: '/rag-knowledge/document/updateDocumentChunk',
method: 'put',
data,
});
}
// 删除文档分段
export function deleteDocumentChunk(id: string) {
return request({
url: '/rag-knowledge/document/deleteDocumentChunk',
method: 'delete',
params: { id },
});
}
// 获取文件向量化处理进度
export function getDocumentProcess(id: string) {
return request({

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getProductStats(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/analysis/product',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getRegionStats(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/analysis/region',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getShopScore(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/analysis/shop',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getDistributionEffect(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/distribution/effect',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getDistributionOrderList(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/distribution/order/list',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,17 @@
import request from '/@/utils/request';
export function getOrderList(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/order/list',
method: 'get',
params: params,
});
}
export function getOrderDetail(id: number) {
return request({
url: '/admin-go/api/v1/trade/operation/order/detail',
method: 'get',
params: { id },
});
}

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getAnchorStats(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/stats/anchor',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getShopStats(params: any) {
return request({
url: '/admin-go/api/v1/trade/operation/stats/shop',
method: 'get',
params: params,
});
}

View File

@@ -7,7 +7,7 @@
<span class="back-link" @click="onBackToknowledge">知识库</span>
</el-breadcrumb-item>
<el-breadcrumb-item>
<span class="back-link" @click="onBackToknowledge">{{ knowledgeName }}</span>
<span class="back-link" @click="onBackToDocumentList">{{ knowledgeName }}</span>
</el-breadcrumb-item>
<el-breadcrumb-item>{{ documentInfo.name }}</el-breadcrumb-item>
</el-breadcrumb>
@@ -162,7 +162,7 @@ const onBackToknowledge = () => {
};
// 返回数据集详情
const onBackToknowledge = () => {
const onBackToDocumentList = () => {
router.push({
path: '/knowledge/document',
query: { knowledgeId: knowledgeId.value, knowledgeName: knowledgeName.value },

View File

@@ -18,7 +18,7 @@
</el-select>
</el-form-item>
<el-form-item label="文档名称">
<el-input size="default" v-model="tableData.param.keyword" placeholder="请输入文档名称" clearable style="width: 200px" />
<el-input size="default" v-model="tableData.param.title" placeholder="请输入文档名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="文件类型">
<el-select size="default" v-model="tableData.param.fileType" placeholder="请选择类型" clearable style="width: 120px">
@@ -161,7 +161,7 @@ import { ref, reactive, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listDocuments, deleteDocument, batchDeleteDocuments, reprocessDocument } from '/@/api/knowledge/document';
import { listknowledges } from '/@/api/knowledge/knowledge';
import { listknowledges } from '/@/api/knowledge/dataset';
import UploadDocument from './component/uploadDocument.vue';
import PreviewDocument from './component/previewDocument.vue';
import DocumentChunks from './component/documentChunks.vue';
@@ -186,7 +186,7 @@ const tableData = reactive({
total: 0,
loading: false,
param: {
keyword: '',
title: '',
knowledgeId: '',
fileType: '',
status: undefined as string | undefined,
@@ -323,7 +323,7 @@ const getIndexStatusText = (status: string) => {
// 重置查询
const onResetQuery = () => {
tableData.param.keyword = '';
tableData.param.title = '';
if (!currentknowledge.id) {
tableData.param.knowledgeId = '';
}

View File

@@ -0,0 +1,269 @@
<template>
<div class="trade-operation-analysis-product">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>商品数据统计</span>
</div>
</template>
<!-- 商品类目分布 -->
<div class="chart-container">
<el-card>
<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%">
<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>
<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-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } 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 categories = ref<Category[]>([]);
const skuList = ref<SkuItem[]>([]);
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
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: '美妆护肤' },
];
};
// 模拟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 initCategoryChart = (categoryDistribution: any[]) => {
if (!categoryChartRef.value) return;
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);
};
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;
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="trade-operation-analysis-region">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>地域分布分析</span>
</div>
</template>
<!-- 地域分布图表 -->
<div class="chart-container">
<el-card>
<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%">
<el-table-column prop="rank" label="排名" width="80" />
<el-table-column prop="regionName" label="地区名称" />
<el-table-column prop="sales" label="销售额" />
<el-table-column prop="orderCount" label="订单数" />
<el-table-column prop="avgOrderValue" label="客单价" />
<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"
/>
</div>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import * as echarts from 'echarts';
const searchParams = reactive({
dateRange: [],
regionLevel: 'province',
});
const regionList = ref([]);
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
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 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 initRegionChart = (regionDistribution: any[]) => {
if (!regionChartRef.value) return;
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);
};
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;
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<div class="trade-operation-analysis-shop">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>店铺评分监控</span>
</div>
</template>
<!-- 评分趋势 -->
<div class="chart-container">
<el-card>
<template #header>
<div class="card-header">评分趋势</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">
<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>
</el-card>
</div>
<!-- 评分详情 -->
<div class="score-detail">
<el-card>
<template #header>
<div class="card-header">评分详情</div>
</template>
<el-table :data="scoreDetail" 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="commentCount" label="评价数量" />
</el-table>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } 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;
}
const scoreDetail = ref<ScoreDetail[]>([]);
const scoreChartRef = ref();
let scoreChart: echarts.ECharts | null = null;
// 模拟评分趋势数据
const getMockScoreTrend = () => {
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
return dates.map((date) => ({
date,
reputationScore: 3.8 + Math.random() * 0.7,
experienceScore: 4.0 + Math.random() * 0.5,
}));
};
// 模拟评分详情数据
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 handleSearch = () => {
// 使用模拟数据
scoreDetail.value = getMockScoreDetail();
initScoreChart(getMockScoreTrend());
};
const handleReset = () => {
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(() => {
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;
}
</style>

View File

@@ -0,0 +1,311 @@
<template>
<div class="trade-operation-distribution-effect">
<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-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>
<div class="card-header">分销效果趋势</div>
</template>
<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>
</div>
<!-- 达人推广效果 -->
<div class="anchor-effect">
<el-card>
<template #header>
<div class="card-header">达人推广效果</div>
</template>
<el-table :data="anchorList" style="width: 100%">
<el-table-column prop="rank" label="排名" width="80" />
<el-table-column prop="anchorName" label="达人名称" />
<el-table-column prop="sales" label="销售额" />
<el-table-column prop="commission" label="佣金金额" />
<el-table-column prop="commissionRate" label="佣金率" />
<el-table-column prop="orderCount" label="订单数" />
<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"
/>
</div>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } 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 anchorList = ref<AnchorItem[]>([]);
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
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 handleSearch = () => {
// 使用模拟数据
statsData.totalSales = 5000000 + Math.random() * 5000000;
statsData.totalCommission = 500000 + Math.random() * 500000;
statsData.avgCommissionRate = 15 + Math.random() * 5;
statsData.anchorCount = 50 + Math.floor(Math.random() * 50);
anchorList.value = getMockAnchorList();
pagination.total = 20;
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 initEffectChart = (effectTrend: any[]) => {
if (!effectChartRef.value) return;
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',
},
],
series: [
{
name: '销售额',
type: 'line',
data: effectTrend.map((item) => item.sales),
smooth: true,
},
{
name: '佣金金额',
type: 'line',
data: effectTrend.map((item) => item.commission),
smooth: true,
},
],
};
effectChart.setOption(option);
};
onMounted(() => {
handleSearch();
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;
}
</style>

View File

@@ -0,0 +1,307 @@
<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>
<!-- 订单列表 -->
<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>
</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.anchorName }}</el-descriptions-item>
<el-descriptions-item label="订单金额">{{ orderDetail.amount }}</el-descriptions-item>
<el-descriptions-item label="佣金金额">{{ orderDetail.commission }}</el-descriptions-item>
<el-descriptions-item label="佣金比例">{{ orderDetail.commissionRate }}%</el-descriptions-item>
<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>
</el-dialog>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
const searchParams = reactive({
orderType: '',
orderNo: '',
anchorName: '',
dateRange: [],
});
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 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();
});
</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;
}
</style>

View File

@@ -0,0 +1,383 @@
<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>
<!-- 订单列表 -->
<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>
</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.amount }}</el-descriptions-item>
<el-descriptions-item label="订单状态">{{ getStatusText(orderDetail.status) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ orderDetail.createTime }}</el-descriptions-item>
<el-descriptions-item label="支付时间">{{ orderDetail.payTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="发货时间">{{ orderDetail.shipTime || '-' }}</el-descriptions-item>
<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>
</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>
</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>
</div>
</div>
</el-dialog>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
const searchParams = reactive({
orderNo: '',
productName: '',
status: '',
dateRange: [],
});
interface OrderItem {
id: number;
orderNo: string;
productName: string;
amount: number;
status: string;
createTime: string;
}
const orderList = ref<OrderItem[]>([]);
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
interface OrderDetail {
orderNo: string;
amount: number;
status: string;
createTime: string;
payTime: string;
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;
}[];
}
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 handleSearch = () => {
// 使用模拟数据
orderList.value = getMockOrderList();
pagination.total = 20;
};
const handleReset = () => {
searchParams.orderNo = '';
searchParams.productName = '';
searchParams.status = '';
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();
});
</script>
<style scoped>
.trade-operation-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;
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="trade-operation-stats-anchor">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>主播维度统计</span>
</div>
</template>
<!-- 主播销售趋势 -->
<div class="chart-container">
<el-card>
<template #header>
<div class="card-header">主播销售趋势</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%">
<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>
<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-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import * as echarts from 'echarts';
const searchParams = reactive({
dateRange: [],
anchorName: '',
});
const anchorList = ref([]);
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
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 getMockSalesTrend = () => {
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
const anchors = ['主播A', '主播B', '主播C'];
return anchors.map((anchorName) => ({
anchorName,
dates,
sales: dates.map(() => 100000 + Math.random() * 300000),
}));
};
const handleSearch = () => {
// 使用模拟数据
anchorList.value = getMockAnchorList();
pagination.total = 100;
initSalesChart(getMockSalesTrend());
};
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 initSalesChart = (salesTrend: any[]) => {
if (!salesChartRef.value) return;
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);
};
onMounted(() => {
handleSearch();
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;
}
</style>

View File

@@ -0,0 +1,274 @@
<template>
<div class="trade-operation-stats-shop">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>店铺维度统计</span>
</div>
</template>
<!-- 销售趋势图表 -->
<div class="chart-container">
<el-card>
<template #header>
<div class="card-header">销售趋势</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-select v-model="searchParams.granularity" placeholder="选择时间粒度">
<el-option label="日" value="day" />
<el-option label="周" value="week" />
<el-option label="月" value="month" />
<el-option label="季度" value="quarter" />
</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="stats-cards">
<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 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 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 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>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import * as echarts from 'echarts';
const searchParams = reactive({
dateRange: [],
granularity: 'day',
});
const statsData = reactive({
totalSales: 1258000,
orderCount: 5230,
refundRate: 2.5,
logisticsRate: 98.2,
salesGrowth: 15.8,
orderGrowth: 12.3,
refundRateChange: -0.5,
logisticsRateGrowth: 1.2,
});
const salesChartRef = ref();
let salesChart: echarts.ECharts | null = null;
// 模拟销售趋势数据
const getMockSalesTrend = () => {
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
return dates.map((date) => ({
date,
sales: 1000000 + Math.random() * 500000,
orders: 4000 + Math.random() * 2000,
xx: 4000 + Math.random() * 2020,
ss: 4000 + Math.random() * 2010,
}));
};
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(),
};
Object.assign(statsData, mockData);
initSalesChart(mockData.salesTrend);
};
const handleReset = () => {
searchParams.dateRange = [];
searchParams.granularity = 'day';
};
const initSalesChart = (salesTrend: any[]) => {
if (!salesChartRef.value) return;
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),
},
yAxis: [
{
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',
yAxisIndex: 1,
data: salesTrend.map((item) => item.orders),
smooth: true,
},
],
};
salesChart.setOption(option);
};
onMounted(() => {
handleSearch();
window.addEventListener('resize', () => {
salesChart?.resize();
});
});
</script>
<style scoped>
.trade-operation-stats-shop {
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;
margin-bottom: 5px;
}
.stats-card-change {
font-size: 12px;
}
.positive {
color: #67c23a;
}
.negative {
color: #f56c6c;
}
.chart-container {
margin: 20px 0;
}
.chart {
width: 100%;
height: 400px;
}
</style>