From 78257097f1d531595c0f587e9485f3b5d66693a1 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 23 May 2026 21:07:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E9=83=A8=E5=8A=A0=E4=BB=B7=E5=88=86?= =?UTF-8?q?=E7=A6=BB,=20=E4=B8=8D=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/systemconfig/dto.go | 1 - .../internal/modules/systemconfig/handler.go | 9 + .../modules/systemconfig/publish_options.go | 17 -- .../modules/systemconfig/repository.go | 1 + .../modules/systemconfig/sale_price_config.go | 33 ++++ .../internal/modules/systemconfig/service.go | 36 +++- backend/internal/router/router.go | 1 + frontend/src/api/listingOptions.ts | 20 +- .../views/admin/AdminSystemConfigsView.vue | 187 ++++++++++++------ .../mobile/MobileSellerListingCreateView.css | 6 - .../mobile/MobileSellerListingCreateView.vue | 153 +++++++++++--- 11 files changed, 347 insertions(+), 117 deletions(-) create mode 100644 backend/internal/modules/systemconfig/sale_price_config.go diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 8251ee5..aeb24ac 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -52,7 +52,6 @@ type PublishOptionsDTO struct { FireLevelMin int `json:"fire_level_min"` PriceConfig PublishPriceConfig `json:"price_config"` RatioConfig PublishRatioConfig `json:"ratio_config"` - SalePriceConfig PublishSalePriceConfig `json:"sale_price_config"` } type PublishOptionGroup struct { diff --git a/backend/internal/modules/systemconfig/handler.go b/backend/internal/modules/systemconfig/handler.go index 1c200e8..518ef2e 100644 --- a/backend/internal/modules/systemconfig/handler.go +++ b/backend/internal/modules/systemconfig/handler.go @@ -36,6 +36,15 @@ func (h *Handler) PublishOptions(c *gin.Context) { response.OK(c, options) } +func (h *Handler) SalePriceConfig(c *gin.Context) { + config, err := h.service.SalePriceConfig() + if err != nil { + writeConfigError(c, err) + return + } + response.OK(c, config) +} + func (h *Handler) HomeAnnouncements(c *gin.Context) { announcements, err := h.service.HomeAnnouncements() if err != nil { diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go index 29afd36..19d3fc5 100644 --- a/backend/internal/modules/systemconfig/publish_options.go +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -108,22 +108,5 @@ func DefaultPublishOptions() PublishOptionsDTO { {ThresholdM: 530, Correction: 5}, }, }, - SalePriceConfig: PublishSalePriceConfig{ - FixedMarkupRules: []PublishSaleFixedMarkupRule{ - {MinM: 10, MaxM: 30, MarkupAmount: 28}, - {MinM: 30, MaxM: 50, MarkupAmount: 31}, - {MinM: 50, MaxM: 70, MarkupAmount: 34}, - {MinM: 70, MaxM: 90, MarkupAmount: 38}, - }, - RatioAdjustmentRules: []PublishSaleRatioAdjustmentRule{ - {MinM: 90, MaxM: 150, RatioSubtract: 5}, - {MinM: 150, MaxM: 230, RatioSubtract: 4}, - {MinM: 230, MaxM: 310, RatioSubtract: 3.5}, - {MinM: 310, MaxM: 390, RatioSubtract: 3}, - {MinM: 390, MaxM: 470, RatioSubtract: 0}, - {MinM: 470, MaxM: 550, RatioSubtract: 0}, - {MinM: 550, RatioSubtract: 0}, - }, - }, } } diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 09dad5b..040fa49 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -43,6 +43,7 @@ var defaultConfigs = []defaultConfig{ {Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"}, {Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"}, {Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"}, + {Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"}, } func NewRepository(db *gorm.DB) *Repository { diff --git a/backend/internal/modules/systemconfig/sale_price_config.go b/backend/internal/modules/systemconfig/sale_price_config.go new file mode 100644 index 0000000..d63e4e1 --- /dev/null +++ b/backend/internal/modules/systemconfig/sale_price_config.go @@ -0,0 +1,33 @@ +package systemconfig + +import "encoding/json" + +const salePriceConfigKey = "listing.sale_price_config" + +func defaultSalePriceConfigValue() string { + raw, err := json.Marshal(DefaultSalePriceConfig()) + if err != nil { + return "{}" + } + return string(raw) +} + +func DefaultSalePriceConfig() PublishSalePriceConfig { + return PublishSalePriceConfig{ + FixedMarkupRules: []PublishSaleFixedMarkupRule{ + {MinM: 10, MaxM: 30, MarkupAmount: 28}, + {MinM: 30, MaxM: 50, MarkupAmount: 31}, + {MinM: 50, MaxM: 70, MarkupAmount: 34}, + {MinM: 70, MaxM: 90, MarkupAmount: 38}, + }, + RatioAdjustmentRules: []PublishSaleRatioAdjustmentRule{ + {MinM: 90, MaxM: 150, RatioSubtract: 5}, + {MinM: 150, MaxM: 230, RatioSubtract: 4}, + {MinM: 230, MaxM: 310, RatioSubtract: 3.5}, + {MinM: 310, MaxM: 390, RatioSubtract: 3}, + {MinM: 390, MaxM: 470, RatioSubtract: 0}, + {MinM: 470, MaxM: 550, RatioSubtract: 0}, + {MinM: 550, RatioSubtract: 0}, + }, + } +} diff --git a/backend/internal/modules/systemconfig/service.go b/backend/internal/modules/systemconfig/service.go index 9c59e5a..c2f303d 100644 --- a/backend/internal/modules/systemconfig/service.go +++ b/backend/internal/modules/systemconfig/service.go @@ -37,6 +37,17 @@ func (s *Service) PublishOptions() (*PublishOptionsDTO, error) { return &options, nil } +func (s *Service) SalePriceConfig() (*PublishSalePriceConfig, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + config, err := s.salePriceConfig() + if err != nil { + return nil, err + } + return &config, nil +} + func (s *Service) publishOptions() (PublishOptionsDTO, error) { value, err := s.repo.FindValue(publishOptionsConfigKey) if err != nil { @@ -50,6 +61,19 @@ func (s *Service) publishOptions() (PublishOptionsDTO, error) { return options, nil } +func (s *Service) salePriceConfig() (PublishSalePriceConfig, error) { + value, err := s.repo.FindValue(salePriceConfigKey) + if err != nil { + return PublishSalePriceConfig{}, err + } + config := DefaultSalePriceConfig() + if err := json.Unmarshal([]byte(value), &config); err != nil { + config = DefaultSalePriceConfig() + } + normalizeSalePriceConfig(&config) + return config, nil +} + func (s *Service) HomeAnnouncements() (*HomeAnnouncementsDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable @@ -214,10 +238,14 @@ func normalizePublishOptions(options *PublishOptionsDTO) { if len(options.RatioConfig.CoinCorrections) == 0 { options.RatioConfig.CoinCorrections = defaults.RatioConfig.CoinCorrections } - if len(options.SalePriceConfig.FixedMarkupRules) == 0 { - options.SalePriceConfig.FixedMarkupRules = defaults.SalePriceConfig.FixedMarkupRules +} + +func normalizeSalePriceConfig(config *PublishSalePriceConfig) { + defaults := DefaultSalePriceConfig() + if len(config.FixedMarkupRules) == 0 { + config.FixedMarkupRules = defaults.FixedMarkupRules } - if len(options.SalePriceConfig.RatioAdjustmentRules) == 0 { - options.SalePriceConfig.RatioAdjustmentRules = defaults.SalePriceConfig.RatioAdjustmentRules + if len(config.RatioAdjustmentRules) == 0 { + config.RatioAdjustmentRules = defaults.RatioAdjustmentRules } } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 6e20a35..cdb5f9c 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -127,6 +127,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { { api.GET("/health", health.Check) api.GET("/listing-publish-options", systemConfigHandler.PublishOptions) + api.GET("/listing-sale-price-config", systemConfigHandler.SalePriceConfig) api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements) api.GET("/mobile-home-config", systemConfigHandler.HomeConfig) api.GET("/public/files/object", fileHandler.PublicObject) diff --git a/frontend/src/api/listingOptions.ts b/frontend/src/api/listingOptions.ts index c8990d6..158cc8d 100644 --- a/frontend/src/api/listingOptions.ts +++ b/frontend/src/api/listingOptions.ts @@ -91,7 +91,6 @@ export interface ListingPublishOptions { fire_level_min: number price_config: PublishPriceConfig ratio_config: PublishRatioConfig - sale_price_config: PublishSalePriceConfig } interface ApiResponse { @@ -124,10 +123,11 @@ export const emptyListingPublishOptions: ListingPublishOptions = { config_items: [], coin_corrections: [], }, - sale_price_config: { - fixed_markup_rules: [], - ratio_adjustment_rules: [], - }, +} + +export const emptyListingSalePriceConfig: PublishSalePriceConfig = { + fixed_markup_rules: [], + ratio_adjustment_rules: [], } export async function fetchListingPublishOptions() { @@ -135,6 +135,11 @@ export async function fetchListingPublishOptions() { return mergeListingPublishOptions(data.data) } +export async function fetchListingSalePriceConfig() { + const { data } = await apiClient.get>('/listing-sale-price-config') + return mergeListingSalePriceConfig(data.data) +} + export function mergeListingPublishOptions(options?: Partial): ListingPublishOptions { return { server_options: normalizeStringList(options?.server_options), @@ -152,10 +157,13 @@ export function mergeListingPublishOptions(options?: Partial): PublishSalePriceConfig { + return normalizeSalePriceConfig(options) +} + function normalizeStringList(values?: unknown[]) { return Array.isArray(values) ? values.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean) diff --git a/frontend/src/views/admin/AdminSystemConfigsView.vue b/frontend/src/views/admin/AdminSystemConfigsView.vue index 03e5de3..0cfaa2b 100644 --- a/frontend/src/views/admin/AdminSystemConfigsView.vue +++ b/frontend/src/views/admin/AdminSystemConfigsView.vue @@ -3,9 +3,12 @@ import { ElMessage } from 'element-plus' import { computed, onMounted, ref } from 'vue' import { + emptyListingSalePriceConfig, emptyListingPublishOptions, + mergeListingSalePriceConfig, mergeListingPublishOptions, type ListingPublishOptions, + type PublishSalePriceConfig, } from '@/api/listingOptions' import { defaultHomeAnnouncements, @@ -24,22 +27,26 @@ const activeConfig = ref(null) const value = ref('') const description = ref('') const publishOptionsDraft = ref(cloneOptions(emptyListingPublishOptions)) +const salePriceConfigDraft = ref(cloneSalePriceConfig(emptyListingSalePriceConfig)) const homeAnnouncementLines = ref(itemsToLines(defaultHomeAnnouncements)) const homeBannersDraft = ref(cloneHomeBanners(defaultHomeBanners)) const uploadingBannerIndex = ref(null) const publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || null) +const salePriceConfig = computed(() => configs.value.find((item) => item.key === 'listing.sale_price_config') || null) const homeAnnouncementsConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_announcements') || null) const homeBannersConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_banners') || null) const regularConfigs = computed(() => configs.value.filter( (item) => item.key !== 'listing.publish_options' && + item.key !== 'listing.sale_price_config' && item.key !== 'mobile.home_announcements' && item.key !== 'mobile.home_banners', ), ) const isPublishOptionsConfig = computed(() => activeConfig.value?.key === 'listing.publish_options') +const isSalePriceConfig = computed(() => activeConfig.value?.key === 'listing.sale_price_config') const isHomeAnnouncementsConfig = computed(() => activeConfig.value?.key === 'mobile.home_announcements') const isHomeBannersConfig = computed(() => activeConfig.value?.key === 'mobile.home_banners') const publishStats = computed(() => { @@ -54,9 +61,13 @@ const publishStats = computed(() => { resourceCount: options.quantity_items.length, screenshotCount: options.screenshot_slots.length, regionCount: options.region_options.length, - saleRuleCount: - options.sale_price_config.fixed_markup_rules.length + - options.sale_price_config.ratio_adjustment_rules.length, + } +}) +const salePriceStats = computed(() => { + const config = safeParseSalePriceConfig(salePriceConfig.value?.value || '') + return { + fixedCount: config.fixed_markup_rules.length, + ratioCount: config.ratio_adjustment_rules.length, } }) const homeStats = computed(() => { @@ -72,6 +83,7 @@ const isStructuredConfig = computed(() => { const trimmed = value.value.trim() return ( isPublishOptionsConfig.value || + isSalePriceConfig.value || isHomeAnnouncementsConfig.value || isHomeBannersConfig.value || trimmed.startsWith('{') || @@ -80,7 +92,7 @@ const isStructuredConfig = computed(() => { }) const dialogWidth = computed(() => { - if (isPublishOptionsConfig.value || isHomeBannersConfig.value) return '920px' + if (isPublishOptionsConfig.value || isSalePriceConfig.value || isHomeBannersConfig.value) return '920px' if (isHomeAnnouncementsConfig.value) return '680px' return '560px' }) @@ -103,6 +115,9 @@ function openEdit(row: SystemConfig) { if (row.key === 'listing.publish_options') { publishOptionsDraft.value = parsePublishOptions(row.value) } + if (row.key === 'listing.sale_price_config') { + salePriceConfigDraft.value = parseSalePriceConfig(row.value) + } if (row.key === 'mobile.home_announcements') { homeAnnouncementLines.value = itemsToLines(parseHomeAnnouncements(row.value, true)) } @@ -118,6 +133,9 @@ async function handleSave() { if (isPublishOptionsConfig.value) { value.value = JSON.stringify(publishOptionsDraft.value, null, 2) } + if (isSalePriceConfig.value) { + value.value = JSON.stringify(salePriceConfigDraft.value, null, 2) + } if (isHomeAnnouncementsConfig.value) { value.value = JSON.stringify(linesToItems(homeAnnouncementLines.value), null, 2) } @@ -161,6 +179,25 @@ function safeParsePublishOptions(raw: string) { } } +function parseSalePriceConfig(raw: string) { + try { + const parsed = raw.trim() ? JSON.parse(raw) : emptyListingSalePriceConfig + return cloneSalePriceConfig(mergeListingSalePriceConfig(parsed)) + } catch { + ElMessage.warning('出售定价规则 JSON 解析失败,已清空草稿') + return cloneSalePriceConfig(emptyListingSalePriceConfig) + } +} + +function safeParseSalePriceConfig(raw: string) { + try { + const parsed = raw.trim() ? JSON.parse(raw) : emptyListingSalePriceConfig + return cloneSalePriceConfig(mergeListingSalePriceConfig(parsed)) + } catch { + return cloneSalePriceConfig(emptyListingSalePriceConfig) + } +} + function parseHomeAnnouncements(raw: string, showWarning = false) { try { const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeAnnouncements @@ -185,6 +222,10 @@ function cloneOptions(options: ListingPublishOptions) { return JSON.parse(JSON.stringify(options)) as ListingPublishOptions } +function cloneSalePriceConfig(config: PublishSalePriceConfig) { + return JSON.parse(JSON.stringify(config)) as PublishSalePriceConfig +} + function cloneHomeBanners(banners: HomeBannerSlide[]) { return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[] } @@ -285,7 +326,7 @@ function removeCoinCorrection(index: number) { } function addSaleFixedMarkupRule() { - publishOptionsDraft.value.sale_price_config.fixed_markup_rules.push({ + salePriceConfigDraft.value.fixed_markup_rules.push({ min_m: 0, max_m: 0, markup_amount: 0, @@ -293,11 +334,11 @@ function addSaleFixedMarkupRule() { } function removeSaleFixedMarkupRule(index: number) { - publishOptionsDraft.value.sale_price_config.fixed_markup_rules.splice(index, 1) + salePriceConfigDraft.value.fixed_markup_rules.splice(index, 1) } function addSaleRatioAdjustmentRule() { - publishOptionsDraft.value.sale_price_config.ratio_adjustment_rules.push({ + salePriceConfigDraft.value.ratio_adjustment_rules.push({ min_m: 0, max_m: 0, ratio_subtract: 0, @@ -305,7 +346,7 @@ function addSaleRatioAdjustmentRule() { } function removeSaleRatioAdjustmentRule(index: number) { - publishOptionsDraft.value.sale_price_config.ratio_adjustment_rules.splice(index, 1) + salePriceConfigDraft.value.ratio_adjustment_rules.splice(index, 1) } function addHomeBanner() { @@ -414,9 +455,38 @@ function readError(error: unknown, fallback: string) { {{ publishStats.regionCount }} 地区 + + + +
+
+
+

Sale Pricing

+

内部出售定价规则

+ 管理出售加价和比例扣减规则,仅用于平台内部定价计算,不在发布页展示。 +
+ 编辑出售规则 +
+
- {{ publishStats.saleRuleCount }} - 出售规则 + {{ salePriceStats.fixedCount }} + 固定加价 +
+
+ {{ salePriceStats.ratioCount }} + 比例扣减 +
+
+ 内部 + 不展示给发布用户 +
+
+ 配置 + listing.sale_price_config +
+
+ 更新 + {{ formatHomeConfigStatus(salePriceConfig, '未初始化') }}
@@ -673,53 +743,6 @@ function readError(error: unknown, fallback: string) { -
-
- 出售加价规则 -
-
- 90M 及以下固定加价:出售价格 = 回收价格 + 固定加价 - 添加固定加价 -
- - - - - - - - - - - - - - - -
- 90M 以上比例调整:出售比例 = 回收比例 - 比例扣减;最大 M 为 0 表示无上限 - 添加比例调整 -
- - - - - - - - - - - - - - -
-
皮肤分类 @@ -792,6 +815,56 @@ function readError(error: unknown, fallback: string) {
+
+
+ 内部出售定价规则 +
+ +
+
+ 90M 及以下固定加价:出售价格 = 回收价格 + 固定加价 + 添加固定加价 +
+ + + + + + + + + + + + + + + +
+ 90M 以上比例调整:出售比例 = 回收比例 - 比例扣减;最大 M 为 0 表示无上限 + 添加比例调整 +
+ + + + + + + + + + + + + + +
+
+
首页公告 diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.css b/frontend/src/views/mobile/MobileSellerListingCreateView.css index 08e98a3..4ec19dd 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.css +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.css @@ -498,12 +498,6 @@ font-weight: 800; } -.muted-result-field :deep(.van-field__control) { - color: #5c6470; - font-size: 12px; - font-weight: 700; -} - .total-price-field :deep(.van-field__control) { font-size: 16px; } diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.vue b/frontend/src/views/mobile/MobileSellerListingCreateView.vue index a1fd1ad..255f63d 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.vue +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.vue @@ -5,10 +5,13 @@ import { showDialog, showToast } from "vant"; import { fetchFileBlobByURL, uploadFile } from "@/api/files"; import { + emptyListingSalePriceConfig, emptyListingPublishOptions, fetchListingPublishOptions, + fetchListingSalePriceConfig, type ChargeMode, type ListingPublishOptions, + type PublishSalePriceConfig, type QuantityKey, type ScreenshotKey, } from "@/api/listingOptions"; @@ -21,6 +24,8 @@ type PublishForm = { rank_level: string; secret_kd: string; fire_level: number | ""; + daily_loss_m: number | ""; + accelerated_sale_ratio: number | ""; season_insurance: string; stamina_level: string; load_level: string; @@ -42,12 +47,14 @@ interface PublishDraft { } const draftKey = "hfb.mobile.publish.draft"; +const dailyLossOptions = [10, 20, 30, 40, 50]; const router = useRouter(); const route = useRoute(); const loading = ref(false); const uploading = ref(false); const suppressDraftSave = ref(false); const publishOptions = ref(emptyListingPublishOptions); +const salePriceConfig = ref(emptyListingSalePriceConfig); function isNavActive(path: string) { if (path === "/m") return route.path === "/m"; @@ -62,6 +69,8 @@ function defaultForm(): PublishForm { rank_level: "", secret_kd: "", fire_level: "", + daily_loss_m: 10, + accelerated_sale_ratio: "", season_insurance: "", stamina_level: "", load_level: "", @@ -117,7 +126,6 @@ const fireLevelPlaceholder = computed( () => `等级低于${fireLevelMin.value}级的号无法发布` ); const ratioConfig = computed(() => publishOptions.value.ratio_config); -const salePriceConfig = computed(() => publishOptions.value.sale_price_config); const skinGroups = computed(() => publishOptions.value.skin_groups); const quantityItems = computed(() => publishOptions.value.quantity_items); const screenshotSlots = computed(() => publishOptions.value.screenshot_slots); @@ -128,11 +136,26 @@ const screenshotUrls = computed(() => ); const coinMAmount = computed(() => Number(form.haf_coin_amount || 0)); const coinWanAmount = computed(() => coinMAmount.value * 100); +const dailyLossMAmount = computed(() => Number(form.daily_loss_m || 10)); +const dailyLossRatioAdjustment = computed(() => + Math.min(Math.max(Math.floor((dailyLossMAmount.value - 10) / 10), 0), 4) +); const calculatedRecycleRatio = computed(() => calculatePublishRatio()); const calculatedSalePricing = computed(() => calculateSalePricing()); -const calculatedRatio = computed(() => calculatedSalePricing.value.saleRatio); -const calculatedCoinBasePrice = computed(() => calculatedSalePricing.value.coinBasePrice); -const calculatedSaleRuleText = computed(() => calculatedSalePricing.value.ruleText); +const calculatedDefaultSaleRatio = computed(() => calculatedSalePricing.value.saleRatio); +const maxAcceleratedSaleRatio = computed(() => + calculatedDefaultSaleRatio.value > 0 ? roundRatio(calculatedDefaultSaleRatio.value + 10) : 0 +); +const calculatedRatio = computed(() => readFinalSaleRatio()); +const calculatedCoinBasePrice = computed(() => { + if (calculatedRatio.value <= 0) return 0; + if (!hasAcceleratedSaleRatioInput()) return calculatedSalePricing.value.coinBasePrice; + return roundMoney(coinWanAmount.value / calculatedRatio.value); +}); +const acceleratedSaleRatioPlaceholder = computed(() => { + if (calculatedDefaultSaleRatio.value <= 0) return "填写资料后自动生成可设置范围"; + return `默认 ${formatNumber(calculatedDefaultSaleRatio.value)},最高 ${formatNumber(maxAcceleratedSaleRatio.value)}`; +}); const calculatedConsumablePrice = computed(() => calculateConsumablePrice()); const calculatedFinalPrice = computed(() => calculatedRatio.value > 0 @@ -162,9 +185,15 @@ watch( async function loadPublishOptions() { try { - publishOptions.value = await fetchListingPublishOptions(); + const [nextPublishOptions, nextSalePriceConfig] = await Promise.all([ + fetchListingPublishOptions(), + fetchListingSalePriceConfig(), + ]); + publishOptions.value = nextPublishOptions; + salePriceConfig.value = nextSalePriceConfig; } catch { publishOptions.value = emptyListingPublishOptions; + salePriceConfig.value = emptyListingSalePriceConfig; } } @@ -353,6 +382,27 @@ function handleFireLevelInput(value: string | number) { form.fire_level = Math.trunc(level); } +function handleAcceleratedSaleRatioInput(value: string | number) { + if (value === "") { + form.accelerated_sale_ratio = ""; + return; + } + const ratio = Number(value); + form.accelerated_sale_ratio = Number.isFinite(ratio) ? ratio : ""; +} + +function clampAcceleratedSaleRatioInput() { + if (!hasAcceleratedSaleRatioInput() || calculatedDefaultSaleRatio.value <= 0) return; + const ratio = Number(form.accelerated_sale_ratio); + if (!Number.isFinite(ratio)) { + form.accelerated_sale_ratio = ""; + return; + } + const minRatio = calculatedDefaultSaleRatio.value; + const maxRatio = maxAcceleratedSaleRatio.value; + form.accelerated_sale_ratio = roundRatio(Math.min(Math.max(ratio, minRatio), maxRatio)); +} + async function handleSubmit() { const error = validateForm(); if (error) { @@ -409,6 +459,17 @@ function validateForm() { if (!form.season_insurance) return "请选择赛季保险"; if (!form.stamina_level) return "请选择体力等级"; if (!form.load_level) return "请选择负重等级"; + if (!dailyLossOptions.includes(dailyLossMAmount.value)) return "请选择每日损耗"; + if (hasAcceleratedSaleRatioInput()) { + const ratio = Number(form.accelerated_sale_ratio); + if (!Number.isFinite(ratio) || ratio <= 0) return "加速出售比例格式不正确"; + if (calculatedDefaultSaleRatio.value > 0 && ratio < calculatedDefaultSaleRatio.value) { + return `加速出售比例不能低于默认比例 1:${formatNumber(calculatedDefaultSaleRatio.value)}`; + } + if (maxAcceleratedSaleRatio.value > 0 && ratio > maxAcceleratedSaleRatio.value) { + return `加速出售比例不能超过 1:${formatNumber(maxAcceleratedSaleRatio.value)}`; + } + } if (banRecordOptions.value.length && !form.ban_record) return "请选择封禁记录"; if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) { return "请填写押金"; @@ -443,16 +504,20 @@ function buildAssetSummary() { face_owner: form.face_owner, secret_kd: form.secret_kd, fire_level: Number(form.fire_level), + daily_loss_m: dailyLossMAmount.value, publish_ratio: calculatedRatio.value, price_breakdown: { recycle_coin_base_price: calculatedSalePricing.value.recycleCoinBasePrice, + default_coin_base_price: calculatedSalePricing.value.coinBasePrice, coin_base_price: calculatedCoinBasePrice.value, consumable_price: calculatedConsumablePrice.value, final_price: calculatedFinalPrice.value, recycle_ratio: calculatedRecycleRatio.value, + daily_loss_ratio_adjustment: dailyLossRatioAdjustment.value, + default_sale_ratio: calculatedDefaultSaleRatio.value, sale_ratio: calculatedRatio.value, + accelerated_sale_ratio: hasAcceleratedSaleRatioInput() ? Number(form.accelerated_sale_ratio) : calculatedDefaultSaleRatio.value, sale_rule_type: calculatedSalePricing.value.ruleType, - sale_rule_text: calculatedSalePricing.value.ruleText, }, season_insurance: form.season_insurance, stamina_level: form.stamina_level, @@ -518,7 +583,25 @@ function calculatePublishRatio() { const baseRatio = getInsuranceBaseRatio(form.season_insurance); if (baseRatio <= 0) return 0; - return baseRatio + calculateConfigPenalty() + getCoinCorrection(coinMAmount.value); + return ( + baseRatio + + calculateConfigPenalty() + + getCoinCorrection(coinMAmount.value) + + dailyLossRatioAdjustment.value + ); +} + +function readFinalSaleRatio() { + const defaultRatio = calculatedDefaultSaleRatio.value; + if (defaultRatio <= 0) return 0; + if (!hasAcceleratedSaleRatioInput()) return defaultRatio; + const ratio = Number(form.accelerated_sale_ratio); + if (!Number.isFinite(ratio) || ratio <= 0) return defaultRatio; + return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio.value)); +} + +function hasAcceleratedSaleRatioInput() { + return form.accelerated_sale_ratio !== "" && form.accelerated_sale_ratio !== null; } function calculateSalePricing() { @@ -535,7 +618,6 @@ function calculateSalePricing() { coinBasePrice, saleRatio: calculateEffectiveRatio(coinBasePrice), ruleType: "fixed_markup", - ruleText: `${formatSaleRange(fixedRule)} 加价 ${formatNumber(fixedRule.markup_amount)}元`, }; } @@ -550,9 +632,6 @@ function calculateSalePricing() { coinBasePrice: roundMoney(coinWanAmount.value / saleRatio), saleRatio, ruleType: ratioRule ? "ratio_subtract" : "none", - ruleText: ratioRule - ? `${formatSaleRange(ratioRule)} 出售比例=回收比例-${formatNumber(ratioSubtract)}` - : "未匹配出售加价规则,按回收比例计算", }; } @@ -562,7 +641,6 @@ function emptySalePricing(recycleCoinBasePrice = 0) { coinBasePrice: 0, saleRatio: 0, ruleType: "none", - ruleText: "", }; } @@ -585,13 +663,7 @@ function isCoinInSaleRange(item: { min_m: number; max_m: number }) { function calculateEffectiveRatio(price: number) { if (price <= 0) return 0; - return Math.round((coinWanAmount.value / price) * 10) / 10; -} - -function formatSaleRange(item: { min_m: number; max_m: number }) { - const minM = formatNumber(Number(item.min_m || 0)); - const maxM = Number(item.max_m || 0); - return maxM > 0 ? `${minM}M-${formatNumber(maxM)}M` : `${minM}M以上`; + return roundRatio(coinWanAmount.value / price); } function getInsuranceBaseRatio(insurance: string) { @@ -641,6 +713,10 @@ function formatNumber(value: number) { return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`; } +function roundRatio(value: number) { + return Math.round(value * 10) / 10; +} + function readError(error: unknown, fallback: string) { if (typeof error === "object" && error && "response" in error) { const response = (error as { response?: { data?: { message?: string } } }) @@ -997,6 +1073,37 @@ function readError(error: unknown, fallback: string) { 押金 + + + +

+ 比例调整:在默认10M/天,每增加10M,出租比例相应+1,请根据实际需求合理设置,感谢您的配合。 +

+ +

+ 请根据实际需求合理设置,感谢您的配合。 +

出售比例 -

- 纯币基础价先按回收比例计算,再套出售加价规则;发布总价 = 纯币基础价 + 额外消耗品。 + 发布总价 = 纯币基础价 + 额外消耗品;加速比例仅影响纯币基础价。