feat(distribution): 分站可下调收号比例且不超过主站上限
让分销站能按自身能力下调收号比例扩大利润,同时把主站回收比例作为硬边界,避免分站把比例加得比主站更高。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export interface DistributionSite {
|
||||
tax_rate_bps: number
|
||||
settlement_hold_days: number
|
||||
listing_qrcode_url?: string
|
||||
receive_ratio_delta?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -152,6 +153,7 @@ export async function updateDistributionSite(
|
||||
| 'tax_rate_bps'
|
||||
| 'settlement_hold_days'
|
||||
| 'listing_qrcode_url'
|
||||
| 'receive_ratio_delta'
|
||||
>
|
||||
>
|
||||
) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Clock,
|
||||
Picture,
|
||||
Upload,
|
||||
Coin,
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchDistributionSites,
|
||||
@@ -45,6 +46,14 @@ const settlementForm = reactive({
|
||||
settlement_hold_days: defaultSettlementHoldDays.value,
|
||||
})
|
||||
|
||||
const receiveRatioDialogVisible = ref(false)
|
||||
const receiveRatioSubmitting = ref(false)
|
||||
const receiveRatioForm = reactive({
|
||||
site_id: 0,
|
||||
site_name: '',
|
||||
receive_ratio_delta: 0,
|
||||
})
|
||||
|
||||
const qrDialogVisible = ref(false)
|
||||
const qrSubmitting = ref(false)
|
||||
const qrUploading = ref(false)
|
||||
@@ -77,6 +86,29 @@ async function handleSettlementSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenReceiveRatio(row: DistributionSite) {
|
||||
receiveRatioForm.site_id = row.id
|
||||
receiveRatioForm.site_name = row.site_name
|
||||
receiveRatioForm.receive_ratio_delta = Number(row.receive_ratio_delta || 0)
|
||||
receiveRatioDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleReceiveRatioSubmit() {
|
||||
receiveRatioSubmitting.value = true
|
||||
try {
|
||||
await updateDistributionSite(receiveRatioForm.site_id, {
|
||||
receive_ratio_delta: Number(receiveRatioForm.receive_ratio_delta),
|
||||
})
|
||||
ElMessage.success(`【${receiveRatioForm.site_name}】收号比例下调已更新`)
|
||||
receiveRatioDialogVisible.value = false
|
||||
await loadSites()
|
||||
} catch (err) {
|
||||
ElMessage.error(readError(err, '更新收号比例失败'))
|
||||
} finally {
|
||||
receiveRatioSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenQrcode(row: DistributionSite) {
|
||||
qrForm.site_id = row.id
|
||||
qrForm.site_name = row.site_name
|
||||
@@ -257,8 +289,8 @@ onMounted(() => {
|
||||
<div>
|
||||
<h2 class="page-title">分销站点管理</h2>
|
||||
<p class="page-subtitle">
|
||||
开通分销站、配置结算冷却期与发布群固定二维码。利润按 35% 收号 / 40% 卖出 / 25%
|
||||
服务费切分。
|
||||
开通分销站、配置结算冷却期、收号比例上限与发布群固定二维码。利润按 35% 收号 / 40% 卖出 /
|
||||
25% 服务费切分。
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@@ -314,6 +346,17 @@ onMounted(() => {
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收号下调" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="Number(row.receive_ratio_delta || 0) > 0 ? 'warning' : 'info'" size="small">
|
||||
{{
|
||||
Number(row.receive_ratio_delta || 0) > 0
|
||||
? `-${Number(row.receive_ratio_delta).toFixed(1)} 点`
|
||||
: '与主站一致'
|
||||
}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布群二维码" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<AuthImage
|
||||
@@ -331,7 +374,7 @@ onMounted(() => {
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<el-table-column label="操作" width="340" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -345,6 +388,9 @@ onMounted(() => {
|
||||
<el-button type="primary" link :icon="Clock" @click="handleOpenSettlement(row)">
|
||||
结算设置
|
||||
</el-button>
|
||||
<el-button type="primary" link :icon="Coin" @click="handleOpenReceiveRatio(row)">
|
||||
收号比例
|
||||
</el-button>
|
||||
<el-button type="primary" link :icon="Picture" @click="handleOpenQrcode(row)">
|
||||
二维码
|
||||
</el-button>
|
||||
@@ -522,6 +568,43 @@ onMounted(() => {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分站收号比例(主站为上限) -->
|
||||
<el-dialog
|
||||
v-model="receiveRatioDialogVisible"
|
||||
title="分站收号比例"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="主站回收比例是上限。下调点数越大,号主底价越高、分站利润空间越大,但不能高于主站比例。0 表示与主站一致。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="分销站点">
|
||||
<el-input :model-value="receiveRatioForm.site_name" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="相对主站下调">
|
||||
<el-input-number
|
||||
v-model="receiveRatioForm.receive_ratio_delta"
|
||||
:min="0"
|
||||
:max="20"
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
/>
|
||||
<span class="settlement-hint">点(0-20)</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="receiveRatioDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="receiveRatioSubmitting" @click="handleReceiveRatioSubmit">
|
||||
保存设置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 发布群固定二维码 -->
|
||||
<el-dialog v-model="qrDialogVisible" title="发布群固定二维码" width="520px" destroy-on-close>
|
||||
<el-alert
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import type { PublishForm } from '@/shared/types/publish'
|
||||
import { yuanToCent } from '@/shared/utils/money'
|
||||
import { dailyLossOptions, formatNumber, roundRatio } from '@/shared/utils/pricing'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const draftSaveDelay = 400
|
||||
const multiScreenshotLimit = 3
|
||||
@@ -54,6 +55,9 @@ interface UsePublishFormOptions {
|
||||
export function usePublishForm(options: UsePublishFormOptions) {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const siteStore = useDistributionSiteStore()
|
||||
const isDistribution = computed(() => siteStore.isDistribution)
|
||||
const receiveRatioDelta = computed(() => Number(siteStore.receiveRatioDelta || 0))
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const suppressDraftSave = ref(false)
|
||||
@@ -89,6 +93,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
quantityModes,
|
||||
screenshotFiles,
|
||||
selectedSkins,
|
||||
isDistribution,
|
||||
receiveRatioDelta,
|
||||
})
|
||||
|
||||
// ========== 上号方式互斥 ==========
|
||||
@@ -911,6 +917,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
|
||||
return {
|
||||
...pricing,
|
||||
isDistribution,
|
||||
dailyLossOptions,
|
||||
formatNumber,
|
||||
router,
|
||||
|
||||
@@ -692,6 +692,14 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ratio-inherit-hint {
|
||||
margin: 0;
|
||||
color: #1677ff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ratio-mode-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -49,6 +49,7 @@ const {
|
||||
priceConfig,
|
||||
fireLevelPlaceholder,
|
||||
dailyLossMAmount,
|
||||
isDistribution,
|
||||
calculatedDefaultSaleRatio,
|
||||
maxAcceleratedSaleRatio,
|
||||
calculatedCoinBasePrice,
|
||||
@@ -694,11 +695,16 @@ const screenshotGuides: Record<string, string> = {
|
||||
</p>
|
||||
<div class="ratio-panel">
|
||||
<div class="ratio-reference">
|
||||
<span>参考比例</span>
|
||||
<span>{{ isDistribution ? '主站收号上限' : '参考比例' }}</span>
|
||||
<strong>{{ calculatedDefaultSaleRatioText }}</strong>
|
||||
</div>
|
||||
<p class="ratio-range">{{ saleRatioRangeText }},比例越高出租速度越快,自行选择。</p>
|
||||
<div class="ratio-mode-grid">
|
||||
<p v-if="isDistribution" class="ratio-inherit-hint">
|
||||
本站可下调收号比例换取更高利润,但不能高于主站比例。当前按站点配置计算。
|
||||
</p>
|
||||
<p class="ratio-range">
|
||||
{{ saleRatioRangeText }}{{ isDistribution ? '。' : ',比例越高出租速度越快,自行选择。' }}
|
||||
</p>
|
||||
<div v-if="!isDistribution" class="ratio-mode-grid">
|
||||
<button
|
||||
type="button"
|
||||
class="ratio-mode-btn"
|
||||
|
||||
@@ -675,6 +675,12 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ratio-inherit-hint {
|
||||
margin: 8px 0 0 !important;
|
||||
color: #1677ff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ratio-reference {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -67,6 +67,7 @@ const {
|
||||
recommendedDepositAmount,
|
||||
depositBreakdownItems,
|
||||
publishTitle,
|
||||
isDistribution,
|
||||
hasAcceleratedSaleRatioInput,
|
||||
isQuantityItemDisabled,
|
||||
handleSaveDraft,
|
||||
@@ -557,11 +558,14 @@ function selectDailyLoss(value: string | number) {
|
||||
|
||||
<div class="ratio-panel">
|
||||
<div class="ratio-reference">
|
||||
<span>参考比例</span>
|
||||
<span>{{ isDistribution ? '主站收号上限' : '参考比例' }}</span>
|
||||
<strong>{{ calculatedDefaultSaleRatioText }}</strong>
|
||||
</div>
|
||||
<p>{{ saleRatioRangeText }},比例越高出租速度越快。</p>
|
||||
<div class="ratio-mode-grid">
|
||||
<p v-if="isDistribution" class="ratio-inherit-hint">
|
||||
本站可下调收号比例换取更高利润,但不能高于主站比例。当前按站点配置计算。
|
||||
</p>
|
||||
<p>{{ saleRatioRangeText }}{{ isDistribution ? '。' : ',比例越高出租速度越快。' }}</p>
|
||||
<div v-if="!isDistribution" class="ratio-mode-grid">
|
||||
<button
|
||||
type="button"
|
||||
class="ratio-mode-btn"
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
hasAcceleratedSaleRatioInput as hasAcceleratedSaleRatioValue,
|
||||
isQuantityItemDisabledForInsurance,
|
||||
readFinalSaleRatio,
|
||||
applySiteReceiveRatioDelta,
|
||||
roundMoney,
|
||||
roundRatio,
|
||||
} from '@/shared/utils/pricing'
|
||||
@@ -31,6 +32,8 @@ export function usePricingCalculator(options: {
|
||||
quantityModes: Record<QuantityKey, ChargeMode>
|
||||
screenshotFiles: Record<ScreenshotKey, string[]>
|
||||
selectedSkins: Ref<string[]>
|
||||
isDistribution?: Ref<boolean>
|
||||
receiveRatioDelta?: Ref<number>
|
||||
}) {
|
||||
const serverOptions = computed(() => options.publishOptions.value.server_options)
|
||||
const faceOptions = computed(() => options.publishOptions.value.face_options)
|
||||
@@ -85,13 +88,20 @@ export function usePricingCalculator(options: {
|
||||
const maxAcceleratedSaleRatio = computed(() =>
|
||||
calculatedDefaultSaleRatio.value > 0 ? roundRatio(calculatedDefaultSaleRatio.value + 10) : 0
|
||||
)
|
||||
const calculatedRatio = computed(() =>
|
||||
readFinalSaleRatio(
|
||||
const siteReceiveRatioDelta = computed(() =>
|
||||
Math.max(0, Number(options.receiveRatioDelta?.value || 0))
|
||||
)
|
||||
const calculatedRatio = computed(() => {
|
||||
if (calculatedDefaultSaleRatio.value <= 0) return 0
|
||||
if (options.isDistribution?.value) {
|
||||
return applySiteReceiveRatioDelta(calculatedDefaultSaleRatio.value, siteReceiveRatioDelta.value)
|
||||
}
|
||||
return readFinalSaleRatio(
|
||||
calculatedDefaultSaleRatio.value,
|
||||
options.form.accelerated_sale_ratio,
|
||||
maxAcceleratedSaleRatio.value
|
||||
)
|
||||
)
|
||||
})
|
||||
const calculatedCoinBasePrice = computed(() => {
|
||||
if (calculatedRatio.value <= 0) return 0
|
||||
return roundMoney(coinWanAmount.value / calculatedRatio.value)
|
||||
@@ -131,6 +141,9 @@ export function usePricingCalculator(options: {
|
||||
)
|
||||
const saleRatioRangeText = computed(() => {
|
||||
if (calculatedDefaultSaleRatio.value <= 0) return '完成基础信息后自动计算参考比例'
|
||||
if (options.isDistribution?.value) {
|
||||
return `主站上限 1:${formatNumber(calculatedDefaultSaleRatio.value)},本站收号 1:${formatNumber(calculatedRatio.value)}`
|
||||
}
|
||||
return `可设置 1:${formatNumber(calculatedDefaultSaleRatio.value)} ~ 1:${formatNumber(maxAcceleratedSaleRatio.value)}`
|
||||
})
|
||||
const acceleratedSaleRatioPlaceholder = computed(() => {
|
||||
@@ -195,6 +208,7 @@ export function usePricingCalculator(options: {
|
||||
calculatedSellerReferenceRatio,
|
||||
calculatedDefaultSaleRatio,
|
||||
maxAcceleratedSaleRatio,
|
||||
siteReceiveRatioDelta,
|
||||
calculatedRatio,
|
||||
calculatedCoinBasePrice,
|
||||
calculatedConsumablePrice,
|
||||
|
||||
@@ -159,6 +159,12 @@ export function readFinalSaleRatio(
|
||||
return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio))
|
||||
}
|
||||
|
||||
export function applySiteReceiveRatioDelta(referenceRatio: number, delta: number) {
|
||||
if (referenceRatio <= 0) return 0
|
||||
const safeDelta = Math.min(20, Math.max(0, Number(delta) || 0))
|
||||
return roundRatio(Math.max(referenceRatio - safeDelta, 0.01))
|
||||
}
|
||||
|
||||
export function hasAcceleratedSaleRatioInput(value: number | '' | null | undefined) {
|
||||
const ratio = Number(value)
|
||||
return (
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface DistributionSiteInfo {
|
||||
banners_json?: string
|
||||
announcements_json?: string
|
||||
footer_info_json?: string
|
||||
receive_ratio_delta?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +73,7 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
banners: [] as DistributionBanner[],
|
||||
announcements: [] as string[],
|
||||
footerInfo: null as DistributionFooterInfo | null,
|
||||
receiveRatioDelta: 0,
|
||||
initialized: false,
|
||||
}
|
||||
},
|
||||
@@ -175,6 +177,8 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
this.footerInfo = null
|
||||
}
|
||||
|
||||
this.receiveRatioDelta = Math.max(0, Number(info.receive_ratio_delta || 0))
|
||||
|
||||
sessionStorage.setItem('hfb_site_code', this.siteCode)
|
||||
sessionStorage.setItem('hfb_site_name', this.siteName)
|
||||
|
||||
@@ -212,6 +216,7 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
|
||||
this.banners = []
|
||||
this.announcements = []
|
||||
this.footerInfo = null
|
||||
this.receiveRatioDelta = 0
|
||||
sessionStorage.removeItem('hfb_site_code')
|
||||
sessionStorage.removeItem('hfb_site_name')
|
||||
localStorage.removeItem('hfb_site_code')
|
||||
|
||||
Reference in New Issue
Block a user