From e6cc9f125521151d5e4672a94ae066ceb537970c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 23 May 2026 13:47:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=E7=95=8C=E9=9D=A2=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/systemconfig/dto.go | 40 ++++ .../modules/systemconfig/publish_options.go | 35 +++- .../modules/systemconfig/repository.go | 43 +++- .../internal/modules/systemconfig/service.go | 27 +++ docs/cf临时穿透.md | 47 +++++ docs/发布比例计算.md | 4 +- frontend/src/api/listingOptions.ts | 138 +++++++++++++ .../views/admin/AdminSystemConfigsView.vue | 183 +++++++++++++++++- .../views/mobile/MobileListingDetailView.vue | 2 +- .../mobile/MobileSellerListingCreateView.vue | 130 ++++++++----- frontend/src/views/mobile/listingDisplay.ts | 20 +- frontend/vite.config.ts | 3 + 12 files changed, 607 insertions(+), 65 deletions(-) create mode 100644 docs/cf临时穿透.md diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 002a4b5..cd0b673 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -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"` +} diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go index 89434f8..b309c3a 100644 --- a/backend/internal/modules/systemconfig/publish_options.go +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -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}, + }, + }, } } diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 081b6da..ca2d3e4 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -148,14 +148,51 @@ 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) { + 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 } - row.Value = item.Value - row.Description = item.Description 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 { legacyMarkers := []string{ `"kit5"`, diff --git a/backend/internal/modules/systemconfig/service.go b/backend/internal/modules/systemconfig/service.go index a7ec823..54269c2 100644 --- a/backend/internal/modules/systemconfig/service.go +++ b/backend/internal/modules/systemconfig/service.go @@ -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 + } } diff --git a/docs/cf临时穿透.md b/docs/cf临时穿透.md new file mode 100644 index 0000000..91007f2 --- /dev/null +++ b/docs/cf临时穿透.md @@ -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 diff --git a/docs/发布比例计算.md b/docs/发布比例计算.md index 95db984..f7f1cc0 100644 --- a/docs/发布比例计算.md +++ b/docs/发布比例计算.md @@ -2,6 +2,8 @@ 整体的核心逻辑是:**账号底子越差(缺配置)或包里哈夫币越多,比例数字就越大($1:\text{数字}$,数字越大代表给客户的哈夫币越多,号越便宜、越容易租)。** +前台输入和展示统一使用 **M** 作为哈夫币单位:发布页填写 `100` 表示 `100M`。内部比例仍按“万”为计算单位,最终对用户展示为 `1M=¥xx`。 + 以下是最终的完整逻辑链条: --- @@ -58,4 +60,4 @@ 3. **叠大额币**:$250\text{M}$ 触发了 $> 230\text{M}$ 的档位 = $+2.5$ 4. **总计**:$40 + 3 + 2.5 = 45.5$ -**最终该号的出租比例就是:$1:45.5$** \ No newline at end of file +**最终该号的出租比例就是:$1:45.5$** diff --git a/frontend/src/api/listingOptions.ts b/frontend/src/api/listingOptions.ts index ff467c6..cfb8a72 100644 --- a/frontend/src/api/listingOptions.ts +++ b/frontend/src/api/listingOptions.ts @@ -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 { @@ -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 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 { return typeof value === 'object' && value !== null } diff --git a/frontend/src/views/admin/AdminSystemConfigsView.vue b/frontend/src/views/admin/AdminSystemConfigsView.vue index b54dc7a..ba7ae1b 100644 --- a/frontend/src/views/admin/AdminSystemConfigsView.vue +++ b/frontend/src/views/admin/AdminSystemConfigsView.vue @@ -197,7 +197,7 @@ function itemsToLines(items: string[]) { return items.join('\n') } -function updateOptionLines(key: keyof Pick, nextValue: string) { +function updateOptionLines(key: keyof Pick, 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) { /> +
+ + + + + + +
+ +
+
+ 押金与价格提示 +
+ + + + + + + + + +
+ +
+
+ 比例计算配置 +
+
+ 保险基础比例 + 添加保险比例 +
+ + + + + + + + + + + + +
+ 配置项缺失加成 + 添加配置项 +
+ + + + + + + + + + + + + + + + + + + + + +
+ 大额币修正 + 添加修正 +
+ + + + + + + + + + + + +
+ 租期规则 + 添加租期 +
+ + + + + + + + + + + +
+
皮肤分类 @@ -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; diff --git a/frontend/src/views/mobile/MobileListingDetailView.vue b/frontend/src/views/mobile/MobileListingDetailView.vue index 0bd17d4..902f67f 100644 --- a/frontend/src/views/mobile/MobileListingDetailView.vue +++ b/frontend/src/views/mobile/MobileListingDetailView.vue @@ -252,7 +252,7 @@ function isNavActive(path: string) {
- 比例 + M单价 {{ formatRatio(listing) }}
diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.vue b/frontend/src/views/mobile/MobileSellerListingCreateView.vue index bf5299d..22776aa 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.vue +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.vue @@ -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 = { - 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) { @@ -788,15 +798,22 @@ function readError(error: unknown, fallback: string) { 此在线时间指的是百分百能够联系上您的时间,若是在此期间联系不上您导致无法上号会扣除您的部分租金或上架押金,在线时长太短可能无法上架,请预留充足时间用于扫码以及冻结人脸,请谨慎填写

- + + +
@@ -826,7 +843,7 @@ function readError(error: unknown, fallback: string) {
- {{ slot.label }}* + {{ slot.label }}* {{ slot.hint }}
@@ -862,17 +879,24 @@ function readError(error: unknown, fallback: string) { label="押金" type="number" required - placeholder="温馨提示:本押金用于保障账号安全。若买家在租用期间,使用非纯币类道具/资源(如消耗品、材料等)或导致账号被封禁,相关损失将直接从押金中扣除进行赔付。押金过高会延长出租等待时间(请自行衡量)感谢理解。" + :placeholder="priceConfig.deposit_placeholder" suffix="元" class="publish-field" />
+
diff --git a/frontend/src/views/mobile/listingDisplay.ts b/frontend/src/views/mobile/listingDisplay.ts index a8846f3..e680e31 100644 --- a/frontend/src/views/mobile/listingDisplay.ts +++ b/frontend/src/views/mobile/listingDisplay.ts @@ -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); +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9a245b8..48720aa 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -22,5 +22,8 @@ export default defineConfig({ proxy: { "/api": "http://127.0.0.1:8080", }, + allowedHosts: [ + '221329.cc.cd' + ], }, });