发布界面优化

This commit is contained in:
yml2213
2026-05-23 13:47:26 +08:00
parent d90f31360f
commit e6cc9f1255
12 changed files with 607 additions and 65 deletions
@@ -47,6 +47,10 @@ type PublishOptionsDTO struct {
SkinGroups []PublishOptionGroup `json:"skin_groups"`
QuantityItems []PublishQuantityItem `json:"quantity_items"`
ScreenshotSlots []PublishScreenshotSlot `json:"screenshot_slots"`
BanRecordOptions []string `json:"ban_record_options"`
BanEvidenceOptions []string `json:"ban_evidence_options"`
PriceConfig PublishPriceConfig `json:"price_config"`
RatioConfig PublishRatioConfig `json:"ratio_config"`
}
type PublishOptionGroup struct {
@@ -68,3 +72,39 @@ type PublishScreenshotSlot struct {
Required bool `json:"required"`
Hint string `json:"hint"`
}
type PublishPriceConfig struct {
DepositPlaceholder string `json:"deposit_placeholder"`
PricePlaceholder string `json:"price_placeholder"`
RatioDescription string `json:"ratio_description"`
}
type PublishRatioConfig struct {
InsuranceBaseRatios []PublishInsuranceBaseRatio `json:"insurance_base_ratios"`
ConfigItems []PublishRatioConfigItem `json:"config_items"`
CoinCorrections []PublishCoinCorrection `json:"coin_corrections"`
RentRules []PublishRentRule `json:"rent_rules"`
}
type PublishInsuranceBaseRatio struct {
Insurance string `json:"insurance"`
Ratio float64 `json:"ratio"`
}
type PublishRatioConfigItem struct {
Key string `json:"key"`
Label string `json:"label"`
Kind string `json:"kind"`
GroupKey string `json:"group_key,omitempty"`
MissingPenalty float64 `json:"missing_penalty"`
}
type PublishCoinCorrection struct {
ThresholdM float64 `json:"threshold_m"`
Correction float64 `json:"correction"`
}
type PublishRentRule struct {
ThresholdM float64 `json:"threshold_m"`
Days int `json:"days"`
}
@@ -76,8 +76,41 @@ func DefaultPublishOptions() PublishOptionsDTO {
{Key: "coin", Label: "纯币截图", Required: true, Hint: "请上传纯币截图(必传)"},
{Key: "gameId", Label: "游戏ID截图", Required: true, Hint: "请上传游戏ID截图(必传)"},
{Key: "totalAsset", Label: "总资产截图", Required: true, Hint: "请上传总资产截图(必传)"},
{Key: "tencentSecurity", Label: "腾讯安全中心截图", Required: true, Hint: "请上传腾讯安全中心截图(必传"},
{Key: "tencentSecurity", Label: "腾讯安全中心截图", Required: false, Hint: "有封禁记录时必传"},
{Key: "skin", Label: "皮肤截图", Required: false, Hint: "请上传皮肤截图(选传)"},
},
BanRecordOptions: []string{"无封禁记录", "有封禁记录"},
BanEvidenceOptions: []string{"有封禁记录"},
PriceConfig: PublishPriceConfig{
DepositPlaceholder: "温馨提示:本押金用于保障账号安全。若买家在租用期间,使用非纯币类道具/资源(如消耗品、材料等)或导致账号被封禁,相关损失将直接从押金中扣除进行赔付。押金过高会延长出租等待时间(请自行衡量)感谢理解。",
PricePlaceholder: "填写币数、保险、体力和负重后自动计算",
RatioDescription: "1:xx 表示 1 元人民币(RMB)等价兑换 xx 万哈夫币",
},
RatioConfig: PublishRatioConfig{
InsuranceBaseRatios: []PublishInsuranceBaseRatio{
{Insurance: "3*3", Ratio: 40},
{Insurance: "2*3", Ratio: 42},
{Insurance: "2*2", Ratio: 47},
{Insurance: "2*1", Ratio: 48},
},
ConfigItems: []PublishRatioConfigItem{
{Key: "operatorRed", Label: "干员红皮", Kind: "skin_group", GroupKey: "operatorRed", MissingPenalty: 1},
{Key: "melee", Label: "刀皮", Kind: "skin_group", GroupKey: "melee", MissingPenalty: 1},
{Key: "staminaMax", Label: "满体力", Kind: "max_stamina", MissingPenalty: 1},
{Key: "loadMax", Label: "满负重", Kind: "max_load", MissingPenalty: 1},
{Key: "weapon", Label: "砖皮", Kind: "skin_group", GroupKey: "weapon", MissingPenalty: 1},
},
CoinCorrections: []PublishCoinCorrection{
{ThresholdM: 130, Correction: 1},
{ThresholdM: 230, Correction: 2.5},
{ThresholdM: 380, Correction: 4},
{ThresholdM: 530, Correction: 5},
},
RentRules: []PublishRentRule{
{ThresholdM: 300, Days: 7},
{ThresholdM: 100, Days: 3},
{ThresholdM: 0, Days: 1},
},
},
}
}
@@ -148,12 +148,49 @@ func updateLegacyPublishOptions(tx *gorm.DB, item defaultConfig) error {
if err := tx.Where("`key` = ?", item.Key).First(&row).Error; err != nil {
return err
}
if !isLegacyPublishOptionsValue(row.Value) {
return nil
}
if isLegacyPublishOptionsValue(row.Value) {
row.Value = item.Value
row.Description = item.Description
return tx.Save(&row).Error
}
var options PublishOptionsDTO
if err := json.Unmarshal([]byte(row.Value), &options); err != nil {
row.Value = item.Value
row.Description = item.Description
return tx.Save(&row).Error
}
before := row.Value
isOldPublishOptionsSchema := !strings.Contains(before, `"ban_record_options"`)
normalizePublishOptions(&options)
if isOldPublishOptionsSchema {
applyConditionalBanEvidenceDefault(&options)
}
raw, err := json.Marshal(options)
if err != nil {
return err
}
row.Value = string(raw)
row.Description = item.Description
if row.Value == before {
return nil
}
return tx.Save(&row).Error
}
func applyConditionalBanEvidenceDefault(options *PublishOptionsDTO) {
defaults := DefaultPublishOptions()
defaultSlots := make(map[string]PublishScreenshotSlot, len(defaults.ScreenshotSlots))
for _, item := range defaults.ScreenshotSlots {
defaultSlots[item.Key] = item
}
for index, item := range options.ScreenshotSlots {
if item.Key != "tencentSecurity" {
continue
}
if next, ok := defaultSlots[item.Key]; ok {
options.ScreenshotSlots[index] = next
}
}
}
func isLegacyPublishOptionsValue(value string) bool {
@@ -187,4 +187,31 @@ func normalizePublishOptions(options *PublishOptionsDTO) {
if len(options.ScreenshotSlots) == 0 {
options.ScreenshotSlots = defaults.ScreenshotSlots
}
if len(options.BanRecordOptions) == 0 {
options.BanRecordOptions = defaults.BanRecordOptions
}
if len(options.BanEvidenceOptions) == 0 {
options.BanEvidenceOptions = defaults.BanEvidenceOptions
}
if options.PriceConfig.DepositPlaceholder == "" {
options.PriceConfig.DepositPlaceholder = defaults.PriceConfig.DepositPlaceholder
}
if options.PriceConfig.PricePlaceholder == "" {
options.PriceConfig.PricePlaceholder = defaults.PriceConfig.PricePlaceholder
}
if options.PriceConfig.RatioDescription == "" {
options.PriceConfig.RatioDescription = defaults.PriceConfig.RatioDescription
}
if len(options.RatioConfig.InsuranceBaseRatios) == 0 {
options.RatioConfig.InsuranceBaseRatios = defaults.RatioConfig.InsuranceBaseRatios
}
if len(options.RatioConfig.ConfigItems) == 0 {
options.RatioConfig.ConfigItems = defaults.RatioConfig.ConfigItems
}
if len(options.RatioConfig.CoinCorrections) == 0 {
options.RatioConfig.CoinCorrections = defaults.RatioConfig.CoinCorrections
}
if len(options.RatioConfig.RentRules) == 0 {
options.RatioConfig.RentRules = defaults.RatioConfig.RentRules
}
}
+47
View File
@@ -0,0 +1,47 @@
你这样配置,直接运行 `cloudflared tunnel run` 是**不能**成功把 `5173` 转发出去的。
虽然你用第一条命令把域名 `221329.cc.cd` 绑定到了你的隧道上,但第二条命令在执行时,**`--url` 参数会被直接忽略掉**。
---
### 为什么不行?
`cloudflared` 的设计逻辑是:
当你使用 **命名隧道(Named Tunnel,即你创建的 `mac-mini-tunnel`** 时,它**强制要求**使用本地的 `config.yml` 配置文件来管理转发规则(Ingress Rules),或者通过 Cloudflare 网页后台(Zero Trust Dashboard)来配置。
你在命令行里强行拼凑的 `--url http://localhost:5173` 只有在运行临时隧道(Quick Tunnels,不指定隧道名的那种)时才会生效。
---
#### 做法 B:正规搞法,用你的固定域名(使用本地配置文件)
如果你非要用 `221329.cc.cd` 这个域名,你需要两步:
**第一步**:在你的 `~/.cloudflared/` 目录下(或者当前执行目录下)新建一个 `config.yml` 文件,里面写入转发规则:
```yaml
tunnel: mac-mini-tunnel
credentials-file: /Users/你的用户名/.cloudflared/你的隧道ID.json
ingress:
- hostname: 221329.cc.cd
service: http://localhost:5173
- service: http_status:404
```
**第二步**:启动隧道时**不要**加 `--url`,直接让它读配置运行:
```bash
cloudflared tunnel run mac-mini-tunnel
```
cloudflared tunnel route dns mac-mini-tunnel 221329.cc.cd
cloudflared tunnel run --url http://localhost:5173 mac-mini-tunnel
+2
View File
@@ -2,6 +2,8 @@
整体的核心逻辑是:**账号底子越差(缺配置)或包里哈夫币越多,比例数字就越大($1:\text{数字}$,数字越大代表给客户的哈夫币越多,号越便宜、越容易租)。**
前台输入和展示统一使用 **M** 作为哈夫币单位:发布页填写 `100` 表示 `100M`。内部比例仍按“万”为计算单位,最终对用户展示为 `1M=¥xx`
以下是最终的完整逻辑链条:
---
+138
View File
@@ -28,6 +28,42 @@ export interface PublishScreenshotSlot {
hint: string
}
export interface PublishPriceConfig {
deposit_placeholder: string
price_placeholder: string
ratio_description: string
}
export interface PublishInsuranceBaseRatio {
insurance: string
ratio: number
}
export interface PublishRatioConfigItem {
key: string
label: string
kind: string
group_key?: string
missing_penalty: number
}
export interface PublishCoinCorrection {
threshold_m: number
correction: number
}
export interface PublishRentRule {
threshold_m: number
days: number
}
export interface PublishRatioConfig {
insurance_base_ratios: PublishInsuranceBaseRatio[]
config_items: PublishRatioConfigItem[]
coin_corrections: PublishCoinCorrection[]
rent_rules: PublishRentRule[]
}
export interface ListingPublishOptions {
server_options: string[]
face_options: string[]
@@ -39,6 +75,10 @@ export interface ListingPublishOptions {
skin_groups: PublishOptionGroup[]
quantity_items: PublishQuantityItem[]
screenshot_slots: PublishScreenshotSlot[]
ban_record_options: string[]
ban_evidence_options: string[]
price_config: PublishPriceConfig
ratio_config: PublishRatioConfig
}
interface ApiResponse<T> {
@@ -58,6 +98,19 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
skin_groups: [],
quantity_items: [],
screenshot_slots: [],
ban_record_options: [],
ban_evidence_options: [],
price_config: {
deposit_placeholder: '',
price_placeholder: '',
ratio_description: '',
},
ratio_config: {
insurance_base_ratios: [],
config_items: [],
coin_corrections: [],
rent_rules: [],
},
}
export async function fetchListingPublishOptions() {
@@ -77,6 +130,10 @@ export function mergeListingPublishOptions(options?: Partial<ListingPublishOptio
skin_groups: normalizeOptionGroups(options?.skin_groups),
quantity_items: normalizeQuantityItems(options?.quantity_items),
screenshot_slots: normalizeScreenshotSlots(options?.screenshot_slots),
ban_record_options: normalizeStringList(options?.ban_record_options),
ban_evidence_options: normalizeStringList(options?.ban_evidence_options),
price_config: normalizePriceConfig(options?.price_config),
ratio_config: normalizeRatioConfig(options?.ratio_config),
}
}
@@ -130,6 +187,87 @@ function normalizeScreenshotSlots(values?: unknown[]): PublishScreenshotSlot[] {
.filter((item) => item.key && item.label)
}
function normalizePriceConfig(value?: unknown): PublishPriceConfig {
const row = isRecord(value) ? value : {}
return {
deposit_placeholder: typeof row.deposit_placeholder === 'string' ? row.deposit_placeholder.trim() : '',
price_placeholder: typeof row.price_placeholder === 'string' ? row.price_placeholder.trim() : '',
ratio_description: typeof row.ratio_description === 'string' ? row.ratio_description.trim() : '',
}
}
function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
const row = isRecord(value) ? value : {}
return {
insurance_base_ratios: normalizeInsuranceBaseRatios(
Array.isArray(row.insurance_base_ratios) ? row.insurance_base_ratios : [],
),
config_items: normalizeRatioConfigItems(
Array.isArray(row.config_items) ? row.config_items : [],
),
coin_corrections: normalizeCoinCorrections(
Array.isArray(row.coin_corrections) ? row.coin_corrections : [],
),
rent_rules: normalizeRentRules(Array.isArray(row.rent_rules) ? row.rent_rules : []),
}
}
function normalizeInsuranceBaseRatios(values: unknown[]): PublishInsuranceBaseRatio[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
insurance: typeof row.insurance === 'string' ? row.insurance.trim() : '',
ratio: readNumber(row.ratio),
}
})
.filter((item) => item.insurance && item.ratio > 0)
}
function normalizeRatioConfigItems(values: unknown[]): PublishRatioConfigItem[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
key: typeof row.key === 'string' ? row.key.trim() : '',
label: typeof row.label === 'string' ? row.label.trim() : '',
kind: typeof row.kind === 'string' ? row.kind.trim() : '',
group_key: typeof row.group_key === 'string' ? row.group_key.trim() : '',
missing_penalty: readNumber(row.missing_penalty),
}
})
.filter((item) => item.key && item.label && item.kind)
}
function normalizeCoinCorrections(values: unknown[]): PublishCoinCorrection[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
threshold_m: readNumber(row.threshold_m),
correction: readNumber(row.correction),
}
})
.filter((item) => item.threshold_m >= 0 && item.correction > 0)
}
function normalizeRentRules(values: unknown[]): PublishRentRule[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
threshold_m: readNumber(row.threshold_m),
days: Math.trunc(readNumber(row.days)),
}
})
.filter((item) => item.threshold_m >= 0 && item.days > 0)
}
function readNumber(value: unknown) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
@@ -197,7 +197,7 @@ function itemsToLines(items: string[]) {
return items.join('\n')
}
function updateOptionLines(key: keyof Pick<ListingPublishOptions, 'server_options' | 'face_options' | 'rank_options' | 'insurance_options' | 'level_options' | 'login_method_options' | 'region_options'>, nextValue: string) {
function updateOptionLines(key: keyof Pick<ListingPublishOptions, 'server_options' | 'face_options' | 'rank_options' | 'insurance_options' | 'level_options' | 'login_method_options' | 'region_options' | 'ban_record_options' | 'ban_evidence_options'>, nextValue: string) {
publishOptionsDraft.value[key] = linesToItems(nextValue)
}
@@ -245,6 +245,53 @@ function removeScreenshotSlot(index: number) {
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
}
function addInsuranceBaseRatio() {
publishOptionsDraft.value.ratio_config.insurance_base_ratios.push({
insurance: '',
ratio: 0,
})
}
function removeInsuranceBaseRatio(index: number) {
publishOptionsDraft.value.ratio_config.insurance_base_ratios.splice(index, 1)
}
function addRatioConfigItem() {
publishOptionsDraft.value.ratio_config.config_items.push({
key: `config_${Date.now()}`,
label: '',
kind: 'skin_group',
group_key: '',
missing_penalty: 1,
})
}
function removeRatioConfigItem(index: number) {
publishOptionsDraft.value.ratio_config.config_items.splice(index, 1)
}
function addCoinCorrection() {
publishOptionsDraft.value.ratio_config.coin_corrections.push({
threshold_m: 0,
correction: 0,
})
}
function removeCoinCorrection(index: number) {
publishOptionsDraft.value.ratio_config.coin_corrections.splice(index, 1)
}
function addRentRule() {
publishOptionsDraft.value.ratio_config.rent_rules.push({
threshold_m: 0,
days: 1,
})
}
function removeRentRule(index: number) {
publishOptionsDraft.value.ratio_config.rent_rules.splice(index, 1)
}
function addHomeBanner() {
homeBannersDraft.value.push({
eyebrow: '首页推荐',
@@ -486,6 +533,134 @@ function readError(error: unknown, fallback: string) {
/>
</el-form-item>
<div class="editor-grid">
<el-form-item label="封禁记录">
<el-input
:model-value="itemsToLines(publishOptionsDraft.ban_record_options)"
type="textarea"
:rows="3"
placeholder="一行一个选项"
@update:model-value="updateOptionLines('ban_record_options', $event)"
/>
</el-form-item>
<el-form-item label="需传安全图">
<el-input
:model-value="itemsToLines(publishOptionsDraft.ban_evidence_options)"
type="textarea"
:rows="3"
placeholder="选择这些封禁记录时,腾讯安全中心截图必传"
@update:model-value="updateOptionLines('ban_evidence_options', $event)"
/>
</el-form-item>
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>押金与价格提示</strong>
</div>
<el-form-item label="押金提示">
<el-input v-model="publishOptionsDraft.price_config.deposit_placeholder" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="价格提示">
<el-input v-model="publishOptionsDraft.price_config.price_placeholder" />
</el-form-item>
<el-form-item label="比例说明">
<el-input v-model="publishOptionsDraft.price_config.ratio_description" />
</el-form-item>
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>比例计算配置</strong>
</div>
<div class="editor-block-title subtle-title">
<span>保险基础比例</span>
<el-button size="small" @click="addInsuranceBaseRatio">添加保险比例</el-button>
</div>
<el-table :data="publishOptionsDraft.ratio_config.insurance_base_ratios" size="small" border>
<el-table-column label="保险" min-width="160">
<template #default="{ row }"><el-input v-model="row.insurance" placeholder="3*3" /></template>
</el-table-column>
<el-table-column label="基础比例" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.ratio" :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="removeInsuranceBaseRatio($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="editor-block-title subtle-title">
<span>配置项缺失加成</span>
<el-button size="small" @click="addRatioConfigItem">添加配置项</el-button>
</div>
<el-table :data="publishOptionsDraft.ratio_config.config_items" size="small" border>
<el-table-column label="Key" min-width="140">
<template #default="{ row }"><el-input v-model="row.key" /></template>
</el-table-column>
<el-table-column label="名称" min-width="140">
<template #default="{ row }"><el-input v-model="row.label" /></template>
</el-table-column>
<el-table-column label="类型" min-width="160">
<template #default="{ row }">
<el-select v-model="row.kind">
<el-option label="皮肤分组" value="skin_group" />
<el-option label="满体力" value="max_stamina" />
<el-option label="满负重" value="max_load" />
</el-select>
</template>
</el-table-column>
<el-table-column label="皮肤分组 Key" min-width="150">
<template #default="{ row }"><el-input v-model="row.group_key" /></template>
</el-table-column>
<el-table-column label="缺失加成" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.missing_penalty" :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="removeRatioConfigItem($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="editor-block-title subtle-title">
<span>大额币修正</span>
<el-button size="small" @click="addCoinCorrection">添加修正</el-button>
</div>
<el-table :data="publishOptionsDraft.ratio_config.coin_corrections" size="small" border>
<el-table-column label="大于 M" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.threshold_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.correction" :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="removeCoinCorrection($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="editor-block-title subtle-title">
<span>租期规则</span>
<el-button size="small" @click="addRentRule">添加租期</el-button>
</div>
<el-table :data="publishOptionsDraft.ratio_config.rent_rules" size="small" border>
<el-table-column label="大于等于 M" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.threshold_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.days" :min="1" :step="1" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeRentRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>皮肤分类</strong>
@@ -786,6 +961,12 @@ function readError(error: unknown, fallback: string) {
gap: 10px;
}
.subtle-title {
margin-top: 6px;
color: #4b5563;
font-size: 13px;
}
.skin-config-row {
display: grid;
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
@@ -252,7 +252,7 @@ function isNavActive(path: string) {
</span>
</div>
<div class="info-line">
<span class="info-label">比例</span>
<span class="info-label">M单价</span>
<span class="info-text">{{ formatRatio(listing) }}</span>
</div>
<div class="info-line">
@@ -109,6 +109,10 @@ const loginMethodOptions = computed(
() => publishOptions.value.login_method_options
);
const regionOptions = computed(() => publishOptions.value.region_options);
const banRecordOptions = computed(() => publishOptions.value.ban_record_options);
const banEvidenceOptions = computed(() => publishOptions.value.ban_evidence_options);
const priceConfig = computed(() => publishOptions.value.price_config);
const ratioConfig = computed(() => publishOptions.value.ratio_config);
const skinGroups = computed(() => publishOptions.value.skin_groups);
const quantityItems = computed(() => publishOptions.value.quantity_items);
const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
@@ -117,7 +121,8 @@ const screenshotUrls = computed(() =>
.map((item) => screenshotFiles[item.key])
.filter((url): url is string => Boolean(url))
);
const coinWanAmount = computed(() => Number(form.haf_coin_amount || 0));
const coinMAmount = computed(() => Number(form.haf_coin_amount || 0));
const coinWanAmount = computed(() => coinMAmount.value * 100);
const calculatedRatio = computed(() => calculatePublishRatio());
const calculatedConsumablePrice = computed(() => calculateConsumablePrice());
const calculatedFinalPrice = computed(() =>
@@ -125,8 +130,11 @@ const calculatedFinalPrice = computed(() =>
? roundMoney(coinWanAmount.value / calculatedRatio.value + calculatedConsumablePrice.value)
: 0
);
const calculatedRatioText = computed(() =>
calculatedRatio.value > 0 ? `1:${formatNumber(calculatedRatio.value)}` : ""
);
const calculatedRentDays = computed(() =>
coinWanAmount.value > 0 ? calculateRentDays(coinWanAmount.value) : 0
coinMAmount.value > 0 ? calculateRentDays(coinMAmount.value) : 0
);
onMounted(() => {
@@ -347,7 +355,7 @@ async function handleSubmit() {
}
loading.value = true;
try {
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}哈夫币`;
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}M哈夫币`;
const rentDays = Math.max(calculatedRentDays.value, 1);
const dailyPrice = roundMoney(calculatedFinalPrice.value / rentDays);
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
@@ -358,7 +366,7 @@ async function handleSubmit() {
server_region: form.server_region,
login_platform: form.login_method,
rank_level: form.rank_level,
haf_coin_amount: Number(form.haf_coin_amount) * 10000,
haf_coin_amount: coinMAmount.value * 1000000,
asset_summary: buildAssetSummary(),
screenshot_urls: screenshotUrls.value,
price_hourly: hourlyPrice,
@@ -383,13 +391,14 @@ async function handleSubmit() {
function validateForm() {
if (!form.server_region) return "请选择区服";
if (Number(form.haf_coin_amount) <= 0) return "请填写哈夫币/";
if (coinMAmount.value <= 0) return "请填写哈夫币/M";
if (!form.rank_level) return "请选择段位";
if (!form.fire_level) return "请填写烽火等级";
if (Number(form.fire_level) < 38) return "烽火等级低于38级的号无法发布";
if (!form.season_insurance) return "请选择赛季保险";
if (!form.stamina_level) return "请选择体力等级";
if (!form.load_level) return "请选择负重等级";
if (banRecordOptions.value.length && !form.ban_record) return "请选择封禁记录";
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
return "请填写押金";
}
@@ -397,13 +406,21 @@ function validateForm() {
return "请完善币数、保险、体力和负重后再发布";
}
for (const item of screenshotSlots.value) {
if (item.required && !screenshotFiles[item.key]) {
if (isScreenshotRequired(item) && !screenshotFiles[item.key]) {
return `请上传${item.label}`;
}
}
return "";
}
function isScreenshotRequired(item: { key: string; required: boolean }) {
return item.required || (item.key === "tencentSecurity" && shouldRequireBanEvidence());
}
function shouldRequireBanEvidence() {
return banEvidenceOptions.value.includes(form.ban_record);
}
function buildAssetSummary() {
return {
face_owner: form.face_owner,
@@ -462,7 +479,7 @@ function readUnitPrice(priceText: string) {
function calculatePublishRatio() {
if (
coinWanAmount.value <= 0 ||
coinMAmount.value <= 0 ||
!form.season_insurance ||
!form.stamina_level ||
!form.load_level
@@ -472,35 +489,26 @@ function calculatePublishRatio() {
const baseRatio = getInsuranceBaseRatio(form.season_insurance);
if (baseRatio <= 0) return 0;
return baseRatio + (5 - getConfigHitCount()) + getCoinCorrection(coinWanAmount.value);
return baseRatio + calculateConfigPenalty() + getCoinCorrection(coinMAmount.value);
}
function getInsuranceBaseRatio(insurance: string) {
const slots = getInsuranceSlots(insurance);
const ratioBySlots: Record<number, number> = {
9: 40,
6: 42,
4: 47,
2: 48,
};
return ratioBySlots[slots] || 0;
return ratioConfig.value.insurance_base_ratios.find(
(item) => item.insurance === insurance
)?.ratio || 0;
}
function getInsuranceSlots(insurance: string) {
const match = insurance.match(/^(\d+)\*(\d+)$/);
if (!match) return 0;
return Number(match[1]) * Number(match[2]);
function calculateConfigPenalty() {
return ratioConfig.value.config_items.reduce((sum, item) => {
return isRatioConfigItemMatched(item) ? sum : sum + Number(item.missing_penalty || 0);
}, 0);
}
function getConfigHitCount() {
const checks = [
hasSelectedSkinGroup("operatorRed"),
hasSelectedSkinGroup("melee"),
isMaxLevel(form.stamina_level),
isMaxLevel(form.load_level),
hasSelectedSkinGroup("weapon"),
];
return checks.filter(Boolean).length;
function isRatioConfigItemMatched(item: { kind: string; group_key?: string }) {
if (item.kind === "skin_group") return hasSelectedSkinGroup(item.group_key || "");
if (item.kind === "max_stamina") return isMaxLevel(form.stamina_level);
if (item.kind === "max_load") return isMaxLevel(form.load_level);
return false;
}
function hasSelectedSkinGroup(groupKey: string) {
@@ -521,18 +529,20 @@ function readLevelNumber(value: string) {
return match ? Number(match[0]) : 0;
}
function getCoinCorrection(coinWan: number) {
if (coinWan > 53000) return 5;
if (coinWan > 38000) return 4;
if (coinWan > 23000) return 2.5;
if (coinWan > 13000) return 1;
return 0;
function getCoinCorrection(coinM: number) {
return [...ratioConfig.value.coin_corrections]
.sort((a, b) => b.threshold_m - a.threshold_m)
.find((item) => coinM > item.threshold_m)?.correction || 0;
}
function calculateRentDays(coinWan: number) {
if (coinWan >= 30000) return 7;
if (coinWan >= 10000) return 3;
return 1;
function calculateRentDays(coinM: number) {
return [...ratioConfig.value.rent_rules]
.sort((a, b) => b.threshold_m - a.threshold_m)
.find((item) => coinM >= item.threshold_m)?.days || 1;
}
function formatNumber(value: number) {
return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`;
}
function readError(error: unknown, fallback: string) {
@@ -595,10 +605,10 @@ function readError(error: unknown, fallback: string) {
<van-field
v-model="form.haf_coin_amount"
label="哈夫币/"
label="哈夫币/M"
type="digit"
required
placeholder="只填写仓库右上角纯币数额,总资产不计算在内(建议清理仓库中所有非保留物品),100M写作10000"
placeholder="只填写仓库右上角纯币数额,总资产不计算在内100M 写 100"
class="publish-field"
/>
@@ -788,15 +798,22 @@ function readError(error: unknown, fallback: string) {
此在线时间指的是百分百能够联系上您的时间若是在此期间联系不上您导致无法上号会扣除您的部分租金或上架押金在线时长太短可能无法上架请预留充足时间用于扫码以及冻结人脸请谨慎填写
</p>
<van-field
v-model="form.ban_record"
label="封禁记录"
type="textarea"
rows="2"
autosize
placeholder="请自行填写封禁等信息"
class="publish-field"
/>
<van-field label="封禁记录" required class="publish-field">
<template #input>
<div class="radio-group">
<button
v-for="opt in banRecordOptions"
:key="opt"
type="button"
class="radio-btn"
:class="{ active: form.ban_record === opt }"
@click="selectRadio(opt, (value) => (form.ban_record = value))"
>
{{ opt }}
</button>
</div>
</template>
</van-field>
<div class="region-panel">
<div class="region-title">
@@ -826,7 +843,7 @@ function readError(error: unknown, fallback: string) {
<div class="upload-list">
<div v-for="slot in screenshotSlots" :key="slot.key" class="upload-line">
<div class="upload-meta">
<strong>{{ slot.label }}<span v-if="slot.required">*</span></strong>
<strong>{{ slot.label }}<span v-if="isScreenshotRequired(slot)">*</span></strong>
<small>{{ slot.hint }}</small>
</div>
<div v-if="screenshotFiles[slot.key]" class="upload-preview">
@@ -862,17 +879,24 @@ function readError(error: unknown, fallback: string) {
label="押金"
type="number"
required
placeholder="温馨提示:本押金用于保障账号安全。若买家在租用期间,使用非纯币类道具/资源(如消耗品、材料等)或导致账号被封禁,相关损失将直接从押金中扣除进行赔付。押金过高会延长出租等待时间(请自行衡量)感谢理解。"
:placeholder="priceConfig.deposit_placeholder"
suffix="元"
class="publish-field"
/>
<div class="result-grid">
<van-field
:model-value="calculatedRatioText"
label="比例"
readonly
:placeholder="priceConfig.ratio_description"
class="publish-field result-field"
/>
<van-field
:model-value="calculatedFinalPrice ? `¥${calculatedFinalPrice}` : ''"
label="价格"
readonly
required
placeholder="填写币数、保险、体力和负重后自动计算"
:placeholder="priceConfig.price_placeholder"
class="publish-field result-field"
/>
</div>
+15 -5
View File
@@ -46,14 +46,19 @@ export function getRatioValue(item: Listing) {
}
export function formatRatio(item: Listing) {
const ratio = getRatioValue(item);
return ratio > 0 ? `1:${Math.round(ratio)}` : "--";
const pricePerM = getPricePerM(item);
return pricePerM > 0 ? `1M=¥${formatMoney(pricePerM)}` : "--";
}
export function getValuePerYuanText(item: Listing) {
const price = getListingDisplayPrice(item);
if (price <= 0) return "";
return `1元=${Math.round(getCoinWan(item) / price)}w哈夫币`;
const pricePerM = getPricePerM(item);
return pricePerM > 0 ? `1M=¥${formatMoney(pricePerM)}` : "";
}
export function getPricePerM(item: Listing) {
const coinM = getCoinM(item);
if (coinM <= 0) return 0;
return roundMoney(getListingDisplayPrice(item) / coinM);
}
export function getLoginMethod(item: Listing) {
@@ -219,3 +224,8 @@ function formatResourceShort(item: Listing, key: string, label: string) {
function roundMoney(value: number) {
return Math.round(value * 100) / 100;
}
function formatMoney(value: number) {
const rounded = roundMoney(value);
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2);
}
+3
View File
@@ -22,5 +22,8 @@ export default defineConfig({
proxy: {
"/api": "http://127.0.0.1:8080",
},
allowedHosts: [
'221329.cc.cd'
],
},
});