feat(listing): 优化优质号主发布与极品专区体验
This commit is contained in:
@@ -51,19 +51,20 @@ type ListingDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
type CreateRequest struct {
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ServerRegion string `json:"server_region" binding:"required"`
|
ServerRegion string `json:"server_region" binding:"required"`
|
||||||
LoginPlatform string `json:"login_platform"`
|
LoginPlatform string `json:"login_platform"`
|
||||||
RankLevel string `json:"rank_level"`
|
RankLevel string `json:"rank_level"`
|
||||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||||
AssetSummary map[string]any `json:"asset_summary"`
|
AssetSummary map[string]any `json:"asset_summary"`
|
||||||
ScreenshotURLS []string `json:"screenshot_urls"`
|
ScreenshotURLS []string `json:"screenshot_urls"`
|
||||||
PriceCent int64 `json:"price_cent"`
|
PriceCent int64 `json:"price_cent"`
|
||||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||||
IsPremium bool `json:"is_premium"`
|
IsPremium bool `json:"is_premium"`
|
||||||
CompensationPlan string `json:"compensation_plan" binding:"omitempty,oneof=normal insurance"`
|
CompensationPlan string `json:"compensation_plan" binding:"omitempty,oneof=normal insurance"`
|
||||||
InsuranceFeeBps int `json:"-"` // 服务端按配置和用户资格生成,不接受前端指定。
|
InsuranceFeeBps int `json:"-"` // 服务端按配置和用户资格生成,不接受前端指定。
|
||||||
|
OwnerIsQualityOwner bool `json:"-"` // 服务端鉴权结果,用于校验优质号主专享比例。
|
||||||
|
|
||||||
AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"`
|
AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"`
|
||||||
AgreedSellerAgreement bool `json:"agreed_seller_agreement"`
|
AgreedSellerAgreement bool `json:"agreed_seller_agreement"`
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ func writeListingError(c *gin.Context, err error) {
|
|||||||
response.BadRequest(c, "分销站点无效或已停用")
|
response.BadRequest(c, "分销站点无效或已停用")
|
||||||
case errors.Is(err, ErrPremiumOwnerRequired):
|
case errors.Is(err, ErrPremiumOwnerRequired):
|
||||||
response.Error(c, http.StatusForbidden, "quality_owner_required", "仅优质号主可以发布到极品专区")
|
response.Error(c, http.StatusForbidden, "quality_owner_required", "仅优质号主可以发布到极品专区")
|
||||||
|
case errors.Is(err, ErrQualityRatioPremiumOnly):
|
||||||
|
response.BadRequest(c, "优质号主专享比例仅可用于极品专区")
|
||||||
case isUploadValidationError(err):
|
case isUploadValidationError(err):
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
case isFireLevelTooLow(err):
|
case isFireLevelTooLow(err):
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ func TestApplyPublishEntitlementsPremiumInsurance(t *testing.T) {
|
|||||||
if req.InsuranceFeeBps != 400 {
|
if req.InsuranceFeeBps != 400 {
|
||||||
t.Fatalf("极品专区保险费率 = %d, want 400", req.InsuranceFeeBps)
|
t.Fatalf("极品专区保险费率 = %d, want 400", req.InsuranceFeeBps)
|
||||||
}
|
}
|
||||||
|
if !req.OwnerIsQualityOwner {
|
||||||
|
t.Fatal("优质号主发布请求未标记专享比例资格")
|
||||||
|
}
|
||||||
|
|
||||||
ordinary := CreateRequest{CompensationPlan: CompensationPlanInsurance}
|
ordinary := CreateRequest{CompensationPlan: CompensationPlanInsurance}
|
||||||
if err := service.applyPublishEntitlements(t.Context(), owner.ID, &ordinary); err != nil {
|
if err := service.applyPublishEntitlements(t.Context(), owner.ID, &ordinary); err != nil {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (s *Service) applyMainSiteReceivePricing(ctx context.Context, req *CreateRe
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
applyMainSiteReceivePricing(req, options, config, receiveRatioDelta, receiveCostDelta, onDistributionSite)
|
applyMainSiteReceivePricing(req, options, config, receiveRatioDelta, receiveCostDelta, onDistributionSite)
|
||||||
return nil
|
return validateQualityOwnerPricingMode(req, onDistributionSite)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyMainSiteReceivePricing(req *CreateRequest, options systemconfig.PublishOptionsDTO, config salePriceConfig, receiveRatioDelta, receiveCostDelta float64, onDistributionSite bool) {
|
func applyMainSiteReceivePricing(req *CreateRequest, options systemconfig.PublishOptionsDTO, config salePriceConfig, receiveRatioDelta, receiveCostDelta float64, onDistributionSite bool) {
|
||||||
@@ -71,9 +71,16 @@ func applyMainSiteReceivePricing(req *CreateRequest, options systemconfig.Publis
|
|||||||
if requestedRatio <= 0 {
|
if requestedRatio <= 0 {
|
||||||
requestedRatio = readSummaryNumber(breakdown["accelerated_sale_ratio"])
|
requestedRatio = readSummaryNumber(breakdown["accelerated_sale_ratio"])
|
||||||
}
|
}
|
||||||
|
minSaleRatio := referenceRatio
|
||||||
|
if req.OwnerIsQualityOwner {
|
||||||
|
minSaleRatio = roundRatio(referenceRatio - NormalizeQualityOwnerRatioDiscount(options.RatioConfig.QualityOwnerRatioDiscount))
|
||||||
|
if minSaleRatio < minSiteReceiveRatio {
|
||||||
|
minSaleRatio = minSiteReceiveRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
costDelta := NormalizeSiteReceiveCostDelta(receiveCostDelta)
|
costDelta := NormalizeSiteReceiveCostDelta(receiveCostDelta)
|
||||||
ratioDelta := NormalizeSiteReceiveRatioDelta(receiveRatioDelta)
|
ratioDelta := NormalizeSiteReceiveRatioDelta(receiveRatioDelta)
|
||||||
sellerRatio := clampSaleRatio(referenceRatio, requestedRatio)
|
sellerRatio := clampSaleRatio(referenceRatio, requestedRatio, minSaleRatio)
|
||||||
// 分站新口径:收号比例 = R + 压价点数(收号价更低),买家定价锚定主站参考比例 R,不随收号价联动。
|
// 分站新口径:收号比例 = R + 压价点数(收号价更低),买家定价锚定主站参考比例 R,不随收号价联动。
|
||||||
if onDistributionSite {
|
if onDistributionSite {
|
||||||
sellerRatio = ApplySiteReceiveCostDelta(referenceRatio, costDelta)
|
sellerRatio = ApplySiteReceiveCostDelta(referenceRatio, costDelta)
|
||||||
@@ -98,6 +105,10 @@ func applyMainSiteReceivePricing(req *CreateRequest, options systemconfig.Publis
|
|||||||
breakdown["consumable_price"] = consumablePrice
|
breakdown["consumable_price"] = consumablePrice
|
||||||
breakdown["daily_loss_ratio_adjustment"] = calculateDailyLossRatioAdjustment(dailyLoss)
|
breakdown["daily_loss_ratio_adjustment"] = calculateDailyLossRatioAdjustment(dailyLoss)
|
||||||
breakdown["accelerated_sale_ratio"] = sellerRatio
|
breakdown["accelerated_sale_ratio"] = sellerRatio
|
||||||
|
if sellerRatio < referenceRatio {
|
||||||
|
breakdown["quality_owner_ratio_discount"] = roundRatio(referenceRatio - sellerRatio)
|
||||||
|
breakdown["pricing_mode"] = "quality_owner_exclusive"
|
||||||
|
}
|
||||||
if onDistributionSite {
|
if onDistributionSite {
|
||||||
breakdown["site_receive_cost_delta"] = costDelta
|
breakdown["site_receive_cost_delta"] = costDelta
|
||||||
breakdown["pricing_model"] = "site_cost_decoupled_v1"
|
breakdown["pricing_model"] = "site_cost_decoupled_v1"
|
||||||
@@ -114,6 +125,18 @@ func applyMainSiteReceivePricing(req *CreateRequest, options systemconfig.Publis
|
|||||||
req.PriceCent = yuanToCent(pricing.BuyerTotalPrice)
|
req.PriceCent = yuanToCent(pricing.BuyerTotalPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateQualityOwnerPricingMode(req *CreateRequest, onDistributionSite bool) error {
|
||||||
|
if req == nil || onDistributionSite || !req.OwnerIsQualityOwner || req.IsPremium {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
breakdown := ensurePriceBreakdown(req.AssetSummary)
|
||||||
|
if readSummaryNumber(breakdown["seller_ratio"]) <
|
||||||
|
readSummaryNumber(breakdown["seller_reference_ratio"]) {
|
||||||
|
return ErrQualityRatioPremiumOnly
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NormalizeSiteReceiveCostDelta 将分站收号压价点数限制在 [0, 20]。
|
// NormalizeSiteReceiveCostDelta 将分站收号压价点数限制在 [0, 20]。
|
||||||
// 点数越大,分站收号比例越高(相对主站参考比例),收号价越低;0 表示与主站同价收号。
|
// 点数越大,分站收号比例越高(相对主站参考比例),收号价越低;0 表示与主站同价收号。
|
||||||
func NormalizeSiteReceiveCostDelta(delta float64) float64 {
|
func NormalizeSiteReceiveCostDelta(delta float64) float64 {
|
||||||
@@ -136,6 +159,17 @@ func ApplySiteReceiveCostDelta(referenceRatio, delta float64) float64 {
|
|||||||
return siteRatio
|
return siteRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeQualityOwnerRatioDiscount 限制优质号主专享比例下调值,默认 1、最大 10。
|
||||||
|
func NormalizeQualityOwnerRatioDiscount(value float64) float64 {
|
||||||
|
if value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if value > maxAcceleratedSaleRatioIncrement {
|
||||||
|
return maxAcceleratedSaleRatioIncrement
|
||||||
|
}
|
||||||
|
return roundRatio(value)
|
||||||
|
}
|
||||||
|
|
||||||
// referenceSellerCoinBasePrice 计算主站参考比例下的号主基础价,用作买家定价锚点。
|
// referenceSellerCoinBasePrice 计算主站参考比例下的号主基础价,用作买家定价锚点。
|
||||||
func referenceSellerCoinBasePrice(coinWan, referenceRatio float64) float64 {
|
func referenceSellerCoinBasePrice(coinWan, referenceRatio float64) float64 {
|
||||||
if referenceRatio <= 0 {
|
if referenceRatio <= 0 {
|
||||||
@@ -182,7 +216,7 @@ func (s *Service) publishRatioOptions(ctx context.Context) (systemconfig.Publish
|
|||||||
if strings.TrimSpace(value) == "" {
|
if strings.TrimSpace(value) == "" {
|
||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
var parsed systemconfig.PublishOptionsDTO
|
parsed := options
|
||||||
if err := json.Unmarshal([]byte(value), &parsed); err != nil {
|
if err := json.Unmarshal([]byte(value), &parsed); err != nil {
|
||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
@@ -225,13 +259,13 @@ func calculateSellerReferenceRatio(coinM float64, summary map[string]any, option
|
|||||||
calculateDailyLossRatioAdjustment(dailyLoss)
|
calculateDailyLossRatioAdjustment(dailyLoss)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clampSaleRatio(referenceRatio, requestedRatio float64) float64 {
|
func clampSaleRatio(referenceRatio, requestedRatio, minRatio float64) float64 {
|
||||||
maxRatio := roundRatio(referenceRatio + maxAcceleratedSaleRatioIncrement)
|
maxRatio := roundRatio(referenceRatio + maxAcceleratedSaleRatioIncrement)
|
||||||
if requestedRatio <= 0 {
|
if requestedRatio <= 0 {
|
||||||
return referenceRatio
|
return referenceRatio
|
||||||
}
|
}
|
||||||
if requestedRatio < referenceRatio {
|
if requestedRatio < minRatio {
|
||||||
return referenceRatio
|
return minRatio
|
||||||
}
|
}
|
||||||
if requestedRatio > maxRatio {
|
if requestedRatio > maxRatio {
|
||||||
return maxRatio
|
return maxRatio
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package listing
|
package listing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/modules/systemconfig"
|
"hfb_sys/backend/internal/modules/systemconfig"
|
||||||
@@ -81,6 +82,63 @@ func TestApplyMainSiteReceivePricingKeepsAcceleratedRatioWithinMainSiteRange(t *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyMainSiteReceivePricingAllowsQualityOwnerExclusiveRatio(t *testing.T) {
|
||||||
|
req := fullReceivePricingRequest(42)
|
||||||
|
req.OwnerIsQualityOwner = true
|
||||||
|
options := systemconfig.DefaultPublishOptions()
|
||||||
|
applyMainSiteReceivePricing(&req, options, defaultSalePriceConfig(), 0, 0, false)
|
||||||
|
breakdown := ensurePriceBreakdown(req.AssetSummary)
|
||||||
|
if readSummaryNumber(breakdown["seller_ratio"]) != 42 {
|
||||||
|
t.Fatalf("quality owner seller_ratio = %v, want 42", breakdown["seller_ratio"])
|
||||||
|
}
|
||||||
|
if readSummaryNumber(breakdown["quality_owner_ratio_discount"]) != 1 {
|
||||||
|
t.Fatalf(
|
||||||
|
"quality owner discount = %v, want 1",
|
||||||
|
breakdown["quality_owner_ratio_discount"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ordinary := fullReceivePricingRequest(42)
|
||||||
|
applyMainSiteReceivePricing(&ordinary, options, defaultSalePriceConfig(), 0, 0, false)
|
||||||
|
ordinaryBreakdown := ensurePriceBreakdown(ordinary.AssetSummary)
|
||||||
|
if readSummaryNumber(ordinaryBreakdown["seller_ratio"]) != 43 {
|
||||||
|
t.Fatalf("ordinary seller_ratio = %v, want reference 43", ordinaryBreakdown["seller_ratio"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQualityOwnerExclusiveRatioUsesConfiguredDiscount(t *testing.T) {
|
||||||
|
req := fullReceivePricingRequest(40)
|
||||||
|
req.OwnerIsQualityOwner = true
|
||||||
|
options := systemconfig.DefaultPublishOptions()
|
||||||
|
options.RatioConfig.QualityOwnerRatioDiscount = 3
|
||||||
|
applyMainSiteReceivePricing(&req, options, defaultSalePriceConfig(), 0, 0, false)
|
||||||
|
breakdown := ensurePriceBreakdown(req.AssetSummary)
|
||||||
|
if readSummaryNumber(breakdown["seller_ratio"]) != 40 {
|
||||||
|
t.Fatalf("configured quality owner seller_ratio = %v, want 40", breakdown["seller_ratio"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQualityOwnerExclusiveRatioRequiresPremiumZone(t *testing.T) {
|
||||||
|
req := fullReceivePricingRequest(42)
|
||||||
|
req.OwnerIsQualityOwner = true
|
||||||
|
applyMainSiteReceivePricing(
|
||||||
|
&req,
|
||||||
|
systemconfig.DefaultPublishOptions(),
|
||||||
|
defaultSalePriceConfig(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err := validateQualityOwnerPricingMode(&req, false); !errors.Is(err, ErrQualityRatioPremiumOnly) {
|
||||||
|
t.Fatalf("error = %v, want ErrQualityRatioPremiumOnly", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.IsPremium = true
|
||||||
|
if err := validateQualityOwnerPricingMode(&req, false); err != nil {
|
||||||
|
t.Fatalf("极品专区发布不应拒绝优质号主专享比例: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDistributionReceiveCostDeltaZeroKeepsMainSitePrice(t *testing.T) {
|
func TestDistributionReceiveCostDeltaZeroKeepsMainSitePrice(t *testing.T) {
|
||||||
req := fullReceivePricingRequest(50)
|
req := fullReceivePricingRequest(50)
|
||||||
applyMainSiteReceivePricing(&req, systemconfig.DefaultPublishOptions(), defaultSalePriceConfig(), 0, 0, true)
|
applyMainSiteReceivePricing(&req, systemconfig.DefaultPublishOptions(), defaultSalePriceConfig(), 0, 0, true)
|
||||||
@@ -146,10 +204,10 @@ func TestApplySiteReceiveRatioDeltaCapsAtMainSite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClampSaleRatioRejectsBelowReference(t *testing.T) {
|
func TestClampSaleRatioRejectsBelowReference(t *testing.T) {
|
||||||
if got := clampSaleRatio(43, 10); got != 43 {
|
if got := clampSaleRatio(43, 10, 43); got != 43 {
|
||||||
t.Fatalf("clamp below reference = %v, want 43", got)
|
t.Fatalf("clamp below reference = %v, want 43", got)
|
||||||
}
|
}
|
||||||
if got := clampSaleRatio(43, 80); got != 53 {
|
if got := clampSaleRatio(43, 80, 43); got != 53 {
|
||||||
t.Fatalf("clamp above max = %v, want 53", got)
|
t.Fatalf("clamp above max = %v, want 53", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ var (
|
|||||||
ErrSaleRatioOutOfRange = errors.New("sale ratio out of range")
|
ErrSaleRatioOutOfRange = errors.New("sale ratio out of range")
|
||||||
ErrDistributionSiteInvalid = errors.New("invalid distribution site")
|
ErrDistributionSiteInvalid = errors.New("invalid distribution site")
|
||||||
ErrPremiumOwnerRequired = errors.New("premium listing requires quality owner")
|
ErrPremiumOwnerRequired = errors.New("premium listing requires quality owner")
|
||||||
|
ErrQualityRatioPremiumOnly = errors.New("quality owner exclusive ratio requires premium zone")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest,
|
|||||||
if len(siteCode) > 0 {
|
if len(siteCode) > 0 {
|
||||||
effectiveSiteCode = strings.TrimSpace(siteCode[0])
|
effectiveSiteCode = strings.TrimSpace(siteCode[0])
|
||||||
}
|
}
|
||||||
|
if err := s.applyPublishEntitlements(ctx, ownerID, &req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
delta, onDistribution := s.repo.resolveSiteReceiveCostDelta(ctx, 0, effectiveSiteCode)
|
delta, onDistribution := s.repo.resolveSiteReceiveCostDelta(ctx, 0, effectiveSiteCode)
|
||||||
if err := s.applyMainSiteReceivePricing(ctx, &req, 0, delta, onDistribution); err != nil {
|
if err := s.applyMainSiteReceivePricing(ctx, &req, 0, delta, onDistribution); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -32,9 +35,6 @@ func (s *Service) Create(ctx context.Context, ownerID uint64, req CreateRequest,
|
|||||||
if err := validateRequiredOnlineTime(req); err != nil {
|
if err := validateRequiredOnlineTime(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.applyPublishEntitlements(ctx, ownerID, &req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cooldown, err := s.publishCooldown(ctx)
|
cooldown, err := s.publishCooldown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -54,6 +54,9 @@ func (s *Service) Update(ctx context.Context, ownerID uint64, id uint64, req Upd
|
|||||||
if len(siteCode) > 0 {
|
if len(siteCode) > 0 {
|
||||||
effectiveSiteCode = strings.TrimSpace(siteCode[0])
|
effectiveSiteCode = strings.TrimSpace(siteCode[0])
|
||||||
}
|
}
|
||||||
|
if err := s.applyPublishEntitlements(ctx, ownerID, &req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
delta, onDistribution := s.repo.resolveSiteReceiveCostDelta(ctx, id, effectiveSiteCode)
|
delta, onDistribution := s.repo.resolveSiteReceiveCostDelta(ctx, id, effectiveSiteCode)
|
||||||
if err := s.applyMainSiteReceivePricing(ctx, &req, 0, delta, onDistribution); err != nil {
|
if err := s.applyMainSiteReceivePricing(ctx, &req, 0, delta, onDistribution); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -68,9 +71,6 @@ func (s *Service) Update(ctx context.Context, ownerID uint64, id uint64, req Upd
|
|||||||
if err := validateRequiredOnlineTime(req); err != nil {
|
if err := validateRequiredOnlineTime(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.applyPublishEntitlements(ctx, ownerID, &req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
reviewRequired, err := s.reviewRequired(ctx)
|
reviewRequired, err := s.reviewRequired(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -94,6 +94,7 @@ func (s *Service) applyPublishEntitlements(ctx context.Context, ownerID uint64,
|
|||||||
if req.IsPremium && !isQualityOwner {
|
if req.IsPremium && !isQualityOwner {
|
||||||
return ErrPremiumOwnerRequired
|
return ErrPremiumOwnerRequired
|
||||||
}
|
}
|
||||||
|
req.OwnerIsQualityOwner = isQualityOwner
|
||||||
|
|
||||||
req.InsuranceFeeBps = 0
|
req.InsuranceFeeBps = 0
|
||||||
if req.CompensationPlan != CompensationPlanInsurance {
|
if req.CompensationPlan != CompensationPlanInsurance {
|
||||||
|
|||||||
@@ -182,9 +182,10 @@ type PublishDepositSkinGroupRule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PublishRatioConfig struct {
|
type PublishRatioConfig struct {
|
||||||
InsuranceBaseRatios []PublishInsuranceBaseRatio `json:"insurance_base_ratios"`
|
QualityOwnerRatioDiscount float64 `json:"quality_owner_ratio_discount"`
|
||||||
ConfigItems []PublishRatioConfigItem `json:"config_items"`
|
InsuranceBaseRatios []PublishInsuranceBaseRatio `json:"insurance_base_ratios"`
|
||||||
CoinCorrections []PublishCoinCorrection `json:"coin_corrections"`
|
ConfigItems []PublishRatioConfigItem `json:"config_items"`
|
||||||
|
CoinCorrections []PublishCoinCorrection `json:"coin_corrections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublishInsuranceBaseRatio struct {
|
type PublishInsuranceBaseRatio struct {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func DefaultPublishOptions() PublishOptionsDTO {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
RatioConfig: PublishRatioConfig{
|
RatioConfig: PublishRatioConfig{
|
||||||
|
QualityOwnerRatioDiscount: 1,
|
||||||
InsuranceBaseRatios: []PublishInsuranceBaseRatio{
|
InsuranceBaseRatios: []PublishInsuranceBaseRatio{
|
||||||
{Insurance: "3*3", Ratio: 40},
|
{Insurance: "3*3", Ratio: 40},
|
||||||
{Insurance: "2*3", Ratio: 42},
|
{Insurance: "2*3", Ratio: 42},
|
||||||
|
|||||||
@@ -106,6 +106,54 @@ func TestNormalizePublishOptionsValuePreservesCurrentSchema(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizePublishOptionsValueDefaultsQualityOwnerRatioDiscount(t *testing.T) {
|
||||||
|
legacy := `{
|
||||||
|
"schema_version": 2,
|
||||||
|
"ratio_config": {
|
||||||
|
"insurance_base_ratios": [{"insurance": "3*3", "ratio": 40}]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
normalized, err := normalizePublishOptionsValue(legacy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("规范化失败: %v", err)
|
||||||
|
}
|
||||||
|
var got PublishOptionsDTO
|
||||||
|
if err := json.Unmarshal([]byte(normalized), &got); err != nil {
|
||||||
|
t.Fatalf("解析规范化结果失败: %v", err)
|
||||||
|
}
|
||||||
|
if got.RatioConfig.QualityOwnerRatioDiscount != 1 {
|
||||||
|
t.Fatalf(
|
||||||
|
"quality_owner_ratio_discount = %v, want 1",
|
||||||
|
got.RatioConfig.QualityOwnerRatioDiscount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizePublishOptionsValuePreservesExplicitZeroQualityOwnerDiscount(t *testing.T) {
|
||||||
|
options := DefaultPublishOptions()
|
||||||
|
options.RatioConfig.QualityOwnerRatioDiscount = 0
|
||||||
|
raw, err := json.Marshal(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("构造配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, err := normalizePublishOptionsValue(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("规范化失败: %v", err)
|
||||||
|
}
|
||||||
|
var got PublishOptionsDTO
|
||||||
|
if err := json.Unmarshal([]byte(normalized), &got); err != nil {
|
||||||
|
t.Fatalf("解析规范化结果失败: %v", err)
|
||||||
|
}
|
||||||
|
if got.RatioConfig.QualityOwnerRatioDiscount != 0 {
|
||||||
|
t.Fatalf(
|
||||||
|
"quality_owner_ratio_discount = %v, want explicit 0",
|
||||||
|
got.RatioConfig.QualityOwnerRatioDiscount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizePublishOptionsValueMigratesSchemaV1Switches(t *testing.T) {
|
func TestNormalizePublishOptionsValueMigratesSchemaV1Switches(t *testing.T) {
|
||||||
legacy := `{"schema_version":1,"next_season_pass_enabled":false,"server_options":["QQ"]}`
|
legacy := `{"schema_version":1,"next_season_pass_enabled":false,"server_options":["QQ"]}`
|
||||||
|
|
||||||
|
|||||||
@@ -333,6 +333,10 @@ func normalizePublishOptionsValue(value string) (string, error) {
|
|||||||
if err := json.Unmarshal([]byte(value), &fields); err != nil {
|
if err := json.Unmarshal([]byte(value), &fields); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if !hasNestedJSONField(fields, "ratio_config", "quality_owner_ratio_discount") {
|
||||||
|
options.RatioConfig.QualityOwnerRatioDiscount =
|
||||||
|
DefaultPublishOptions().RatioConfig.QualityOwnerRatioDiscount
|
||||||
|
}
|
||||||
originalVersion := options.SchemaVersion
|
originalVersion := options.SchemaVersion
|
||||||
if options.SchemaVersion < publishOptionsSchemaVersion {
|
if options.SchemaVersion < publishOptionsSchemaVersion {
|
||||||
if hasLegacyPublishQuantityItem(options.QuantityItems) {
|
if hasLegacyPublishQuantityItem(options.QuantityItems) {
|
||||||
@@ -360,6 +364,19 @@ func normalizePublishOptionsValue(value string) (string, error) {
|
|||||||
return string(raw), nil
|
return string(raw), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasNestedJSONField(fields map[string]json.RawMessage, parent, child string) bool {
|
||||||
|
raw, ok := fields[parent]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var nested map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &nested); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok = nested[child]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func hasLegacyPublishQuantityItem(items []PublishQuantityItem) bool {
|
func hasLegacyPublishQuantityItem(items []PublishQuantityItem) bool {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if item.Key == "kit5" {
|
if item.Key == "kit5" {
|
||||||
|
|||||||
@@ -401,6 +401,17 @@ async function handleSave() {
|
|||||||
<div class="editor-block-title">
|
<div class="editor-block-title">
|
||||||
<strong>比例计算配置</strong>
|
<strong>比例计算配置</strong>
|
||||||
</div>
|
</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">
|
<div class="editor-block-title subtle-title">
|
||||||
<span>保险基础比例</span>
|
<span>保险基础比例</span>
|
||||||
<el-button size="small" @click="addInsuranceBaseRatio">添加保险比例</el-button>
|
<el-button size="small" @click="addInsuranceBaseRatio">添加保险比例</el-button>
|
||||||
@@ -635,6 +646,13 @@ async function handleSave() {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-config-hint {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #8b95a5;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.skin-config-row {
|
.skin-config-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
|
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export interface PublishSaleRatioAdjustmentRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PublishRatioConfig {
|
export interface PublishRatioConfig {
|
||||||
|
quality_owner_ratio_discount: number
|
||||||
insurance_base_ratios: PublishInsuranceBaseRatio[]
|
insurance_base_ratios: PublishInsuranceBaseRatio[]
|
||||||
config_items: PublishRatioConfigItem[]
|
config_items: PublishRatioConfigItem[]
|
||||||
coin_corrections: PublishCoinCorrection[]
|
coin_corrections: PublishCoinCorrection[]
|
||||||
@@ -151,6 +152,7 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
ratio_config: {
|
ratio_config: {
|
||||||
|
quality_owner_ratio_discount: 1,
|
||||||
insurance_base_ratios: [],
|
insurance_base_ratios: [],
|
||||||
config_items: [],
|
config_items: [],
|
||||||
coin_corrections: [],
|
coin_corrections: [],
|
||||||
@@ -363,6 +365,10 @@ function normalizeDepositSkinGroupRules(values: unknown[]): PublishDepositSkinGr
|
|||||||
function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
|
function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
|
||||||
const row = isRecord(value) ? value : {}
|
const row = isRecord(value) ? value : {}
|
||||||
return {
|
return {
|
||||||
|
quality_owner_ratio_discount: Math.min(
|
||||||
|
10,
|
||||||
|
Math.max(0, readNumber(row.quality_owner_ratio_discount ?? 1))
|
||||||
|
),
|
||||||
insurance_base_ratios: normalizeInsuranceBaseRatios(
|
insurance_base_ratios: normalizeInsuranceBaseRatios(
|
||||||
Array.isArray(row.insurance_base_ratios) ? row.insurance_base_ratios : []
|
Array.isArray(row.insurance_base_ratios) ? row.insurance_base_ratios : []
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function buildPublishDraft(options: {
|
|||||||
quantityModes: Record<QuantityKey, ChargeMode>
|
quantityModes: Record<QuantityKey, ChargeMode>
|
||||||
screenshotFiles: Record<ScreenshotKey, string[]>
|
screenshotFiles: Record<ScreenshotKey, string[]>
|
||||||
selectedSkins: string[]
|
selectedSkins: string[]
|
||||||
|
qualityOwnerRatioDefaultApplied: boolean
|
||||||
}): PublishDraft {
|
}): PublishDraft {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -47,6 +48,7 @@ export function buildPublishDraft(options: {
|
|||||||
quantityModes: { ...options.quantityModes },
|
quantityModes: { ...options.quantityModes },
|
||||||
screenshotFiles: cloneScreenshotFiles(options.screenshotFiles),
|
screenshotFiles: cloneScreenshotFiles(options.screenshotFiles),
|
||||||
selectedSkins: [...options.selectedSkins],
|
selectedSkins: [...options.selectedSkins],
|
||||||
|
qualityOwnerRatioDefaultApplied: options.qualityOwnerRatioDefaultApplied,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +65,10 @@ export function readPublishDraft(draftKey: string) {
|
|||||||
selectedSkins: Array.isArray(draft.selectedSkins)
|
selectedSkins: Array.isArray(draft.selectedSkins)
|
||||||
? draft.selectedSkins.filter((skin): skin is string => typeof skin === 'string')
|
? draft.selectedSkins.filter((skin): skin is string => typeof skin === 'string')
|
||||||
: [],
|
: [],
|
||||||
|
qualityOwnerRatioDefaultApplied:
|
||||||
|
typeof draft.qualityOwnerRatioDefaultApplied === 'boolean'
|
||||||
|
? draft.qualityOwnerRatioDefaultApplied
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(draftKey)
|
localStorage.removeItem(draftKey)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
const unlockDemulanEnabled = computed(() => publishOptions.value.unlock_demulan_enabled)
|
const unlockDemulanEnabled = computed(() => publishOptions.value.unlock_demulan_enabled)
|
||||||
const nextSeasonPassEnabled = computed(() => publishOptions.value.next_season_pass_enabled)
|
const nextSeasonPassEnabled = computed(() => publishOptions.value.next_season_pass_enabled)
|
||||||
const isQualityOwner = computed(() => qualityOwnerStatus.value?.active === true)
|
const isQualityOwner = computed(() => qualityOwnerStatus.value?.active === true)
|
||||||
|
const qualityOwnerRatioDefaultApplied = ref(true)
|
||||||
const insuranceFeeBps = computed(() =>
|
const insuranceFeeBps = computed(() =>
|
||||||
form.is_premium && isQualityOwner.value
|
form.is_premium && isQualityOwner.value
|
||||||
? Number(qualityOwnerStatus.value?.premium_insurance_fee_bps || 400)
|
? Number(qualityOwnerStatus.value?.premium_insurance_fee_bps || 400)
|
||||||
@@ -110,7 +111,43 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
isDistribution,
|
isDistribution,
|
||||||
receiveRatioDelta,
|
receiveRatioDelta,
|
||||||
receiveCostDelta,
|
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,
|
quantityModes,
|
||||||
screenshotFiles,
|
screenshotFiles,
|
||||||
selectedSkins: selectedSkins.value,
|
selectedSkins: selectedSkins.value,
|
||||||
|
qualityOwnerRatioDefaultApplied: qualityOwnerRatioDefaultApplied.value,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -304,6 +342,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
Object.assign(quantityModes, draft.quantityModes)
|
Object.assign(quantityModes, draft.quantityModes)
|
||||||
Object.assign(screenshotFiles, draft.screenshotFiles)
|
Object.assign(screenshotFiles, draft.screenshotFiles)
|
||||||
selectedSkins.value = draft.selectedSkins
|
selectedSkins.value = draft.selectedSkins
|
||||||
|
qualityOwnerRatioDefaultApplied.value =
|
||||||
|
draft.qualityOwnerRatioDefaultApplied ?? (form.accelerated_sale_ratio === '' ? true : false)
|
||||||
hydrateScreenshotPreviews()
|
hydrateScreenshotPreviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +408,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedSkins.value = Object.values(skinGroups).flatMap(value => stringArray(value))
|
selectedSkins.value = Object.values(skinGroups).flatMap(value => stringArray(value))
|
||||||
|
qualityOwnerRatioDefaultApplied.value = false
|
||||||
virtualAssetSaleAgreementChecked.value = true
|
virtualAssetSaleAgreementChecked.value = true
|
||||||
sellerAgreementChecked.value = true
|
sellerAgreementChecked.value = true
|
||||||
passwordAndDeviceConfirmed.value = true
|
passwordAndDeviceConfirmed.value = true
|
||||||
@@ -385,6 +426,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
sellerAgreementChecked.value = false
|
sellerAgreementChecked.value = false
|
||||||
passwordAndDeviceConfirmed.value = false
|
passwordAndDeviceConfirmed.value = false
|
||||||
activeUploadKey.value = 'coin'
|
activeUploadKey.value = 'coin'
|
||||||
|
qualityOwnerRatioDefaultApplied.value = true
|
||||||
|
|
||||||
// Also re-initialize quantityValues to 0
|
// Also re-initialize quantityValues to 0
|
||||||
for (const item of publishOptions.value.quantity_items) {
|
for (const item of publishOptions.value.quantity_items) {
|
||||||
@@ -588,6 +630,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleAcceleratedSaleRatioInput(value: string | number | undefined) {
|
function handleAcceleratedSaleRatioInput(value: string | number | undefined) {
|
||||||
|
qualityOwnerRatioDefaultApplied.value = false
|
||||||
if (value === '' || value === undefined) {
|
if (value === '' || value === undefined) {
|
||||||
form.accelerated_sale_ratio = ''
|
form.accelerated_sale_ratio = ''
|
||||||
return
|
return
|
||||||
@@ -645,21 +688,36 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
}
|
}
|
||||||
form.accelerated_sale_ratio = roundRatio(
|
form.accelerated_sale_ratio = roundRatio(
|
||||||
Math.min(
|
Math.min(
|
||||||
Math.max(ratio, pricing.calculatedDefaultSaleRatio.value),
|
Math.max(ratio, pricing.minAcceleratedSaleRatio.value),
|
||||||
pricing.maxAcceleratedSaleRatio.value
|
pricing.maxAcceleratedSaleRatio.value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useReferenceSaleRatio() {
|
function useReferenceSaleRatio() {
|
||||||
|
qualityOwnerRatioDefaultApplied.value = false
|
||||||
form.accelerated_sale_ratio = ''
|
form.accelerated_sale_ratio = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function useMaxAcceleratedSaleRatio() {
|
function useMaxAcceleratedSaleRatio() {
|
||||||
if (pricing.maxAcceleratedSaleRatio.value <= 0) return
|
if (pricing.maxAcceleratedSaleRatio.value <= 0) return
|
||||||
|
qualityOwnerRatioDefaultApplied.value = false
|
||||||
form.accelerated_sale_ratio = pricing.maxAcceleratedSaleRatio.value
|
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() {
|
async function handleSubmit() {
|
||||||
normalizeOnlineTime()
|
normalizeOnlineTime()
|
||||||
const error = validateForm()
|
const error = validateForm()
|
||||||
@@ -740,10 +798,12 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
const ratio = Number(form.accelerated_sale_ratio)
|
const ratio = Number(form.accelerated_sale_ratio)
|
||||||
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
|
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
|
||||||
if (
|
if (
|
||||||
pricing.calculatedDefaultSaleRatio.value > 0 &&
|
pricing.minAcceleratedSaleRatio.value > 0 &&
|
||||||
ratio < pricing.calculatedDefaultSaleRatio.value
|
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 (
|
if (
|
||||||
pricing.maxAcceleratedSaleRatio.value > 0 &&
|
pricing.maxAcceleratedSaleRatio.value > 0 &&
|
||||||
@@ -779,6 +839,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
if (!Number.isFinite(pricing.calculatedFinalPrice.value))
|
if (!Number.isFinite(pricing.calculatedFinalPrice.value))
|
||||||
return '发布价格计算异常,请检查填写内容'
|
return '发布价格计算异常,请检查填写内容'
|
||||||
if (form.is_premium && !isQualityOwner.value) return '仅优质号主可以发布到极品专区'
|
if (form.is_premium && !isQualityOwner.value) return '仅优质号主可以发布到极品专区'
|
||||||
|
if (isQualityOwnerExclusiveRatioActive.value && !form.is_premium) {
|
||||||
|
return '优质号主专享比例仅可用于极品专区'
|
||||||
|
}
|
||||||
if (!virtualAssetSaleAgreementChecked.value || !sellerAgreementChecked.value) {
|
if (!virtualAssetSaleAgreementChecked.value || !sellerAgreementChecked.value) {
|
||||||
return '请先阅读并勾选两份发布协议'
|
return '请先阅读并勾选两份发布协议'
|
||||||
}
|
}
|
||||||
@@ -837,6 +900,16 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
buyer_ratio: pricing.calculatedPlatformPricing.value.buyerRatio,
|
buyer_ratio: pricing.calculatedPlatformPricing.value.buyerRatio,
|
||||||
platform_markup_amount: pricing.calculatedPlatformPricing.value.platformMarkupAmount,
|
platform_markup_amount: pricing.calculatedPlatformPricing.value.platformMarkupAmount,
|
||||||
platform_rule_type: pricing.calculatedPlatformPricing.value.ruleType,
|
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,
|
season_insurance: form.season_insurance,
|
||||||
stamina_level: form.stamina_level,
|
stamina_level: form.stamina_level,
|
||||||
@@ -955,6 +1028,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
publishAgreements,
|
publishAgreements,
|
||||||
qualityOwnerStatus,
|
qualityOwnerStatus,
|
||||||
isQualityOwner,
|
isQualityOwner,
|
||||||
|
qualityOwnerRatioDefaultApplied,
|
||||||
|
qualityOwnerSaleRatioSelected,
|
||||||
|
isQualityOwnerExclusiveRatioActive,
|
||||||
insuranceFeeBps,
|
insuranceFeeBps,
|
||||||
insuranceFeePercent,
|
insuranceFeePercent,
|
||||||
virtualAssetSaleAgreementChecked,
|
virtualAssetSaleAgreementChecked,
|
||||||
@@ -1013,6 +1089,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
clampAcceleratedSaleRatioInput,
|
clampAcceleratedSaleRatioInput,
|
||||||
useReferenceSaleRatio,
|
useReferenceSaleRatio,
|
||||||
useMaxAcceleratedSaleRatio,
|
useMaxAcceleratedSaleRatio,
|
||||||
|
useQualityOwnerSaleRatio,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
validateForm,
|
validateForm,
|
||||||
isScreenshotRequired,
|
isScreenshotRequired,
|
||||||
|
|||||||
@@ -706,6 +706,19 @@
|
|||||||
gap: 8px;
|
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 {
|
.ratio-mode-btn {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
@@ -737,6 +750,44 @@
|
|||||||
line-height: 1.25;
|
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 {
|
.ratio-mode-btn.active {
|
||||||
border-color: #ff6a00;
|
border-color: #ff6a00;
|
||||||
background: #fff7f0;
|
background: #fff7f0;
|
||||||
@@ -880,6 +931,16 @@
|
|||||||
background: #f8fafc;
|
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,
|
.premium-zone-row,
|
||||||
.coverage-heading {
|
.coverage-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -888,6 +949,34 @@
|
|||||||
gap: 12px;
|
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,
|
.premium-zone-row strong,
|
||||||
.coverage-heading strong {
|
.coverage-heading strong {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -905,16 +994,46 @@
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-owner-link {
|
.premium-zone-action {
|
||||||
justify-self: start;
|
display: flex;
|
||||||
padding: 0;
|
align-items: center;
|
||||||
border: none;
|
flex-shrink: 0;
|
||||||
background: transparent;
|
gap: 7px;
|
||||||
color: #7c3aed;
|
}
|
||||||
font-size: 12px;
|
|
||||||
|
.premium-zone-action > span {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10px;
|
||||||
font-weight: 800;
|
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 {
|
.coverage-option-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -939,6 +1058,32 @@
|
|||||||
font-weight: 800;
|
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 {
|
.coverage-option span {
|
||||||
color: #858c96;
|
color: #858c96;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -952,6 +1097,13 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coverage-option .coverage-special-rate {
|
||||||
|
width: max-content;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3ebff;
|
||||||
|
}
|
||||||
|
|
||||||
.coverage-option.active {
|
.coverage-option.active {
|
||||||
border-color: #7c3aed;
|
border-color: #7c3aed;
|
||||||
background: #faf5ff;
|
background: #faf5ff;
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ const {
|
|||||||
passwordAndDeviceConfirmed,
|
passwordAndDeviceConfirmed,
|
||||||
canPublishAfterAgreements,
|
canPublishAfterAgreements,
|
||||||
isQualityOwner,
|
isQualityOwner,
|
||||||
|
isQualityOwnerExclusiveRatioActive,
|
||||||
|
qualityOwnerSaleRatioSelected,
|
||||||
qualityOwnerStatus,
|
qualityOwnerStatus,
|
||||||
|
qualityOwnerRatioDiscount,
|
||||||
|
qualityOwnerSaleRatio,
|
||||||
|
minAcceleratedSaleRatio,
|
||||||
insuranceFeePercent,
|
insuranceFeePercent,
|
||||||
showBanRecordRiskHint,
|
showBanRecordRiskHint,
|
||||||
unlockDemulanEnabled,
|
unlockDemulanEnabled,
|
||||||
@@ -91,6 +96,7 @@ const {
|
|||||||
clampAcceleratedSaleRatioInput,
|
clampAcceleratedSaleRatioInput,
|
||||||
useReferenceSaleRatio,
|
useReferenceSaleRatio,
|
||||||
useMaxAcceleratedSaleRatio,
|
useMaxAcceleratedSaleRatio,
|
||||||
|
useQualityOwnerSaleRatio,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
isScreenshotRequired,
|
isScreenshotRequired,
|
||||||
} = usePublishForm({
|
} = usePublishForm({
|
||||||
@@ -732,7 +738,11 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
{{ saleRatioRangeText
|
{{ saleRatioRangeText
|
||||||
}}{{ isDistribution ? '。' : ',比例越高出租速度越快,自行选择。' }}
|
}}{{ isDistribution ? '。' : ',比例越高出租速度越快,自行选择。' }}
|
||||||
</p>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ratio-mode-btn"
|
class="ratio-mode-btn"
|
||||||
@@ -749,18 +759,36 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ratio-mode-btn"
|
class="ratio-mode-btn"
|
||||||
:class="{ active: hasAcceleratedSaleRatioInput() }"
|
:class="{ active: Number(form.accelerated_sale_ratio) === maxAcceleratedSaleRatio }"
|
||||||
:disabled="maxAcceleratedSaleRatio <= 0"
|
:disabled="maxAcceleratedSaleRatio <= 0"
|
||||||
@click="useMaxAcceleratedSaleRatio"
|
@click="useMaxAcceleratedSaleRatio"
|
||||||
>
|
>
|
||||||
<strong>加速比例</strong>
|
<strong>最高比例</strong>
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
maxAcceleratedSaleRatio > 0
|
maxAcceleratedSaleRatio > 0
|
||||||
? `1元=${formatNumber(calculatedDefaultSaleRatio)}~${formatNumber(maxAcceleratedSaleRatio)}万`
|
? `1元=${formatNumber(maxAcceleratedSaleRatio)}万`
|
||||||
: '自动计算'
|
: '自动计算'
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
@@ -770,13 +798,14 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
:model-value="form.accelerated_sale_ratio"
|
:model-value="form.accelerated_sale_ratio"
|
||||||
label="自定比例"
|
label="自定比例"
|
||||||
type="number"
|
type="number"
|
||||||
|
:min="minAcceleratedSaleRatio || 0"
|
||||||
:placeholder="acceleratedSaleRatioPlaceholder"
|
:placeholder="acceleratedSaleRatioPlaceholder"
|
||||||
class="publish-field ratio-input-field"
|
class="publish-field ratio-input-field"
|
||||||
@update:model-value="handleAcceleratedSaleRatioInput"
|
@update:model-value="handleAcceleratedSaleRatioInput"
|
||||||
@blur="clampAcceleratedSaleRatioInput"
|
@blur="clampAcceleratedSaleRatioInput"
|
||||||
/>
|
/>
|
||||||
<p class="field-hint ratio-input-hint">
|
<p class="field-hint ratio-input-hint">
|
||||||
留空使用参考比例;填写后范围为参考比例到参考比例+10,请根据实际需求合理设置。
|
留空使用参考比例;优质号主可按后台配置下调基础比例。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-grid">
|
<div class="result-grid">
|
||||||
@@ -824,21 +853,46 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
</div>
|
</div>
|
||||||
<p class="price-breakdown-hint">卖家价格 = 纯币基础价 + 额外消耗品。</p>
|
<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 class="premium-zone-row">
|
||||||
<div>
|
<div class="premium-zone-copy">
|
||||||
<strong>极品专区</strong>
|
<div class="premium-zone-title">
|
||||||
|
<strong>极品专区</strong>
|
||||||
|
<em v-if="isQualityOwner">优质号主已认证</em>
|
||||||
|
<em v-else class="is-locked">优质号主专享</em>
|
||||||
|
</div>
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
isQualityOwner
|
isQualityOwner
|
||||||
? '优质号主专享曝光专区'
|
? '发布后获得首页极品专区曝光'
|
||||||
: `保证金 ¥${formatMoney(
|
: `缴纳 ¥${formatMoney(
|
||||||
(qualityOwnerStatus?.required_deposit_cent || 20000) / 100
|
(qualityOwnerStatus?.required_deposit_cent || 20000) / 100
|
||||||
)}`
|
)} 保证金后可开通`
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="!isQualityOwner"
|
v-if="!isQualityOwner"
|
||||||
@@ -846,7 +900,8 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
class="quality-owner-link"
|
class="quality-owner-link"
|
||||||
@click="router.push('/m/profile')"
|
@click="router.push('/m/profile')"
|
||||||
>
|
>
|
||||||
前往个人中心开通优质号主
|
<span>前往个人中心开通优质号主</span>
|
||||||
|
<van-icon name="arrow" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="coverage-heading">
|
<div class="coverage-heading">
|
||||||
@@ -860,7 +915,10 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
:class="{ active: form.compensation_plan === 'normal' }"
|
:class="{ active: form.compensation_plan === 'normal' }"
|
||||||
@click="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>
|
<span>租金 + 押金规则赔付</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -869,13 +927,18 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
:class="{ active: form.compensation_plan === 'insurance' }"
|
:class="{ active: form.compensation_plan === 'insurance' }"
|
||||||
@click="form.compensation_plan = 'insurance'"
|
@click="form.compensation_plan = 'insurance'"
|
||||||
>
|
>
|
||||||
<strong>无忧租号保险</strong>
|
<span class="coverage-option-top">
|
||||||
<span>服务费 {{ insuranceFeePercent.toFixed(1) }}%</span>
|
<strong>无忧租号保险</strong>
|
||||||
<em v-if="isQualityOwner && form.is_premium">极品专享费率</em>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="coverage-note">
|
<p class="coverage-note">
|
||||||
无忧租号保险在普通赔付基础上额外保障账号市场价值;选择后按号主实际到手租金扣除服务费。
|
无忧租号保险在普通赔付基础上增加账号市场价值保障,服务费按实际到手租金扣除。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -705,7 +705,7 @@
|
|||||||
|
|
||||||
.ratio-mode-grid {
|
.ratio-mode-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,6 +727,50 @@
|
|||||||
font-size: 12px;
|
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 {
|
.ratio-custom-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1022,6 +1066,16 @@
|
|||||||
background: #f8fafc;
|
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,
|
.premium-zone-row,
|
||||||
.coverage-heading {
|
.coverage-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1035,6 +1089,33 @@
|
|||||||
min-width: 0;
|
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,
|
.premium-zone-row strong,
|
||||||
.coverage-heading strong {
|
.coverage-heading strong {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1051,17 +1132,52 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-owner-link {
|
.premium-zone-action {
|
||||||
justify-self: start;
|
display: flex;
|
||||||
padding: 0;
|
align-items: center;
|
||||||
border: none;
|
flex-shrink: 0;
|
||||||
background: transparent;
|
gap: 9px;
|
||||||
color: #7c3aed;
|
}
|
||||||
font-size: 12px;
|
|
||||||
|
.premium-zone-action > span {
|
||||||
font-weight: 800;
|
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;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality-owner-link:hover {
|
||||||
|
border-color: #c7acdf;
|
||||||
|
background: #f4ebff;
|
||||||
|
}
|
||||||
|
|
||||||
.coverage-option-grid {
|
.coverage-option-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -1086,6 +1202,33 @@
|
|||||||
font-weight: 900;
|
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 {
|
.coverage-option span {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1100,6 +1243,13 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coverage-option .coverage-special-rate {
|
||||||
|
width: max-content;
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #f3ebff;
|
||||||
|
}
|
||||||
|
|
||||||
.coverage-option.active {
|
.coverage-option.active {
|
||||||
border-color: #7c3aed;
|
border-color: #7c3aed;
|
||||||
background: #faf5ff;
|
background: #faf5ff;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
|
ArrowRight,
|
||||||
Delete,
|
Delete,
|
||||||
DocumentChecked,
|
DocumentChecked,
|
||||||
Picture,
|
Picture,
|
||||||
@@ -25,7 +26,12 @@ const {
|
|||||||
router,
|
router,
|
||||||
publishAgreements,
|
publishAgreements,
|
||||||
isQualityOwner,
|
isQualityOwner,
|
||||||
|
isQualityOwnerExclusiveRatioActive,
|
||||||
|
qualityOwnerSaleRatioSelected,
|
||||||
qualityOwnerStatus,
|
qualityOwnerStatus,
|
||||||
|
qualityOwnerRatioDiscount,
|
||||||
|
qualityOwnerSaleRatio,
|
||||||
|
minAcceleratedSaleRatio,
|
||||||
insuranceFeePercent,
|
insuranceFeePercent,
|
||||||
virtualAssetSaleAgreementChecked,
|
virtualAssetSaleAgreementChecked,
|
||||||
sellerAgreementChecked,
|
sellerAgreementChecked,
|
||||||
@@ -101,6 +107,7 @@ const {
|
|||||||
clampAcceleratedSaleRatioInput,
|
clampAcceleratedSaleRatioInput,
|
||||||
useReferenceSaleRatio,
|
useReferenceSaleRatio,
|
||||||
useMaxAcceleratedSaleRatio,
|
useMaxAcceleratedSaleRatio,
|
||||||
|
useQualityOwnerSaleRatio,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
isScreenshotRequired,
|
isScreenshotRequired,
|
||||||
} = usePublishForm({
|
} = usePublishForm({
|
||||||
@@ -595,6 +602,22 @@ function selectDailyLoss(value: string | number) {
|
|||||||
: '自动计算'
|
: '自动计算'
|
||||||
}}</span>
|
}}</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ratio-mode-btn"
|
class="ratio-mode-btn"
|
||||||
@@ -618,13 +641,14 @@ function selectDailyLoss(value: string | number) {
|
|||||||
:class="{
|
:class="{
|
||||||
active:
|
active:
|
||||||
hasAcceleratedSaleRatioInput() &&
|
hasAcceleratedSaleRatioInput() &&
|
||||||
Number(form.accelerated_sale_ratio) !== maxAcceleratedSaleRatio,
|
Number(form.accelerated_sale_ratio) !== maxAcceleratedSaleRatio &&
|
||||||
|
!qualityOwnerSaleRatioSelected,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<strong>自定比例</strong>
|
<strong>自定比例</strong>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
:model-value="form.accelerated_sale_ratio"
|
:model-value="form.accelerated_sale_ratio"
|
||||||
:min="0"
|
:min="minAcceleratedSaleRatio || 0"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:placeholder="acceleratedSaleRatioPlaceholder"
|
:placeholder="acceleratedSaleRatioPlaceholder"
|
||||||
@update:model-value="handleAcceleratedSaleRatioInput"
|
@update:model-value="handleAcceleratedSaleRatioInput"
|
||||||
@@ -657,25 +681,45 @@ function selectDailyLoss(value: string | number) {
|
|||||||
</div>
|
</div>
|
||||||
</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 class="premium-zone-row">
|
||||||
<div>
|
<div class="premium-zone-copy">
|
||||||
<strong>极品专区</strong>
|
<div class="premium-zone-title">
|
||||||
|
<strong>极品专区</strong>
|
||||||
|
<em v-if="isQualityOwner">优质号主已认证</em>
|
||||||
|
<em v-else class="is-locked">优质号主专享</em>
|
||||||
|
</div>
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
isQualityOwner
|
isQualityOwner
|
||||||
? '优质号主专享曝光专区'
|
? '发布后进入首页极品专区,获得高端账号专属曝光'
|
||||||
: `缴纳 ¥${formatMoney(
|
: `缴纳 ¥${formatMoney(
|
||||||
(qualityOwnerStatus?.required_deposit_cent || 20000) / 100
|
(qualityOwnerStatus?.required_deposit_cent || 20000) / 100
|
||||||
)} 保证金后可开通优质号主`
|
)} 保证金后可开通优质号主`
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<el-switch
|
<div class="premium-zone-action" :class="{ 'is-active': form.is_premium }">
|
||||||
v-model="form.is_premium"
|
<span>{{
|
||||||
:disabled="!isQualityOwner"
|
isQualityOwnerExclusiveRatioActive
|
||||||
active-text="发布到极品专区"
|
? '专享比例已关联专区'
|
||||||
/>
|
: form.is_premium
|
||||||
|
? '已开启极品专区'
|
||||||
|
: isQualityOwner
|
||||||
|
? '开启极品专区'
|
||||||
|
: '开通后可发布'
|
||||||
|
}}</span>
|
||||||
|
<el-switch
|
||||||
|
v-model="form.is_premium"
|
||||||
|
:disabled="!isQualityOwner || isQualityOwnerExclusiveRatioActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="!isQualityOwner"
|
v-if="!isQualityOwner"
|
||||||
@@ -683,7 +727,8 @@ function selectDailyLoss(value: string | number) {
|
|||||||
class="quality-owner-link"
|
class="quality-owner-link"
|
||||||
@click="router.push('/profile')"
|
@click="router.push('/profile')"
|
||||||
>
|
>
|
||||||
前往个人中心开通优质号主
|
<span>前往个人中心开通优质号主</span>
|
||||||
|
<el-icon><ArrowRight /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="coverage-heading">
|
<div class="coverage-heading">
|
||||||
@@ -697,7 +742,10 @@ function selectDailyLoss(value: string | number) {
|
|||||||
:class="{ active: form.compensation_plan === 'normal' }"
|
:class="{ active: form.compensation_plan === 'normal' }"
|
||||||
@click="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>
|
<span>按平台规则赔付租金与押金</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -706,13 +754,20 @@ function selectDailyLoss(value: string | number) {
|
|||||||
:class="{ active: form.compensation_plan === 'insurance' }"
|
:class="{ active: form.compensation_plan === 'insurance' }"
|
||||||
@click="form.compensation_plan = 'insurance'"
|
@click="form.compensation_plan = 'insurance'"
|
||||||
>
|
>
|
||||||
<strong>无忧租号保险</strong>
|
<span class="coverage-option-top">
|
||||||
<span> 服务费 {{ insuranceFeePercent.toFixed(1) }}%,额外保障账号市场价值 </span>
|
<strong>无忧租号保险</strong>
|
||||||
<em v-if="isQualityOwner && form.is_premium">极品专区专享费率</em>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="coverage-note">
|
<p class="coverage-note">
|
||||||
普通赔付:账号封禁、追回、洗号等情况最高赔付全额租金与押金。无忧租号保险:在普通赔付基础上,额外按账号市场价值赔付;选择后按实际到手租金扣除服务费。
|
普通赔付覆盖封禁、追回、洗号等情况;无忧租号保险在此基础上增加账号市场价值保障。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function usePricingCalculator(options: {
|
|||||||
isDistribution?: Ref<boolean>
|
isDistribution?: Ref<boolean>
|
||||||
receiveRatioDelta?: Ref<number>
|
receiveRatioDelta?: Ref<number>
|
||||||
receiveCostDelta?: Ref<number>
|
receiveCostDelta?: Ref<number>
|
||||||
|
isQualityOwner?: Ref<boolean>
|
||||||
}) {
|
}) {
|
||||||
const serverOptions = computed(() => options.publishOptions.value.server_options)
|
const serverOptions = computed(() => options.publishOptions.value.server_options)
|
||||||
const faceOptions = computed(() => options.publishOptions.value.face_options)
|
const faceOptions = computed(() => options.publishOptions.value.face_options)
|
||||||
@@ -88,6 +89,24 @@ export function usePricingCalculator(options: {
|
|||||||
const maxAcceleratedSaleRatio = computed(() =>
|
const maxAcceleratedSaleRatio = computed(() =>
|
||||||
calculatedDefaultSaleRatio.value > 0 ? roundRatio(calculatedDefaultSaleRatio.value + 10) : 0
|
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(() =>
|
const siteReceiveRatioDelta = computed(() =>
|
||||||
Math.max(0, Number(options.receiveRatioDelta?.value || 0))
|
Math.max(0, Number(options.receiveRatioDelta?.value || 0))
|
||||||
)
|
)
|
||||||
@@ -104,7 +123,8 @@ export function usePricingCalculator(options: {
|
|||||||
return readFinalSaleRatio(
|
return readFinalSaleRatio(
|
||||||
calculatedDefaultSaleRatio.value,
|
calculatedDefaultSaleRatio.value,
|
||||||
options.form.accelerated_sale_ratio,
|
options.form.accelerated_sale_ratio,
|
||||||
maxAcceleratedSaleRatio.value
|
maxAcceleratedSaleRatio.value,
|
||||||
|
minAcceleratedSaleRatio.value
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const anchorRatio = computed(() => {
|
const anchorRatio = computed(() => {
|
||||||
@@ -163,11 +183,16 @@ export function usePricingCalculator(options: {
|
|||||||
if (options.isDistribution?.value) {
|
if (options.isDistribution?.value) {
|
||||||
return `主站参考 1:${formatNumber(calculatedDefaultSaleRatio.value)},本站收号 1:${formatNumber(calculatedRatio.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)}`
|
return `可设置 1:${formatNumber(calculatedDefaultSaleRatio.value)} ~ 1:${formatNumber(maxAcceleratedSaleRatio.value)}`
|
||||||
})
|
})
|
||||||
const acceleratedSaleRatioPlaceholder = computed(() => {
|
const acceleratedSaleRatioPlaceholder = computed(() => {
|
||||||
if (calculatedDefaultSaleRatio.value <= 0) return '填写资料后自动生成可设置范围'
|
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(() =>
|
const recommendedDepositAmount = computed(() =>
|
||||||
calculateRecommendedDeposit({
|
calculateRecommendedDeposit({
|
||||||
@@ -227,6 +252,9 @@ export function usePricingCalculator(options: {
|
|||||||
calculatedSellerReferenceRatio,
|
calculatedSellerReferenceRatio,
|
||||||
calculatedDefaultSaleRatio,
|
calculatedDefaultSaleRatio,
|
||||||
maxAcceleratedSaleRatio,
|
maxAcceleratedSaleRatio,
|
||||||
|
minAcceleratedSaleRatio,
|
||||||
|
qualityOwnerRatioDiscount,
|
||||||
|
qualityOwnerSaleRatio,
|
||||||
siteReceiveRatioDelta,
|
siteReceiveRatioDelta,
|
||||||
siteReceiveCostDelta,
|
siteReceiveCostDelta,
|
||||||
calculatedRatio,
|
calculatedRatio,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface PublishDraft {
|
|||||||
quantityModes: Record<QuantityKey, ChargeMode>
|
quantityModes: Record<QuantityKey, ChargeMode>
|
||||||
screenshotFiles: Record<ScreenshotKey, string[]>
|
screenshotFiles: Record<ScreenshotKey, string[]>
|
||||||
selectedSkins: string[]
|
selectedSkins: string[]
|
||||||
|
qualityOwnerRatioDefaultApplied?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DepositBreakdownItem {
|
export interface DepositBreakdownItem {
|
||||||
|
|||||||
@@ -150,13 +150,14 @@ export function calculateSellerReferenceRatio(options: {
|
|||||||
export function readFinalSaleRatio(
|
export function readFinalSaleRatio(
|
||||||
defaultRatio: number,
|
defaultRatio: number,
|
||||||
acceleratedSaleRatio: number | '',
|
acceleratedSaleRatio: number | '',
|
||||||
maxAcceleratedSaleRatio: number
|
maxAcceleratedSaleRatio: number,
|
||||||
|
minAcceleratedSaleRatio = defaultRatio
|
||||||
) {
|
) {
|
||||||
if (defaultRatio <= 0) return 0
|
if (defaultRatio <= 0) return 0
|
||||||
if (!hasAcceleratedSaleRatioInput(acceleratedSaleRatio)) return defaultRatio
|
if (!hasAcceleratedSaleRatioInput(acceleratedSaleRatio)) return defaultRatio
|
||||||
const ratio = Number(acceleratedSaleRatio)
|
const ratio = Number(acceleratedSaleRatio)
|
||||||
if (!Number.isFinite(ratio) || ratio <= 0) return defaultRatio
|
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) {
|
export function applySiteReceiveRatioDelta(referenceRatio: number, delta: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user