Files
hfb_sys/frontend/src/features/listings/api/listingOptions.ts
T
yml2213andClaude Opus 4.7 405abfa4f7 feat: P2阶段完成 - listings和auth模块迁移
## P2.1: 商品浏览模块(listings)

### 完整迁移(23个文件)
- API: listings.ts, listingOptions.ts, homeConfig.ts
- Views: 5个页面(HomeView, ListingsView, ListingDetailView + 移动端2个)
- Composables: 3个(useHomeFilters, useFilterOptions, useListingQuery)
- Components: 9个(ListingCard, 各种过滤器组件)
- Tests: 2个测试文件

**功能:**
- 首页浏览、商品列表、详情查看
- 高级筛选(服务器、等级、哈夫币、皮肤等)
- 排序和分区功能
- 横幅、公告、统计展示

**技术改进:**
- 更新所有导入路径到 @/shared/
- 建立清晰的模块导出

---

## P2.2: 用户认证模块(auth)

### 完整迁移(12个文件)
- API: auth.ts, realname.ts, notifications.ts
- Views: 8个页面(登录、注册、个人资料、实名认证、通知 + 移动端)
- 模块导出

**功能:**
- 用户注册、登录、Token管理
- 实名认证、风险检查
- 个人资料编辑、头像上传
- 消息/通知中心

**技术改进:**
- 统一 API 导入路径
- 类型安全增强

---

## 里程碑

🎉 **P2 阶段完成!**

**累计完成:** 6个模块
-  P0: shared(基础设施)- 22个文件
-  P1: wallet, chats, orders - 24个文件
-  P2: listings, auth - 35个文件

**总计:** 81个文件已迁移

**剩余:** P3阶段(seller, disputes, admin)+ 清理工作

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 08:56:42 +08:00

399 lines
13 KiB
TypeScript

