This commit is contained in:
2026-03-02 12:59:39 +08:00
parent b98b39bee5
commit 868701337c
9 changed files with 603 additions and 152 deletions

View File

@@ -0,0 +1,68 @@
import { requestClient } from '#/api/request';
export namespace DashboardApi {
export interface OrderStatistics {
today_count: number;
month_count: number;
total_count: number;
yesterday_count: number;
change_rate: number;
}
export interface RevenueStatistics {
today_amount: number;
month_amount: number;
total_amount: number;
yesterday_amount: number;
change_rate: number;
}
export interface AgentStatistics {
total_count: number;
today_new: number;
month_new: number;
}
export interface ProfitDetail {
revenue: number;
commission: number;
rebate: number;
company_tax: number;
api_cost: number;
tax_income: number;
profit: number;
profit_rate: number;
}
export interface ProfitStatistics {
today_profit: number;
month_profit: number;
total_profit: number;
today_profit_rate: number;
month_profit_rate: number;
total_profit_rate: number;
today_detail: ProfitDetail;
month_detail: ProfitDetail;
total_detail: ProfitDetail;
}
export interface TrendData {
date: string;
value: number;
}
export interface DashboardStatistics {
order_stats: OrderStatistics;
revenue_stats: RevenueStatistics;
agent_stats: AgentStatistics;
profit_stats: ProfitStatistics;
order_trend: TrendData[];
revenue_trend: TrendData[];
}
}
export async function getDashboardStatistics(): Promise<DashboardApi.DashboardStatistics> {
return await requestClient.get<DashboardApi.DashboardStatistics>(
'/dashboard/statistics',
);
}

View File

@@ -0,0 +1,2 @@
export { getDashboardStatistics } from './dashboard';
export type { DashboardApi } from './dashboard';

View File

