feat(listing): 优化优质号主发布与极品专区体验

This commit is contained in:
yml2213
2026-11-04 15:59:53 +08:00
parent 98174c0e33
commit e9102026ae
22 changed files with 812 additions and 88 deletions
@@ -401,6 +401,17 @@ async function handleSave() {
<div class="editor-block-title">
<strong>比例计算配置</strong>
</div>
<el-form-item label="优质号主专享比例下调">
<el-input-number
v-model="publishOptionsDraft.ratio_config.quality_owner_ratio_discount"
:min="0"
:max="10"
:step="0.5"
/>
<span class="inline-config-hint">
默认 1,表示优质号主可在基础比例上降低 1;普通号主不能使用该区间。
</span>
</el-form-item>
<div class="editor-block-title subtle-title">
<span>保险基础比例</span>
<el-button size="small" @click="addInsuranceBaseRatio">添加保险比例</el-button>
@@ -635,6 +646,13 @@ async function handleSave() {
font-size: 13px;
}
.inline-config-hint {
margin-left: 10px;
color: #8b95a5;
font-size: 12px;
line-height: 1.5;
}
.skin-config-row {
display: grid;
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
@@ -78,6 +78,7 @@ export interface PublishSaleRatioAdjustmentRule {
}
export interface PublishRatioConfig {
quality_owner_ratio_discount: number
insurance_base_ratios: PublishInsuranceBaseRatio[]
config_items: PublishRatioConfigItem[]
coin_corrections: PublishCoinCorrection[]
@@ -151,6 +152,7 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
],
},
ratio_config: {
quality_owner_ratio_discount: 1,
insurance_base_ratios: [],
config_items: [],
coin_corrections: [],
@@ -363,6 +365,10 @@ function normalizeDepositSkinGroupRules(values: unknown[]): PublishDepositSkinGr
function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
const row = isRecord(value) ? value : {}
return {
quality_owner_ratio_discount: Math.min(
10,
Math.max(0, readNumber(row.quality_owner_ratio_discount ?? 1))
),
insurance_base_ratios: normalizeInsuranceBaseRatios(
Array.isArray(row.insurance_base_ratios) ? row.insurance_base_ratios : []
),
@@ -37,6 +37,7 @@ export function buildPublishDraft(options: {
quantityModes: Record<QuantityKey, ChargeMode>
screenshotFiles: Record<ScreenshotKey, string[]>
selectedSkins: string[]
qualityOwnerRatioDefaultApplied: boolean
}): PublishDraft {
return {
form: {
@@ -47,6 +48,7 @@ export function buildPublishDraft(options: {
quantityModes: { ...options.quantityModes },
screenshotFiles: cloneScreenshotFiles(options.screenshotFiles),
selectedSkins: [...options.selectedSkins],
qualityOwnerRatioDefaultApplied: options.qualityOwnerRatioDefaultApplied,
}
}
@@ -63,6 +65,10 @@ export function readPublishDraft(draftKey: string) {
selectedSkins: Array.isArray(draft.selectedSkins)
? draft.selectedSkins.filter((skin): skin is string => typeof skin === 'string')
: [],
qualityOwnerRatioDefaultApplied:
typeof draft.qualityOwnerRatioDefaultApplied === 'boolean'
? draft.qualityOwnerRatioDefaultApplied
: undefined,
}
} catch {
localStorage.removeItem(draftKey)
@@ -92,6 +92,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
const unlockDemulanEnabled = computed(() => publishOptions.value.unlock_demulan_enabled)
const nextSeasonPassEnabled = computed(() => publishOptions.value.next_season_pass_enabled)
const isQualityOwner = computed(() => qualityOwnerStatus.value?.active === true)
const qualityOwnerRatioDefaultApplied = ref(true)
const insuranceFeeBps = computed(() =>
form.is_premium && isQualityOwner.value
? Number(qualityOwnerStatus.value?.premium_insurance_fee_bps || 400)
@@ -110,7 +111,43 @@ export function usePublishForm(options: UsePublishFormOptions) {
isDistribution,
receiveRatioDelta,
receiveCostDelta,
isQualityOwner,
})
const qualityOwnerSaleRatioSelected = computed(
() =>
isQualityOwner.value &&
pricing.qualityOwnerRatioDiscount.value > 0 &&
pricing.qualityOwnerSaleRatio.value > 0 &&
Math.abs(Number(form.accelerated_sale_ratio) - pricing.qualityOwnerSaleRatio.value) < 0.05
)
const isQualityOwnerExclusiveRatioActive = computed(
() =>
isQualityOwner.value &&
pricing.calculatedSellerReferenceRatio.value > 0 &&
pricing.hasAcceleratedSaleRatioInput() &&
Number(form.accelerated_sale_ratio) < pricing.calculatedSellerReferenceRatio.value
)
watch(
[isQualityOwner, pricing.qualityOwnerRatioDiscount, pricing.qualityOwnerSaleRatio, isEditMode],
([owner, discount, ratio, editing]) => {
if (editing || !owner || discount <= 0 || !qualityOwnerRatioDefaultApplied.value) return
if (ratio <= 0) return
if (Number(form.accelerated_sale_ratio) !== ratio) {
form.accelerated_sale_ratio = ratio
}
},
{ immediate: true }
)
watch(
[isQualityOwnerExclusiveRatioActive, () => form.is_premium],
([exclusiveRatio, premium]) => {
if (exclusiveRatio && !premium) {
form.is_premium = true
}
},
{ immediate: true }
)
// ========== 上号方式互斥 ==========
// 微信区账号只能扫码登录:选择微信区后,「账号密码」不可选,默认固定为「扫码」。
@@ -279,6 +316,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
quantityModes,
screenshotFiles,
selectedSkins: selectedSkins.value,
qualityOwnerRatioDefaultApplied: qualityOwnerRatioDefaultApplied.value,
})
)
}
@@ -304,6 +342,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
Object.assign(quantityModes, draft.quantityModes)
Object.assign(screenshotFiles, draft.screenshotFiles)
selectedSkins.value = draft.selectedSkins
qualityOwnerRatioDefaultApplied.value =
draft.qualityOwnerRatioDefaultApplied ?? (form.accelerated_sale_ratio === '' ? true : false)
hydrateScreenshotPreviews()
}
@@ -368,6 +408,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
selectedSkins.value = Object.values(skinGroups).flatMap(value => stringArray(value))
qualityOwnerRatioDefaultApplied.value = false
virtualAssetSaleAgreementChecked.value = true
sellerAgreementChecked.value = true
passwordAndDeviceConfirmed.value = true
@@ -385,6 +426,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
sellerAgreementChecked.value = false
passwordAndDeviceConfirmed.value = false
activeUploadKey.value = 'coin'
qualityOwnerRatioDefaultApplied.value = true
// Also re-initialize quantityValues to 0
for (const item of publishOptions.value.quantity_items) {
@@ -588,6 +630,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
function handleAcceleratedSaleRatioInput(value: string | number | undefined) {
qualityOwnerRatioDefaultApplied.value = false
if (value === '' || value === undefined) {
form.accelerated_sale_ratio = ''
return
@@ -645,21 +688,36 @@ export function usePublishForm(options: UsePublishFormOptions) {
}
form.accelerated_sale_ratio = roundRatio(
Math.min(
Math.max(ratio, pricing.calculatedDefaultSaleRatio.value),
Math.max(ratio, pricing.minAcceleratedSaleRatio.value),
pricing.maxAcceleratedSaleRatio.value
)
)
}
function useReferenceSaleRatio() {
qualityOwnerRatioDefaultApplied.value = false
form.accelerated_sale_ratio = ''
}
function useMaxAcceleratedSaleRatio() {
if (pricing.maxAcceleratedSaleRatio.value <= 0) return
qualityOwnerRatioDefaultApplied.value = false
form.accelerated_sale_ratio = pricing.maxAcceleratedSaleRatio.value
}
function useQualityOwnerSaleRatio() {
if (
!isQualityOwner.value ||
pricing.qualityOwnerRatioDiscount.value <= 0 ||
pricing.qualityOwnerSaleRatio.value <= 0
) {
return
}
qualityOwnerRatioDefaultApplied.value = true
form.accelerated_sale_ratio = pricing.qualityOwnerSaleRatio.value
form.is_premium = true
}
async function handleSubmit() {
normalizeOnlineTime()
const error = validateForm()
@@ -740,10 +798,12 @@ export function usePublishForm(options: UsePublishFormOptions) {
const ratio = Number(form.accelerated_sale_ratio)
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
if (
pricing.calculatedDefaultSaleRatio.value > 0 &&
ratio < pricing.calculatedDefaultSaleRatio.value
pricing.minAcceleratedSaleRatio.value > 0 &&
ratio < pricing.minAcceleratedSaleRatio.value
) {
return `加速出售比例不能低于默认比例 1:${formatNumber(pricing.calculatedDefaultSaleRatio.value)}`
return isQualityOwner.value && pricing.qualityOwnerRatioDiscount.value > 0
? `优质号主专享比例不能低于 1:${formatNumber(pricing.minAcceleratedSaleRatio.value)}`
: `加速出售比例不能低于默认比例 1:${formatNumber(pricing.calculatedDefaultSaleRatio.value)}`
}
if (
pricing.maxAcceleratedSaleRatio.value > 0 &&
@@ -779,6 +839,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
if (!Number.isFinite(pricing.calculatedFinalPrice.value))
return '发布价格计算异常,请检查填写内容'
if (form.is_premium && !isQualityOwner.value) return '仅优质号主可以发布到极品专区'
if (isQualityOwnerExclusiveRatioActive.value && !form.is_premium) {
return '优质号主专享比例仅可用于极品专区'
}
if (!virtualAssetSaleAgreementChecked.value || !sellerAgreementChecked.value) {
return '请先阅读并勾选两份发布协议'
}
@@ -837,6 +900,16 @@ export function usePublishForm(options: UsePublishFormOptions) {
buyer_ratio: pricing.calculatedPlatformPricing.value.buyerRatio,
platform_markup_amount: pricing.calculatedPlatformPricing.value.platformMarkupAmount,
platform_rule_type: pricing.calculatedPlatformPricing.value.ruleType,
quality_owner_ratio_discount:
pricing.calculatedRatio.value < pricing.calculatedSellerReferenceRatio.value
? roundRatio(
pricing.calculatedSellerReferenceRatio.value - pricing.calculatedRatio.value
)
: 0,
pricing_mode:
pricing.calculatedRatio.value < pricing.calculatedSellerReferenceRatio.value
? 'quality_owner_exclusive'
: 'standard',
},
season_insurance: form.season_insurance,
stamina_level: form.stamina_level,
@@ -955,6 +1028,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
publishAgreements,
qualityOwnerStatus,
isQualityOwner,
qualityOwnerRatioDefaultApplied,
qualityOwnerSaleRatioSelected,
isQualityOwnerExclusiveRatioActive,
insuranceFeeBps,
insuranceFeePercent,
virtualAssetSaleAgreementChecked,
@@ -1013,6 +1089,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
clampAcceleratedSaleRatioInput,
useReferenceSaleRatio,
useMaxAcceleratedSaleRatio,
useQualityOwnerSaleRatio,
handleSubmit,
validateForm,
isScreenshotRequired,
@@ -706,6 +706,19 @@
gap: 8px;
}
.ratio-mode-grid.has-quality-owner .ratio-mode-btn:nth-of-type(1) {
order: 1;
}
.ratio-mode-grid.has-quality-owner .ratio-mode-btn:nth-of-type(2) {
order: 3;
grid-column: 1 / -1;
}
.ratio-mode-grid.has-quality-owner .ratio-mode-btn:nth-of-type(3) {
order: 2;
}
.ratio-mode-btn {
display: grid;
gap: 5px;
@@ -737,6 +750,44 @@
line-height: 1.25;
}
.ratio-mode-btn em {
width: max-content;
margin: 0 auto;
padding: 2px 7px;
border-radius: 9px;
background: #fff3d6;
color: #91600d;
font-size: 10px;
font-style: normal;
font-weight: 900;
}
.ratio-mode-btn--quality {
border-color: #dfc98c;
background: linear-gradient(135deg, #fffaf0 0%, #faf6ff 100%);
text-align: left;
}
.ratio-mode-btn--quality strong {
color: #7a4b0d;
}
.ratio-mode-btn.ratio-mode-btn--quality.active {
border-color: #b7791f;
background: linear-gradient(135deg, #fff4d8 0%, #f5edff 100%);
box-shadow: 0 0 0 2px rgba(183, 121, 31, 0.12);
color: #7a4b0d;
}
.ratio-mode-btn.ratio-mode-btn--quality.active em {
background: #b7791f;
color: #fff;
}
.ratio-mode-btn.ratio-mode-btn--quality.active span {
color: #7a4b0d;
}
.ratio-mode-btn.active {
border-color: #ff6a00;
background: #fff7f0;
@@ -880,6 +931,16 @@
background: #f8fafc;
}
.premium-coverage-card.is-quality-owner {
border-color: #e6d7b5;
background: linear-gradient(145deg, #fffdf8 0%, #faf8ff 100%);
}
.premium-coverage-card.is-premium-active {
border-color: #d4ad5e;
box-shadow: 0 8px 20px rgba(139, 92, 20, 0.08);
}
.premium-zone-row,
.coverage-heading {
display: flex;
@@ -888,6 +949,34 @@
gap: 12px;
}
.premium-zone-copy {
display: grid;
min-width: 0;
gap: 4px;
}
.premium-zone-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.premium-zone-title em {
padding: 2px 7px;
border-radius: 9px;
background: #fff3d6;
color: #8b5a0d;
font-size: 9px;
font-style: normal;
font-weight: 900;
}
.premium-zone-title em.is-locked {
background: #f1f3f6;
color: #7b8492;
}
.premium-zone-row strong,
.coverage-heading strong {
display: block;
@@ -905,16 +994,46 @@
line-height: 1.45;
}
.quality-owner-link {
justify-self: start;
padding: 0;
border: none;
background: transparent;
color: #7c3aed;
font-size: 12px;
.premium-zone-action {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 7px;
}
.premium-zone-action > span {
margin: 0;
font-size: 10px;
font-weight: 800;
}
.premium-zone-action.is-active > span {
color: #6d35c7;
}
.premium-zone-action :deep(.van-switch--on) {
background: #7c3aed;
}
.premium-zone-action :deep(.van-switch--disabled) {
opacity: 0.78;
}
.quality-owner-link {
display: inline-flex;
align-items: center;
justify-self: start;
gap: 4px;
min-height: 30px;
padding: 0 10px;
border: 1px solid #ddd0f2;
border-radius: 15px;
background: #faf5ff;
color: #6d35c7;
font-size: 12px;
font-weight: 900;
}
.coverage-option-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -939,6 +1058,32 @@
font-weight: 800;
}
.coverage-option > .coverage-option-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
color: inherit;
font-size: inherit;
line-height: normal;
}
.coverage-option .coverage-rate {
flex-shrink: 0;
padding: 2px 6px;
border-radius: 8px;
background: #f0f2f5;
color: #505a68;
font-size: 10px;
font-style: normal;
font-weight: 900;
}
.coverage-option .coverage-rate.is-insurance {
background: #f3ebff;
color: #6d35c7;
}
.coverage-option span {
color: #858c96;
font-size: 10px;
@@ -952,6 +1097,13 @@
font-weight: 800;
}
.coverage-option .coverage-special-rate {
width: max-content;
padding: 2px 6px;
border-radius: 8px;
background: #f3ebff;
}
.coverage-option.active {
border-color: #7c3aed;
background: #faf5ff;
@@ -27,7 +27,12 @@ const {
passwordAndDeviceConfirmed,
canPublishAfterAgreements,
isQualityOwner,
isQualityOwnerExclusiveRatioActive,
qualityOwnerSaleRatioSelected,
qualityOwnerStatus,
qualityOwnerRatioDiscount,
qualityOwnerSaleRatio,
minAcceleratedSaleRatio,
insuranceFeePercent,
showBanRecordRiskHint,
unlockDemulanEnabled,
@@ -91,6 +96,7 @@ const {
clampAcceleratedSaleRatioInput,
useReferenceSaleRatio,
useMaxAcceleratedSaleRatio,
useQualityOwnerSaleRatio,
handleSubmit,
isScreenshotRequired,
} = usePublishForm({
@@ -732,7 +738,11 @@ const screenshotGuides: Record<string, string> = {
{{ saleRatioRangeText
}}{{ isDistribution ? '。' : ',比例越高出租速度越快,自行选择。' }}
</p>
<div v-if="!isDistribution" class="ratio-mode-grid">
<div
v-if="!isDistribution"
class="ratio-mode-grid"
:class="{ 'has-quality-owner': isQualityOwner && qualityOwnerRatioDiscount > 0 }"
>
<button
type="button"
class="ratio-mode-btn"
@@ -749,18 +759,36 @@ const screenshotGuides: Record<string, string> = {
}}
</span>
</button>
<button
v-if="isQualityOwner && qualityOwnerRatioDiscount > 0"
type="button"
class="ratio-mode-btn ratio-mode-btn--quality"
:class="{ active: qualityOwnerSaleRatioSelected }"
:disabled="qualityOwnerSaleRatio <= 0"
@click="useQualityOwnerSaleRatio"
>
<em>优质号主专享</em>
<strong>基础比例 - {{ formatNumber(qualityOwnerRatioDiscount) }}</strong>
<span>
{{
qualityOwnerSaleRatio > 0
? `默认 1元=${formatNumber(qualityOwnerSaleRatio)}万`
: '自动计算'
}}
</span>
</button>
<button
type="button"
class="ratio-mode-btn"
:class="{ active: hasAcceleratedSaleRatioInput() }"
:class="{ active: Number(form.accelerated_sale_ratio) === maxAcceleratedSaleRatio }"
:disabled="maxAcceleratedSaleRatio <= 0"
@click="useMaxAcceleratedSaleRatio"
>
<strong>加速比例</strong>
<strong>最高比例</strong>
<span>
{{
maxAcceleratedSaleRatio > 0
? `1元=${formatNumber(calculatedDefaultSaleRatio)}~${formatNumber(maxAcceleratedSaleRatio)}万`
? `1元=${formatNumber(maxAcceleratedSaleRatio)}万`
: '自动计算'
}}
</span>
@@ -770,13 +798,14 @@ const screenshotGuides: Record<string, string> = {
:model-value="form.accelerated_sale_ratio"
label="自定比例"
type="number"
:min="minAcceleratedSaleRatio || 0"
:placeholder="acceleratedSaleRatioPlaceholder"
class="publish-field ratio-input-field"
@update:model-value="handleAcceleratedSaleRatioInput"
@blur="clampAcceleratedSaleRatioInput"
/>
<p class="field-hint ratio-input-hint">
留空使用参考比例;填写后范围为参考比例到参考比例+10,请根据实际需求合理设置。
留空使用参考比例;优质号主可按后台配置下调基础比例。
</p>
</div>
<div class="result-grid">
@@ -824,21 +853,46 @@ const screenshotGuides: Record<string, string> = {
</div>
<p class="price-breakdown-hint">卖家价格 = 纯币基础价 + 额外消耗品。</p>
<div class="premium-coverage-card">
<div
class="premium-coverage-card"
:class="{
'is-quality-owner': isQualityOwner,
'is-premium-active': form.is_premium,
}"
>
<div class="premium-zone-row">
<div>
<strong>极品专区</strong>
<div class="premium-zone-copy">
<div class="premium-zone-title">
<strong>极品专区</strong>
<em v-if="isQualityOwner">优质号主已认证</em>
<em v-else class="is-locked">优质号主专享</em>
</div>
<span>
{{
isQualityOwner
? '优质号主专享曝光专区'
: `保证金 ¥${formatMoney(
? '发布后获得首页极品专区曝光'
: `缴纳 ¥${formatMoney(
(qualityOwnerStatus?.required_deposit_cent || 20000) / 100
)}`
)} 保证金后可开通`
}}
</span>
</div>
<van-switch v-model="form.is_premium" :disabled="!isQualityOwner" size="22px" />
<div class="premium-zone-action" :class="{ 'is-active': form.is_premium }">
<span>{{
isQualityOwnerExclusiveRatioActive
? '专享比例已关联专区'
: form.is_premium
? '已开启极品专区'
: isQualityOwner
? '开启极品专区'
: '开通后可发布'
}}</span>
<van-switch
v-model="form.is_premium"
:disabled="!isQualityOwner || isQualityOwnerExclusiveRatioActive"
size="22px"
/>
</div>
</div>
<button
v-if="!isQualityOwner"
@@ -846,7 +900,8 @@ const screenshotGuides: Record<string, string> = {
class="quality-owner-link"
@click="router.push('/m/profile')"
>
前往个人中心开通优质号主
<span>前往个人中心开通优质号主</span>
<van-icon name="arrow" />
</button>
<div class="coverage-heading">
@@ -860,7 +915,10 @@ const screenshotGuides: Record<string, string> = {
:class="{ active: form.compensation_plan === 'normal' }"
@click="form.compensation_plan = 'normal'"
>
<strong>普通赔付</strong>
<span class="coverage-option-top">
<strong>普通赔付</strong>
<em class="coverage-rate">0%</em>
</span>
<span>租金 + 押金规则赔付</span>
</button>
<button
@@ -869,13 +927,18 @@ const screenshotGuides: Record<string, string> = {
:class="{ active: form.compensation_plan === 'insurance' }"
@click="form.compensation_plan = 'insurance'"
>
<strong>无忧租号保险</strong>
<span>服务费 {{ insuranceFeePercent.toFixed(1) }}%</span>
<em v-if="isQualityOwner && form.is_premium">极品专享费率</em>
<span class="coverage-option-top">
<strong>无忧租号保险</strong>
<em class="coverage-rate is-insurance"> {{ insuranceFeePercent.toFixed(1) }}% </em>
</span>
<span>额外保障账号市场价值</span>
<em v-if="isQualityOwner && form.is_premium" class="coverage-special-rate">
已享极品专区专享费率
</em>
</button>
</div>
<p class="coverage-note">
无忧租号保险在普通赔付基础上额外保障账号市场价值;选择后按号主实际到手租金扣除服务费。
无忧租号保险在普通赔付基础上增加账号市场价值保障,服务费按实际到手租金扣除。
</p>
</div>
@@ -705,7 +705,7 @@
.ratio-mode-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
@@ -727,6 +727,50 @@
font-size: 12px;
}
.ratio-mode-btn em {
width: max-content;
padding: 3px 7px;
border-radius: 10px;
background: #fff3d6;
color: #9a6700;
font-size: 10px;
font-style: normal;
font-weight: 900;
letter-spacing: 0;
}
.ratio-mode-btn--quality {
position: relative;
overflow: hidden;
border-color: #dfc98c;
background: linear-gradient(135deg, #fffaf0 0%, #f8f3ff 100%);
}
.ratio-mode-btn--quality strong {
color: #7a4b0d;
}
.ratio-mode-btn--quality > * {
position: relative;
z-index: 1;
}
.ratio-mode-btn--quality.active {
border-color: #b7791f;
background: linear-gradient(135deg, #fff4d8 0%, #f5edff 100%);
color: #7a4b0d;
box-shadow: 0 0 0 2px rgba(183, 121, 31, 0.12);
}
.ratio-mode-btn--quality.active em {
background: #b7791f;
color: #fff;
}
.ratio-mode-btn--quality.active span {
color: #7a4b0d;
}
.ratio-custom-card {
display: flex;
flex-direction: column;
@@ -1022,6 +1066,16 @@
background: #f8fafc;
}
.premium-coverage-panel.is-quality-owner {
border-color: #e6d7b5;
background: linear-gradient(145deg, #fffdf8 0%, #faf8ff 100%);
}
.premium-coverage-panel.is-premium-active {
border-color: #d4ad5e;
box-shadow: 0 10px 28px rgba(139, 92, 20, 0.08);
}
.premium-zone-row,
.coverage-heading {
display: flex;
@@ -1035,6 +1089,33 @@
min-width: 0;
}
.premium-zone-copy {
display: grid;
gap: 5px;
}
.premium-zone-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.premium-zone-title em {
padding: 3px 8px;
border-radius: 10px;
background: #fff3d6;
color: #8b5a0d;
font-size: 10px;
font-style: normal;
font-weight: 900;
}
.premium-zone-title em.is-locked {
background: #f1f3f6;
color: #7b8492;
}
.premium-zone-row strong,
.coverage-heading strong {
display: block;
@@ -1051,17 +1132,52 @@
line-height: 1.5;
}
.quality-owner-link {
justify-self: start;
padding: 0;
border: none;
background: transparent;
color: #7c3aed;
font-size: 12px;
.premium-zone-action {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 9px;
}
.premium-zone-action > span {
font-weight: 800;
}
.premium-zone-action.is-active > span {
color: #6d35c7;
}
.premium-zone-action :deep(.el-switch.is-checked .el-switch__core) {
border-color: #7c3aed;
background: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.1);
}
.premium-zone-action :deep(.el-switch.is-disabled) {
opacity: 0.78;
}
.quality-owner-link {
display: inline-flex;
align-items: center;
justify-self: start;
gap: 4px;
min-height: 30px;
padding: 0 10px;
border: 1px solid #ddd0f2;
border-radius: 15px;
background: #faf5ff;
color: #6d35c7;
font-size: 12px;
font-weight: 900;
cursor: pointer;
}
.quality-owner-link:hover {
border-color: #c7acdf;
background: #f4ebff;
}
.coverage-option-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1086,6 +1202,33 @@
font-weight: 900;
}
.coverage-option > .coverage-option-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: inherit;
font-size: inherit;
font-weight: inherit;
line-height: normal;
}
.coverage-option .coverage-rate {
flex-shrink: 0;
padding: 3px 8px;
border-radius: 10px;
background: #f0f2f5;
color: #505a68;
font-size: 12px;
font-style: normal;
font-weight: 900;
}
.coverage-option .coverage-rate.is-insurance {
background: #f3ebff;
color: #6d35c7;
}
.coverage-option span {
color: #6b7280;
font-size: 12px;
@@ -1100,6 +1243,13 @@
font-weight: 800;
}
.coverage-option .coverage-special-rate {
width: max-content;
padding: 3px 7px;
border-radius: 9px;
background: #f3ebff;
}
.coverage-option.active {
border-color: #7c3aed;
background: #faf5ff;
@@ -1,5 +1,6 @@
<script setup lang="ts">
import {
ArrowRight,
Delete,
DocumentChecked,
Picture,
@@ -25,7 +26,12 @@ const {
router,
publishAgreements,
isQualityOwner,
isQualityOwnerExclusiveRatioActive,
qualityOwnerSaleRatioSelected,
qualityOwnerStatus,
qualityOwnerRatioDiscount,
qualityOwnerSaleRatio,
minAcceleratedSaleRatio,
insuranceFeePercent,
virtualAssetSaleAgreementChecked,
sellerAgreementChecked,
@@ -101,6 +107,7 @@ const {
clampAcceleratedSaleRatioInput,
useReferenceSaleRatio,
useMaxAcceleratedSaleRatio,
useQualityOwnerSaleRatio,
handleSubmit,
isScreenshotRequired,
} = usePublishForm({
@@ -595,6 +602,22 @@ function selectDailyLoss(value: string | number) {
: '自动计算'
}}</span>
</button>
<button
v-if="isQualityOwner && qualityOwnerRatioDiscount > 0"
type="button"
class="ratio-mode-btn ratio-mode-btn--quality"
:class="{ active: qualityOwnerSaleRatioSelected }"
:disabled="qualityOwnerSaleRatio <= 0"
@click="useQualityOwnerSaleRatio"
>
<em>优质号主专享</em>
<strong>基础比例 - {{ formatNumber(qualityOwnerRatioDiscount) }}</strong>
<span>{{
qualityOwnerSaleRatio > 0
? `默认 1元=${formatNumber(qualityOwnerSaleRatio)}万`
: '自动计算'
}}</span>
</button>
<button
type="button"
class="ratio-mode-btn"
@@ -618,13 +641,14 @@ function selectDailyLoss(value: string | number) {
:class="{
active:
hasAcceleratedSaleRatioInput() &&
Number(form.accelerated_sale_ratio) !== maxAcceleratedSaleRatio,
Number(form.accelerated_sale_ratio) !== maxAcceleratedSaleRatio &&
!qualityOwnerSaleRatioSelected,
}"
>
<strong>自定比例</strong>
<el-input-number
:model-value="form.accelerated_sale_ratio"
:min="0"
:min="minAcceleratedSaleRatio || 0"
:controls="false"
:placeholder="acceleratedSaleRatioPlaceholder"
@update:model-value="handleAcceleratedSaleRatioInput"
@@ -657,25 +681,45 @@ function selectDailyLoss(value: string | number) {
</div>
</div>
<div class="premium-coverage-panel">
<div
class="premium-coverage-panel"
:class="{
'is-quality-owner': isQualityOwner,
'is-premium-active': form.is_premium,
}"
>
<div class="premium-zone-row">
<div>
<strong>极品专区</strong>
<div class="premium-zone-copy">
<div class="premium-zone-title">
<strong>极品专区</strong>
<em v-if="isQualityOwner">优质号主已认证</em>
<em v-else class="is-locked">优质号主专享</em>
</div>
<span>
{{
isQualityOwner
? '优质号主专享曝光专区'
? '发布后进入首页极品专区,获得高端账号专属曝光'
: `缴纳 ¥${formatMoney(
(qualityOwnerStatus?.required_deposit_cent || 20000) / 100
)} 保证金后可开通优质号主`
}}
</span>
</div>
<el-switch
v-model="form.is_premium"
:disabled="!isQualityOwner"
active-text="发布到极品专区"
/>
<div class="premium-zone-action" :class="{ 'is-active': form.is_premium }">
<span>{{
isQualityOwnerExclusiveRatioActive
? '专享比例已关联专区'
: form.is_premium
? '已开启极品专区'
: isQualityOwner
? '开启极品专区'
: '开通后可发布'
}}</span>
<el-switch
v-model="form.is_premium"
:disabled="!isQualityOwner || isQualityOwnerExclusiveRatioActive"
/>
</div>
</div>
<button
v-if="!isQualityOwner"
@@ -683,7 +727,8 @@ function selectDailyLoss(value: string | number) {
class="quality-owner-link"
@click="router.push('/profile')"
>
前往个人中心开通优质号主
<span>前往个人中心开通优质号主</span>
<el-icon><ArrowRight /></el-icon>
</button>
<div class="coverage-heading">
@@ -697,7 +742,10 @@ function selectDailyLoss(value: string | number) {
:class="{ active: form.compensation_plan === 'normal' }"
@click="form.compensation_plan = 'normal'"
>
<strong>普通赔付</strong>
<span class="coverage-option-top">
<strong>普通赔付</strong>
<em class="coverage-rate">0%</em>
</span>
<span>按平台规则赔付租金与押金</span>
</button>
<button
@@ -706,13 +754,20 @@ function selectDailyLoss(value: string | number) {
:class="{ active: form.compensation_plan === 'insurance' }"
@click="form.compensation_plan = 'insurance'"
>
<strong>无忧租号保险</strong>
<span> 服务费 {{ insuranceFeePercent.toFixed(1) }}%,额外保障账号市场价值 </span>
<em v-if="isQualityOwner && form.is_premium">极品专区专享费率</em>
<span class="coverage-option-top">
<strong>无忧租号保险</strong>
<em class="coverage-rate is-insurance">
{{ insuranceFeePercent.toFixed(1) }}%
</em>
</span>
<span>额外保障账号市场价值,费率按实际到手租金扣除</span>
<em v-if="isQualityOwner && form.is_premium" class="coverage-special-rate">
已享极品专区专享费率
</em>
</button>
</div>
<p class="coverage-note">
普通赔付:账号封禁、追回、洗号等情况最高赔付全额租金与押金。无忧租号保险:在普通赔付基础上,额外按账号市场价值赔付;选择后按实际到手租金扣除服务费。
普通赔付覆盖封禁、追回、洗号等情况;无忧租号保险在此基础上增加账号市场价值保障。
</p>
</div>
@@ -34,6 +34,7 @@ export function usePricingCalculator(options: {
isDistribution?: Ref<boolean>
receiveRatioDelta?: Ref<number>
receiveCostDelta?: Ref<number>
isQualityOwner?: Ref<boolean>
}) {
const serverOptions = computed(() => options.publishOptions.value.server_options)
const faceOptions = computed(() => options.publishOptions.value.face_options)
@@ -88,6 +89,24 @@ export function usePricingCalculator(options: {
const maxAcceleratedSaleRatio = computed(() =>
calculatedDefaultSaleRatio.value > 0 ? roundRatio(calculatedDefaultSaleRatio.value + 10) : 0
)
const qualityOwnerRatioDiscount = computed(() =>
Math.min(
10,
Math.max(
0,
Number(options.publishOptions.value.ratio_config.quality_owner_ratio_discount ?? 1)
)
)
)
const minAcceleratedSaleRatio = computed(() => {
if (!options.isQualityOwner?.value || calculatedDefaultSaleRatio.value <= 0) {
return calculatedDefaultSaleRatio.value
}
return roundRatio(
Math.max(0.1, calculatedDefaultSaleRatio.value - qualityOwnerRatioDiscount.value)
)
})
const qualityOwnerSaleRatio = computed(() => minAcceleratedSaleRatio.value)
const siteReceiveRatioDelta = computed(() =>
Math.max(0, Number(options.receiveRatioDelta?.value || 0))
)
@@ -104,7 +123,8 @@ export function usePricingCalculator(options: {
return readFinalSaleRatio(
calculatedDefaultSaleRatio.value,
options.form.accelerated_sale_ratio,
maxAcceleratedSaleRatio.value
maxAcceleratedSaleRatio.value,
minAcceleratedSaleRatio.value
)
})
const anchorRatio = computed(() => {
@@ -163,11 +183,16 @@ export function usePricingCalculator(options: {
if (options.isDistribution?.value) {
return `主站参考 1:${formatNumber(calculatedDefaultSaleRatio.value)},本站收号 1:${formatNumber(calculatedRatio.value)}`
}
if (options.isQualityOwner?.value) {
return `优质号主可设置 1:${formatNumber(minAcceleratedSaleRatio.value)} ~ 1:${formatNumber(maxAcceleratedSaleRatio.value)}`
}
return `可设置 1:${formatNumber(calculatedDefaultSaleRatio.value)} ~ 1:${formatNumber(maxAcceleratedSaleRatio.value)}`
})
const acceleratedSaleRatioPlaceholder = computed(() => {
if (calculatedDefaultSaleRatio.value <= 0) return '填写资料后自动生成可设置范围'
return `默认 ${formatNumber(calculatedDefaultSaleRatio.value)},最高 ${formatNumber(maxAcceleratedSaleRatio.value)}`
return options.isQualityOwner?.value
? `优质专享 ${formatNumber(minAcceleratedSaleRatio.value)} - 最高 ${formatNumber(maxAcceleratedSaleRatio.value)}`
: `默认 ${formatNumber(calculatedDefaultSaleRatio.value)},最高 ${formatNumber(maxAcceleratedSaleRatio.value)}`
})
const recommendedDepositAmount = computed(() =>
calculateRecommendedDeposit({
@@ -227,6 +252,9 @@ export function usePricingCalculator(options: {
calculatedSellerReferenceRatio,
calculatedDefaultSaleRatio,
maxAcceleratedSaleRatio,
minAcceleratedSaleRatio,
qualityOwnerRatioDiscount,
qualityOwnerSaleRatio,
siteReceiveRatioDelta,
siteReceiveCostDelta,
calculatedRatio,
+1
View File
@@ -41,6 +41,7 @@ export interface PublishDraft {
quantityModes: Record<QuantityKey, ChargeMode>
screenshotFiles: Record<ScreenshotKey, string[]>
selectedSkins: string[]
qualityOwnerRatioDefaultApplied?: boolean
}
export interface DepositBreakdownItem {
+3 -2
View File
@@ -150,13 +150,14 @@ export function calculateSellerReferenceRatio(options: {
export function readFinalSaleRatio(
defaultRatio: number,
acceleratedSaleRatio: number | '',
maxAcceleratedSaleRatio: number
maxAcceleratedSaleRatio: number,
minAcceleratedSaleRatio = defaultRatio
) {
if (defaultRatio <= 0) return 0
if (!hasAcceleratedSaleRatioInput(acceleratedSaleRatio)) return defaultRatio
const ratio = Number(acceleratedSaleRatio)
if (!Number.isFinite(ratio) || ratio <= 0) return defaultRatio
return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio))
return roundRatio(Math.min(Math.max(ratio, minAcceleratedSaleRatio), maxAcceleratedSaleRatio))
}
export function applySiteReceiveRatioDelta(referenceRatio: number, delta: number) {