import { apiClient } from '@/shared/api/client'
import type { ApiResponse } from '@/shared/types/types'
export type ChargeMode = '赠送' | '收费'
export type QuantityKey = string
export type ScreenshotKey = string
export type SkinCategoryKey = string
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 PublishPriceConfig {
deposit_placeholder: string
price_placeholder: string
ratio_description: string
}
export interface PublishDepositSkinGroupRule {
group_key: string
label: string
amount_per_item: number
}
export interface PublishDepositRecommendConfig {
base_amount: number
skin_group_rules: PublishDepositSkinGroupRule[]
}
export interface PublishInsuranceBaseRatio {
insurance: string
ratio: number
}
export interface PublishRatioConfigItem {
key: string
label: string
kind: string
group_key?: string
missing_penalty: number
}
export interface PublishCoinCorrection {
threshold_m: number
correction: number
}
export interface PublishSaleFixedMarkupRule {
min_m: number
max_m: number
markup_amount: number
}
export interface PublishSaleRatioAdjustmentRule {
min_m: number
max_m: number
ratio_subtract: number
}
export interface PublishRatioConfig {
insurance_base_ratios: PublishInsuranceBaseRatio[]
config_items: PublishRatioConfigItem[]
coin_corrections: PublishCoinCorrection[]
}
export interface PublishSalePriceConfig {
fixed_markup_rules: PublishSaleFixedMarkupRule[]
ratio_adjustment_rules: PublishSaleRatioAdjustmentRule[]
}
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[]
ban_record_options: string[]
ban_evidence_options: string[]
fire_level_min: number
price_config: PublishPriceConfig
deposit_recommend_config: PublishDepositRecommendConfig
ratio_config: PublishRatioConfig
}
export const emptyListingPublishOptions: ListingPublishOptions = {
server_options: [],
face_options: [],
rank_options: [],
insurance_options: [],
level_options: [],
login_method_options: [],
region_options: [],
skin_groups: [],
quantity_items: [],
screenshot_slots: [],
ban_record_options: [],
ban_evidence_options: [],
fire_level_min: 38,
price_config: {
deposit_placeholder: '',
price_placeholder: '',
ratio_description: '',
},
deposit_recommend_config: {
base_amount: 50,
skin_group_rules: [
{ group_key: 'melee', label: '刀皮', amount_per_item: 5 },
{ group_key: 'operatorGold', label: '干员金皮', amount_per_item: 10 },
{ group_key: 'operatorRed', label: '干员红皮', amount_per_item: 30 },
],
},
ratio_config: {
insurance_base_ratios: [],
config_items: [],
coin_corrections: [],
},
}
export const emptyListingSalePriceConfig: PublishSalePriceConfig = {
fixed_markup_rules: [
{ min_m: 10, max_m: 30, markup_amount: 28 },
{ min_m: 30, max_m: 50, markup_amount: 31 },
{ min_m: 50, max_m: 70, markup_amount: 34 },
{ min_m: 70, max_m: 90, markup_amount: 38 },
],
ratio_adjustment_rules: [
{ min_m: 90, max_m: 150, ratio_subtract: 5 },
{ min_m: 150, max_m: 230, ratio_subtract: 4 },
{ min_m: 230, max_m: 310, ratio_subtract: 3.5 },
{ min_m: 310, max_m: 390, ratio_subtract: 3 },
{ min_m: 390, max_m: 470, ratio_subtract: 0 },
{ min_m: 470, max_m: 550, ratio_subtract: 0 },
{ min_m: 550, max_m: 0, ratio_subtract: 0 },
],
}
export async function fetchListingPublishOptions() {
const { data } = await apiClient.get<ApiResponse<ListingPublishOptions>>('/listing-publish-options')
return mergeListingPublishOptions(data.data)
}
export async function fetchListingSalePriceConfig() {
const { data } = await apiClient.get<ApiResponse<PublishSalePriceConfig>>('/listing-sale-price-config')
return mergeListingSalePriceConfig(data.data)
}
export function mergeListingPublishOptions(options?: Partial<ListingPublishOptions>): ListingPublishOptions {
return {
server_options: normalizeStringList(options?.server_options),
face_options: normalizeStringList(options?.face_options),
rank_options: normalizeStringList(options?.rank_options),
insurance_options: normalizeStringList(options?.insurance_options),
level_options: normalizeStringList(options?.level_options),
login_method_options: normalizeStringList(options?.login_method_options),
region_options: normalizeStringList(options?.region_options),
skin_groups: normalizeOptionGroups(options?.skin_groups),
quantity_items: normalizeQuantityItems(options?.quantity_items),
screenshot_slots: normalizeScreenshotSlots(options?.screenshot_slots),
ban_record_options: normalizeStringList(options?.ban_record_options),
ban_evidence_options: normalizeStringList(options?.ban_evidence_options),
fire_level_min: readPositiveInteger(options?.fire_level_min, 38),
price_config: normalizePriceConfig(options?.price_config),
deposit_recommend_config: normalizeDepositRecommendConfig(options?.deposit_recommend_config),
ratio_config: normalizeRatioConfig(options?.ratio_config),
}
}
export function mergeListingSalePriceConfig(options?: Partial<PublishSalePriceConfig>): PublishSalePriceConfig {
return normalizeSalePriceConfig(options)
}
function normalizeStringList(values?: unknown[]) {
return Array.isArray(values)
? values.map((item) => (typeof item === 'string' ? item.trim() : '')).filter(Boolean)
: []
}
function normalizeOptionGroups(values?: unknown[]): PublishOptionGroup[] {
if (!Array.isArray(values)) return []
return values
.map((item) => {
const group = isRecord(item) ? item : {}
return {
key: typeof group.key === 'string' ? group.key.trim() : '',
title: typeof group.title === 'string' ? group.title.trim() : '',
options: normalizeStringList(Array.isArray(group.options) ? group.options : []),
}
})
.filter((item) => item.key && item.title)
}
function normalizeQuantityItems(values?: unknown[]): PublishQuantityItem[] {
if (!Array.isArray(values)) return []
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
key: typeof row.key === 'string' ? row.key.trim() : '',
label: typeof row.label === 'string' ? row.label.trim() : '',
price: typeof row.price === 'string' ? row.price.trim() : '',
placeholder: typeof row.placeholder === 'string' ? row.placeholder.trim() : undefined,
}
})
.filter((item) => item.key && item.label)
}
function normalizeScreenshotSlots(values?: unknown[]): PublishScreenshotSlot[] {
if (!Array.isArray(values)) return []
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
key: typeof row.key === 'string' ? row.key.trim() : '',
label: typeof row.label === 'string' ? row.label.trim() : '',
required: row.required === true,
hint: typeof row.hint === 'string' ? row.hint.trim() : '',
}
})
.filter((item) => item.key && item.label)
}
function normalizePriceConfig(value?: unknown): PublishPriceConfig {
const row = isRecord(value) ? value : {}
return {
deposit_placeholder: typeof row.deposit_placeholder === 'string' ? row.deposit_placeholder.trim() : '',
price_placeholder: typeof row.price_placeholder === 'string' ? row.price_placeholder.trim() : '',
ratio_description: typeof row.ratio_description === 'string' ? row.ratio_description.trim() : '',
}
}
function normalizeDepositRecommendConfig(value?: unknown): PublishDepositRecommendConfig {
const row = isRecord(value) ? value : {}
const config = {
base_amount: readNumber(row.base_amount),
skin_group_rules: normalizeDepositSkinGroupRules(
Array.isArray(row.skin_group_rules) ? row.skin_group_rules : [],
),
}
if (config.base_amount <= 0) {
config.base_amount = emptyListingPublishOptions.deposit_recommend_config.base_amount
}
if (config.skin_group_rules.length === 0) {
config.skin_group_rules = [...emptyListingPublishOptions.deposit_recommend_config.skin_group_rules]
}
return config
}
function normalizeDepositSkinGroupRules(values: unknown[]): PublishDepositSkinGroupRule[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
group_key: typeof row.group_key === 'string' ? row.group_key.trim() : '',
label: typeof row.label === 'string' ? row.label.trim() : '',
amount_per_item: readNumber(row.amount_per_item),
}
})
.filter((item) => item.group_key && item.label && item.amount_per_item >= 0)
}
function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
const row = isRecord(value) ? value : {}
return {
insurance_base_ratios: normalizeInsuranceBaseRatios(
Array.isArray(row.insurance_base_ratios) ? row.insurance_base_ratios : [],
),
config_items: normalizeRatioConfigItems(
Array.isArray(row.config_items) ? row.config_items : [],
),
coin_corrections: normalizeCoinCorrections(
Array.isArray(row.coin_corrections) ? row.coin_corrections : [],
),
}
}
function normalizeSalePriceConfig(value?: unknown): PublishSalePriceConfig {
const row = isRecord(value) ? value : {}
const config = {
fixed_markup_rules: normalizeSaleFixedMarkupRules(
Array.isArray(row.fixed_markup_rules) ? row.fixed_markup_rules : [],
),
ratio_adjustment_rules: normalizeSaleRatioAdjustmentRules(
Array.isArray(row.ratio_adjustment_rules) ? row.ratio_adjustment_rules : [],
),
}
if (config.fixed_markup_rules.length === 0 && config.ratio_adjustment_rules.length === 0) {
return JSON.parse(JSON.stringify(emptyListingSalePriceConfig)) as PublishSalePriceConfig
}
return config
}
function normalizeInsuranceBaseRatios(values: unknown[]): PublishInsuranceBaseRatio[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
insurance: typeof row.insurance === 'string' ? row.insurance.trim() : '',
ratio: readNumber(row.ratio),
}
})
.filter((item) => item.insurance && item.ratio > 0)
}
function normalizeRatioConfigItems(values: unknown[]): PublishRatioConfigItem[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
key: typeof row.key === 'string' ? row.key.trim() : '',
label: typeof row.label === 'string' ? row.label.trim() : '',
kind: typeof row.kind === 'string' ? row.kind.trim() : '',
group_key: typeof row.group_key === 'string' ? row.group_key.trim() : '',
missing_penalty: readNumber(row.missing_penalty),
}
})
.filter((item) => item.key && item.label && item.kind)
}
function normalizeCoinCorrections(values: unknown[]): PublishCoinCorrection[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
threshold_m: readNumber(row.threshold_m),
correction: readNumber(row.correction),
}
})
.filter((item) => item.threshold_m >= 0 && item.correction > 0)
}
function normalizeSaleFixedMarkupRules(values: unknown[]): PublishSaleFixedMarkupRule[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
min_m: readNumber(row.min_m),
max_m: readNumber(row.max_m),
markup_amount: readNumber(row.markup_amount),
}
})
.filter((item) => item.min_m >= 0 && item.max_m >= item.min_m && item.markup_amount >= 0)
}
function normalizeSaleRatioAdjustmentRules(values: unknown[]): PublishSaleRatioAdjustmentRule[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
min_m: readNumber(row.min_m),
max_m: readNumber(row.max_m),
ratio_subtract: readNumber(row.ratio_subtract),
}
})
.filter(
(item) =>
item.min_m >= 0 &&
(item.max_m === 0 || item.max_m >= item.min_m) &&
item.ratio_subtract >= 0,
)
}
function readNumber(value: unknown) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
function readPositiveInteger(value: unknown, fallback: number) {
const parsed = Math.trunc(readNumber(value))
return parsed > 0 ? parsed : fallback
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}