|
|
@@ -1,112 +1,638 @@
|
|
|
<template>
|
|
|
<div class="settings-page">
|
|
|
- <h2 style="margin-bottom: 20px;">系统配置</h2>
|
|
|
- <el-card shadow="hover">
|
|
|
- <template #header><span>配置项管理</span></template>
|
|
|
- <el-form label-width="200px" v-loading="loading">
|
|
|
- <el-form-item label="每日命盘限额(免费用户)">
|
|
|
- <div class="config-row">
|
|
|
- <el-input-number v-model="config.quota_chart" :min="1" :max="20" />
|
|
|
- <el-button size="small" type="primary" @click="saveConfig('quota_chart')" style="margin-left: 12px;">保存</el-button>
|
|
|
- <span class="config-desc">免费用户每天可生成的命盘数量</span>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="每日聊天限额(免费用户)">
|
|
|
- <div class="config-row">
|
|
|
- <el-input-number v-model="config.quota_chat" :min="1" :max="50" />
|
|
|
- <el-button size="small" type="primary" @click="saveConfig('quota_chat')" style="margin-left: 12px;">保存</el-button>
|
|
|
- <span class="config-desc">免费用户每天可发送的聊天消息数量</span>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="年费价格">
|
|
|
- <div class="config-row">
|
|
|
- <el-input-number v-model="config.vip_price" :min="9900" :max="99900" :step="1000" />
|
|
|
- <el-button size="small" type="primary" @click="saveConfig('vip_price')" style="margin-left: 12px;">保存</el-button>
|
|
|
- <span class="config-desc">VIP 年费价格(单位:分),当前 ¥{{ (config.vip_price / 100).toFixed(0) }}</span>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="一级佣金比例">
|
|
|
- <div class="config-row">
|
|
|
- <el-input-number v-model="config.commission_level1" :min="10" :max="40" />
|
|
|
- <el-button size="small" type="primary" @click="saveConfig('commission_level1')" style="margin-left: 12px;">保存</el-button>
|
|
|
- <span class="config-desc">一级推广佣金比例(%)</span>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="二级佣金比例">
|
|
|
- <div class="config-row">
|
|
|
- <el-input-number v-model="config.commission_level2" :min="5" :max="20" />
|
|
|
- <el-button size="small" type="primary" @click="saveConfig('commission_level2')" style="margin-left: 12px;">保存</el-button>
|
|
|
- <span class="config-desc">二级推广佣金比例(%)</span>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </el-card>
|
|
|
+ <div class="page-header">
|
|
|
+ <h2>系统配置</h2>
|
|
|
+ <el-button type="primary" @click="handleBatchSave" :loading="saving">
|
|
|
+ 保存全部配置
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-loading="loading" class="cards-container">
|
|
|
+ <!-- Card 1: 定价配置 -->
|
|
|
+ <el-card shadow="hover" class="config-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>定价配置</span>
|
|
|
+ <el-tooltip :content="resetTooltipContent(pricingKeys)" placement="top">
|
|
|
+ <el-button size="small" text type="primary" @click="handleReset(pricingKeys)">
|
|
|
+ 恢复初始值
|
|
|
+ </el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">能量师种子价</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['pricing.practitioner.seed']"
|
|
|
+ :min="1" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">能量师标准价</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['pricing.practitioner.standard']"
|
|
|
+ :min="1" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">种子价名额上限</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['pricing.practitioner.seed_limit']"
|
|
|
+ :min="1" :max="99999" :step="50" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="field-hint">
|
|
|
+ 当前已使用:{{ seedUsedCount }} / {{ configValues['pricing.practitioner.seed_limit'] }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">C端年费</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['pricing.annual']"
|
|
|
+ :min="1" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- Card 2: B端佣金配置 -->
|
|
|
+ <el-card shadow="hover" class="config-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>B端佣金配置</span>
|
|
|
+ <el-tooltip :content="resetTooltipContent(bCommissionKeys)" placement="top">
|
|
|
+ <el-button size="small" text type="primary" @click="handleReset(bCommissionKeys)">
|
|
|
+ 恢复初始值
|
|
|
+ </el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">L1 佣金—能量师推荐</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.practitioner.l1']"
|
|
|
+ :min="0" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ <div class="field-hint" v-if="seedPrice > 0 && standardPrice > 0">
|
|
|
+ 实时比例:L1 ¥{{ configValues['commission.practitioner.l1'] }} = 种子价 {{ l1SeedPercent }}% / 标准价 {{ l1StandardPercent }}%
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">L1 佣金—C端推荐</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.practitioner.l1_cend']"
|
|
|
+ :min="0" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ <div class="field-hint" v-if="seedPrice > 0 && standardPrice > 0">
|
|
|
+ 实时比例:L1 ¥{{ configValues['commission.practitioner.l1_cend'] }} = 种子价 {{ l1cendSeedPercent }}% / 标准价 {{ l1cendStandardPercent }}%
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">L2 佣金</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.practitioner.l2']"
|
|
|
+ :min="0" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ <div class="field-hint" v-if="seedPrice > 0 && standardPrice > 0">
|
|
|
+ 实时比例:L2 ¥{{ configValues['commission.practitioner.l2'] }} = 种子价 {{ l2SeedPercent }}% / 标准价 {{ l2StandardPercent }}%
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- Card 3: C端佣金配置 -->
|
|
|
+ <el-card shadow="hover" class="config-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>C端佣金配置</span>
|
|
|
+ <el-tooltip :content="resetTooltipContent(cCommissionKeys)" placement="top">
|
|
|
+ <el-button size="small" text type="primary" @click="handleReset(cCommissionKeys)">
|
|
|
+ 恢复初始值
|
|
|
+ </el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">直接佣金比例</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.annual.direct_rate']"
|
|
|
+ :min="0" :max="100" :step="1" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">%</span>
|
|
|
+ </div>
|
|
|
+ <div class="field-hint" v-if="annualFee > 0">
|
|
|
+ 每笔 ¥{{ annualFee }}:直接 ¥{{ directCommission }} / 上级 ¥{{ upstreamCommission }} / 平台 ¥{{ platformCommission }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">上级佣金比例</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.annual.upstream_rate']"
|
|
|
+ :min="0" :max="50" :step="1" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">%</span>
|
|
|
+ </div>
|
|
|
+ <div class="field-hint" v-if="annualFee > 0">
|
|
|
+ (每笔 ¥{{ annualFee }} 年费中上级分成)
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- Card 4: 人工方案配置 -->
|
|
|
+ <el-card shadow="hover" class="config-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>人工方案配置</span>
|
|
|
+ <el-tooltip :content="resetTooltipContent(planKeys)" placement="top">
|
|
|
+ <el-button size="small" text type="primary" @click="handleReset(planKeys)">
|
|
|
+ 恢复初始值
|
|
|
+ </el-button>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">平台佣金率</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commerce.category.practitioner_plan.commission_rate']"
|
|
|
+ :min="0" :max="100" :step="1" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">分销佣金占比(直接)</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.practitioner_plan.referral_rate']"
|
|
|
+ :min="0" :max="100" :step="1" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">分销佣金占比(上级)</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['commission.practitioner_plan.upstream_rate']"
|
|
|
+ :min="0" :max="100" :step="1" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">方案最低价</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['plan_request.min_price']"
|
|
|
+ :min="0" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">方案最高价</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['plan_request.max_price']"
|
|
|
+ :min="0" :max="999999" :step="100" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="field-unit">元</span>
|
|
|
+ </div>
|
|
|
+ <div class="field-hint" v-if="planCommissionRate > 0">
|
|
|
+ 示例:方案价 ¥299 → 佣金 ¥{{ planPreviewCommission }} → 能量师 ¥{{ planPreviewPractitioner }} → 分销 ¥{{ planPreviewReferral }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="field-item">
|
|
|
+ <div class="field-main">
|
|
|
+ <span class="field-label">最大协商轮次</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="configValues['plan_request.max_negotiation_rounds']"
|
|
|
+ :min="1" :max="20" :step="1" :precision="0"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted } from 'vue'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
+import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import request from '@/utils/request'
|
|
|
|
|
|
+/* ──────────── Constants ──────────── */
|
|
|
+
|
|
|
+const VALUE_TYPES: Record<string, 'price' | 'percent' | 'plain'> = {
|
|
|
+ 'pricing.practitioner.seed': 'price',
|
|
|
+ 'pricing.practitioner.standard': 'price',
|
|
|
+ 'pricing.practitioner.seed_limit': 'plain',
|
|
|
+ 'pricing.annual': 'price',
|
|
|
+ 'commission.practitioner.l1': 'price',
|
|
|
+ 'commission.practitioner.l1_cend': 'price',
|
|
|
+ 'commission.practitioner.l2': 'price',
|
|
|
+ 'commission.annual.direct_rate': 'percent',
|
|
|
+ 'commission.annual.upstream_rate': 'percent',
|
|
|
+ 'commerce.category.practitioner_plan.commission_rate': 'percent',
|
|
|
+ 'commission.practitioner_plan.referral_rate': 'percent',
|
|
|
+ 'commission.practitioner_plan.upstream_rate': 'percent',
|
|
|
+ 'plan_request.max_negotiation_rounds': 'plain',
|
|
|
+ 'plan_request.min_price': 'price',
|
|
|
+ 'plan_request.max_price': 'price',
|
|
|
+}
|
|
|
+
|
|
|
+const ALL_CONFIG_KEYS: string[] = [
|
|
|
+ 'pricing.practitioner.seed',
|
|
|
+ 'pricing.practitioner.standard',
|
|
|
+ 'pricing.practitioner.seed_limit',
|
|
|
+ 'pricing.annual',
|
|
|
+ 'commission.practitioner.l1',
|
|
|
+ 'commission.practitioner.l1_cend',
|
|
|
+ 'commission.practitioner.l2',
|
|
|
+ 'commission.annual.direct_rate',
|
|
|
+ 'commission.annual.upstream_rate',
|
|
|
+ 'commerce.category.practitioner_plan.commission_rate',
|
|
|
+ 'commission.practitioner_plan.referral_rate',
|
|
|
+ 'commission.practitioner_plan.upstream_rate',
|
|
|
+ 'plan_request.enabled',
|
|
|
+ 'plan_request.max_negotiation_rounds',
|
|
|
+ 'plan_request.auto_cancel_hours',
|
|
|
+ 'plan_request.min_price',
|
|
|
+ 'plan_request.max_price',
|
|
|
+ 'quota_chart',
|
|
|
+ 'quota_chat',
|
|
|
+ 'quota_share',
|
|
|
+]
|
|
|
+
|
|
|
+const SEED_DEFAULTS: Record<string, string> = {
|
|
|
+ 'pricing.practitioner.seed': '131400',
|
|
|
+ 'pricing.practitioner.standard': '198600',
|
|
|
+ 'pricing.practitioner.seed_limit': '300',
|
|
|
+ 'pricing.annual': '13100',
|
|
|
+ 'commission.practitioner.l1': '50000',
|
|
|
+ 'commission.practitioner.l1_cend': '20000',
|
|
|
+ 'commission.practitioner.l2': '10000',
|
|
|
+ 'commission.annual.direct_rate': '4000',
|
|
|
+ 'commission.annual.upstream_rate': '500',
|
|
|
+ 'commerce.category.practitioner_plan.commission_rate': '3000',
|
|
|
+ 'commission.practitioner_plan.referral_rate': '2000',
|
|
|
+ 'commission.practitioner_plan.upstream_rate': '500',
|
|
|
+ 'plan_request.enabled': 'true',
|
|
|
+ 'plan_request.max_negotiation_rounds': '5',
|
|
|
+ 'plan_request.auto_cancel_hours': '72',
|
|
|
+ 'plan_request.min_price': '5000',
|
|
|
+ 'plan_request.max_price': '999900',
|
|
|
+}
|
|
|
+
|
|
|
+/* ──────────── Card key groups ──────────── */
|
|
|
+
|
|
|
+const pricingKeys = ['pricing.practitioner.seed', 'pricing.practitioner.standard', 'pricing.practitioner.seed_limit', 'pricing.annual']
|
|
|
+const bCommissionKeys = ['commission.practitioner.l1', 'commission.practitioner.l1_cend', 'commission.practitioner.l2']
|
|
|
+const cCommissionKeys = ['commission.annual.direct_rate', 'commission.annual.upstream_rate']
|
|
|
+const planKeys = ['commerce.category.practitioner_plan.commission_rate', 'commission.practitioner_plan.referral_rate', 'commission.practitioner_plan.upstream_rate', 'plan_request.min_price', 'plan_request.max_price', 'plan_request.max_negotiation_rounds']
|
|
|
+
|
|
|
+/* ──────────── Field labels ──────────── */
|
|
|
+
|
|
|
+const FIELD_LABELS: Record<string, string> = {
|
|
|
+ 'pricing.practitioner.seed': '能量师种子价',
|
|
|
+ 'pricing.practitioner.standard': '能量师标准价',
|
|
|
+ 'pricing.practitioner.seed_limit': '种子价名额上限',
|
|
|
+ 'pricing.annual': 'C端年费',
|
|
|
+ 'commission.practitioner.l1': 'L1 佣金—能量师推荐',
|
|
|
+ 'commission.practitioner.l1_cend': 'L1 佣金—C端推荐',
|
|
|
+ 'commission.practitioner.l2': 'L2 佣金',
|
|
|
+ 'commission.annual.direct_rate': '直接佣金比例',
|
|
|
+ 'commission.annual.upstream_rate': '上级佣金比例',
|
|
|
+ 'commerce.category.practitioner_plan.commission_rate': '平台佣金率',
|
|
|
+ 'commission.practitioner_plan.referral_rate': '分销佣金占比(直接)',
|
|
|
+ 'commission.practitioner_plan.upstream_rate': '分销佣金占比(上级)',
|
|
|
+ 'plan_request.min_price': '方案最低价',
|
|
|
+ 'plan_request.max_price': '方案最高价',
|
|
|
+ 'plan_request.max_negotiation_rounds': '最大协商轮次',
|
|
|
+}
|
|
|
+
|
|
|
+/* ──────────── State ──────────── */
|
|
|
+
|
|
|
const loading = ref(false)
|
|
|
+const saving = ref(false)
|
|
|
+const seedUsedCount = ref(0)
|
|
|
+
|
|
|
+/** Stores raw API values (storage units) for keys not in edit fields */
|
|
|
+const storageValues = reactive<Record<string, string>>({})
|
|
|
|
|
|
-const config = reactive<Record<string, number>>({
|
|
|
- quota_chart: 3,
|
|
|
- quota_chat: 1,
|
|
|
- vip_price: 39800,
|
|
|
- commission_level1: 30,
|
|
|
- commission_level2: 10,
|
|
|
+/** Stores current display-unit editable values */
|
|
|
+const configValues = reactive<Record<string, number>>({})
|
|
|
+
|
|
|
+/** Initialise with seed defaults */
|
|
|
+Object.entries(SEED_DEFAULTS).forEach(([key, value]) => {
|
|
|
+ if (key in VALUE_TYPES) {
|
|
|
+ configValues[key] = storageToDisplay(key, value)
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
+/* ──────────── Unit conversion ──────────── */
|
|
|
+
|
|
|
+function storageToDisplay(key: string, raw: string): number {
|
|
|
+ const vt = VALUE_TYPES[key]
|
|
|
+ const num = Number(raw)
|
|
|
+ if (vt === 'price' || vt === 'percent') return num / 100
|
|
|
+ return num
|
|
|
+}
|
|
|
+
|
|
|
+function displayToStorage(key: string, display: number): number {
|
|
|
+ const vt = VALUE_TYPES[key]
|
|
|
+ if (vt === 'price' || vt === 'percent') return Math.round(display * 100)
|
|
|
+ return Math.round(display)
|
|
|
+}
|
|
|
+
|
|
|
+/* ──────────── Formatting helpers ──────────── */
|
|
|
+
|
|
|
+function formatYuan(val: number): string {
|
|
|
+ return '¥' + val.toLocaleString('zh-CN')
|
|
|
+}
|
|
|
+
|
|
|
+function formatPercent(val: number): string {
|
|
|
+ return val.toFixed(1) + '%'
|
|
|
+}
|
|
|
+
|
|
|
+function resetTooltipContent(keys: string[]): string {
|
|
|
+ return keys
|
|
|
+ .filter(k => k in SEED_DEFAULTS)
|
|
|
+ .map(k => {
|
|
|
+ const label = FIELD_LABELS[k] || k
|
|
|
+ const displayVal = storageToDisplay(k, SEED_DEFAULTS[k])
|
|
|
+ const vt = VALUE_TYPES[k]
|
|
|
+ if (vt === 'price') return `${label}:${formatYuan(displayVal)}`
|
|
|
+ if (vt === 'percent') return `${label}:${displayVal}%`
|
|
|
+ return `${label}:${displayVal}`
|
|
|
+ })
|
|
|
+ .join(',')
|
|
|
+}
|
|
|
+
|
|
|
+/* ──────────── Computed previews ──────────── */
|
|
|
+
|
|
|
+/** Card 1 helpers */
|
|
|
+const seedPrice = computed(() => configValues['pricing.practitioner.seed'] || 0)
|
|
|
+const standardPrice = computed(() => configValues['pricing.practitioner.standard'] || 0)
|
|
|
+
|
|
|
+/** Card 2 — B端佣金实时百分比 */
|
|
|
+const l1SeedPercent = computed(() => {
|
|
|
+ const s = seedPrice.value
|
|
|
+ if (!s) return '0.0'
|
|
|
+ return ((configValues['commission.practitioner.l1'] || 0) / s * 100).toFixed(1)
|
|
|
+})
|
|
|
+const l1StandardPercent = computed(() => {
|
|
|
+ const s = standardPrice.value
|
|
|
+ if (!s) return '0.0'
|
|
|
+ return ((configValues['commission.practitioner.l1'] || 0) / s * 100).toFixed(1)
|
|
|
+})
|
|
|
+const l1cendSeedPercent = computed(() => {
|
|
|
+ const s = seedPrice.value
|
|
|
+ if (!s) return '0.0'
|
|
|
+ return ((configValues['commission.practitioner.l1_cend'] || 0) / s * 100).toFixed(1)
|
|
|
+})
|
|
|
+const l1cendStandardPercent = computed(() => {
|
|
|
+ const s = standardPrice.value
|
|
|
+ if (!s) return '0.0'
|
|
|
+ return ((configValues['commission.practitioner.l1_cend'] || 0) / s * 100).toFixed(1)
|
|
|
+})
|
|
|
+const l2SeedPercent = computed(() => {
|
|
|
+ const s = seedPrice.value
|
|
|
+ if (!s) return '0.0'
|
|
|
+ return ((configValues['commission.practitioner.l2'] || 0) / s * 100).toFixed(1)
|
|
|
+})
|
|
|
+const l2StandardPercent = computed(() => {
|
|
|
+ const s = standardPrice.value
|
|
|
+ if (!s) return '0.0'
|
|
|
+ return ((configValues['commission.practitioner.l2'] || 0) / s * 100).toFixed(1)
|
|
|
+})
|
|
|
+
|
|
|
+/** Card 3 — C端佣金分配 */
|
|
|
+const annualFee = computed(() => configValues['pricing.annual'] || 0)
|
|
|
+const directRate = computed(() => (configValues['commission.annual.direct_rate'] || 0) / 100)
|
|
|
+const upstreamRate = computed(() => (configValues['commission.annual.upstream_rate'] || 0) / 100)
|
|
|
+const directCommission = computed(() => (annualFee.value * directRate.value).toFixed(2))
|
|
|
+const upstreamCommission = computed(() => (annualFee.value * upstreamRate.value).toFixed(2))
|
|
|
+const platformCommission = computed(() => (annualFee.value - annualFee.value * directRate.value - annualFee.value * upstreamRate.value).toFixed(2))
|
|
|
+
|
|
|
+/** Card 4 — 人工方案预览 */
|
|
|
+const PLAN_EXAMPLE = 299
|
|
|
+const planCommissionRate = computed(() => (configValues['commerce.category.practitioner_plan.commission_rate'] || 0) / 100)
|
|
|
+const planReferralRate = computed(() => (configValues['commission.practitioner_plan.referral_rate'] || 0) / 100)
|
|
|
+const planPreviewCommission = computed(() => (PLAN_EXAMPLE * planCommissionRate.value).toFixed(2))
|
|
|
+const planPreviewPractitioner = computed(() => (PLAN_EXAMPLE - PLAN_EXAMPLE * planCommissionRate.value).toFixed(2))
|
|
|
+const planPreviewReferral = computed(() => (PLAN_EXAMPLE * planCommissionRate.value * planReferralRate.value).toFixed(2))
|
|
|
+
|
|
|
+/* ──────────── Data loading ──────────── */
|
|
|
+
|
|
|
async function loadConfig() {
|
|
|
loading.value = true
|
|
|
try {
|
|
|
- const data: Record<string, string> = await request.post('/admin/config', {}) as any
|
|
|
- if (data.quota_chart) config.quota_chart = Number(data.quota_chart)
|
|
|
- if (data.quota_chat) config.quota_chat = Number(data.quota_chat)
|
|
|
- if (data.vip_price) config.vip_price = Number(data.vip_price)
|
|
|
- if (data.commission_level1) config.commission_level1 = Number(data.commission_level1)
|
|
|
- if (data.commission_level2) config.commission_level2 = Number(data.commission_level2)
|
|
|
+ const data: Array<{ configKey: string; configValue: string; valueType?: string }> = await request.post('/admin/config/list', {}) as any
|
|
|
+ for (const item of data) {
|
|
|
+ // Keep raw for all keys
|
|
|
+ storageValues[item.configKey] = item.configValue
|
|
|
+ // Update display values for editable keys
|
|
|
+ if (item.configKey in VALUE_TYPES) {
|
|
|
+ configValues[item.configKey] = storageToDisplay(item.configKey, item.configValue)
|
|
|
+ }
|
|
|
+ }
|
|
|
} catch {
|
|
|
- // Use defaults
|
|
|
+ ElMessage.warning('加载配置失败,使用默认值')
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function saveConfig(key: string) {
|
|
|
+/* ──────────── Save / Reset ──────────── */
|
|
|
+
|
|
|
+function buildSavePayload(): Record<string, string> {
|
|
|
+ const payload: Record<string, string> = {}
|
|
|
+ // Add all known keys
|
|
|
+ for (const key of ALL_CONFIG_KEYS) {
|
|
|
+ if (key in configValues) {
|
|
|
+ payload[key] = String(displayToStorage(key, configValues[key]))
|
|
|
+ } else if (key in storageValues) {
|
|
|
+ payload[key] = storageValues[key]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Also include any extra keys from storageValues not in ALL_CONFIG_KEYS
|
|
|
+ for (const key of Object.keys(storageValues)) {
|
|
|
+ if (!(key in payload)) {
|
|
|
+ payload[key] = storageValues[key]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return payload
|
|
|
+}
|
|
|
+
|
|
|
+async function handleBatchSave() {
|
|
|
+ saving.value = true
|
|
|
try {
|
|
|
- await request.post('/admin/config/update', { key, value: String(config[key]) })
|
|
|
- ElMessage.success(`${getKeyLabel(key)} 已更新`)
|
|
|
- } catch {
|
|
|
- ElMessage.error('保存失败')
|
|
|
+ const payload = buildSavePayload()
|
|
|
+ await request.post('/admin/config/update', payload)
|
|
|
+ ElMessage.success('配置已保存,已实时生效')
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error('保存失败:' + (e.message || '未知错误'))
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function getKeyLabel(key: string): string {
|
|
|
- const map: Record<string, string> = {
|
|
|
- quota_chart: '每日命盘限额',
|
|
|
- quota_chat: '每日聊天限额',
|
|
|
- vip_price: '年费价格',
|
|
|
- commission_level1: '一级佣金比例',
|
|
|
- commission_level2: '二级佣金比例',
|
|
|
+async function handleReset(keys: string[]) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('将恢复本卡片内所有字段为初始值,确定吗?', '确认重置', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ })
|
|
|
+ // Set display values to seed defaults
|
|
|
+ for (const key of keys) {
|
|
|
+ if (key in SEED_DEFAULTS && key in configValues) {
|
|
|
+ configValues[key] = storageToDisplay(key, SEED_DEFAULTS[key])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Batch save only this card's keys
|
|
|
+ const payload: Record<string, string> = {}
|
|
|
+ for (const key of keys) {
|
|
|
+ if (key in configValues) {
|
|
|
+ payload[key] = String(displayToStorage(key, configValues[key]))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await request.post('/admin/config/update', payload)
|
|
|
+ ElMessage.success('已恢复初始值并保存')
|
|
|
+ } catch {
|
|
|
+ // User cancelled or save error — do nothing
|
|
|
}
|
|
|
- return map[key] || key
|
|
|
}
|
|
|
|
|
|
+/* ──────────── Lifecycle ──────────── */
|
|
|
+
|
|
|
onMounted(loadConfig)
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-.config-row {
|
|
|
+.settings-page {
|
|
|
+ padding: 24px;
|
|
|
+ max-width: 820px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.page-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 24px;
|
|
|
+
|
|
|
+ h2 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.cards-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-card {
|
|
|
+ :deep(.el-card__header) {
|
|
|
+ padding: 14px 20px;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- width: 100%;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
}
|
|
|
-.config-desc {
|
|
|
- margin-left: 16px;
|
|
|
+
|
|
|
+.card-body {
|
|
|
+ padding: 4px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.field-item {
|
|
|
+ padding: 14px 0;
|
|
|
+ border-bottom: 1px solid #f9fafb;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.field-main {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ min-width: 170px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #374151;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.field-unit {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #6b7280;
|
|
|
+ min-width: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-hint {
|
|
|
+ margin-top: 8px;
|
|
|
+ margin-left: 182px;
|
|
|
font-size: 12px;
|
|
|
- color: #9CA3AF;
|
|
|
+ color: #9ca3af;
|
|
|
+ line-height: 1.5;
|
|
|
}
|
|
|
</style>
|