内部加价分离, 不展示

This commit is contained in:
yml2213
2026-05-23 21:07:40 +08:00
parent 9f15609afd
commit 78257097f1
11 changed files with 347 additions and 117 deletions
@@ -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 {
@@ -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 {
@@ -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},
},
},
}
}
@@ -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 {
@@ -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},
},
}
}
@@ -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
}
}
+1
View File
@@ -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)
+14 -6
View File
@@ -91,7 +91,6 @@ export interface ListingPublishOptions {
fire_level_min: number
price_config: PublishPriceConfig
ratio_config: PublishRatioConfig
sale_price_config: PublishSalePriceConfig
}
interface ApiResponse<T> {
@@ -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<ApiResponse<PublishSalePriceConfig>>('/listing-sale-price-config')
return mergeListingSalePriceConfig(data.data)
}
export function mergeListingPublishOptions(options?: Partial<ListingPublishOptions>): ListingPublishOptions {
return {
server_options: normalizeStringList(options?.server_options),
@@ -152,10 +157,13 @@ export function mergeListingPublishOptions(options?: Partial<ListingPublishOptio
fire_level_min: readPositiveInteger(options?.fire_level_min, 38),
price_config: normalizePriceConfig(options?.price_config),
ratio_config: normalizeRatioConfig(options?.ratio_config),
sale_price_config: normalizeSalePriceConfig(options?.sale_price_config),
}
}
export function mergeListingSalePriceConfig(options?: Partial<PublishSalePriceConfig>): PublishSalePriceConfig {
return normalizeSalePriceConfig(options)
}
function normalizeStringList(values?: unknown[]) {
return Array.isArray(values)
? values.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean)
@@ -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<SystemConfig | null>(null)
const value = ref('')
const description = ref('')
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(emptyListingPublishOptions))
const salePriceConfigDraft = ref<PublishSalePriceConfig>(cloneSalePriceConfig(emptyListingSalePriceConfig))
const homeAnnouncementLines = ref(itemsToLines(defaultHomeAnnouncements))
const homeBannersDraft = ref<HomeBannerSlide[]>(cloneHomeBanners(defaultHomeBanners))
const uploadingBannerIndex = ref<number | null>(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) {
<strong>{{ publishStats.regionCount }}</strong>
<span>地区</span>
</div>
</div>
</section>
<section v-if="salePriceConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Sale Pricing</p>
<h2>内部出售定价规则</h2>
<span>管理出售加价和比例扣减规则仅用于平台内部定价计算不在发布页展示</span>
</div>
<el-button type="primary" @click="openEdit(salePriceConfig)">编辑出售规则</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ publishStats.saleRuleCount }}</strong>
<span>出售规则</span>
<strong>{{ salePriceStats.fixedCount }}</strong>
<span>固定加价</span>
</div>
<div class="publish-stat">
<strong>{{ salePriceStats.ratioCount }}</strong>
<span>比例扣减</span>
</div>
<div class="publish-stat">
<strong>内部</strong>
<span>不展示给发布用户</span>
</div>
<div class="publish-stat">
<strong>配置</strong>
<span>listing.sale_price_config</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(salePriceConfig, '未初始化') }}</span>
</div>
</div>
</section>
@@ -673,53 +743,6 @@ function readError(error: unknown, fallback: string) {
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>出售加价规则</strong>
</div>
<div class="editor-block-title subtle-title">
<span>90M 及以下固定加价出售价格 = 回收价格 + 固定加价</span>
<el-button size="small" @click="addSaleFixedMarkupRule">添加固定加价</el-button>
</div>
<el-table :data="publishOptionsDraft.sale_price_config.fixed_markup_rules" size="small" border>
<el-table-column label="最小 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="最大 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="加价金额/元" min-width="140">
<template #default="{ row }"><el-input-number v-model="row.markup_amount" :min="0" :step="1" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeSaleFixedMarkupRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="editor-block-title subtle-title">
<span>90M 以上比例调整出售比例 = 回收比例 - 比例扣减最大 M 0 表示无上限</span>
<el-button size="small" @click="addSaleRatioAdjustmentRule">添加比例调整</el-button>
</div>
<el-table :data="publishOptionsDraft.sale_price_config.ratio_adjustment_rules" size="small" border>
<el-table-column label="最小 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="最大 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="比例扣减" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.ratio_subtract" :min="0" :step="0.5" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeSaleRatioAdjustmentRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>皮肤分类</strong>
@@ -792,6 +815,56 @@ function readError(error: unknown, fallback: string) {
</div>
</div>
<div v-else-if="isSalePriceConfig" class="publish-options-editor">
<div class="editor-toolbar">
<span>内部出售定价规则</span>
</div>
<div class="editor-block">
<div class="editor-block-title subtle-title">
<span>90M 及以下固定加价出售价格 = 回收价格 + 固定加价</span>
<el-button size="small" @click="addSaleFixedMarkupRule">添加固定加价</el-button>
</div>
<el-table :data="salePriceConfigDraft.fixed_markup_rules" size="small" border>
<el-table-column label="最小 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="最大 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="加价金额/元" min-width="140">
<template #default="{ row }"><el-input-number v-model="row.markup_amount" :min="0" :step="1" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeSaleFixedMarkupRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="editor-block-title subtle-title">
<span>90M 以上比例调整出售比例 = 回收比例 - 比例扣减最大 M 0 表示无上限</span>
<el-button size="small" @click="addSaleRatioAdjustmentRule">添加比例调整</el-button>
</div>
<el-table :data="salePriceConfigDraft.ratio_adjustment_rules" size="small" border>
<el-table-column label="最小 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="最大 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="比例扣减" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.ratio_subtract" :min="0" :step="0.5" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeSaleRatioAdjustmentRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-else-if="isHomeAnnouncementsConfig" class="home-config-editor">
<div class="editor-toolbar">
<span>首页公告</span>
@@ -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;
}
@@ -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<ListingPublishOptions>(emptyListingPublishOptions);
const salePriceConfig = ref<PublishSalePriceConfig>(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) {
<span class="hint-label" :data-hint="priceConfig.deposit_placeholder" tabindex="0">押金</span>
</template>
</van-field>
<van-field label="每日损耗" required class="publish-field">
<template #input>
<div class="radio-group">
<button
v-for="loss in dailyLossOptions"
:key="loss"
type="button"
class="radio-btn"
:class="{ active: dailyLossMAmount === loss }"
@click="form.daily_loss_m = loss"
>
{{ loss }}M/
</button>
</div>
</template>
</van-field>
<p class="field-hint">
比例调整在默认10M/每增加10M出租比例相应+1请根据实际需求合理设置感谢您的配合
</p>
<van-field
:model-value="form.accelerated_sale_ratio"
label="加速出售比例"
type="number"
:placeholder="acceleratedSaleRatioPlaceholder"
class="publish-field"
@update:model-value="handleAcceleratedSaleRatioInput"
@blur="clampAcceleratedSaleRatioInput"
/>
<p class="field-hint">
请根据实际需求合理设置感谢您的配合
</p>
<div class="result-grid">
<van-field
:model-value="calculatedRatioText"
@@ -1009,12 +1116,6 @@ function readError(error: unknown, fallback: string) {
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0">出售比例</span>
</template>
</van-field>
<van-field
:model-value="calculatedSaleRuleText"
label="出售规则"
readonly
class="publish-field result-field muted-result-field"
/>
<van-field
:model-value="calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : ''"
label="纯币基础价"
@@ -1043,7 +1144,7 @@ function readError(error: unknown, fallback: string) {
/>
</div>
<p class="price-breakdown-hint">
纯币基础价先按回收比例计算再套出售加价规则发布总价 = 纯币基础价 + 额外消耗品
发布总价 = 纯币基础价 + 额外消耗品加速比例仅影响纯币基础价
</p>
<van-field