内部加价分离, 不展示
This commit is contained in:
@@ -52,7 +52,6 @@ type PublishOptionsDTO struct {
|
|||||||
FireLevelMin int `json:"fire_level_min"`
|
FireLevelMin int `json:"fire_level_min"`
|
||||||
PriceConfig PublishPriceConfig `json:"price_config"`
|
PriceConfig PublishPriceConfig `json:"price_config"`
|
||||||
RatioConfig PublishRatioConfig `json:"ratio_config"`
|
RatioConfig PublishRatioConfig `json:"ratio_config"`
|
||||||
SalePriceConfig PublishSalePriceConfig `json:"sale_price_config"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublishOptionGroup struct {
|
type PublishOptionGroup struct {
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ func (h *Handler) PublishOptions(c *gin.Context) {
|
|||||||
response.OK(c, options)
|
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) {
|
func (h *Handler) HomeAnnouncements(c *gin.Context) {
|
||||||
announcements, err := h.service.HomeAnnouncements()
|
announcements, err := h.service.HomeAnnouncements()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -108,22 +108,5 @@ func DefaultPublishOptions() PublishOptionsDTO {
|
|||||||
{ThresholdM: 530, Correction: 5},
|
{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: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"},
|
||||||
{Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"},
|
{Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"},
|
||||||
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
||||||
|
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepository(db *gorm.DB) *Repository {
|
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
|
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) {
|
func (s *Service) publishOptions() (PublishOptionsDTO, error) {
|
||||||
value, err := s.repo.FindValue(publishOptionsConfigKey)
|
value, err := s.repo.FindValue(publishOptionsConfigKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,6 +61,19 @@ func (s *Service) publishOptions() (PublishOptionsDTO, error) {
|
|||||||
return options, nil
|
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) {
|
func (s *Service) HomeAnnouncements() (*HomeAnnouncementsDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
@@ -214,10 +238,14 @@ func normalizePublishOptions(options *PublishOptionsDTO) {
|
|||||||
if len(options.RatioConfig.CoinCorrections) == 0 {
|
if len(options.RatioConfig.CoinCorrections) == 0 {
|
||||||
options.RatioConfig.CoinCorrections = defaults.RatioConfig.CoinCorrections
|
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 {
|
if len(config.RatioAdjustmentRules) == 0 {
|
||||||
options.SalePriceConfig.RatioAdjustmentRules = defaults.SalePriceConfig.RatioAdjustmentRules
|
config.RatioAdjustmentRules = defaults.RatioAdjustmentRules
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
{
|
{
|
||||||
api.GET("/health", health.Check)
|
api.GET("/health", health.Check)
|
||||||
api.GET("/listing-publish-options", systemConfigHandler.PublishOptions)
|
api.GET("/listing-publish-options", systemConfigHandler.PublishOptions)
|
||||||
|
api.GET("/listing-sale-price-config", systemConfigHandler.SalePriceConfig)
|
||||||
api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements)
|
api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements)
|
||||||
api.GET("/mobile-home-config", systemConfigHandler.HomeConfig)
|
api.GET("/mobile-home-config", systemConfigHandler.HomeConfig)
|
||||||
api.GET("/public/files/object", fileHandler.PublicObject)
|
api.GET("/public/files/object", fileHandler.PublicObject)
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export interface ListingPublishOptions {
|
|||||||
fire_level_min: number
|
fire_level_min: number
|
||||||
price_config: PublishPriceConfig
|
price_config: PublishPriceConfig
|
||||||
ratio_config: PublishRatioConfig
|
ratio_config: PublishRatioConfig
|
||||||
sale_price_config: PublishSalePriceConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
@@ -124,10 +123,11 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
|
|||||||
config_items: [],
|
config_items: [],
|
||||||
coin_corrections: [],
|
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() {
|
export async function fetchListingPublishOptions() {
|
||||||
@@ -135,6 +135,11 @@ export async function fetchListingPublishOptions() {
|
|||||||
return mergeListingPublishOptions(data.data)
|
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 {
|
export function mergeListingPublishOptions(options?: Partial<ListingPublishOptions>): ListingPublishOptions {
|
||||||
return {
|
return {
|
||||||
server_options: normalizeStringList(options?.server_options),
|
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),
|
fire_level_min: readPositiveInteger(options?.fire_level_min, 38),
|
||||||
price_config: normalizePriceConfig(options?.price_config),
|
price_config: normalizePriceConfig(options?.price_config),
|
||||||
ratio_config: normalizeRatioConfig(options?.ratio_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[]) {
|
function normalizeStringList(values?: unknown[]) {
|
||||||
return Array.isArray(values)
|
return Array.isArray(values)
|
||||||
? values.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean)
|
? 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 { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
emptyListingSalePriceConfig,
|
||||||
emptyListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
|
mergeListingSalePriceConfig,
|
||||||
mergeListingPublishOptions,
|
mergeListingPublishOptions,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
|
type PublishSalePriceConfig,
|
||||||
} from '@/api/listingOptions'
|
} from '@/api/listingOptions'
|
||||||
import {
|
import {
|
||||||
defaultHomeAnnouncements,
|
defaultHomeAnnouncements,
|
||||||
@@ -24,22 +27,26 @@ const activeConfig = ref<SystemConfig | null>(null)
|
|||||||
const value = ref('')
|
const value = ref('')
|
||||||
const description = ref('')
|
const description = ref('')
|
||||||
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(emptyListingPublishOptions))
|
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(emptyListingPublishOptions))
|
||||||
|
const salePriceConfigDraft = ref<PublishSalePriceConfig>(cloneSalePriceConfig(emptyListingSalePriceConfig))
|
||||||
const homeAnnouncementLines = ref(itemsToLines(defaultHomeAnnouncements))
|
const homeAnnouncementLines = ref(itemsToLines(defaultHomeAnnouncements))
|
||||||
const homeBannersDraft = ref<HomeBannerSlide[]>(cloneHomeBanners(defaultHomeBanners))
|
const homeBannersDraft = ref<HomeBannerSlide[]>(cloneHomeBanners(defaultHomeBanners))
|
||||||
const uploadingBannerIndex = ref<number | null>(null)
|
const uploadingBannerIndex = ref<number | null>(null)
|
||||||
|
|
||||||
const publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || 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 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 homeBannersConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_banners') || null)
|
||||||
const regularConfigs = computed(() =>
|
const regularConfigs = computed(() =>
|
||||||
configs.value.filter(
|
configs.value.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key !== 'listing.publish_options' &&
|
item.key !== 'listing.publish_options' &&
|
||||||
|
item.key !== 'listing.sale_price_config' &&
|
||||||
item.key !== 'mobile.home_announcements' &&
|
item.key !== 'mobile.home_announcements' &&
|
||||||
item.key !== 'mobile.home_banners',
|
item.key !== 'mobile.home_banners',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const isPublishOptionsConfig = computed(() => activeConfig.value?.key === 'listing.publish_options')
|
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 isHomeAnnouncementsConfig = computed(() => activeConfig.value?.key === 'mobile.home_announcements')
|
||||||
const isHomeBannersConfig = computed(() => activeConfig.value?.key === 'mobile.home_banners')
|
const isHomeBannersConfig = computed(() => activeConfig.value?.key === 'mobile.home_banners')
|
||||||
const publishStats = computed(() => {
|
const publishStats = computed(() => {
|
||||||
@@ -54,9 +61,13 @@ const publishStats = computed(() => {
|
|||||||
resourceCount: options.quantity_items.length,
|
resourceCount: options.quantity_items.length,
|
||||||
screenshotCount: options.screenshot_slots.length,
|
screenshotCount: options.screenshot_slots.length,
|
||||||
regionCount: options.region_options.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(() => {
|
const homeStats = computed(() => {
|
||||||
@@ -72,6 +83,7 @@ const isStructuredConfig = computed(() => {
|
|||||||
const trimmed = value.value.trim()
|
const trimmed = value.value.trim()
|
||||||
return (
|
return (
|
||||||
isPublishOptionsConfig.value ||
|
isPublishOptionsConfig.value ||
|
||||||
|
isSalePriceConfig.value ||
|
||||||
isHomeAnnouncementsConfig.value ||
|
isHomeAnnouncementsConfig.value ||
|
||||||
isHomeBannersConfig.value ||
|
isHomeBannersConfig.value ||
|
||||||
trimmed.startsWith('{') ||
|
trimmed.startsWith('{') ||
|
||||||
@@ -80,7 +92,7 @@ const isStructuredConfig = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dialogWidth = 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'
|
if (isHomeAnnouncementsConfig.value) return '680px'
|
||||||
return '560px'
|
return '560px'
|
||||||
})
|
})
|
||||||
@@ -103,6 +115,9 @@ function openEdit(row: SystemConfig) {
|
|||||||
if (row.key === 'listing.publish_options') {
|
if (row.key === 'listing.publish_options') {
|
||||||
publishOptionsDraft.value = parsePublishOptions(row.value)
|
publishOptionsDraft.value = parsePublishOptions(row.value)
|
||||||
}
|
}
|
||||||
|
if (row.key === 'listing.sale_price_config') {
|
||||||
|
salePriceConfigDraft.value = parseSalePriceConfig(row.value)
|
||||||
|
}
|
||||||
if (row.key === 'mobile.home_announcements') {
|
if (row.key === 'mobile.home_announcements') {
|
||||||
homeAnnouncementLines.value = itemsToLines(parseHomeAnnouncements(row.value, true))
|
homeAnnouncementLines.value = itemsToLines(parseHomeAnnouncements(row.value, true))
|
||||||
}
|
}
|
||||||
@@ -118,6 +133,9 @@ async function handleSave() {
|
|||||||
if (isPublishOptionsConfig.value) {
|
if (isPublishOptionsConfig.value) {
|
||||||
value.value = JSON.stringify(publishOptionsDraft.value, null, 2)
|
value.value = JSON.stringify(publishOptionsDraft.value, null, 2)
|
||||||
}
|
}
|
||||||
|
if (isSalePriceConfig.value) {
|
||||||
|
value.value = JSON.stringify(salePriceConfigDraft.value, null, 2)
|
||||||
|
}
|
||||||
if (isHomeAnnouncementsConfig.value) {
|
if (isHomeAnnouncementsConfig.value) {
|
||||||
value.value = JSON.stringify(linesToItems(homeAnnouncementLines.value), null, 2)
|
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) {
|
function parseHomeAnnouncements(raw: string, showWarning = false) {
|
||||||
try {
|
try {
|
||||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeAnnouncements
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeAnnouncements
|
||||||
@@ -185,6 +222,10 @@ function cloneOptions(options: ListingPublishOptions) {
|
|||||||
return JSON.parse(JSON.stringify(options)) as 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[]) {
|
function cloneHomeBanners(banners: HomeBannerSlide[]) {
|
||||||
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
|
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
|
||||||
}
|
}
|
||||||
@@ -285,7 +326,7 @@ function removeCoinCorrection(index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addSaleFixedMarkupRule() {
|
function addSaleFixedMarkupRule() {
|
||||||
publishOptionsDraft.value.sale_price_config.fixed_markup_rules.push({
|
salePriceConfigDraft.value.fixed_markup_rules.push({
|
||||||
min_m: 0,
|
min_m: 0,
|
||||||
max_m: 0,
|
max_m: 0,
|
||||||
markup_amount: 0,
|
markup_amount: 0,
|
||||||
@@ -293,11 +334,11 @@ function addSaleFixedMarkupRule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeSaleFixedMarkupRule(index: number) {
|
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() {
|
function addSaleRatioAdjustmentRule() {
|
||||||
publishOptionsDraft.value.sale_price_config.ratio_adjustment_rules.push({
|
salePriceConfigDraft.value.ratio_adjustment_rules.push({
|
||||||
min_m: 0,
|
min_m: 0,
|
||||||
max_m: 0,
|
max_m: 0,
|
||||||
ratio_subtract: 0,
|
ratio_subtract: 0,
|
||||||
@@ -305,7 +346,7 @@ function addSaleRatioAdjustmentRule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeSaleRatioAdjustmentRule(index: number) {
|
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() {
|
function addHomeBanner() {
|
||||||
@@ -414,9 +455,38 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<strong>{{ publishStats.regionCount }}</strong>
|
<strong>{{ publishStats.regionCount }}</strong>
|
||||||
<span>地区</span>
|
<span>地区</span>
|
||||||
</div>
|
</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">
|
<div class="publish-stat">
|
||||||
<strong>{{ publishStats.saleRuleCount }}</strong>
|
<strong>{{ salePriceStats.fixedCount }}</strong>
|
||||||
<span>出售规则</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -673,53 +743,6 @@ function readError(error: unknown, fallback: string) {
|
|||||||
|
|
||||||
</div>
|
</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">
|
||||||
<div class="editor-block-title">
|
<div class="editor-block-title">
|
||||||
<strong>皮肤分类</strong>
|
<strong>皮肤分类</strong>
|
||||||
@@ -792,6 +815,56 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</div>
|
</div>
|
||||||
</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 v-else-if="isHomeAnnouncementsConfig" class="home-config-editor">
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<span>首页公告</span>
|
<span>首页公告</span>
|
||||||
|
|||||||
@@ -498,12 +498,6 @@
|
|||||||
font-weight: 800;
|
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) {
|
.total-price-field :deep(.van-field__control) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import { showDialog, showToast } from "vant";
|
|||||||
|
|
||||||
import { fetchFileBlobByURL, uploadFile } from "@/api/files";
|
import { fetchFileBlobByURL, uploadFile } from "@/api/files";
|
||||||
import {
|
import {
|
||||||
|
emptyListingSalePriceConfig,
|
||||||
emptyListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
fetchListingPublishOptions,
|
fetchListingPublishOptions,
|
||||||
|
fetchListingSalePriceConfig,
|
||||||
type ChargeMode,
|
type ChargeMode,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
|
type PublishSalePriceConfig,
|
||||||
type QuantityKey,
|
type QuantityKey,
|
||||||
type ScreenshotKey,
|
type ScreenshotKey,
|
||||||
} from "@/api/listingOptions";
|
} from "@/api/listingOptions";
|
||||||
@@ -21,6 +24,8 @@ type PublishForm = {
|
|||||||
rank_level: string;
|
rank_level: string;
|
||||||
secret_kd: string;
|
secret_kd: string;
|
||||||
fire_level: number | "";
|
fire_level: number | "";
|
||||||
|
daily_loss_m: number | "";
|
||||||
|
accelerated_sale_ratio: number | "";
|
||||||
season_insurance: string;
|
season_insurance: string;
|
||||||
stamina_level: string;
|
stamina_level: string;
|
||||||
load_level: string;
|
load_level: string;
|
||||||
@@ -42,12 +47,14 @@ interface PublishDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const draftKey = "hfb.mobile.publish.draft";
|
const draftKey = "hfb.mobile.publish.draft";
|
||||||
|
const dailyLossOptions = [10, 20, 30, 40, 50];
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
const suppressDraftSave = ref(false);
|
const suppressDraftSave = ref(false);
|
||||||
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
||||||
|
const salePriceConfig = ref<PublishSalePriceConfig>(emptyListingSalePriceConfig);
|
||||||
|
|
||||||
function isNavActive(path: string) {
|
function isNavActive(path: string) {
|
||||||
if (path === "/m") return route.path === "/m";
|
if (path === "/m") return route.path === "/m";
|
||||||
@@ -62,6 +69,8 @@ function defaultForm(): PublishForm {
|
|||||||
rank_level: "",
|
rank_level: "",
|
||||||
secret_kd: "",
|
secret_kd: "",
|
||||||
fire_level: "",
|
fire_level: "",
|
||||||
|
daily_loss_m: 10,
|
||||||
|
accelerated_sale_ratio: "",
|
||||||
season_insurance: "",
|
season_insurance: "",
|
||||||
stamina_level: "",
|
stamina_level: "",
|
||||||
load_level: "",
|
load_level: "",
|
||||||
@@ -117,7 +126,6 @@ const fireLevelPlaceholder = computed(
|
|||||||
() => `等级低于${fireLevelMin.value}级的号无法发布`
|
() => `等级低于${fireLevelMin.value}级的号无法发布`
|
||||||
);
|
);
|
||||||
const ratioConfig = computed(() => publishOptions.value.ratio_config);
|
const ratioConfig = computed(() => publishOptions.value.ratio_config);
|
||||||
const salePriceConfig = computed(() => publishOptions.value.sale_price_config);
|
|
||||||
const skinGroups = computed(() => publishOptions.value.skin_groups);
|
const skinGroups = computed(() => publishOptions.value.skin_groups);
|
||||||
const quantityItems = computed(() => publishOptions.value.quantity_items);
|
const quantityItems = computed(() => publishOptions.value.quantity_items);
|
||||||
const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
|
const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
|
||||||
@@ -128,11 +136,26 @@ const screenshotUrls = computed(() =>
|
|||||||
);
|
);
|
||||||
const coinMAmount = computed(() => Number(form.haf_coin_amount || 0));
|
const coinMAmount = computed(() => Number(form.haf_coin_amount || 0));
|
||||||
const coinWanAmount = computed(() => coinMAmount.value * 100);
|
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 calculatedRecycleRatio = computed(() => calculatePublishRatio());
|
||||||
const calculatedSalePricing = computed(() => calculateSalePricing());
|
const calculatedSalePricing = computed(() => calculateSalePricing());
|
||||||
const calculatedRatio = computed(() => calculatedSalePricing.value.saleRatio);
|
const calculatedDefaultSaleRatio = computed(() => calculatedSalePricing.value.saleRatio);
|
||||||
const calculatedCoinBasePrice = computed(() => calculatedSalePricing.value.coinBasePrice);
|
const maxAcceleratedSaleRatio = computed(() =>
|
||||||
const calculatedSaleRuleText = computed(() => calculatedSalePricing.value.ruleText);
|
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 calculatedConsumablePrice = computed(() => calculateConsumablePrice());
|
||||||
const calculatedFinalPrice = computed(() =>
|
const calculatedFinalPrice = computed(() =>
|
||||||
calculatedRatio.value > 0
|
calculatedRatio.value > 0
|
||||||
@@ -162,9 +185,15 @@ watch(
|
|||||||
|
|
||||||
async function loadPublishOptions() {
|
async function loadPublishOptions() {
|
||||||
try {
|
try {
|
||||||
publishOptions.value = await fetchListingPublishOptions();
|
const [nextPublishOptions, nextSalePriceConfig] = await Promise.all([
|
||||||
|
fetchListingPublishOptions(),
|
||||||
|
fetchListingSalePriceConfig(),
|
||||||
|
]);
|
||||||
|
publishOptions.value = nextPublishOptions;
|
||||||
|
salePriceConfig.value = nextSalePriceConfig;
|
||||||
} catch {
|
} catch {
|
||||||
publishOptions.value = emptyListingPublishOptions;
|
publishOptions.value = emptyListingPublishOptions;
|
||||||
|
salePriceConfig.value = emptyListingSalePriceConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +382,27 @@ function handleFireLevelInput(value: string | number) {
|
|||||||
form.fire_level = Math.trunc(level);
|
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() {
|
async function handleSubmit() {
|
||||||
const error = validateForm();
|
const error = validateForm();
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -409,6 +459,17 @@ function validateForm() {
|
|||||||
if (!form.season_insurance) return "请选择赛季保险";
|
if (!form.season_insurance) return "请选择赛季保险";
|
||||||
if (!form.stamina_level) return "请选择体力等级";
|
if (!form.stamina_level) return "请选择体力等级";
|
||||||
if (!form.load_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 (banRecordOptions.value.length && !form.ban_record) return "请选择封禁记录";
|
||||||
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
|
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
|
||||||
return "请填写押金";
|
return "请填写押金";
|
||||||
@@ -443,16 +504,20 @@ function buildAssetSummary() {
|
|||||||
face_owner: form.face_owner,
|
face_owner: form.face_owner,
|
||||||
secret_kd: form.secret_kd,
|
secret_kd: form.secret_kd,
|
||||||
fire_level: Number(form.fire_level),
|
fire_level: Number(form.fire_level),
|
||||||
|
daily_loss_m: dailyLossMAmount.value,
|
||||||
publish_ratio: calculatedRatio.value,
|
publish_ratio: calculatedRatio.value,
|
||||||
price_breakdown: {
|
price_breakdown: {
|
||||||
recycle_coin_base_price: calculatedSalePricing.value.recycleCoinBasePrice,
|
recycle_coin_base_price: calculatedSalePricing.value.recycleCoinBasePrice,
|
||||||
|
default_coin_base_price: calculatedSalePricing.value.coinBasePrice,
|
||||||
coin_base_price: calculatedCoinBasePrice.value,
|
coin_base_price: calculatedCoinBasePrice.value,
|
||||||
consumable_price: calculatedConsumablePrice.value,
|
consumable_price: calculatedConsumablePrice.value,
|
||||||
final_price: calculatedFinalPrice.value,
|
final_price: calculatedFinalPrice.value,
|
||||||
recycle_ratio: calculatedRecycleRatio.value,
|
recycle_ratio: calculatedRecycleRatio.value,
|
||||||
|
daily_loss_ratio_adjustment: dailyLossRatioAdjustment.value,
|
||||||
|
default_sale_ratio: calculatedDefaultSaleRatio.value,
|
||||||
sale_ratio: calculatedRatio.value,
|
sale_ratio: calculatedRatio.value,
|
||||||
|
accelerated_sale_ratio: hasAcceleratedSaleRatioInput() ? Number(form.accelerated_sale_ratio) : calculatedDefaultSaleRatio.value,
|
||||||
sale_rule_type: calculatedSalePricing.value.ruleType,
|
sale_rule_type: calculatedSalePricing.value.ruleType,
|
||||||
sale_rule_text: calculatedSalePricing.value.ruleText,
|
|
||||||
},
|
},
|
||||||
season_insurance: form.season_insurance,
|
season_insurance: form.season_insurance,
|
||||||
stamina_level: form.stamina_level,
|
stamina_level: form.stamina_level,
|
||||||
@@ -518,7 +583,25 @@ function calculatePublishRatio() {
|
|||||||
const baseRatio = getInsuranceBaseRatio(form.season_insurance);
|
const baseRatio = getInsuranceBaseRatio(form.season_insurance);
|
||||||
if (baseRatio <= 0) return 0;
|
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() {
|
function calculateSalePricing() {
|
||||||
@@ -535,7 +618,6 @@ function calculateSalePricing() {
|
|||||||
coinBasePrice,
|
coinBasePrice,
|
||||||
saleRatio: calculateEffectiveRatio(coinBasePrice),
|
saleRatio: calculateEffectiveRatio(coinBasePrice),
|
||||||
ruleType: "fixed_markup",
|
ruleType: "fixed_markup",
|
||||||
ruleText: `${formatSaleRange(fixedRule)} 加价 ${formatNumber(fixedRule.markup_amount)}元`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,9 +632,6 @@ function calculateSalePricing() {
|
|||||||
coinBasePrice: roundMoney(coinWanAmount.value / saleRatio),
|
coinBasePrice: roundMoney(coinWanAmount.value / saleRatio),
|
||||||
saleRatio,
|
saleRatio,
|
||||||
ruleType: ratioRule ? "ratio_subtract" : "none",
|
ruleType: ratioRule ? "ratio_subtract" : "none",
|
||||||
ruleText: ratioRule
|
|
||||||
? `${formatSaleRange(ratioRule)} 出售比例=回收比例-${formatNumber(ratioSubtract)}`
|
|
||||||
: "未匹配出售加价规则,按回收比例计算",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +641,6 @@ function emptySalePricing(recycleCoinBasePrice = 0) {
|
|||||||
coinBasePrice: 0,
|
coinBasePrice: 0,
|
||||||
saleRatio: 0,
|
saleRatio: 0,
|
||||||
ruleType: "none",
|
ruleType: "none",
|
||||||
ruleText: "",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,13 +663,7 @@ function isCoinInSaleRange(item: { min_m: number; max_m: number }) {
|
|||||||
|
|
||||||
function calculateEffectiveRatio(price: number) {
|
function calculateEffectiveRatio(price: number) {
|
||||||
if (price <= 0) return 0;
|
if (price <= 0) return 0;
|
||||||
return Math.round((coinWanAmount.value / price) * 10) / 10;
|
return roundRatio(coinWanAmount.value / price);
|
||||||
}
|
|
||||||
|
|
||||||
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以上`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInsuranceBaseRatio(insurance: string) {
|
function getInsuranceBaseRatio(insurance: string) {
|
||||||
@@ -641,6 +713,10 @@ function formatNumber(value: number) {
|
|||||||
return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`;
|
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) {
|
function readError(error: unknown, fallback: string) {
|
||||||
if (typeof error === "object" && error && "response" in error) {
|
if (typeof error === "object" && error && "response" in error) {
|
||||||
const response = (error as { response?: { data?: { message?: string } } })
|
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>
|
<span class="hint-label" :data-hint="priceConfig.deposit_placeholder" tabindex="0">押金</span>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</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">
|
<div class="result-grid">
|
||||||
<van-field
|
<van-field
|
||||||
:model-value="calculatedRatioText"
|
: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>
|
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0">出售比例</span>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
<van-field
|
|
||||||
:model-value="calculatedSaleRuleText"
|
|
||||||
label="出售规则"
|
|
||||||
readonly
|
|
||||||
class="publish-field result-field muted-result-field"
|
|
||||||
/>
|
|
||||||
<van-field
|
<van-field
|
||||||
:model-value="calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : ''"
|
:model-value="calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : ''"
|
||||||
label="纯币基础价"
|
label="纯币基础价"
|
||||||
@@ -1043,7 +1144,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="price-breakdown-hint">
|
<p class="price-breakdown-hint">
|
||||||
纯币基础价先按回收比例计算,再套出售加价规则;发布总价 = 纯币基础价 + 额外消耗品。
|
发布总价 = 纯币基础价 + 额外消耗品;加速比例仅影响纯币基础价。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
|
|||||||
Reference in New Issue
Block a user