优化后端可以编辑皮肤等等
This commit is contained in:
@@ -16,3 +16,36 @@ type UpdateRequest struct {
|
||||
Value string `json:"value" binding:"required"`
|
||||
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})
|
||||
}
|
||||
|
||||
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) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
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.owner_cycle_days", Value: "0", Description: "号主结算周期天数"},
|
||||
{Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"},
|
||||
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
@@ -59,6 +60,17 @@ func (r *Repository) List() ([]ConfigDTO, error) {
|
||||
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) {
|
||||
var row model.SystemConfig
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package systemconfig
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
@@ -22,6 +25,22 @@ func (s *Service) List() ([]ConfigDTO, error) {
|
||||
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) {
|
||||
if s.repo == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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.GET("/health", health.Check)
|
||||
api.GET("/listing-publish-options", systemConfigHandler.PublishOptions)
|
||||
|
||||
authRoutes := api.Group("/auth")
|
||||
{
|
||||
|
||||
Vendored
+7
@@ -17,14 +17,21 @@ declare module 'vue' {
|
||||
VanCell: typeof import('vant/es')['Cell']
|
||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||
VanCheckbox: typeof import('vant/es')['Checkbox']
|
||||
VanDropdownItem: typeof import('vant/es')['DropdownItem']
|
||||
VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
|
||||
VanEmpty: typeof import('vant/es')['Empty']
|
||||
VanField: typeof import('vant/es')['Field']
|
||||
VanGrid: typeof import('vant/es')['Grid']
|
||||
VanGridItem: typeof import('vant/es')['GridItem']
|
||||
VanIcon: typeof import('vant/es')['Icon']
|
||||
VanImage: typeof import('vant/es')['Image']
|
||||
VanLoading: typeof import('vant/es')['Loading']
|
||||
VanNoticeBar: typeof import('vant/es')['NoticeBar']
|
||||
VanPopup: typeof import('vant/es')['Popup']
|
||||
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
||||
VanSearch: typeof import('vant/es')['Search']
|
||||
VanTabbar: typeof import('vant/es')['Tabbar']
|
||||
VanTabbarItem: typeof import('vant/es')['TabbarItem']
|
||||
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">
|
||||
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'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -10,6 +15,32 @@ const configs = ref<SystemConfig[]>([])
|
||||
const activeConfig = ref<SystemConfig | null>(null)
|
||||
const value = 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)
|
||||
|
||||
@@ -26,12 +57,18 @@ function openEdit(row: SystemConfig) {
|
||||
activeConfig.value = row
|
||||
value.value = row.value
|
||||
description.value = row.description
|
||||
if (row.key === 'listing.publish_options') {
|
||||
publishOptionsDraft.value = parsePublishOptions(row.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!activeConfig.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isPublishOptionsConfig.value) {
|
||||
value.value = JSON.stringify(publishOptionsDraft.value, null, 2)
|
||||
}
|
||||
await updateSystemConfig(activeConfig.value.key, {
|
||||
value: value.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) {
|
||||
if (typeof error === 'object' && error && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||
@@ -63,9 +194,47 @@ function readError(error: unknown, fallback: string) {
|
||||
<p>管理交接超时、归还超时、短信限流、最低押金和抽成比例。</p>
|
||||
</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="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="updated_by" label="更新人" width="100" />
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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="配置说明" />
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -89,3 +405,101 @@ function readError(error: unknown, fallback: string) {
|
||||
</el-dialog>
|
||||
</section>
|
||||
</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">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
|
||||
import { uploadFile } from "@/api/files";
|
||||
import {
|
||||
defaultListingPublishOptions,
|
||||
fetchListingPublishOptions,
|
||||
type ChargeMode,
|
||||
type ListingPublishOptions,
|
||||
type QuantityKey,
|
||||
type ScreenshotKey,
|
||||
} from "@/api/listingOptions";
|
||||
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 route = useRoute();
|
||||
const loading = ref(false);
|
||||
const uploading = ref(false);
|
||||
const publishOptions = ref<ListingPublishOptions>(defaultListingPublishOptions);
|
||||
|
||||
function isNavActive(path: string) {
|
||||
if (path === "/m") return route.path === "/m";
|
||||
@@ -89,166 +79,31 @@ const selectedSkins = ref<string[]>([]);
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const activeUploadKey = ref<ScreenshotKey>("coin");
|
||||
|
||||
const serverOptions = ["QQ", "微信", "Steam"];
|
||||
const faceOptions = ["是", "否"];
|
||||
const rankOptions = [
|
||||
"青铜",
|
||||
"白银",
|
||||
"黄金",
|
||||
"铂金",
|
||||
"钻石",
|
||||
"黑鹰",
|
||||
"三角洲巅峰",
|
||||
];
|
||||
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 serverOptions = computed(() => publishOptions.value.server_options);
|
||||
const faceOptions = computed(() => publishOptions.value.face_options);
|
||||
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 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) {
|
||||
setter(value);
|
||||
@@ -380,7 +235,7 @@ function validateForm() {
|
||||
if (!form.final_price || !form.settlement_amount || !form.rent_days) {
|
||||
return "请先点击计算价格";
|
||||
}
|
||||
for (const item of screenshotSlots) {
|
||||
for (const item of screenshotSlots.value) {
|
||||
if (item.required && !screenshotFiles[item.key]) {
|
||||
return `请上传${item.label}`;
|
||||
}
|
||||
@@ -397,14 +252,14 @@ function buildAssetSummary() {
|
||||
season_insurance: form.season_insurance,
|
||||
stamina_level: form.stamina_level,
|
||||
load_level: form.load_level,
|
||||
resources: quantityItems.map((item) => ({
|
||||
resources: quantityItems.value.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
quantity: Number(quantityValues[item.key] || 0),
|
||||
mode: quantityModes[item.key],
|
||||
})),
|
||||
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) =>
|
||||
selectedSkins.value.includes(skin)
|
||||
);
|
||||
@@ -875,509 +730,4 @@ function readError(error: unknown, fallback: string) {
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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>
|
||||
<style scoped src="./MobileSellerListingCreateView.css"></style>
|
||||
|
||||
Reference in New Issue
Block a user