优化发布界面
This commit is contained in:
@@ -58,12 +58,15 @@ func DefaultPublishOptions() PublishOptionsDTO {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
QuantityItems: []PublishQuantityItem{
|
QuantityItems: []PublishQuantityItem{
|
||||||
{Key: "awmAmmo", Label: "AWM子弹", Price: "0.8元/发"},
|
{Key: "level6Ammo", Label: "6级弹", Price: "0.1元/发", Placeholder: "按单发数量填写,60发=6元"},
|
||||||
{Key: "helmet6", Label: "6头数量", Price: "2元/个"},
|
{Key: "awmAmmo", Label: "AWM子弹", Price: "0.6元/发"},
|
||||||
{Key: "armor6", Label: "6甲数量", Price: "3元/个"},
|
{Key: "barrettAmmo", Label: "巴雷特子弹", Price: "0.3元/发"},
|
||||||
{Key: "kit5", Label: "5级全装套数量", Price: "5元/个"},
|
{Key: "armor6", Label: "6甲", Price: "2.5元/个"},
|
||||||
{Key: "level6Ammo", Label: "6级子弹", Price: "0.26元/发"},
|
{Key: "helmet6", Label: "6头", Price: "1.5元/个"},
|
||||||
{Key: "gridCard9", Label: "9格体验卡", Price: "5元/张", Placeholder: "如果是赛季9格,此处填0即可"},
|
{Key: "gridCard9", Label: "9格体验卡", Price: "3元/张", Placeholder: "如果是赛季9格,此处填0即可"},
|
||||||
|
{Key: "redPacket45", Label: "45格大红包", Price: "1.5元/个"},
|
||||||
|
{Key: "coffeeBean", Label: "高级咖啡豆", Price: "2.5元/个"},
|
||||||
|
{Key: "bulletPart", Label: "高级子弹零件", Price: "3元/个"},
|
||||||
},
|
},
|
||||||
ScreenshotSlots: []PublishScreenshotSlot{
|
ScreenshotSlots: []PublishScreenshotSlot{
|
||||||
{Key: "coin", Label: "纯币截图", Required: true, Hint: "请上传纯币截图(必传)"},
|
{Key: "coin", Label: "纯币截图", Required: true, Hint: "请上传纯币截图(必传)"},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package systemconfig
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
@@ -132,11 +133,54 @@ func (r *Repository) ensureDefaults() error {
|
|||||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
|
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if item.Key == publishOptionsConfigKey {
|
||||||
|
if err := updateLegacyPublishOptions(tx, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLegacyPublishOptions(tx *gorm.DB, item defaultConfig) error {
|
||||||
|
var row model.SystemConfig
|
||||||
|
if err := tx.Where("`key` = ?", item.Key).First(&row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isLegacyPublishOptionsValue(row.Value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
row.Value = item.Value
|
||||||
|
row.Description = item.Description
|
||||||
|
return tx.Save(&row).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLegacyPublishOptionsValue(value string) bool {
|
||||||
|
legacyMarkers := []string{
|
||||||
|
`"kit5"`,
|
||||||
|
"5级全装套",
|
||||||
|
"6级子弹",
|
||||||
|
"6头数量",
|
||||||
|
"6甲数量",
|
||||||
|
"0.26元/发",
|
||||||
|
"0.8元/发",
|
||||||
|
"çº",
|
||||||
|
"å¼",
|
||||||
|
"å…",
|
||||||
|
"å¤",
|
||||||
|
"ä½",
|
||||||
|
"é«",
|
||||||
|
"ï¼",
|
||||||
|
}
|
||||||
|
for _, marker := range legacyMarkers {
|
||||||
|
if strings.Contains(value, marker) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||||
raw, err := json.Marshal(detail)
|
raw, err := json.Marshal(detail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ VALUES
|
|||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 80, 'mode', '收费'),
|
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 80, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 6, 'mode', '收费'),
|
JSON_OBJECT('key', 'helmet6', 'label', '6头', 'quantity', 6, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'armor6', 'label', '6甲数量', 'quantity', 4, 'mode', '收费'),
|
JSON_OBJECT('key', 'armor6', 'label', '6甲', 'quantity', 4, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'level6Ammo', 'label', '6级子弹', 'quantity', 360, 'mode', '收费')
|
JSON_OBJECT('key', 'level6Ammo', 'label', '6级弹', 'quantity', 360, 'mode', '收费')
|
||||||
),
|
),
|
||||||
'skin_groups', JSON_OBJECT(
|
'skin_groups', JSON_OBJECT(
|
||||||
'melee', JSON_ARRAY('龙牙', '信条'),
|
'melee', JSON_ARRAY('龙牙', '信条'),
|
||||||
@@ -116,7 +116,7 @@ VALUES
|
|||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 30, 'mode', '收费'),
|
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 30, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 2, 'mode', '收费')
|
JSON_OBJECT('key', 'helmet6', 'label', '6头', 'quantity', 2, 'mode', '收费')
|
||||||
),
|
),
|
||||||
'skin_groups', JSON_OBJECT(
|
'skin_groups', JSON_OBJECT(
|
||||||
'melee', JSON_ARRAY('怜悯'),
|
'melee', JSON_ARRAY('怜悯'),
|
||||||
@@ -187,9 +187,9 @@ VALUES
|
|||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 120, 'mode', '收费'),
|
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 120, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 10, 'mode', '收费'),
|
JSON_OBJECT('key', 'helmet6', 'label', '6头', 'quantity', 10, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'armor6', 'label', '6甲数量', 'quantity', 8, 'mode', '收费'),
|
JSON_OBJECT('key', 'armor6', 'label', '6甲', 'quantity', 8, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'level6Ammo', 'label', '6级子弹', 'quantity', 800, 'mode', '收费')
|
JSON_OBJECT('key', 'level6Ammo', 'label', '6级弹', 'quantity', 800, 'mode', '收费')
|
||||||
),
|
),
|
||||||
'skin_groups', JSON_OBJECT(
|
'skin_groups', JSON_OBJECT(
|
||||||
'melee', JSON_ARRAY('坠星者', '处刑者'),
|
'melee', JSON_ARRAY('坠星者', '处刑者'),
|
||||||
@@ -225,7 +225,7 @@ VALUES
|
|||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 80, 'mode', '赠送'),
|
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 80, 'mode', '赠送'),
|
||||||
JSON_OBJECT('key', 'kit5', 'label', '5级全装套数量', 'quantity', 6, 'mode', '收费')
|
JSON_OBJECT('key', 'redPacket45', 'label', '45格大红包', 'quantity', 6, 'mode', '收费')
|
||||||
),
|
),
|
||||||
'skin_groups', JSON_OBJECT(
|
'skin_groups', JSON_OBJECT(
|
||||||
'melee', JSON_ARRAY('影锋'),
|
'melee', JSON_ARRAY('影锋'),
|
||||||
@@ -259,8 +259,8 @@ VALUES
|
|||||||
'online_time', JSON_OBJECT('start', '14:00', 'end', '01:00'),
|
'online_time', JSON_OBJECT('start', '14:00', 'end', '01:00'),
|
||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'kit5', 'label', '5级全装套数量', 'quantity', 3, 'mode', '收费'),
|
JSON_OBJECT('key', 'coffeeBean', 'label', '高级咖啡豆', 'quantity', 3, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 1, 'mode', '收费')
|
JSON_OBJECT('key', 'helmet6', 'label', '6头', 'quantity', 1, 'mode', '收费')
|
||||||
),
|
),
|
||||||
'skin_groups', JSON_OBJECT(
|
'skin_groups', JSON_OBJECT(
|
||||||
'melee', JSON_ARRAY('北极星'),
|
'melee', JSON_ARRAY('北极星'),
|
||||||
@@ -295,8 +295,8 @@ VALUES
|
|||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 48, 'mode', '收费'),
|
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 48, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'armor6', 'label', '6甲数量', 'quantity', 3, 'mode', '收费'),
|
JSON_OBJECT('key', 'armor6', 'label', '6甲', 'quantity', 3, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'helmet6', 'label', '6头数量', 'quantity', 4, 'mode', '收费')
|
JSON_OBJECT('key', 'helmet6', 'label', '6头', 'quantity', 4, 'mode', '收费')
|
||||||
),
|
),
|
||||||
'skin_groups', JSON_OBJECT(
|
'skin_groups', JSON_OBJECT(
|
||||||
'melee', JSON_ARRAY('赤枭', '黑海'),
|
'melee', JSON_ARRAY('赤枭', '黑海'),
|
||||||
@@ -331,7 +331,7 @@ VALUES
|
|||||||
'online_time', JSON_OBJECT('start', '10:00', 'end', '24:00'),
|
'online_time', JSON_OBJECT('start', '10:00', 'end', '24:00'),
|
||||||
'ban_record', '无封禁记录',
|
'ban_record', '无封禁记录',
|
||||||
'resources', JSON_ARRAY(
|
'resources', JSON_ARRAY(
|
||||||
JSON_OBJECT('key', 'level6Ammo', 'label', '6级子弹', 'quantity', 520, 'mode', '赠送'),
|
JSON_OBJECT('key', 'level6Ammo', 'label', '6级弹', 'quantity', 520, 'mode', '赠送'),
|
||||||
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 100, 'mode', '收费'),
|
JSON_OBJECT('key', 'awmAmmo', 'label', 'AWM子弹', 'quantity', 100, 'mode', '收费'),
|
||||||
JSON_OBJECT('key', 'gridCard9', 'label', '9格体验卡', 'quantity', 2, 'mode', '收费')
|
JSON_OBJECT('key', 'gridCard9', 'label', '9格体验卡', 'quantity', 2, 'mode', '收费')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
好的,咱们把所有的账目和加减逻辑全部锁死,整理出一套最严密、绝对不会看错的最终版逻辑总览。
|
||||||
|
|
||||||
|
整体的核心逻辑是:**账号底子越差(缺配置)或包里哈夫币越多,比例数字就越大($1:\text{数字}$,数字越大代表给客户的哈夫币越多,号越便宜、越容易租)。**
|
||||||
|
|
||||||
|
以下是最终的完整逻辑链条:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. 核心大框架:基础比例(对标保险箱)
|
||||||
|
|
||||||
|
这是账号在配置全满(完美状态)下的初始比例:
|
||||||
|
|
||||||
|
* **9 格保险箱**:基础比例为 **$1:40$**
|
||||||
|
* **6 格保险箱**:基础比例为 **$1:42$**
|
||||||
|
* **4 格保险箱**:基础比例为 **$1:47$**
|
||||||
|
* **2 格保险箱**:基础比例为 **$1:48$**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 账号配置浮动逻辑(5项标准)
|
||||||
|
|
||||||
|
完美的“理想账号”有 5 个指标:**红皮、刀皮、满体力、满负重、砖皮**。
|
||||||
|
规则是:**每缺少其中任意 1 项,比例数字直接 $+1$**。
|
||||||
|
|
||||||
|
以 **9 格保险箱** 为例,随着配置缺失,比例梯队如下:
|
||||||
|
|
||||||
|
* **5项完美全满**:**$1:40$**
|
||||||
|
* **缺 1 项**(剩4项):**$1:41$**
|
||||||
|
* **缺 2 项**(剩3项):**$1:42$**
|
||||||
|
* **缺 3 项**(比如**只满足了满体力+满负重**):**$1:43$**
|
||||||
|
* **缺 4 项**(剩1项):**$1:44$**
|
||||||
|
* **5项全无**(白号):**$1:45$**
|
||||||
|
|
||||||
|
*(注:如果是 6格/4格/2格 保箱,则在此基础上各自的初始值往上加项即可。)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 大额哈夫币“再叠加”逻辑(高币号修正)
|
||||||
|
|
||||||
|
当号里的哈夫币过多时(由于需求少、难成交),在上面算出的“配置比例”基础上,还要**额外再加上大额币的修正值**:
|
||||||
|
|
||||||
|
* **哈夫币 $> 130\text{M}$**:最终比例再 **$+1$**
|
||||||
|
* **哈夫币 $> 230\text{M}$**:最终比例再 **$+2.5$**
|
||||||
|
* **哈夫币 $> 380\text{M}$**:最终比例再 **$+4$**
|
||||||
|
* **哈夫币 $> 530\text{M}$**:最终比例再 **$+5$**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛠️ 终极公式计算演示
|
||||||
|
|
||||||
|
> **最终出租比例 = 该保箱基础比例 + (5 - 实际拥有项数) + 大额币修正值**
|
||||||
|
|
||||||
|
**实战看盘举例:**
|
||||||
|
一个客户来放号,是个 **9格保箱** 账号(基础 40),包里有 **$250\text{M}$ 哈夫币**,账号里**只有满体和满负重**。
|
||||||
|
|
||||||
|
1. **查保箱基础**:9格 = $40$
|
||||||
|
2. **算缺失项**:5项里只有2项,缺了3项 = $+3$ (此时比例来到 $43$)
|
||||||
|
3. **叠大额币**:$250\text{M}$ 触发了 $> 230\text{M}$ 的档位 = $+2.5$
|
||||||
|
4. **总计**:$40 + 3 + 2.5 = 45.5$
|
||||||
|
|
||||||
|
**最终该号的出租比例就是:$1:45.5$**
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -1,6 +1,5 @@
|
|||||||
import { apiClient } from './client'
|
import { apiClient } from './client'
|
||||||
import {
|
import {
|
||||||
defaultListingPublishOptions,
|
|
||||||
mergeListingPublishOptions,
|
mergeListingPublishOptions,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
} from './listingOptions'
|
} from './listingOptions'
|
||||||
@@ -87,7 +86,7 @@ export function mergeHomeConfig(config?: Partial<MobileHomeConfig>): MobileHomeC
|
|||||||
return {
|
return {
|
||||||
announcements: announcements.length ? announcements : defaultHomeAnnouncements,
|
announcements: announcements.length ? announcements : defaultHomeAnnouncements,
|
||||||
banners: banners.length ? banners : defaultHomeBanners,
|
banners: banners.length ? banners : defaultHomeBanners,
|
||||||
publish_options: mergeListingPublishOptions(config?.publish_options || defaultListingPublishOptions),
|
publish_options: mergeListingPublishOptions(config?.publish_options),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,11 @@ import { apiClient } from './client'
|
|||||||
|
|
||||||
export type ChargeMode = '赠送' | '收费'
|
export type ChargeMode = '赠送' | '收费'
|
||||||
|
|
||||||
export type QuantityKey =
|
export type QuantityKey = string
|
||||||
| 'awmAmmo'
|
|
||||||
| 'helmet6'
|
|
||||||
| 'armor6'
|
|
||||||
| 'kit5'
|
|
||||||
| 'level6Ammo'
|
|
||||||
| 'gridCard9'
|
|
||||||
|
|
||||||
export type ScreenshotKey = 'coin' | 'gameId' | 'totalAsset' | 'tencentSecurity' | 'skin'
|
export type ScreenshotKey = string
|
||||||
|
|
||||||
export type SkinCategoryKey = 'melee' | 'operator' | 'weapon'
|
export type SkinCategoryKey = string
|
||||||
|
|
||||||
export interface PublishOptionGroup {
|
export interface PublishOptionGroup {
|
||||||
key: SkinCategoryKey | string
|
key: SkinCategoryKey | string
|
||||||
@@ -53,63 +47,17 @@ interface ApiResponse<T> {
|
|||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultListingPublishOptions: ListingPublishOptions = {
|
export const emptyListingPublishOptions: ListingPublishOptions = {
|
||||||
server_options: ['QQ', '微信', 'Steam'],
|
server_options: [],
|
||||||
face_options: ['是', '否'],
|
face_options: [],
|
||||||
rank_options: ['青铜', '白银', '黄金', '铂金', '钻石', '黑鹰', '三角洲巅峰'],
|
rank_options: [],
|
||||||
insurance_options: ['2*1', '2*2', '2*3', '3*3'],
|
insurance_options: [],
|
||||||
level_options: ['1级', '2级', '3级', '4级', '5级', '6级', '7级'],
|
level_options: [],
|
||||||
login_method_options: ['扫码', '账号密码'],
|
login_method_options: [],
|
||||||
region_options: [
|
region_options: [],
|
||||||
'北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江',
|
skin_groups: [],
|
||||||
'上海', '江苏', '浙江', '安徽', '福建', '江西', '山东', '河南',
|
quantity_items: [],
|
||||||
'湖北', '湖南', '广东', '广西', '海南', '重庆', '四川', '贵州',
|
screenshot_slots: [],
|
||||||
'云南', '陕西', '甘肃', '青海', '宁夏', '新疆', '西藏', '香港',
|
|
||||||
'澳门', '台湾',
|
|
||||||
],
|
|
||||||
skin_groups: [
|
|
||||||
{
|
|
||||||
key: 'melee',
|
|
||||||
title: '近战皮肤',
|
|
||||||
options: ['坠星者', '暗星', '龙牙', '信条', '怜悯', '赤枭', '影锋', '黑海', '北极星', '电锯惊魂', '处刑者'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'operator',
|
|
||||||
title: '干员皮肤',
|
|
||||||
options: [
|
|
||||||
'威龙-凌霄戍卫', '红狼-蚀金玫瑰', '露娜-黑天际线', '蛊-能天使午夜邮差',
|
|
||||||
'骇爪-维什戴尔', '骇爪-水墨云图', '威龙-吴彦祖', '威龙-铁面判官',
|
|
||||||
'威龙-壮志凌云', '威龙-蛟龙特战队', '蜂医-送葬人', '蜂医-危险物质',
|
|
||||||
'蜂医-黑鹰坠落', '牧羊人-街头之星', '牧羊人-黑鹰坠落', '乌鲁鲁-黑鹰坠落',
|
|
||||||
'红狼-黑鹰坠落', '无名-夜鹰', '深蓝-不破誓约', '红狼-电锯惊魂',
|
|
||||||
'露娜-古墓丽影劳拉', '赛伊德',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'weapon',
|
|
||||||
title: '武器皮肤',
|
|
||||||
options: [
|
|
||||||
'M7棱镜攻势S2', '腾龙气象感应', 'MP7电玩高手S2', 'M250电玩高手S2',
|
|
||||||
'AS Val悬赏令', 'K416命运', 'M4A1棱镜攻势', 'SCAR-H电玩高手',
|
|
||||||
'QBZ95王牌之剑', 'AUG气象感应', 'Vector美杜莎', 'KC17-造物纪元',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
quantity_items: [
|
|
||||||
{ key: 'awmAmmo', label: 'AWM子弹', price: '0.8元/发' },
|
|
||||||
{ key: 'helmet6', label: '6头数量', price: '2元/个' },
|
|
||||||
{ key: 'armor6', label: '6甲数量', price: '3元/个' },
|
|
||||||
{ key: 'kit5', label: '5级全装套数量', price: '5元/个' },
|
|
||||||
{ key: 'level6Ammo', label: '6级子弹', price: '0.26元/发' },
|
|
||||||
{ key: 'gridCard9', label: '9格体验卡', price: '5元/张', placeholder: '如果是赛季9格,此处填0即可' },
|
|
||||||
],
|
|
||||||
screenshot_slots: [
|
|
||||||
{ 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: 'skin', label: '皮肤截图', required: false, hint: '请上传皮肤截图(选传)' },
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchListingPublishOptions() {
|
export async function fetchListingPublishOptions() {
|
||||||
@@ -119,15 +67,69 @@ export async function fetchListingPublishOptions() {
|
|||||||
|
|
||||||
export function mergeListingPublishOptions(options?: Partial<ListingPublishOptions>): ListingPublishOptions {
|
export function mergeListingPublishOptions(options?: Partial<ListingPublishOptions>): ListingPublishOptions {
|
||||||
return {
|
return {
|
||||||
server_options: options?.server_options?.length ? options.server_options : defaultListingPublishOptions.server_options,
|
server_options: normalizeStringList(options?.server_options),
|
||||||
face_options: options?.face_options?.length ? options.face_options : defaultListingPublishOptions.face_options,
|
face_options: normalizeStringList(options?.face_options),
|
||||||
rank_options: options?.rank_options?.length ? options.rank_options : defaultListingPublishOptions.rank_options,
|
rank_options: normalizeStringList(options?.rank_options),
|
||||||
insurance_options: options?.insurance_options?.length ? options.insurance_options : defaultListingPublishOptions.insurance_options,
|
insurance_options: normalizeStringList(options?.insurance_options),
|
||||||
level_options: options?.level_options?.length ? options.level_options : defaultListingPublishOptions.level_options,
|
level_options: normalizeStringList(options?.level_options),
|
||||||
login_method_options: options?.login_method_options?.length ? options.login_method_options : defaultListingPublishOptions.login_method_options,
|
login_method_options: normalizeStringList(options?.login_method_options),
|
||||||
region_options: options?.region_options?.length ? options.region_options : defaultListingPublishOptions.region_options,
|
region_options: normalizeStringList(options?.region_options),
|
||||||
skin_groups: options?.skin_groups?.length ? options.skin_groups : defaultListingPublishOptions.skin_groups,
|
skin_groups: normalizeOptionGroups(options?.skin_groups),
|
||||||
quantity_items: options?.quantity_items?.length ? options.quantity_items as PublishQuantityItem[] : defaultListingPublishOptions.quantity_items,
|
quantity_items: normalizeQuantityItems(options?.quantity_items),
|
||||||
screenshot_slots: options?.screenshot_slots?.length ? options.screenshot_slots as PublishScreenshotSlot[] : defaultListingPublishOptions.screenshot_slots,
|
screenshot_slots: normalizeScreenshotSlots(options?.screenshot_slots),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStringList(values?: unknown[]) {
|
||||||
|
return Array.isArray(values)
|
||||||
|
? values.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean)
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptionGroups(values?: unknown[]): PublishOptionGroup[] {
|
||||||
|
if (!Array.isArray(values)) return []
|
||||||
|
return values
|
||||||
|
.map((item) => {
|
||||||
|
const group = isRecord(item) ? item : {}
|
||||||
|
return {
|
||||||
|
key: typeof group.key === 'string' ? group.key.trim() : '',
|
||||||
|
title: typeof group.title === 'string' ? group.title.trim() : '',
|
||||||
|
options: normalizeStringList(Array.isArray(group.options) ? group.options : []),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => item.key && item.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeQuantityItems(values?: unknown[]): PublishQuantityItem[] {
|
||||||
|
if (!Array.isArray(values)) return []
|
||||||
|
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() : '',
|
||||||
|
price: typeof row.price === 'string' ? row.price.trim() : '',
|
||||||
|
placeholder: typeof row.placeholder === 'string' ? row.placeholder.trim() : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => item.key && item.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeScreenshotSlots(values?: unknown[]): PublishScreenshotSlot[] {
|
||||||
|
if (!Array.isArray(values)) return []
|
||||||
|
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() : '',
|
||||||
|
required: row.required === true,
|
||||||
|
hint: typeof row.hint === 'string' ? row.hint.trim() : '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => item.key && item.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
mergeListingPublishOptions,
|
mergeListingPublishOptions,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
} from '@/api/listingOptions'
|
} from '@/api/listingOptions'
|
||||||
@@ -23,7 +23,7 @@ const configs = ref<SystemConfig[]>([])
|
|||||||
const activeConfig = ref<SystemConfig | null>(null)
|
const activeConfig = ref<SystemConfig | null>(null)
|
||||||
const value = ref('')
|
const value = ref('')
|
||||||
const description = ref('')
|
const description = ref('')
|
||||||
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(defaultListingPublishOptions))
|
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(emptyListingPublishOptions))
|
||||||
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)
|
||||||
@@ -141,20 +141,20 @@ async function handleSave() {
|
|||||||
|
|
||||||
function parsePublishOptions(raw: string) {
|
function parsePublishOptions(raw: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultListingPublishOptions
|
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingPublishOptions
|
||||||
return cloneOptions(mergeListingPublishOptions(parsed))
|
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.warning('发布选项 JSON 解析失败,已使用默认配置')
|
ElMessage.warning('发布选项 JSON 解析失败,已清空草稿')
|
||||||
return cloneOptions(defaultListingPublishOptions)
|
return cloneOptions(emptyListingPublishOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeParsePublishOptions(raw: string) {
|
function safeParsePublishOptions(raw: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultListingPublishOptions
|
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingPublishOptions
|
||||||
return cloneOptions(mergeListingPublishOptions(parsed))
|
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||||
} catch {
|
} catch {
|
||||||
return cloneOptions(defaultListingPublishOptions)
|
return cloneOptions(emptyListingPublishOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ function removeSkinGroup(index: number) {
|
|||||||
|
|
||||||
function addQuantityItem() {
|
function addQuantityItem() {
|
||||||
publishOptionsDraft.value.quantity_items.push({
|
publishOptionsDraft.value.quantity_items.push({
|
||||||
key: `item_${Date.now()}` as never,
|
key: `item_${Date.now()}`,
|
||||||
label: '',
|
label: '',
|
||||||
price: '',
|
price: '',
|
||||||
})
|
})
|
||||||
@@ -234,7 +234,7 @@ function removeQuantityItem(index: number) {
|
|||||||
|
|
||||||
function addScreenshotSlot() {
|
function addScreenshotSlot() {
|
||||||
publishOptionsDraft.value.screenshot_slots.push({
|
publishOptionsDraft.value.screenshot_slots.push({
|
||||||
key: `screenshot_${Date.now()}` as never,
|
key: `screenshot_${Date.now()}`,
|
||||||
label: '',
|
label: '',
|
||||||
required: false,
|
required: false,
|
||||||
hint: '',
|
hint: '',
|
||||||
@@ -245,10 +245,6 @@ function removeScreenshotSlot(index: number) {
|
|||||||
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
|
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPublishOptions() {
|
|
||||||
publishOptionsDraft.value = cloneOptions(defaultListingPublishOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addHomeBanner() {
|
function addHomeBanner() {
|
||||||
homeBannersDraft.value.push({
|
homeBannersDraft.value.push({
|
||||||
eyebrow: '首页推荐',
|
eyebrow: '首页推荐',
|
||||||
@@ -330,7 +326,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Publish Options</p>
|
<p class="eyebrow">Publish Options</p>
|
||||||
<h2>发布表单选项</h2>
|
<h2>发布表单选项</h2>
|
||||||
<span>管理移动端发布页的区服、段位、皮肤、资源道具、截图材料和地区选项。</span>
|
<span>管理移动端发布页的区服、段位、皮肤、额外消耗品、截图材料和地区选项。</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" @click="openEdit(publishConfig)">编辑发布选项</el-button>
|
<el-button type="primary" @click="openEdit(publishConfig)">编辑发布选项</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,7 +341,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="publish-stat">
|
<div class="publish-stat">
|
||||||
<strong>{{ publishStats.resourceCount }}</strong>
|
<strong>{{ publishStats.resourceCount }}</strong>
|
||||||
<span>资源道具</span>
|
<span>额外消耗品</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="publish-stat">
|
<div class="publish-stat">
|
||||||
<strong>{{ publishStats.screenshotCount }}</strong>
|
<strong>{{ publishStats.screenshotCount }}</strong>
|
||||||
@@ -421,7 +417,6 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<div v-if="isPublishOptionsConfig" class="publish-options-editor">
|
<div v-if="isPublishOptionsConfig" class="publish-options-editor">
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<span>发布页选项配置</span>
|
<span>发布页选项配置</span>
|
||||||
<el-button size="small" @click="resetPublishOptions">恢复默认选项</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="editor-grid">
|
<div class="editor-grid">
|
||||||
@@ -512,8 +507,8 @@ function readError(error: unknown, fallback: string) {
|
|||||||
|
|
||||||
<div class="editor-block">
|
<div class="editor-block">
|
||||||
<div class="editor-block-title">
|
<div class="editor-block-title">
|
||||||
<strong>资源道具</strong>
|
<strong>额外消耗品</strong>
|
||||||
<el-button size="small" @click="addQuantityItem">添加道具</el-button>
|
<el-button size="small" @click="addQuantityItem">添加消耗品</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="publishOptionsDraft.quantity_items" size="small" border>
|
<el-table :data="publishOptionsDraft.quantity_items" size="small" border>
|
||||||
<el-table-column label="Key" min-width="150">
|
<el-table-column label="Key" min-width="150">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RouterLink, useRoute } from "vue-router";
|
|||||||
import { showToast } from "vant";
|
import { showToast } from "vant";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
} from "@/api/listingOptions";
|
} from "@/api/listingOptions";
|
||||||
import { fetchListings, type Listing } from "@/api/listings";
|
import { fetchListings, type Listing } from "@/api/listings";
|
||||||
@@ -39,7 +39,7 @@ const route = useRoute();
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadFailed = ref(false);
|
const loadFailed = ref(false);
|
||||||
const listings = ref<Listing[]>([]);
|
const listings = ref<Listing[]>([]);
|
||||||
const publishOptions = ref<ListingPublishOptions>(defaultListingPublishOptions);
|
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
||||||
const sortOpen = ref(false);
|
const sortOpen = ref(false);
|
||||||
const activeSort = ref("comprehensive");
|
const activeSort = ref("comprehensive");
|
||||||
const filterOpen = ref(false);
|
const filterOpen = ref(false);
|
||||||
@@ -176,7 +176,7 @@ async function loadHomeConfig() {
|
|||||||
} catch {
|
} catch {
|
||||||
announcements.value = defaultHomeAnnouncements;
|
announcements.value = defaultHomeAnnouncements;
|
||||||
bannerSlides.value = defaultHomeBanners;
|
bannerSlides.value = defaultHomeBanners;
|
||||||
publishOptions.value = defaultListingPublishOptions;
|
publishOptions.value = emptyListingPublishOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ function isNavActive(path: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="getListingResources(listing).length" class="info-card">
|
<div v-if="getListingResources(listing).length" class="info-card">
|
||||||
<h3 class="card-subtitle">资源道具</h3>
|
<h3 class="card-subtitle">额外消耗品</h3>
|
||||||
<div class="resource-grid">
|
<div class="resource-grid">
|
||||||
<div
|
<div
|
||||||
v-for="resource in getListingResources(listing)"
|
v-for="resource in getListingResources(listing)"
|
||||||
|
|||||||
@@ -416,16 +416,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calc-btn {
|
|
||||||
height: 40px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #1477ff;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-field :deep(.van-field__control) {
|
.result-field :deep(.van-field__control) {
|
||||||
color: #ff6a00;
|
color: #ff6a00;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -507,8 +497,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 430px) {
|
@media (min-width: 430px) {
|
||||||
.time-row,
|
.time-row {
|
||||||
.result-grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { showDialog, showToast } from "vant";
|
|||||||
|
|
||||||
import { uploadFile } from "@/api/files";
|
import { uploadFile } from "@/api/files";
|
||||||
import {
|
import {
|
||||||
defaultListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
fetchListingPublishOptions,
|
fetchListingPublishOptions,
|
||||||
type ChargeMode,
|
type ChargeMode,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
@@ -30,9 +30,6 @@ type PublishForm = {
|
|||||||
ban_record: string;
|
ban_record: string;
|
||||||
common_regions: string[];
|
common_regions: string[];
|
||||||
deposit_amount: number | "";
|
deposit_amount: number | "";
|
||||||
ratio: number | "";
|
|
||||||
final_price: number;
|
|
||||||
rent_days: number;
|
|
||||||
remark: string;
|
remark: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +47,7 @@ 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>(defaultListingPublishOptions);
|
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
||||||
|
|
||||||
function isNavActive(path: string) {
|
function isNavActive(path: string) {
|
||||||
if (path === "/m") return route.path === "/m";
|
if (path === "/m") return route.path === "/m";
|
||||||
@@ -74,43 +71,20 @@ function defaultForm(): PublishForm {
|
|||||||
ban_record: "",
|
ban_record: "",
|
||||||
common_regions: [],
|
common_regions: [],
|
||||||
deposit_amount: "",
|
deposit_amount: "",
|
||||||
ratio: "",
|
|
||||||
final_price: 0,
|
|
||||||
rent_days: 0,
|
|
||||||
remark: "",
|
remark: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultQuantityValues(): Record<QuantityKey, number> {
|
function defaultQuantityValues(): Record<QuantityKey, number> {
|
||||||
return {
|
return {};
|
||||||
awmAmmo: 0,
|
|
||||||
helmet6: 0,
|
|
||||||
armor6: 0,
|
|
||||||
kit5: 0,
|
|
||||||
level6Ammo: 0,
|
|
||||||
gridCard9: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultQuantityModes(): Record<QuantityKey, ChargeMode> {
|
function defaultQuantityModes(): Record<QuantityKey, ChargeMode> {
|
||||||
return {
|
return {};
|
||||||
awmAmmo: "收费",
|
|
||||||
helmet6: "收费",
|
|
||||||
armor6: "收费",
|
|
||||||
kit5: "收费",
|
|
||||||
level6Ammo: "收费",
|
|
||||||
gridCard9: "收费",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultScreenshotFiles(): Record<ScreenshotKey, string> {
|
function defaultScreenshotFiles(): Record<ScreenshotKey, string> {
|
||||||
return {
|
return {};
|
||||||
coin: "",
|
|
||||||
gameId: "",
|
|
||||||
totalAsset: "",
|
|
||||||
tencentSecurity: "",
|
|
||||||
skin: "",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = reactive<PublishForm>(defaultForm());
|
const form = reactive<PublishForm>(defaultForm());
|
||||||
@@ -138,10 +112,17 @@ 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);
|
||||||
const screenshotUrls = computed(() =>
|
const screenshotUrls = computed(() =>
|
||||||
screenshotSlots.value.map((item) => screenshotFiles[item.key]).filter(Boolean)
|
screenshotSlots.value
|
||||||
|
.map((item) => screenshotFiles[item.key])
|
||||||
|
.filter((url): url is string => Boolean(url))
|
||||||
);
|
);
|
||||||
const dailyLossText = computed(() =>
|
const coinWanAmount = computed(() => Number(form.haf_coin_amount || 0));
|
||||||
form.rent_days ? calculateDailyLoss(Number(form.haf_coin_amount || 0)) : ""
|
const calculatedRatio = computed(() => calculatePublishRatio());
|
||||||
|
const calculatedFinalPrice = computed(() =>
|
||||||
|
calculatedRatio.value > 0 ? roundMoney(coinWanAmount.value / calculatedRatio.value) : 0
|
||||||
|
);
|
||||||
|
const calculatedRentDays = computed(() =>
|
||||||
|
coinWanAmount.value > 0 ? calculateRentDays(coinWanAmount.value) : 0
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -161,7 +142,7 @@ async function loadPublishOptions() {
|
|||||||
try {
|
try {
|
||||||
publishOptions.value = await fetchListingPublishOptions();
|
publishOptions.value = await fetchListingPublishOptions();
|
||||||
} catch {
|
} catch {
|
||||||
publishOptions.value = defaultListingPublishOptions;
|
publishOptions.value = emptyListingPublishOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +169,7 @@ function restoreDraft() {
|
|||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
try {
|
try {
|
||||||
const draft = JSON.parse(raw) as Partial<PublishDraft>;
|
const draft = JSON.parse(raw) as Partial<PublishDraft>;
|
||||||
Object.assign(form, defaultForm(), draft.form || {});
|
Object.assign(form, normalizeDraftForm(draft.form));
|
||||||
form.common_regions = Array.isArray(draft.form?.common_regions)
|
|
||||||
? [...draft.form.common_regions]
|
|
||||||
: [];
|
|
||||||
Object.assign(quantityValues, defaultQuantityValues(), draft.quantityValues || {});
|
Object.assign(quantityValues, defaultQuantityValues(), draft.quantityValues || {});
|
||||||
Object.assign(quantityModes, defaultQuantityModes(), draft.quantityModes || {});
|
Object.assign(quantityModes, defaultQuantityModes(), draft.quantityModes || {});
|
||||||
Object.assign(screenshotFiles, defaultScreenshotFiles(), draft.screenshotFiles || {});
|
Object.assign(screenshotFiles, defaultScreenshotFiles(), draft.screenshotFiles || {});
|
||||||
@@ -203,6 +181,27 @@ function restoreDraft() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDraftForm(value: unknown): PublishForm {
|
||||||
|
const next = defaultForm();
|
||||||
|
if (!isRecord(value)) return next;
|
||||||
|
|
||||||
|
for (const key of Object.keys(next) as Array<keyof PublishForm>) {
|
||||||
|
if (key === "common_regions") continue;
|
||||||
|
const draftValue = value[key];
|
||||||
|
if (draftValue !== undefined) {
|
||||||
|
next[key] = draftValue as never;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.common_regions = Array.isArray(value.common_regions)
|
||||||
|
? value.common_regions.filter((region): region is string => typeof region === "string")
|
||||||
|
: [];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
function resetDraftState() {
|
function resetDraftState() {
|
||||||
Object.assign(form, defaultForm());
|
Object.assign(form, defaultForm());
|
||||||
Object.assign(quantityValues, defaultQuantityValues());
|
Object.assign(quantityValues, defaultQuantityValues());
|
||||||
@@ -288,23 +287,6 @@ function handleFireLevelInput(value: string | number) {
|
|||||||
form.fire_level = Math.trunc(level);
|
form.fire_level = Math.trunc(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePrice() {
|
|
||||||
const coinWan = Number(form.haf_coin_amount || 0);
|
|
||||||
const ratio = Number(form.ratio || 0);
|
|
||||||
if (coinWan <= 0) {
|
|
||||||
showToast({ message: "请先填写哈夫币/万", icon: "warning-o" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ratio <= 0) {
|
|
||||||
showToast({ message: "比例需大于0", icon: "warning-o" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const finalPrice = roundMoney(coinWan / ratio);
|
|
||||||
const rentDays = coinWan >= 30000 ? 7 : coinWan >= 10000 ? 3 : 1;
|
|
||||||
form.final_price = finalPrice;
|
|
||||||
form.rent_days = rentDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const error = validateForm();
|
const error = validateForm();
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -314,8 +296,8 @@ async function handleSubmit() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
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}万哈夫币`;
|
||||||
const rentDays = Math.max(Number(form.rent_days || 1), 1);
|
const rentDays = Math.max(calculatedRentDays.value, 1);
|
||||||
const dailyPrice = roundMoney(Number(form.final_price) / rentDays);
|
const dailyPrice = roundMoney(calculatedFinalPrice.value / rentDays);
|
||||||
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
|
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
|
||||||
|
|
||||||
await createListing({
|
await createListing({
|
||||||
@@ -359,8 +341,8 @@ function validateForm() {
|
|||||||
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
|
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
|
||||||
return "请填写押金";
|
return "请填写押金";
|
||||||
}
|
}
|
||||||
if (!form.final_price || !form.rent_days) {
|
if (!calculatedFinalPrice.value || !calculatedRentDays.value) {
|
||||||
return "请先点击计算价格";
|
return "请完善币数、保险、体力和负重后再发布";
|
||||||
}
|
}
|
||||||
for (const item of screenshotSlots.value) {
|
for (const item of screenshotSlots.value) {
|
||||||
if (item.required && !screenshotFiles[item.key]) {
|
if (item.required && !screenshotFiles[item.key]) {
|
||||||
@@ -382,7 +364,7 @@ function buildAssetSummary() {
|
|||||||
key: item.key,
|
key: item.key,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
quantity: Number(quantityValues[item.key] || 0),
|
quantity: Number(quantityValues[item.key] || 0),
|
||||||
mode: quantityModes[item.key],
|
mode: quantityModes[item.key] || "收费",
|
||||||
})),
|
})),
|
||||||
skin_groups: skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
|
skin_groups: skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
|
||||||
groups[group.key] = group.options.filter((skin) =>
|
groups[group.key] = group.options.filter((skin) =>
|
||||||
@@ -404,10 +386,79 @@ function roundMoney(value: number) {
|
|||||||
return Math.round(value * 100) / 100;
|
return Math.round(value * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateDailyLoss(coinWan: number) {
|
function calculatePublishRatio() {
|
||||||
if (coinWan >= 30000) return "30M";
|
if (
|
||||||
if (coinWan >= 10000) return "20M";
|
coinWanAmount.value <= 0 ||
|
||||||
return "10M";
|
!form.season_insurance ||
|
||||||
|
!form.stamina_level ||
|
||||||
|
!form.load_level
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const baseRatio = getInsuranceBaseRatio(form.season_insurance);
|
||||||
|
if (baseRatio <= 0) return 0;
|
||||||
|
|
||||||
|
return baseRatio + (5 - getConfigHitCount()) + getCoinCorrection(coinWanAmount.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInsuranceSlots(insurance: string) {
|
||||||
|
const match = insurance.match(/^(\d+)\*(\d+)$/);
|
||||||
|
if (!match) return 0;
|
||||||
|
return Number(match[1]) * Number(match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigHitCount() {
|
||||||
|
const checks = [
|
||||||
|
hasSelectedSkinGroup("operator"),
|
||||||
|
hasSelectedSkinGroup("melee"),
|
||||||
|
isMaxLevel(form.stamina_level),
|
||||||
|
isMaxLevel(form.load_level),
|
||||||
|
hasSelectedSkinGroup("weapon"),
|
||||||
|
];
|
||||||
|
return checks.filter(Boolean).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSelectedSkinGroup(groupKey: string) {
|
||||||
|
const group = skinGroups.value.find((item) => item.key === groupKey);
|
||||||
|
if (!group) return false;
|
||||||
|
return group.options.some((skin) => selectedSkins.value.includes(skin));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMaxLevel(value: string) {
|
||||||
|
const currentLevel = readLevelNumber(value);
|
||||||
|
const maxLevel = Math.max(...levelOptions.value.map(readLevelNumber).filter(Boolean));
|
||||||
|
if (currentLevel > 0 && maxLevel > 0) return currentLevel >= maxLevel;
|
||||||
|
return value === levelOptions.value[levelOptions.value.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLevelNumber(value: string) {
|
||||||
|
const match = value.match(/\d+/);
|
||||||
|
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 calculateRentDays(coinWan: number) {
|
||||||
|
if (coinWan >= 30000) return 7;
|
||||||
|
if (coinWan >= 10000) return 3;
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readError(error: unknown, fallback: string) {
|
function readError(error: unknown, fallback: string) {
|
||||||
@@ -562,7 +613,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2 class="section-title">资源道具</h2>
|
<h2 class="section-title">额外消耗品</h2>
|
||||||
<div v-for="item in quantityItems" :key="item.key" class="quantity-row">
|
<div v-for="item in quantityItems" :key="item.key" class="quantity-row">
|
||||||
<div class="quantity-card">
|
<div class="quantity-card">
|
||||||
<div class="quantity-meta">
|
<div class="quantity-meta">
|
||||||
@@ -588,7 +639,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="{ active: quantityModes[item.key] === '收费' }"
|
:class="{ active: (quantityModes[item.key] || '收费') === '收费' }"
|
||||||
@click="quantityModes[item.key] = '收费'"
|
@click="quantityModes[item.key] = '收费'"
|
||||||
>
|
>
|
||||||
收费
|
收费
|
||||||
@@ -731,7 +782,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2 class="section-title">价格结算</h2>
|
<h2 class="section-title">押金与价格</h2>
|
||||||
<van-field
|
<van-field
|
||||||
v-model="form.deposit_amount"
|
v-model="form.deposit_amount"
|
||||||
label="押金"
|
label="押金"
|
||||||
@@ -741,49 +792,13 @@ function readError(error: unknown, fallback: string) {
|
|||||||
suffix="元"
|
suffix="元"
|
||||||
class="publish-field"
|
class="publish-field"
|
||||||
/>
|
/>
|
||||||
<van-field
|
|
||||||
v-model="form.ratio"
|
|
||||||
label="比例"
|
|
||||||
type="number"
|
|
||||||
placeholder="完成基础信息填写后请点击计算比例价格 1:请点击计算价格获取比例,最低比例0-建议6,比例越高出的速度越快,自行选择"
|
|
||||||
class="publish-field"
|
|
||||||
>
|
|
||||||
<template #left-icon>1:</template>
|
|
||||||
</van-field>
|
|
||||||
|
|
||||||
<van-button block class="calc-btn" @click="calculatePrice">
|
|
||||||
计算价格
|
|
||||||
</van-button>
|
|
||||||
|
|
||||||
<div class="result-grid">
|
<div class="result-grid">
|
||||||
<van-field
|
<van-field
|
||||||
:model-value="form.final_price ? `¥${form.final_price}` : ''"
|
:model-value="calculatedFinalPrice ? `¥${calculatedFinalPrice}` : ''"
|
||||||
label="价格"
|
label="价格"
|
||||||
readonly
|
readonly
|
||||||
required
|
required
|
||||||
placeholder="请点击计算价格"
|
placeholder="填写币数、保险、体力和负重后自动计算"
|
||||||
class="publish-field result-field"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="form.final_price ? `¥${form.final_price}` : ''"
|
|
||||||
label="结算金额"
|
|
||||||
readonly
|
|
||||||
required
|
|
||||||
placeholder="请点击计算价格获取结算金额"
|
|
||||||
class="publish-field result-field"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="form.rent_days ? `${form.rent_days}天` : ''"
|
|
||||||
label="租期"
|
|
||||||
readonly
|
|
||||||
placeholder="自动计算后显示"
|
|
||||||
class="publish-field result-field"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="dailyLossText"
|
|
||||||
label="每日损耗"
|
|
||||||
readonly
|
|
||||||
placeholder="自动计算 10M/20M/30M"
|
|
||||||
class="publish-field result-field"
|
class="publish-field result-field"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
type HomeBannerSlide,
|
type HomeBannerSlide,
|
||||||
} from "@/api/homeConfig";
|
} from "@/api/homeConfig";
|
||||||
import {
|
import {
|
||||||
defaultListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
} from "@/api/listingOptions";
|
} from "@/api/listingOptions";
|
||||||
import { fetchListings, type Listing } from "@/api/listings";
|
import { fetchListings, type Listing } from "@/api/listings";
|
||||||
@@ -37,7 +37,7 @@ const loading = ref(false);
|
|||||||
const listings = ref<Listing[]>([]);
|
const listings = ref<Listing[]>([]);
|
||||||
const announcements = ref<string[]>(defaultHomeAnnouncements);
|
const announcements = ref<string[]>(defaultHomeAnnouncements);
|
||||||
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners);
|
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners);
|
||||||
const publishOptions = ref<ListingPublishOptions>(defaultListingPublishOptions);
|
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: "",
|
keyword: "",
|
||||||
server: "",
|
server: "",
|
||||||
@@ -131,7 +131,7 @@ async function loadHome() {
|
|||||||
} catch {
|
} catch {
|
||||||
announcements.value = defaultHomeAnnouncements;
|
announcements.value = defaultHomeAnnouncements;
|
||||||
banners.value = defaultHomeBanners;
|
banners.value = defaultHomeBanners;
|
||||||
publishOptions.value = defaultListingPublishOptions;
|
publishOptions.value = emptyListingPublishOptions;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user