优化后端可以编辑皮肤等等
This commit is contained in:
@@ -16,3 +16,36 @@ type UpdateRequest struct {
|
|||||||
Value string `json:"value" binding:"required"`
|
Value string `json:"value" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PublishOptionsDTO struct {
|
||||||
|
ServerOptions []string `json:"server_options"`
|
||||||
|
FaceOptions []string `json:"face_options"`
|
||||||
|
RankOptions []string `json:"rank_options"`
|
||||||
|
InsuranceOptions []string `json:"insurance_options"`
|
||||||
|
LevelOptions []string `json:"level_options"`
|
||||||
|
LoginMethodOptions []string `json:"login_method_options"`
|
||||||
|
RegionOptions []string `json:"region_options"`
|
||||||
|
SkinGroups []PublishOptionGroup `json:"skin_groups"`
|
||||||
|
QuantityItems []PublishQuantityItem `json:"quantity_items"`
|
||||||
|
ScreenshotSlots []PublishScreenshotSlot `json:"screenshot_slots"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishOptionGroup struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Options []string `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishQuantityItem struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Price string `json:"price"`
|
||||||
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishScreenshotSlot struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Hint string `json:"hint"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ func (h *Handler) List(c *gin.Context) {
|
|||||||
response.OK(c, gin.H{"items": items})
|
response.OK(c, gin.H{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) PublishOptions(c *gin.Context) {
|
||||||
|
options, err := h.service.PublishOptions()
|
||||||
|
if err != nil {
|
||||||
|
writeConfigError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, options)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Update(c *gin.Context) {
|
func (h *Handler) Update(c *gin.Context) {
|
||||||
adminID, ok := currentAdminID(c)
|
adminID, ok := currentAdminID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package systemconfig
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const publishOptionsConfigKey = "listing.publish_options"
|
||||||
|
|
||||||
|
func defaultPublishOptionsConfigValue() string {
|
||||||
|
raw, err := json.Marshal(DefaultPublishOptions())
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultPublishOptions() PublishOptionsDTO {
|
||||||
|
return PublishOptionsDTO{
|
||||||
|
ServerOptions: []string{"QQ", "微信", "Steam"},
|
||||||
|
FaceOptions: []string{"是", "否"},
|
||||||
|
RankOptions: []string{"青铜", "白银", "黄金", "铂金", "钻石", "黑鹰", "三角洲巅峰"},
|
||||||
|
InsuranceOptions: []string{"2*1", "2*2", "2*3", "3*3"},
|
||||||
|
LevelOptions: []string{"1级", "2级", "3级", "4级", "5级", "6级", "7级"},
|
||||||
|
LoginMethodOptions: []string{"扫码", "账号密码"},
|
||||||
|
RegionOptions: []string{
|
||||||
|
"北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江",
|
||||||
|
"上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南",
|
||||||
|
"湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州",
|
||||||
|
"云南", "陕西", "甘肃", "青海", "宁夏", "新疆", "西藏", "香港",
|
||||||
|
"澳门", "台湾",
|
||||||
|
},
|
||||||
|
SkinGroups: []PublishOptionGroup{
|
||||||
|
{
|
||||||
|
Key: "melee",
|
||||||
|
Title: "近战皮肤",
|
||||||
|
Options: []string{
|
||||||
|
"坠星者", "暗星", "龙牙", "信条", "怜悯", "赤枭", "影锋", "黑海", "北极星", "电锯惊魂", "处刑者",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "operator",
|
||||||
|
Title: "干员皮肤",
|
||||||
|
Options: []string{
|
||||||
|
"威龙-凌霄戍卫", "红狼-蚀金玫瑰", "露娜-黑天际线", "蛊-能天使午夜邮差",
|
||||||
|
"骇爪-维什戴尔", "骇爪-水墨云图", "威龙-吴彦祖", "威龙-铁面判官",
|
||||||
|
"威龙-壮志凌云", "威龙-蛟龙特战队", "蜂医-送葬人", "蜂医-危险物质",
|
||||||
|
"蜂医-黑鹰坠落", "牧羊人-街头之星", "牧羊人-黑鹰坠落", "乌鲁鲁-黑鹰坠落",
|
||||||
|
"红狼-黑鹰坠落", "无名-夜鹰", "深蓝-不破誓约", "红狼-电锯惊魂",
|
||||||
|
"露娜-古墓丽影劳拉", "赛伊德",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "weapon",
|
||||||
|
Title: "武器皮肤",
|
||||||
|
Options: []string{
|
||||||
|
"M7棱镜攻势S2", "腾龙气象感应", "MP7电玩高手S2", "M250电玩高手S2",
|
||||||
|
"AS Val悬赏令", "K416命运", "M4A1棱镜攻势", "SCAR-H电玩高手",
|
||||||
|
"QBZ95王牌之剑", "AUG气象感应", "Vector美杜莎", "KC17-造物纪元",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuantityItems: []PublishQuantityItem{
|
||||||
|
{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: "armor6Ammo", Label: "6甲子弹", Price: "0.26元/发"},
|
||||||
|
{Key: "gridCard9", Label: "9格体验卡", Price: "5元/张", Placeholder: "如果是赛季9格,此处填0即可"},
|
||||||
|
},
|
||||||
|
ScreenshotSlots: []PublishScreenshotSlot{
|
||||||
|
{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: "请上传皮肤截图(选传)"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ var defaultConfigs = []defaultConfig{
|
|||||||
{Key: "settlement.platform_fee_rate", Value: "0.00", Description: "平台抽成比例"},
|
{Key: "settlement.platform_fee_rate", Value: "0.00", Description: "平台抽成比例"},
|
||||||
{Key: "settlement.owner_cycle_days", Value: "0", Description: "号主结算周期天数"},
|
{Key: "settlement.owner_cycle_days", Value: "0", Description: "号主结算周期天数"},
|
||||||
{Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"},
|
{Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"},
|
||||||
|
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepository(db *gorm.DB) *Repository {
|
func NewRepository(db *gorm.DB) *Repository {
|
||||||
@@ -59,6 +60,17 @@ func (r *Repository) List() ([]ConfigDTO, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FindValue(key string) (string, error) {
|
||||||
|
if err := r.ensureDefaults(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var row model.SystemConfig
|
||||||
|
if err := r.db.Where("`key` = ?", key).First(&row).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return row.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
func (r *Repository) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
||||||
var row model.SystemConfig
|
var row model.SystemConfig
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package systemconfig
|
package systemconfig
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||||
@@ -22,6 +25,22 @@ func (s *Service) List() ([]ConfigDTO, error) {
|
|||||||
return s.repo.List()
|
return s.repo.List()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) PublishOptions() (*PublishOptionsDTO, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
value, err := s.repo.FindValue(publishOptionsConfigKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
options := DefaultPublishOptions()
|
||||||
|
if err := json.Unmarshal([]byte(value), &options); err != nil {
|
||||||
|
options = DefaultPublishOptions()
|
||||||
|
}
|
||||||
|
normalizePublishOptions(&options)
|
||||||
|
return &options, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
func (s *Service) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
@@ -31,3 +50,37 @@ func (s *Service) Update(actorID uint64, key string, req UpdateRequest, meta Aud
|
|||||||
}
|
}
|
||||||
return s.repo.Update(actorID, key, req, meta)
|
return s.repo.Update(actorID, key, req, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizePublishOptions(options *PublishOptionsDTO) {
|
||||||
|
defaults := DefaultPublishOptions()
|
||||||
|
if len(options.ServerOptions) == 0 {
|
||||||
|
options.ServerOptions = defaults.ServerOptions
|
||||||
|
}
|
||||||
|
if len(options.FaceOptions) == 0 {
|
||||||
|
options.FaceOptions = defaults.FaceOptions
|
||||||
|
}
|
||||||
|
if len(options.RankOptions) == 0 {
|
||||||
|
options.RankOptions = defaults.RankOptions
|
||||||
|
}
|
||||||
|
if len(options.InsuranceOptions) == 0 {
|
||||||
|
options.InsuranceOptions = defaults.InsuranceOptions
|
||||||
|
}
|
||||||
|
if len(options.LevelOptions) == 0 {
|
||||||
|
options.LevelOptions = defaults.LevelOptions
|
||||||
|
}
|
||||||
|
if len(options.LoginMethodOptions) == 0 {
|
||||||
|
options.LoginMethodOptions = defaults.LoginMethodOptions
|
||||||
|
}
|
||||||
|
if len(options.RegionOptions) == 0 {
|
||||||
|
options.RegionOptions = defaults.RegionOptions
|
||||||
|
}
|
||||||
|
if len(options.SkinGroups) == 0 {
|
||||||
|
options.SkinGroups = defaults.SkinGroups
|
||||||
|
}
|
||||||
|
if len(options.QuantityItems) == 0 {
|
||||||
|
options.QuantityItems = defaults.QuantityItems
|
||||||
|
}
|
||||||
|
if len(options.ScreenshotSlots) == 0 {
|
||||||
|
options.ScreenshotSlots = defaults.ScreenshotSlots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
api := engine.Group("/api")
|
api := engine.Group("/api")
|
||||||
{
|
{
|
||||||
api.GET("/health", health.Check)
|
api.GET("/health", health.Check)
|
||||||
|
api.GET("/listing-publish-options", systemConfigHandler.PublishOptions)
|
||||||
|
|
||||||
authRoutes := api.Group("/auth")
|
authRoutes := api.Group("/auth")
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+7
@@ -17,14 +17,21 @@ declare module 'vue' {
|
|||||||
VanCell: typeof import('vant/es')['Cell']
|
VanCell: typeof import('vant/es')['Cell']
|
||||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||||
VanCheckbox: typeof import('vant/es')['Checkbox']
|
VanCheckbox: typeof import('vant/es')['Checkbox']
|
||||||
|
VanDropdownItem: typeof import('vant/es')['DropdownItem']
|
||||||
|
VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
|
||||||
VanEmpty: typeof import('vant/es')['Empty']
|
VanEmpty: typeof import('vant/es')['Empty']
|
||||||
VanField: typeof import('vant/es')['Field']
|
VanField: typeof import('vant/es')['Field']
|
||||||
|
VanGrid: typeof import('vant/es')['Grid']
|
||||||
|
VanGridItem: typeof import('vant/es')['GridItem']
|
||||||
VanIcon: typeof import('vant/es')['Icon']
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
|
VanImage: typeof import('vant/es')['Image']
|
||||||
VanLoading: typeof import('vant/es')['Loading']
|
VanLoading: typeof import('vant/es')['Loading']
|
||||||
VanNoticeBar: typeof import('vant/es')['NoticeBar']
|
VanNoticeBar: typeof import('vant/es')['NoticeBar']
|
||||||
VanPopup: typeof import('vant/es')['Popup']
|
VanPopup: typeof import('vant/es')['Popup']
|
||||||
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
||||||
VanSearch: typeof import('vant/es')['Search']
|
VanSearch: typeof import('vant/es')['Search']
|
||||||
|
VanTabbar: typeof import('vant/es')['Tabbar']
|
||||||
|
VanTabbarItem: typeof import('vant/es')['TabbarItem']
|
||||||
VanTag: typeof import('vant/es')['Tag']
|
VanTag: typeof import('vant/es')['Tag']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { apiClient } from './client'
|
||||||
|
|
||||||
|
export type ChargeMode = '赠送' | '收费'
|
||||||
|
|
||||||
|
export type QuantityKey =
|
||||||
|
| 'awmAmmo'
|
||||||
|
| 'helmet6'
|
||||||
|
| 'armor6'
|
||||||
|
| 'kit5'
|
||||||
|
| 'armor6Ammo'
|
||||||
|
| 'gridCard9'
|
||||||
|
|
||||||
|
export type ScreenshotKey = 'coin' | 'gameId' | 'totalAsset' | 'tencentSecurity' | 'skin'
|
||||||
|
|
||||||
|
export type SkinCategoryKey = 'melee' | 'operator' | 'weapon'
|
||||||
|
|
||||||
|
export interface PublishOptionGroup {
|
||||||
|
key: SkinCategoryKey | string
|
||||||
|
title: string
|
||||||
|
options: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublishQuantityItem {
|
||||||
|
key: QuantityKey
|
||||||
|
label: string
|
||||||
|
price: string
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublishScreenshotSlot {
|
||||||
|
key: ScreenshotKey
|
||||||
|
label: string
|
||||||
|
required: boolean
|
||||||
|
hint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListingPublishOptions {
|
||||||
|
server_options: string[]
|
||||||
|
face_options: string[]
|
||||||
|
rank_options: string[]
|
||||||
|
insurance_options: string[]
|
||||||
|
level_options: string[]
|
||||||
|
login_method_options: string[]
|
||||||
|
region_options: string[]
|
||||||
|
skin_groups: PublishOptionGroup[]
|
||||||
|
quantity_items: PublishQuantityItem[]
|
||||||
|
screenshot_slots: PublishScreenshotSlot[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultListingPublishOptions: ListingPublishOptions = {
|
||||||
|
server_options: ['QQ', '微信', 'Steam'],
|
||||||
|
face_options: ['是', '否'],
|
||||||
|
rank_options: ['青铜', '白银', '黄金', '铂金', '钻石', '黑鹰', '三角洲巅峰'],
|
||||||
|
insurance_options: ['2*1', '2*2', '2*3', '3*3'],
|
||||||
|
level_options: ['1级', '2级', '3级', '4级', '5级', '6级', '7级'],
|
||||||
|
login_method_options: ['扫码', '账号密码'],
|
||||||
|
region_options: [
|
||||||
|
'北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江',
|
||||||
|
'上海', '江苏', '浙江', '安徽', '福建', '江西', '山东', '河南',
|
||||||
|
'湖北', '湖南', '广东', '广西', '海南', '重庆', '四川', '贵州',
|
||||||
|
'云南', '陕西', '甘肃', '青海', '宁夏', '新疆', '西藏', '香港',
|
||||||
|
'澳门', '台湾',
|
||||||
|
],
|
||||||
|
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: 'armor6Ammo', 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() {
|
||||||
|
const { data } = await apiClient.get<ApiResponse<ListingPublishOptions>>('/listing-publish-options')
|
||||||
|
return mergeListingPublishOptions(data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeListingPublishOptions(options?: Partial<ListingPublishOptions>): ListingPublishOptions {
|
||||||
|
return {
|
||||||
|
server_options: options?.server_options?.length ? options.server_options : defaultListingPublishOptions.server_options,
|
||||||
|
face_options: options?.face_options?.length ? options.face_options : defaultListingPublishOptions.face_options,
|
||||||
|
rank_options: options?.rank_options?.length ? options.rank_options : defaultListingPublishOptions.rank_options,
|
||||||
|
insurance_options: options?.insurance_options?.length ? options.insurance_options : defaultListingPublishOptions.insurance_options,
|
||||||
|
level_options: options?.level_options?.length ? options.level_options : defaultListingPublishOptions.level_options,
|
||||||
|
login_method_options: options?.login_method_options?.length ? options.login_method_options : defaultListingPublishOptions.login_method_options,
|
||||||
|
region_options: options?.region_options?.length ? options.region_options : defaultListingPublishOptions.region_options,
|
||||||
|
skin_groups: options?.skin_groups?.length ? options.skin_groups : defaultListingPublishOptions.skin_groups,
|
||||||
|
quantity_items: options?.quantity_items?.length ? options.quantity_items as PublishQuantityItem[] : defaultListingPublishOptions.quantity_items,
|
||||||
|
screenshot_slots: options?.screenshot_slots?.length ? options.screenshot_slots as PublishScreenshotSlot[] : defaultListingPublishOptions.screenshot_slots,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultListingPublishOptions,
|
||||||
|
mergeListingPublishOptions,
|
||||||
|
type ListingPublishOptions,
|
||||||
|
} from '@/api/listingOptions'
|
||||||
import { fetchSystemConfigs, updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
import { fetchSystemConfigs, updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -10,6 +15,32 @@ 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 publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || null)
|
||||||
|
const regularConfigs = computed(() => configs.value.filter((item) => item.key !== 'listing.publish_options'))
|
||||||
|
const isPublishOptionsConfig = computed(() => activeConfig.value?.key === 'listing.publish_options')
|
||||||
|
const publishStats = computed(() => {
|
||||||
|
const options = safeParsePublishOptions(publishConfig.value?.value || '')
|
||||||
|
return {
|
||||||
|
baseCount:
|
||||||
|
options.server_options.length +
|
||||||
|
options.rank_options.length +
|
||||||
|
options.insurance_options.length +
|
||||||
|
options.level_options.length,
|
||||||
|
skinCount: options.skin_groups.reduce((sum, group) => sum + group.options.length, 0),
|
||||||
|
resourceCount: options.quantity_items.length,
|
||||||
|
screenshotCount: options.screenshot_slots.length,
|
||||||
|
regionCount: options.region_options.length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isStructuredConfig = computed(() => {
|
||||||
|
const trimmed = value.value.trim()
|
||||||
|
return isPublishOptionsConfig.value || trimmed.startsWith('{') || trimmed.startsWith('[')
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogWidth = computed(() => (isPublishOptionsConfig.value ? '920px' : '560px'))
|
||||||
|
|
||||||
onMounted(loadConfigs)
|
onMounted(loadConfigs)
|
||||||
|
|
||||||
@@ -26,12 +57,18 @@ function openEdit(row: SystemConfig) {
|
|||||||
activeConfig.value = row
|
activeConfig.value = row
|
||||||
value.value = row.value
|
value.value = row.value
|
||||||
description.value = row.description
|
description.value = row.description
|
||||||
|
if (row.key === 'listing.publish_options') {
|
||||||
|
publishOptionsDraft.value = parsePublishOptions(row.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!activeConfig.value) return
|
if (!activeConfig.value) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
|
if (isPublishOptionsConfig.value) {
|
||||||
|
value.value = JSON.stringify(publishOptionsDraft.value, null, 2)
|
||||||
|
}
|
||||||
await updateSystemConfig(activeConfig.value.key, {
|
await updateSystemConfig(activeConfig.value.key, {
|
||||||
value: value.value,
|
value: value.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
@@ -46,6 +83,100 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePublishOptions(raw: string) {
|
||||||
|
try {
|
||||||
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultListingPublishOptions
|
||||||
|
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('发布选项 JSON 解析失败,已使用默认配置')
|
||||||
|
return cloneOptions(defaultListingPublishOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParsePublishOptions(raw: string) {
|
||||||
|
try {
|
||||||
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultListingPublishOptions
|
||||||
|
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||||
|
} catch {
|
||||||
|
return cloneOptions(defaultListingPublishOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneOptions(options: ListingPublishOptions) {
|
||||||
|
return JSON.parse(JSON.stringify(options)) as ListingPublishOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
function linesToItems(value: string) {
|
||||||
|
return value
|
||||||
|
.split('\n')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
publishOptionsDraft.value[key] = linesToItems(nextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSkinGroupOptions(index: number, nextValue: string) {
|
||||||
|
const group = publishOptionsDraft.value.skin_groups[index]
|
||||||
|
if (group) {
|
||||||
|
group.options = linesToItems(nextValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSkinGroup() {
|
||||||
|
publishOptionsDraft.value.skin_groups.push({
|
||||||
|
key: `group_${Date.now()}`,
|
||||||
|
title: '新皮肤分类',
|
||||||
|
options: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSkinGroup(index: number) {
|
||||||
|
publishOptionsDraft.value.skin_groups.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQuantityItem() {
|
||||||
|
publishOptionsDraft.value.quantity_items.push({
|
||||||
|
key: `item_${Date.now()}` as never,
|
||||||
|
label: '',
|
||||||
|
price: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeQuantityItem(index: number) {
|
||||||
|
publishOptionsDraft.value.quantity_items.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScreenshotSlot() {
|
||||||
|
publishOptionsDraft.value.screenshot_slots.push({
|
||||||
|
key: `screenshot_${Date.now()}` as never,
|
||||||
|
label: '',
|
||||||
|
required: false,
|
||||||
|
hint: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScreenshotSlot(index: number) {
|
||||||
|
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPublishOptions() {
|
||||||
|
publishOptionsDraft.value = cloneOptions(defaultListingPublishOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatConfigValue(row: SystemConfig) {
|
||||||
|
const trimmed = row.value.trim()
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
return 'JSON 配置'
|
||||||
|
}
|
||||||
|
return row.value
|
||||||
|
}
|
||||||
|
|
||||||
function readError(error: unknown, fallback: string) {
|
function readError(error: unknown, fallback: string) {
|
||||||
if (typeof error === 'object' && error && 'response' in error) {
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
@@ -63,9 +194,47 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<p>管理交接超时、归还超时、短信限流、最低押金和抽成比例。</p>
|
<p>管理交接超时、归还超时、短信限流、最低押金和抽成比例。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="configs">
|
<section v-if="publishConfig" class="publish-config-panel">
|
||||||
|
<div class="publish-config-main">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Publish Options</p>
|
||||||
|
<h2>发布表单选项</h2>
|
||||||
|
<span>管理移动端发布页的区服、段位、皮肤、资源道具、截图材料和地区选项。</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="openEdit(publishConfig)">编辑发布选项</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat-grid">
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ publishStats.baseCount }}</strong>
|
||||||
|
<span>基础选项</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ publishStats.skinCount }}</strong>
|
||||||
|
<span>皮肤项</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ publishStats.resourceCount }}</strong>
|
||||||
|
<span>资源道具</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ publishStats.screenshotCount }}</strong>
|
||||||
|
<span>截图材料</span>
|
||||||
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ publishStats.regionCount }}</strong>
|
||||||
|
<span>地区</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" class="table-panel" :data="regularConfigs">
|
||||||
<el-table-column prop="key" label="配置项" min-width="260" />
|
<el-table-column prop="key" label="配置项" min-width="260" />
|
||||||
<el-table-column prop="value" label="当前值" width="150" />
|
<el-table-column label="当前值" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="formatConfigValue(row) === 'JSON 配置'" type="info">JSON 配置</el-tag>
|
||||||
|
<span v-else class="config-value">{{ formatConfigValue(row) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="description" label="说明" min-width="260" show-overflow-tooltip />
|
<el-table-column prop="description" label="说明" min-width="260" show-overflow-tooltip />
|
||||||
<el-table-column prop="updated_by" label="更新人" width="100" />
|
<el-table-column prop="updated_by" label="更新人" width="100" />
|
||||||
<el-table-column prop="updated_at" label="更新时间" min-width="180" />
|
<el-table-column prop="updated_at" label="更新时间" min-width="180" />
|
||||||
@@ -76,10 +245,157 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-dialog :model-value="!!activeConfig" title="编辑系统配置" width="560px" @update:model-value="activeConfig = null">
|
<el-dialog :model-value="!!activeConfig" title="编辑系统配置" :width="dialogWidth" @update:model-value="activeConfig = null">
|
||||||
<div v-if="activeConfig" class="dialog-body">
|
<div v-if="activeConfig" class="dialog-body">
|
||||||
<p><strong>{{ activeConfig.key }}</strong></p>
|
<p><strong>{{ activeConfig.key }}</strong></p>
|
||||||
<el-input v-model="value" placeholder="配置值" />
|
|
||||||
|
<div v-if="isPublishOptionsConfig" class="publish-options-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span>发布页选项配置</span>
|
||||||
|
<el-button size="small" @click="resetPublishOptions">恢复默认选项</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-grid">
|
||||||
|
<el-form-item label="区服">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.server_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('server_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="段位">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.rank_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('rank_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="保险">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.insurance_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('insurance_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="体力/负重">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.level_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('level_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="人脸选项">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.face_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('face_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="上号方式">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.login_method_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('login_method_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="常用登录地区">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.region_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
placeholder="一行一个省市"
|
||||||
|
@update:model-value="updateOptionLines('region_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>皮肤分类</strong>
|
||||||
|
<el-button size="small" @click="addSkinGroup">添加分类</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-for="(group, index) in publishOptionsDraft.skin_groups" :key="`${group.key}-${index}`" class="skin-config-row">
|
||||||
|
<el-input v-model="group.key" placeholder="分类 key" />
|
||||||
|
<el-input v-model="group.title" placeholder="分类名称" />
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(group.options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="皮肤名称,一行一个"
|
||||||
|
@update:model-value="updateSkinGroupOptions(index, $event)"
|
||||||
|
/>
|
||||||
|
<el-button type="danger" plain @click="removeSkinGroup(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>资源道具</strong>
|
||||||
|
<el-button size="small" @click="addQuantityItem">添加道具</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.quantity_items" size="small" border>
|
||||||
|
<el-table-column label="Key" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="名称" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格" min-width="130">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.price" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="提示" min-width="220">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.placeholder" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeQuantityItem($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>截图材料</strong>
|
||||||
|
<el-button size="small" @click="addScreenshotSlot">添加截图项</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.screenshot_slots" size="small" border>
|
||||||
|
<el-table-column label="Key" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="名称" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="必填" width="90">
|
||||||
|
<template #default="{ row }"><el-switch v-model="row.required" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="提示" min-width="260">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.hint" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeScreenshotSlot($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-input v-else-if="isStructuredConfig" v-model="value" type="textarea" :rows="12" placeholder="配置值 JSON" />
|
||||||
|
<el-input v-else v-model="value" placeholder="配置值" />
|
||||||
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -89,3 +405,101 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.publish-config-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-config-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-config-main h2 {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-config-main span {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-stat {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-stat strong {
|
||||||
|
color: #1477ff;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-stat span,
|
||||||
|
.config-value {
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-options-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar,
|
||||||
|
.editor-block-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar span {
|
||||||
|
color: #30343a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-config-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.publish-stat-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,504 @@
|
|||||||
|
.mobile-publish {
|
||||||
|
min-height: 100dvh;
|
||||||
|
background: #f4f6f8;
|
||||||
|
padding-bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #eceff3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: grid;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
place-items: center;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #25282d;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-spacer {
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 12px 14px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #edf0f4;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #171a1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
margin: -2px 0 8px;
|
||||||
|
color: #858c96;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-field :deep(.van-field__label) {
|
||||||
|
width: 88px;
|
||||||
|
color: #30343a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-field :deep(.van-field__control) {
|
||||||
|
color: #20242a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group,
|
||||||
|
.multi-chip-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group.small {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-btn {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: 1px solid #d7dce3;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff;
|
||||||
|
color: #5c6470;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-btn.active {
|
||||||
|
border-color: #1477ff;
|
||||||
|
background: #1477ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-row,
|
||||||
|
.result-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 58px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-label {
|
||||||
|
color: #30343a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-label span {
|
||||||
|
margin-right: 2px;
|
||||||
|
color: #ee0a24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn {
|
||||||
|
min-width: 0;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #d7dce3;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #fff;
|
||||||
|
color: #5c6470;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn.active {
|
||||||
|
border-color: #1477ff;
|
||||||
|
background: #1477ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 92px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 72px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-meta {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-meta strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #30343a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-meta span {
|
||||||
|
margin-right: 2px;
|
||||||
|
color: #ee0a24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-meta small {
|
||||||
|
color: #858c96;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid #e0e5ec;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #20242a;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input:focus {
|
||||||
|
border-color: #1477ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle button {
|
||||||
|
border: 1px solid #d7dce3;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #606873;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle button.active {
|
||||||
|
border-color: #ff6a00;
|
||||||
|
background: #fff3ea;
|
||||||
|
color: #e65f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-group {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-group-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #30343a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-group-title small {
|
||||||
|
color: #858c96;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #30343a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-title small {
|
||||||
|
color: #858c96;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn {
|
||||||
|
min-width: 0;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 2px;
|
||||||
|
border: 1px solid #d7dce3;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff;
|
||||||
|
color: #5c6470;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn.active {
|
||||||
|
border-color: #1477ff;
|
||||||
|
background: #1477ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 86px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #edf0f4;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-meta strong {
|
||||||
|
color: #242830;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-meta span {
|
||||||
|
color: #f04438;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-meta small {
|
||||||
|
color: #858c96;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-add,
|
||||||
|
.upload-preview {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-add {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px dashed #c7cdd6;
|
||||||
|
background: #fff;
|
||||||
|
color: #7b8490;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-add:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #e8ecf1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px 0 0;
|
||||||
|
background: rgba(0, 0, 0, 0.62);
|
||||||
|
color: #fff;
|
||||||
|
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) {
|
||||||
|
color: #ff6a00;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: none !important;
|
||||||
|
background: #ff6a00 !important;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
height: 50px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: #1477ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-pill {
|
||||||
|
display: grid;
|
||||||
|
width: 36px;
|
||||||
|
height: 26px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 13px;
|
||||||
|
background: #ff6a00;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-publish span,
|
||||||
|
.nav-publish.active span {
|
||||||
|
color: #ff6a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 430px) {
|
||||||
|
.time-row,
|
||||||
|
.result-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 370px) {
|
||||||
|
.level-options,
|
||||||
|
.region-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { showToast } from "vant";
|
import { showToast } from "vant";
|
||||||
|
|
||||||
import { uploadFile } from "@/api/files";
|
import { uploadFile } from "@/api/files";
|
||||||
|
import {
|
||||||
|
defaultListingPublishOptions,
|
||||||
|
fetchListingPublishOptions,
|
||||||
|
type ChargeMode,
|
||||||
|
type ListingPublishOptions,
|
||||||
|
type QuantityKey,
|
||||||
|
type ScreenshotKey,
|
||||||
|
} from "@/api/listingOptions";
|
||||||
import { createListing } from "@/api/listings";
|
import { createListing } from "@/api/listings";
|
||||||
|
|
||||||
type ChargeMode = "赠送" | "收费";
|
|
||||||
|
|
||||||
type QuantityKey =
|
|
||||||
| "awmAmmo"
|
|
||||||
| "helmet6"
|
|
||||||
| "armor6"
|
|
||||||
| "kit5"
|
|
||||||
| "armor6Ammo"
|
|
||||||
| "gridCard9";
|
|
||||||
|
|
||||||
type ScreenshotKey =
|
|
||||||
| "coin"
|
|
||||||
| "gameId"
|
|
||||||
| "totalAsset"
|
|
||||||
| "tencentSecurity"
|
|
||||||
| "skin";
|
|
||||||
|
|
||||||
type SkinCategoryKey = "melee" | "operator" | "weapon";
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
|
const publishOptions = ref<ListingPublishOptions>(defaultListingPublishOptions);
|
||||||
|
|
||||||
function isNavActive(path: string) {
|
function isNavActive(path: string) {
|
||||||
if (path === "/m") return route.path === "/m";
|
if (path === "/m") return route.path === "/m";
|
||||||
@@ -89,166 +79,31 @@ const selectedSkins = ref<string[]>([]);
|
|||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
const activeUploadKey = ref<ScreenshotKey>("coin");
|
const activeUploadKey = ref<ScreenshotKey>("coin");
|
||||||
|
|
||||||
const serverOptions = ["QQ", "微信", "Steam"];
|
const serverOptions = computed(() => publishOptions.value.server_options);
|
||||||
const faceOptions = ["是", "否"];
|
const faceOptions = computed(() => publishOptions.value.face_options);
|
||||||
const rankOptions = [
|
const rankOptions = computed(() => publishOptions.value.rank_options);
|
||||||
"青铜",
|
const insuranceOptions = computed(() => publishOptions.value.insurance_options);
|
||||||
"白银",
|
const levelOptions = computed(() => publishOptions.value.level_options);
|
||||||
"黄金",
|
const loginMethodOptions = computed(
|
||||||
"铂金",
|
() => publishOptions.value.login_method_options
|
||||||
"钻石",
|
|
||||||
"黑鹰",
|
|
||||||
"三角洲巅峰",
|
|
||||||
];
|
|
||||||
const insuranceOptions = ["2*1", "2*2", "2*3", "3*3"];
|
|
||||||
const levelOptions = ["1级", "2级", "3级", "4级", "5级", "6级", "7级"];
|
|
||||||
const loginMethodOptions = ["扫码", "账号密码"];
|
|
||||||
const regionOptions = [
|
|
||||||
"北京",
|
|
||||||
"天津",
|
|
||||||
"河北",
|
|
||||||
"山西",
|
|
||||||
"内蒙古",
|
|
||||||
"辽宁",
|
|
||||||
"吉林",
|
|
||||||
"黑龙江",
|
|
||||||
"上海",
|
|
||||||
"江苏",
|
|
||||||
"浙江",
|
|
||||||
"安徽",
|
|
||||||
"福建",
|
|
||||||
"江西",
|
|
||||||
"山东",
|
|
||||||
"河南",
|
|
||||||
"湖北",
|
|
||||||
"湖南",
|
|
||||||
"广东",
|
|
||||||
"广西",
|
|
||||||
"海南",
|
|
||||||
"重庆",
|
|
||||||
"四川",
|
|
||||||
"贵州",
|
|
||||||
"云南",
|
|
||||||
"陕西",
|
|
||||||
"甘肃",
|
|
||||||
"青海",
|
|
||||||
"宁夏",
|
|
||||||
"新疆",
|
|
||||||
"西藏",
|
|
||||||
"香港",
|
|
||||||
"澳门",
|
|
||||||
"台湾",
|
|
||||||
];
|
|
||||||
const skinGroups: Array<{
|
|
||||||
key: SkinCategoryKey;
|
|
||||||
title: string;
|
|
||||||
options: string[];
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
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-造物纪元",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const quantityItems: Array<{
|
|
||||||
key: QuantityKey;
|
|
||||||
label: string;
|
|
||||||
price: string;
|
|
||||||
placeholder?: string;
|
|
||||||
}> = [
|
|
||||||
{ 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: "armor6Ammo", label: "6甲子弹", price: "0.26元/发" },
|
|
||||||
{
|
|
||||||
key: "gridCard9",
|
|
||||||
label: "9格体验卡",
|
|
||||||
price: "5元/张",
|
|
||||||
placeholder: "如果是赛季9格,此处填0即可",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const screenshotSlots: Array<{
|
|
||||||
key: ScreenshotKey;
|
|
||||||
label: string;
|
|
||||||
required: boolean;
|
|
||||||
hint: string;
|
|
||||||
}> = [
|
|
||||||
{ 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: "请上传皮肤截图(选传)" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const screenshotUrls = computed(() =>
|
|
||||||
screenshotSlots.map((item) => screenshotFiles[item.key]).filter(Boolean)
|
|
||||||
);
|
);
|
||||||
|
const regionOptions = computed(() => publishOptions.value.region_options);
|
||||||
|
const skinGroups = computed(() => publishOptions.value.skin_groups);
|
||||||
|
const quantityItems = computed(() => publishOptions.value.quantity_items);
|
||||||
|
const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
|
||||||
|
const screenshotUrls = computed(() =>
|
||||||
|
screenshotSlots.value.map((item) => screenshotFiles[item.key]).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(loadPublishOptions);
|
||||||
|
|
||||||
|
async function loadPublishOptions() {
|
||||||
|
try {
|
||||||
|
publishOptions.value = await fetchListingPublishOptions();
|
||||||
|
} catch {
|
||||||
|
publishOptions.value = defaultListingPublishOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
|
||||||
setter(value);
|
setter(value);
|
||||||
@@ -380,7 +235,7 @@ function validateForm() {
|
|||||||
if (!form.final_price || !form.settlement_amount || !form.rent_days) {
|
if (!form.final_price || !form.settlement_amount || !form.rent_days) {
|
||||||
return "请先点击计算价格";
|
return "请先点击计算价格";
|
||||||
}
|
}
|
||||||
for (const item of screenshotSlots) {
|
for (const item of screenshotSlots.value) {
|
||||||
if (item.required && !screenshotFiles[item.key]) {
|
if (item.required && !screenshotFiles[item.key]) {
|
||||||
return `请上传${item.label}`;
|
return `请上传${item.label}`;
|
||||||
}
|
}
|
||||||
@@ -397,14 +252,14 @@ function buildAssetSummary() {
|
|||||||
season_insurance: form.season_insurance,
|
season_insurance: form.season_insurance,
|
||||||
stamina_level: form.stamina_level,
|
stamina_level: form.stamina_level,
|
||||||
load_level: form.load_level,
|
load_level: form.load_level,
|
||||||
resources: quantityItems.map((item) => ({
|
resources: quantityItems.value.map((item) => ({
|
||||||
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],
|
||||||
})),
|
})),
|
||||||
skins: selectedSkins.value,
|
skins: selectedSkins.value,
|
||||||
skin_groups: skinGroups.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) =>
|
||||||
selectedSkins.value.includes(skin)
|
selectedSkins.value.includes(skin)
|
||||||
);
|
);
|
||||||
@@ -875,509 +730,4 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="./MobileSellerListingCreateView.css"></style>
|
||||||
.mobile-publish {
|
|
||||||
min-height: 100dvh;
|
|
||||||
background: #f4f6f8;
|
|
||||||
padding-bottom: 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 48px;
|
|
||||||
padding: 0 12px;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid #eceff3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h1 {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
display: grid;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
place-items: center;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: #25282d;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-spacer {
|
|
||||||
width: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-body {
|
|
||||||
padding: 12px 14px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
padding: 14px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #edf0f4;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #171a1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-hint {
|
|
||||||
margin: -2px 0 8px;
|
|
||||||
color: #858c96;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-field {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fafc;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-field :deep(.van-field__label) {
|
|
||||||
width: 88px;
|
|
||||||
color: #30343a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-field :deep(.van-field__control) {
|
|
||||||
color: #20242a;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-group,
|
|
||||||
.multi-chip-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-group.small {
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-btn {
|
|
||||||
min-height: 28px;
|
|
||||||
padding: 5px 12px;
|
|
||||||
border: 1px solid #d7dce3;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #fff;
|
|
||||||
color: #5c6470;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-btn.active {
|
|
||||||
border-color: #1477ff;
|
|
||||||
background: #1477ff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-row,
|
|
||||||
.result-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 58px minmax(0, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-label {
|
|
||||||
color: #30343a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-label span {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: #ee0a24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-options {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-btn {
|
|
||||||
min-width: 0;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid #d7dce3;
|
|
||||||
border-radius: 15px;
|
|
||||||
background: #fff;
|
|
||||||
color: #5c6470;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-btn.active {
|
|
||||||
border-color: #1477ff;
|
|
||||||
background: #1477ff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 92px;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: stretch;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-card {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 72px;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 72px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-meta {
|
|
||||||
display: grid;
|
|
||||||
min-width: 0;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-meta strong {
|
|
||||||
overflow: hidden;
|
|
||||||
color: #30343a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.25;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-meta span {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: #ee0a24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-meta small {
|
|
||||||
color: #858c96;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 8px;
|
|
||||||
border: 1px solid #e0e5ec;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
color: #20242a;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input:focus {
|
|
||||||
border-color: #1477ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-toggle {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 6px;
|
|
||||||
min-height: 72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-toggle button {
|
|
||||||
border: 1px solid #d7dce3;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
color: #606873;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-toggle button.active {
|
|
||||||
border-color: #ff6a00;
|
|
||||||
background: #fff3ea;
|
|
||||||
color: #e65f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skin-groups {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skin-group {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skin-group-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #30343a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skin-group-title small {
|
|
||||||
color: #858c96;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: #30343a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-title small {
|
|
||||||
color: #858c96;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-btn {
|
|
||||||
min-width: 0;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 2px;
|
|
||||||
border: 1px solid #d7dce3;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #fff;
|
|
||||||
color: #5c6470;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-btn.active {
|
|
||||||
border-color: #1477ff;
|
|
||||||
background: #1477ff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-line {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 86px;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 76px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #edf0f4;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-meta {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-meta strong {
|
|
||||||
color: #242830;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-meta span {
|
|
||||||
color: #f04438;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-meta small {
|
|
||||||
color: #858c96;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-add,
|
|
||||||
.upload-preview {
|
|
||||||
width: 76px;
|
|
||||||
height: 76px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-add {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 1px dashed #c7cdd6;
|
|
||||||
background: #fff;
|
|
||||||
color: #7b8490;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-add:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-preview {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #e8ecf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-preview img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-preview button {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px 0 0;
|
|
||||||
background: rgba(0, 0, 0, 0.62);
|
|
||||||
color: #fff;
|
|
||||||
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) {
|
|
||||||
color: #ff6a00;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
height: 44px;
|
|
||||||
margin-top: 8px;
|
|
||||||
border: none !important;
|
|
||||||
background: #ff6a00 !important;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-nav {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
height: 50px;
|
|
||||||
background: #fff;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2px;
|
|
||||||
color: #999;
|
|
||||||
font-size: 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
color: #1477ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item span {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-pill {
|
|
||||||
display: grid;
|
|
||||||
width: 36px;
|
|
||||||
height: 26px;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 13px;
|
|
||||||
background: #ff6a00;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-publish span,
|
|
||||||
.nav-publish.active span {
|
|
||||||
color: #ff6a00;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 430px) {
|
|
||||||
.time-row,
|
|
||||||
.result-grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 370px) {
|
|
||||||
.level-options,
|
|
||||||
.region-grid {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user