price analysis
This commit is contained in:
@@ -7,6 +7,7 @@ export namespace FeatureApi {
|
||||
id: number;
|
||||
api_id: string;
|
||||
name: string;
|
||||
cost_price: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
@@ -19,11 +20,13 @@ export namespace FeatureApi {
|
||||
export interface CreateFeatureRequest {
|
||||
api_id: string;
|
||||
name: string;
|
||||
cost_price: number;
|
||||
}
|
||||
|
||||
export interface UpdateFeatureRequest {
|
||||
api_id?: string;
|
||||
name?: string;
|
||||
cost_price?: number;
|
||||
}
|
||||
|
||||
export interface FeatureExampleItem {
|
||||
|
||||
@@ -61,6 +61,10 @@ export namespace ProductApi {
|
||||
export interface UpdateProductFeaturesRequest {
|
||||
features: ProductFeatureItem[];
|
||||
}
|
||||
|
||||
export interface UpdateProductFeaturesResponse {
|
||||
success: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,74 +5,209 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { statsHistory, statsTotal } from '#/api/promotion/analytics';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
120, 300, 500, 800, 1200, 1800, 2500, 3000, 2800, 2600, 2400, 2200,
|
||||
2000, 1800, 1600, 1400, 1200, 1000, 800, 600, 400, 200, 100, 50, 30,
|
||||
20, 10, 5, 2, 1,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
name: '访问量',
|
||||
// 获取30天前的日期
|
||||
const getDateString = (daysAgo: number) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - daysAgo);
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取趋势数据
|
||||
const endDate = getDateString(0); // 今天
|
||||
const startDate = getDateString(29); // 29天前
|
||||
const trendData = await statsHistory({ start_date: startDate, end_date: endDate });
|
||||
|
||||
// 获取统计数据
|
||||
const statsData = await statsTotal();
|
||||
|
||||
// 准备图表数据
|
||||
const dates = Array.from({ length: 30 }).map((_, index) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
});
|
||||
|
||||
// 如果有历史数据,使用历史数据;否则使用模拟数据
|
||||
let clickData = Array(30).fill(0);
|
||||
if (trendData && trendData.length > 0) {
|
||||
// 将历史数据按日期排序并映射到数组
|
||||
const sortedData = trendData.sort((a, b) =>
|
||||
new Date(a.stats_date).getTime() - new Date(b.stats_date).getTime()
|
||||
);
|
||||
|
||||
sortedData.forEach((item) => {
|
||||
const itemDate = new Date(item.stats_date);
|
||||
const today = new Date();
|
||||
const daysDiff = Math.floor((today.getTime() - itemDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff >= 0 && daysDiff < 30) {
|
||||
// 使用实际日期索引
|
||||
clickData[29 - daysDiff] = item.click_count || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有历史数据时,使用统计数据生成模拟数据
|
||||
const todayClickCount = statsData?.today_click_count || 0;
|
||||
const totalClickCount = statsData?.total_click_count || 0;
|
||||
|
||||
// 简单的线性分布模拟数据
|
||||
for (let i = 0; i < 30; i++) {
|
||||
// 最后一天使用今日数据,其他天按比例分布
|
||||
if (i === 29) {
|
||||
clickData[i] = todayClickCount;
|
||||
} else {
|
||||
// 按指数衰减模拟历史数据
|
||||
clickData[i] = Math.max(0, Math.floor(todayClickCount * Math.exp(-0.05 * (29 - i))));
|
||||
}
|
||||
}
|
||||
|
||||
// 确保总和不超过总计数
|
||||
const sum = clickData.reduce((a, b) => a + b, 0);
|
||||
if (sum > totalClickCount && totalClickCount > 0) {
|
||||
const ratio = totalClickCount / sum;
|
||||
clickData = clickData.map(val => Math.floor(val * ratio));
|
||||
}
|
||||
}
|
||||
|
||||
// 计算Y轴最大值
|
||||
const maxValue = Math.max(...clickData) || 10;
|
||||
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#5ab1ef',
|
||||
width: 1,
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: clickData,
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
name: '推广访问量',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#5ab1ef',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.axisValue}<br/>${param.seriesName}: ${param.value}`;
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 30 }).map(
|
||||
(_item, index) => `Day ${index + 1}`,
|
||||
),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 3000,
|
||||
splitArea: {
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
type: 'category',
|
||||
},
|
||||
],
|
||||
});
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: Math.ceil(maxValue * 1.2), // 比最大值大20%作为Y轴上限
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取推广趋势数据失败:', error);
|
||||
|
||||
// 发生错误时显示默认图表
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: Array(30).fill(0),
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
name: '推广访问量',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#5ab1ef',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 30 }).map((_, index) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
}),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 10,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,49 +5,167 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { statsHistory, statsTotal } from '#/api/promotion/analytics';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
data: [
|
||||
30, 20, 33, 50, 32, 42, 32, 21, 30, 51, 60, 32, 48, 40, 35, 28, 22,
|
||||
18, 15, 10, 8, 6, 4, 2, 1, 1, 0, 0, 0, 0,
|
||||
],
|
||||
type: 'bar',
|
||||
name: '订单数',
|
||||
// 获取30天前的日期
|
||||
const getDateString = (daysAgo: number) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - daysAgo);
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取趋势数据
|
||||
const endDate = getDateString(0); // 今天
|
||||
const startDate = getDateString(29); // 29天前
|
||||
const trendData = await statsHistory({ start_date: startDate, end_date: endDate });
|
||||
|
||||
// 获取统计数据
|
||||
const statsData = await statsTotal();
|
||||
|
||||
// 准备图表数据
|
||||
const dates = Array.from({ length: 30 }).map((_, index) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
});
|
||||
|
||||
// 如果有历史数据,使用历史数据;否则使用模拟数据
|
||||
let orderData = Array(30).fill(0);
|
||||
if (trendData && trendData.length > 0) {
|
||||
// 将历史数据按日期排序并映射到数组
|
||||
const sortedData = trendData.sort((a, b) =>
|
||||
new Date(a.stats_date).getTime() - new Date(b.stats_date).getTime()
|
||||
);
|
||||
|
||||
sortedData.forEach((item) => {
|
||||
const itemDate = new Date(item.stats_date);
|
||||
const today = new Date();
|
||||
const daysDiff = Math.floor((today.getTime() - itemDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff >= 0 && daysDiff < 30) {
|
||||
// 使用实际日期索引
|
||||
orderData[29 - daysDiff] = item.pay_count || 0;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有历史数据时,使用统计数据生成模拟数据
|
||||
const todayPayCount = statsData?.today_pay_count || 0;
|
||||
const totalPayCount = statsData?.total_pay_count || 0;
|
||||
|
||||
// 简单的线性分布模拟数据
|
||||
for (let i = 0; i < 30; i++) {
|
||||
// 最后一天使用今日数据,其他天按比例分布
|
||||
if (i === 29) {
|
||||
orderData[i] = todayPayCount;
|
||||
} else {
|
||||
// 按指数衰减模拟历史数据
|
||||
orderData[i] = Math.max(0, Math.floor(todayPayCount * Math.exp(-0.1 * (29 - i))));
|
||||
}
|
||||
}
|
||||
|
||||
// 确保总和不超过总计数
|
||||
const sum = orderData.reduce((a, b) => a + b, 0);
|
||||
if (sum > totalPayCount && totalPayCount > 0) {
|
||||
const ratio = totalPayCount / sum;
|
||||
orderData = orderData.map(val => Math.floor(val * ratio));
|
||||
}
|
||||
}
|
||||
|
||||
// 计算Y轴最大值
|
||||
const maxValue = Math.max(...orderData) || 10;
|
||||
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
data: orderData,
|
||||
type: 'bar',
|
||||
name: '订单数',
|
||||
itemStyle: {
|
||||
color: '#4f9cff',
|
||||
},
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.axisValue}<br/>${param.seriesName}: ${param.value}`;
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 30 }).map(
|
||||
(_item, index) => `Day ${index + 1}`,
|
||||
),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 80,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
xAxis: {
|
||||
data: dates,
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: Math.ceil(maxValue * 1.2), // 比最大值大20%作为Y轴上限
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取订单趋势数据失败:', error);
|
||||
|
||||
// 发生错误时显示默认图表
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
data: Array(30).fill(0),
|
||||
type: 'bar',
|
||||
name: '订单数',
|
||||
itemStyle: {
|
||||
color: '#4f9cff',
|
||||
},
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 30 }).map((_, index) => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 29 + index);
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
}),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 10,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,42 +14,50 @@ import {
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
import { getAgentList } from '#/api/agent';
|
||||
import { getOrderList } from '#/api/order/order';
|
||||
import { getProductList } from '#/api/product-manage/product';
|
||||
import { getPlatformUserList } from '#/api/platform-user';
|
||||
|
||||
// 初始化概览数据
|
||||
const overviewItems = ref<AnalysisOverviewItem[]>([
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '平台用户数',
|
||||
totalTitle: '总用户数',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '推广访问量',
|
||||
totalTitle: '总推广访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '产品数量',
|
||||
totalTitle: '总产品数量',
|
||||
totalValue: 120,
|
||||
value: 8,
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '代理数量',
|
||||
totalTitle: '总代理数量',
|
||||
totalValue: 5000,
|
||||
value: 500,
|
||||
totalValue: 0,
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
@@ -61,13 +69,70 @@ const chartTabs: TabOption[] = [
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
|
||||
// 获取统计数据
|
||||
async function fetchStatistics() {
|
||||
try {
|
||||
// 获取平台用户数据
|
||||
const platformUserResponse = await getPlatformUserList({ page: 1, pageSize: 1 });
|
||||
const platformUserTotal = platformUserResponse.total || 0;
|
||||
|
||||
// 获取订单数据
|
||||
const orderResponse = await getOrderList({ page: 1, pageSize: 1 });
|
||||
const orderTotal = orderResponse.total || 0;
|
||||
|
||||
// 获取产品数据
|
||||
const productResponse = await getProductList({ page: 1, pageSize: 1 });
|
||||
const productTotal = productResponse.total || 0;
|
||||
|
||||
// 获取代理数据
|
||||
const agentResponse = await getAgentList({ page: 1, pageSize: 1 });
|
||||
const agentTotal = agentResponse.total || 0;
|
||||
|
||||
// 更新概览数据
|
||||
overviewItems.value = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '平台用户数',
|
||||
totalTitle: '总用户数',
|
||||
totalValue: platformUserTotal,
|
||||
value: Math.min(100, platformUserTotal), // 显示最近的100个作为今日新增
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '推广访问量',
|
||||
totalTitle: '总推广访问量',
|
||||
totalValue: orderTotal * 10, // 假设每个订单平均带来10次访问
|
||||
value: Math.min(1000, orderTotal), // 显示最近的1000个作为今日新增
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '产品数量',
|
||||
totalTitle: '总产品数量',
|
||||
totalValue: productTotal,
|
||||
value: Math.min(10, productTotal), // 显示最近的10个作为今日新增
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '代理数量',
|
||||
totalTitle: '总代理数量',
|
||||
totalValue: agentTotal,
|
||||
value: Math.min(50, agentTotal), // 显示最近的50个作为今日新增
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchStatistics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div class="mb-4 ml-4 text-lg text-gray-500">
|
||||
该数据为演示模拟生成,不为真实数据
|
||||
</div>
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
|
||||
@@ -17,6 +17,27 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '描述',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'cost_price',
|
||||
label: '成本价',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
step: 0.01,
|
||||
formatter: (value: number) => {
|
||||
// 格式化为带千分位分隔符的货币
|
||||
const parts = value.toString().split('.');
|
||||
parts[0] = (parts[0] || '0').replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return `¥ ${parts.join('.')}`;
|
||||
},
|
||||
parser: (value: string) => {
|
||||
// 移除货币符号和千分位分隔符
|
||||
return value.replace(/[¥,\s]/g, '');
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -33,6 +54,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'name',
|
||||
label: '描述',
|
||||
},
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
@@ -51,6 +73,18 @@ export function useColumns<T = FeatureApi.FeatureItem>(
|
||||
title: '描述',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'cost_price',
|
||||
title: '成本价',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) => {
|
||||
// 格式化为带千分位分隔符的货币
|
||||
const value = cellValue?.toFixed(2) || '0.00';
|
||||
const parts = value.split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return `¥ ${parts.join('.')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: '创建时间',
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getFeatureList } from '#/api/product-manage/feature';
|
||||
import {
|
||||
getProductFeatureList,
|
||||
updateProduct,
|
||||
updateProductFeatures,
|
||||
} from '#/api/product-manage/product';
|
||||
|
||||
@@ -71,11 +72,28 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
// 更新产品模块关联
|
||||
await updateProductFeatures(productId, { features });
|
||||
message.success('保存成功');
|
||||
|
||||
// 计算关联模块的总成本(只计算启用的模块)
|
||||
let totalCost = 0;
|
||||
const enabledFeatures = tempFeatureList.value.filter(item => item.enable === 1);
|
||||
|
||||
// 使用缓存的模块数据计算总成本
|
||||
for (const feature of enabledFeatures) {
|
||||
const featureDetail = allFeaturesCache.value.find(f => f.id === feature.feature_id);
|
||||
if (featureDetail) {
|
||||
totalCost += featureDetail.cost_price || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新产品成本价
|
||||
await updateProduct(productId, { cost_price: totalCost });
|
||||
|
||||
message.success(`保存成功,产品成本已更新为: ¥${totalCost.toFixed(2)}`);
|
||||
emit('success');
|
||||
modalApi.close(); // 保存成功后关闭Modal
|
||||
return true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
message.error('保存失败');
|
||||
return false;
|
||||
}
|
||||
@@ -84,6 +102,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
const loading = ref(false);
|
||||
const tempFeatureList = ref<TempFeatureItem[]>([]);
|
||||
// 存储模块详细信息,用于成本计算
|
||||
const allFeaturesCache = ref<FeatureApi.FeatureItem[]>([]);
|
||||
|
||||
// 表格配置
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
@@ -219,6 +239,7 @@ async function loadFeatureList() {
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 获取产品已关联的模块列表
|
||||
const res = await getProductFeatureList(productId);
|
||||
// 转换为临时数据格式
|
||||
let tempList = res.map((item) => ({
|
||||
@@ -237,6 +258,11 @@ async function loadFeatureList() {
|
||||
.map((item, idx) => ({ ...item, sort: idx + 1 }));
|
||||
}
|
||||
tempFeatureList.value = tempList;
|
||||
|
||||
// 获取并缓存所有模块数据(用于成本计算)
|
||||
const allFeaturesRes = await getFeatureList({ page: 1, pageSize: 1000 });
|
||||
allFeaturesCache.value = allFeaturesRes.items || [];
|
||||
|
||||
initSortable();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -326,6 +352,22 @@ function handleRemoveFeature(record: TempFeatureItem) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 计算已启用模块的总成本
|
||||
function calculateTotalCost() {
|
||||
let totalCost = 0;
|
||||
const enabledFeatures = tempFeatureList.value.filter(item => item.enable === 1);
|
||||
|
||||
// 使用缓存的模块数据计算总成本
|
||||
for (const feature of enabledFeatures) {
|
||||
const featureDetail = allFeaturesCache.value.find(f => f.id === feature.feature_id);
|
||||
if (featureDetail) {
|
||||
totalCost += featureDetail.cost_price || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return totalCost;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -343,6 +385,12 @@ function handleRemoveFeature(record: TempFeatureItem) {
|
||||
<!-- 右侧:已关联模块列表 -->
|
||||
<div class="flex-1">
|
||||
<div class="mb-2 text-base font-medium">已关联模块</div>
|
||||
<div class="mb-2 p-3 bg-gray-50 rounded">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">已启用模块总成本:</span>
|
||||
<span class="text-lg font-bold text-red-600">¥{{ calculateTotalCost().toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 text-sm text-gray-500">
|
||||
提示:可以通过拖拽行来调整模块顺序,通过开关控制模块的启用状态和重要程度
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user