@@ -7,6 +7,8 @@ export namespace FeatureApi {
id: number; id: number;
api_id: string; api_id: string;
name: string; name: string;
whitelist_price: number;
cost_price: number;
create_time: string; create_time: string;
update_time: string; update_time: string;
} }
@@ -19,11 +21,15 @@ export namespace FeatureApi {
export interface CreateFeatureRequest { export interface CreateFeatureRequest {
api_id: string; api_id: string;
name: string; name: string;
whitelist_price?: number;
cost_price?: number;
} }
export interface UpdateFeatureRequest { export interface UpdateFeatureRequest {
api_id?: string; api_id?: string;
name?: string; name?: string;
whitelist_price?: number;
cost_price?: number;
} }
export interface FeatureExampleItem { export interface FeatureExampleItem {

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import type { DashboardApi } from '#/api/dashboard/dashboard';
interface Props {
profitStats?: DashboardApi.ProfitStatistics;
}
defineProps<Props>();
// 格式化金额
const formatAmount = (amount: number) => {
if (amount >= 10000) {
return `${(amount / 10000).toFixed(2)}`;
}
return amount.toFixed(2);
};
</script>
<template>
<div v-if="profitStats" class="revenue-panel">
<div class="revenue-panel-header">
<h3 class="text-lg font-semibold text-gray-800">收入统计</h3>
</div>
<div class="revenue-panel-content">
<div class="revenue-grid">
<div class="revenue-card">
<div class="card-header">
<span class="card-title">今日</span>
<div class="card-profit">
<span class="profit-label">利润</span>
<span class="profit-value" :class="profitStats.today_profit >= 0 ? 'text-green-600' : 'text-red-600'">
¥{{ formatAmount(profitStats.today_profit) }}
</span>
</div>
</div>
<div class="card-content">
<div class="detail-section">
<div class="section-title">收入</div>
<div class="detail-item">
<span class="detail-label">营收</span>
<span class="detail-value text-green-600">+¥{{ formatAmount(profitStats.today_detail?.revenue || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">提现收税</span>
<span class="detail-value text-green-600">+¥{{ formatAmount(profitStats.today_detail?.tax_income || 0) }}</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">收入合计</span>
<span class="detail-value font-semibold text-green-600">
+¥{{ formatAmount((profitStats.today_detail?.revenue || 0) + (profitStats.today_detail?.tax_income || 0)) }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">成本</div>
<div class="detail-item">
<span class="detail-label">代理佣金</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.today_detail?.commission || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">代理返利</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.today_detail?.rebate || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">税务成本</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.today_detail?.company_tax || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">API调用成本</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.today_detail?.api_cost || 0) }}</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">成本合计</span>
<span class="detail-value font-semibold text-red-600">
-¥{{ formatAmount((profitStats.today_detail?.commission || 0) + (profitStats.today_detail?.rebate || 0) + (profitStats.today_detail?.company_tax || 0) + (profitStats.today_detail?.api_cost || 0)) }}
</span>
</div>
</div>
</div>
</div>
<div class="revenue-card">
<div class="card-header">
<span class="card-title">当月</span>
<div class="card-profit">
<span class="profit-label">利润</span>
<span class="profit-value" :class="profitStats.month_profit >= 0 ? 'text-green-600' : 'text-red-600'">
¥{{ formatAmount(profitStats.month_profit) }}
</span>
</div>
</div>
<div class="card-content">
<div class="detail-section">
<div class="section-title">收入</div>
<div class="detail-item">
<span class="detail-label">营收</span>
<span class="detail-value text-green-600">+¥{{ formatAmount(profitStats.month_detail?.revenue || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">提现收税</span>
<span class="detail-value text-green-600">+¥{{ formatAmount(profitStats.month_detail?.tax_income || 0) }}</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">收入合计</span>
<span class="detail-value font-semibold text-green-600">
+¥{{ formatAmount((profitStats.month_detail?.revenue || 0) + (profitStats.month_detail?.tax_income || 0)) }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">成本</div>
<div class="detail-item">
<span class="detail-label">代理佣金</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.month_detail?.commission || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">代理返利</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.month_detail?.rebate || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">税务成本</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.month_detail?.company_tax || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">API调用成本</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.month_detail?.api_cost || 0) }}</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">成本合计</span>
<span class="detail-value font-semibold text-red-600">
-¥{{ formatAmount((profitStats.month_detail?.commission || 0) + (profitStats.month_detail?.rebate || 0) + (profitStats.month_detail?.company_tax || 0) + (profitStats.month_detail?.api_cost || 0)) }}
</span>
</div>
</div>
</div>
</div>
<div class="revenue-card">
<div class="card-header">
<span class="card-title">总计</span>
<div class="card-profit">
<span class="profit-label">利润</span>
<span class="profit-value" :class="profitStats.total_profit >= 0 ? 'text-green-600' : 'text-red-600'">
¥{{ formatAmount(profitStats.total_profit) }}
</span>
</div>
</div>
<div class="card-content">
<div class="detail-section">
<div class="section-title">收入</div>
<div class="detail-item">
<span class="detail-label">营收</span>
<span class="detail-value text-green-600">+¥{{ formatAmount(profitStats.total_detail?.revenue || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">提现收税</span>
<span class="detail-value text-green-600">+¥{{ formatAmount(profitStats.total_detail?.tax_income || 0) }}</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">收入合计</span>
<span class="detail-value font-semibold text-green-600">
+¥{{ formatAmount((profitStats.total_detail?.revenue || 0) + (profitStats.total_detail?.tax_income || 0)) }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">成本</div>
<div class="detail-item">
<span class="detail-label">代理佣金</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.total_detail?.commission || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">代理返利</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.total_detail?.rebate || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">税务成本</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.total_detail?.company_tax || 0) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">API调用成本</span>
<span class="detail-value text-red-600">-¥{{ formatAmount(profitStats.total_detail?.api_cost || 0) }}</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">成本合计</span>
<span class="detail-value font-semibold text-red-600">
-¥{{ formatAmount((profitStats.total_detail?.commission || 0) + (profitStats.total_detail?.rebate || 0) + (profitStats.total_detail?.company_tax || 0) + (profitStats.total_detail?.api_cost || 0)) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.revenue-panel {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.revenue-panel-header { margin-bottom: 16px; }
.revenue-panel-content .revenue-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
@media (max-width: 1200px) { grid-template-columns: 1fr; }
}
.revenue-card {
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
.card-header {
background: #fff;
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
.card-title { font-size: 15px; font-weight: 600; color: #333; }
.card-profit { display: flex; flex-direction: column; align-items: flex-end;
.profit-label { font-size: 11px; color: #999; margin-bottom: 2px; }
.profit-value { font-size: 16px; font-weight: 600; }
}
}
.card-content {
padding: 12px 16px;
.detail-section {
margin-bottom: 16px;
&:last-child { margin-bottom: 0; }
.section-title { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 10px; }
.detail-item {
display: flex; justify-content: space-between; align-items: center;
padding: 6px 0; border-bottom: 1px solid #f0f0f0;
&.total { border-bottom: 2px solid #e0e0e0; margin-top: 8px; padding-top: 10px; }
.detail-label { color: #666; font-size: 12px; }
.detail-value { font-size: 12px; font-weight: 500; }
}
}
}
}
}
</style>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard/dashboard';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const updateChart = () => {
if (!props.data || props.data.length === 0) return;
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: { bottom: 0, containLabel: true, left: '1%', right: '1%', top: '2%' },
series: [{ areaStyle: {}, data: values, itemStyle: { color: '#019680' }, smooth: true, type: 'line', name: '营收金额' }],
tooltip: {
axisPointer: { lineStyle: { color: '#019680', width: 1 } },
trigger: 'axis',
formatter: (params: any) => {
const param = params[0];
return `${param.name}<br/>${param.seriesName}: ¥${param.value.toFixed(2)}`;
},
},
xAxis: {
axisTick: { show: false },
boundaryGap: false,
data: dates,
splitLine: { lineStyle: { type: 'solid', width: 1 }, show: true },
type: 'category',
},
yAxis: [{
axisTick: { show: false },
splitArea: { show: true },
splitNumber: 4,
type: 'value',
axisLabel: {
formatter: (value: number) => (value >= 10000 ? `${(value / 10000).toFixed(1)}` : value.toString()),
},
}],
});
};
watch(() => props.data, () => updateChart(), { deep: true });
onMounted(() => updateChart());
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -1,79 +1,41 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts'; import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard/dashboard';
import { onMounted, ref } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);
onMounted(() => { const updateChart = () => {
if (!props.data || props.data.length === 0) return;
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({ renderEcharts({
grid: { grid: { bottom: 0, containLabel: true, left: '1%', right: '1%', top: '2%' },
bottom: 0, series: [{ areaStyle: {}, data: values, itemStyle: { color: '#5ab1ef' }, smooth: true, type: 'line', name: '订单数' }],
containLabel: true, tooltip: { axisPointer: { lineStyle: { color: '#5ab1ef', width: 1 } }, trigger: 'axis' },
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: '访问量',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#5ab1ef',
width: 1,
},
},
trigger: 'axis',
},
xAxis: { xAxis: {
axisTick: { axisTick: { show: false },
show: false,
},
boundaryGap: false, boundaryGap: false,
data: Array.from({ length: 30 }).map( data: dates,
(_item, index) => `Day ${index + 1}`, splitLine: { lineStyle: { type: 'solid', width: 1 }, show: true },
),
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category', type: 'category',
}, },
yAxis: [ yAxis: [{ axisTick: { show: false }, splitArea: { show: true }, splitNumber: 4, type: 'value' }],
{
axisTick: {
show: false,
},
max: 3000,
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value',
},
],
}); });
}); };
watch(() => props.data, () => updateChart(), { deep: true });
onMounted(() => updateChart());
</script> </script>
<template> <template>

View File

@@ -1,102 +1,150 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types'; import type { TabOption } from '@vben/types';
import { import { onMounted, ref } from 'vue';
AnalysisChartCard,
AnalysisChartsTabs, import { AnalysisChartCard, AnalysisChartsTabs } from '@vben/common-ui';
AnalysisOverview, import { message } from 'ant-design-vue';
} from '@vben/common-ui';
import { import { getDashboardStatistics } from '#/api/dashboard/dashboard';
SvgBellIcon, import type { DashboardApi } from '#/api/dashboard/dashboard';
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import AnalyticsTrends from './analytics-trends.vue'; import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue'; import AnalyticsRevenueTrend from './analytics-revenue-trend.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue'; import AnalyticsProfitPanel from './analytics-profit-panel.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
import AnalyticsVisits from './analytics-visits.vue';
const overviewItems: AnalysisOverviewItem[] = [ const loading = ref(false);
{ const statistics = ref<DashboardApi.DashboardStatistics | null>(null);
icon: SvgCardIcon,
title: '平台用户数', const formatNumber = (num: number) => {
totalTitle: '总用户数', if (num >= 10000) return `${(num / 10000).toFixed(2)}`;
totalValue: 120_000, return num.toString();
value: 2000, };
},
{ const loadStatistics = async () => {
icon: SvgCakeIcon, loading.value = true;
title: '推广访问量', try {
totalTitle: '总推广访问量', const data = await getDashboardStatistics();
totalValue: 500_000, statistics.value = data;
value: 20_000, } catch {
}, message.error('加载统计数据失败');
{ } finally {
icon: SvgDownloadIcon, loading.value = false;
title: '产品数量', }
totalTitle: '总产品数量', };
totalValue: 120,
value: 8,
},
{
icon: SvgBellIcon,
title: '代理数量',
totalTitle: '总代理数量',
totalValue: 5000,
value: 500,
},
];
const chartTabs: TabOption[] = [ const chartTabs: TabOption[] = [
{ { label: '订单趋势', value: 'order' },
label: '推广访问趋势', { label: '营收趋势', value: 'revenue' },
value: 'trends',
},
{
label: '订单趋势',
value: 'visits',
},
]; ];
onMounted(() => {
loadStatistics();
});
</script> </script>
<template> <template>
<div class="p-5"> <div class="p-5 dashboard-wrapper">
<div class="mb-4 ml-4 text-lg text-gray-500"> <a-spin :spinning="loading">
该数据为演示模拟生成不为真实数据 <div v-if="statistics" class="dashboard-container">
</div> <div class="dashboard-main">
<AnalysisOverview :items="overviewItems" /> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5"> <AnalysisChartCard title="订单统计">
<template #trends> <div class="py-2">
<AnalyticsTrends /> <div class="mb-3">
</template> <div class="text-xs text-gray-500 mb-1">今日订单</div>
<template #visits> <div class="flex items-baseline justify-between">
<AnalyticsVisits /> <div class="text-3xl font-bold text-blue-600">
</template> {{ statistics.order_stats.today_count }}<span class="text-lg font-normal ml-1 text-gray-500"></span>
</AnalysisChartsTabs> </div>
<div v-if="statistics.order_stats.change_rate" class="text-xs">
<span :class="statistics.order_stats.change_rate > 0 ? 'text-green-500' : 'text-red-500'">
{{ statistics.order_stats.change_rate > 0 ? '↑' : '↓' }}
{{ Math.abs(statistics.order_stats.change_rate).toFixed(1) }}%
</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-3 pt-3 border-t">
<div>
<div class="text-xs text-gray-500 mb-1">当月</div>
<div class="text-lg font-semibold text-gray-700">
{{ formatNumber(statistics.order_stats.month_count) }}<span class="text-sm font-normal text-gray-500 ml-1"></span>
</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">总计</div>
<div class="text-lg font-semibold text-gray-700">
{{ formatNumber(statistics.order_stats.total_count) }}<span class="text-sm font-normal text-gray-500 ml-1"></span>
</div>
</div>
</div>
</div>
</AnalysisChartCard>
<div class="mt-5 w-full md:flex"> <AnalysisChartCard title="代理统计">
<AnalysisChartCard <div class="py-2">
class="mt-5 md:mr-4 md:mt-0 md:w-1/3" <div class="mb-3">
title="推广数据分析" <div class="text-xs text-gray-500 mb-1">代理总数</div>
> <div class="text-3xl font-bold text-purple-600">
<AnalyticsVisitsData /> {{ statistics.agent_stats.total_count }}<span class="text-lg font-normal ml-1 text-gray-500"></span>
</AnalysisChartCard> </div>
<AnalysisChartCard </div>
class="mt-5 md:mr-4 md:mt-0 md:w-1/3" <div class="grid grid-cols-2 gap-3 pt-3 border-t">
title="订单来源分析" <div>
> <div class="text-xs text-gray-500 mb-1">今日新增</div>
<AnalyticsVisitsSource /> <div class="text-lg font-semibold text-gray-700">{{ statistics.agent_stats.today_new }}<span class="text-sm font-normal text-gray-500 ml-1"></span></div>
</AnalysisChartCard> </div>
<AnalysisChartCard <div>
class="mt-5 md:mt-0 md:w-1/3" <div class="text-xs text-gray-500 mb-1">当月新增</div>
title="佣金/奖励/提现统计" <div class="text-lg font-semibold text-gray-700">{{ statistics.agent_stats.month_new }}<span class="text-sm font-normal text-gray-500 ml-1"></span></div>
> </div>
<AnalyticsVisitsSales /> </div>
</AnalysisChartCard> </div>
</div> </AnalysisChartCard>
</div>
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #order>
<AnalyticsTrends :data="statistics.order_trend" />
</template>
<template #revenue>
<AnalyticsRevenueTrend :data="statistics.revenue_trend" />
</template>
</AnalysisChartsTabs>
</div>
<div class="dashboard-sidebar">
<AnalyticsProfitPanel :profit-stats="statistics.profit_stats" />
</div>
</div>
</a-spin>
</div> </div>
</template> </template>
<style scoped lang="less">
.dashboard-wrapper {
min-height: 100vh;
box-sizing: border-box;
}
.dashboard-container {
display: flex;
gap: 16px;
align-items: flex-start;
.dashboard-main { flex: 1; min-width: 0; }
.dashboard-sidebar {
width: 800px;
flex-shrink: 0;
position: sticky;
top: 16px;
max-height: calc(100vh - 32px);
overflow: auto;
}
}
@media (max-width: 1600px) {
.dashboard-container {
flex-direction: column;
.dashboard-sidebar { width: 100%; position: static; max-height: none; overflow: visible; }
}
}
</style>

View File

@@ -17,6 +17,36 @@ export function useFormSchema(): VbenFormSchema[] {
label: '描述', label: '描述',
rules: 'required', rules: 'required',
}, },
{
component: 'Switch',
fieldName: 'no_offline',
label: '不支持下架',
defaultValue: false,
help: '勾选后该模块不开放下架功能,提交时白名单价格传 -1',
},
{
component: 'InputNumber',
fieldName: 'whitelist_price',
label: '白名单屏蔽价格(元)',
componentProps: {
min: 0,
precision: 2,
placeholder: '0=免费下架,>0=付费下架;勾选「不支持下架」时此项忽略',
},
dependencies: {
triggerFields: ['no_offline'],
if(values) {
return !values?.no_offline;
},
},
},
{
component: 'InputNumber',
componentProps: { min: 0, precision: 2 },
fieldName: 'cost_price',
label: '成本价(元)',
rules: 'required',
},
]; ];
} }
@@ -51,6 +81,28 @@ export function useColumns<T = FeatureApi.FeatureItem>(
title: '描述', title: '描述',
minWidth: 200, minWidth: 200,
}, },
{
field: 'whitelist_price',
title: '白名单屏蔽价格(元)',
minWidth: 150,
cellRender: {
name: 'VxeCellRender',
props: {
render: ({ row }: { row: FeatureApi.FeatureItem }) => {
const price = (row as FeatureApi.FeatureItem).whitelist_price ?? 0;
if (price < 0) return '不支持下架';
if (price === 0) return '免费下架';
return `¥${price.toFixed(2)}`;
},
},
},
},
{
field: 'cost_price',
formatter: ({ cellValue }) => `¥${(cellValue || 0).toFixed(2)}`,
title: '成本价(元)',
minWidth: 120,
},
{ {
field: 'create_time', field: 'create_time',
title: '创建时间', title: '创建时间',

View File

@@ -24,12 +24,17 @@ const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) return; if (!valid) return;
const values = await formApi.getValues(); const values = (await formApi.getValues()) as Record<string, unknown>;
if (values.no_offline) {
values.whitelist_price = -1;
}
delete values.no_offline;
drawerApi.lock(); drawerApi.lock();
try { try {
const payload = values as unknown as FeatureApi.CreateFeatureRequest & FeatureApi.UpdateFeatureRequest;
await (id.value await (id.value
? updateFeature(id.value, values as FeatureApi.UpdateFeatureRequest) ? updateFeature(id.value, payload)
: createFeature(values as FeatureApi.CreateFeatureRequest)); : createFeature(payload));
emit('success'); emit('success');
drawerApi.close(); drawerApi.close();
} catch { } catch {
@@ -43,7 +48,12 @@ const [Drawer, drawerApi] = useVbenDrawer({
if (data) { if (data) {
formData.value = data; formData.value = data;
id.value = data.id; id.value = data.id;
formApi.setValues(data); const noOffline = (data.whitelist_price ?? 0) < 0;
formApi.setValues({
...data,
no_offline: noOffline,
whitelist_price: noOffline ? 0 : (data.whitelist_price ?? 0),
});
} else { } else {
id.value = undefined; id.value = undefined;
} }