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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
f415832c19
commit
405abfa4f7
@@ -0,0 +1,95 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
import {
|
||||
mergeListingPublishOptions,
|
||||
type ListingPublishOptions,
|
||||
} from './listingOptions'
|
||||
|
||||
export interface HomeBannerSlide {
|
||||
eyebrow: string
|
||||
title: string
|
||||
badge: string
|
||||
pill: string
|
||||
tone: string
|
||||
image_url?: string
|
||||
}
|
||||
|
||||
export interface MobileHomeConfig {
|
||||
announcements: string[]
|
||||
banners: HomeBannerSlide[]
|
||||
publish_options: ListingPublishOptions
|
||||
}
|
||||
|
||||
export const defaultHomeAnnouncements = [
|
||||
'平台担保交易,拒绝私下转账/共享验证码,交接全程留痕。',
|
||||
'优先推荐同地区账号,减少异地登录保护触发。',
|
||||
'下单前请核对哈夫币、保险、体力负重和截图信息。',
|
||||
]
|
||||
|
||||
export const defaultHomeBanners: HomeBannerSlide[] = [
|
||||
{
|
||||
eyebrow: '三角洲行动账号专区',
|
||||
title: '高哈夫币 · 安全交接 · 随租随玩',
|
||||
badge: 'HOT',
|
||||
pill: '新人领券最高减 20 元',
|
||||
tone: 'blue',
|
||||
image_url: '',
|
||||
},
|
||||
{
|
||||
eyebrow: '平台担保交易',
|
||||
title: '交接全程留痕,拒绝私下转账',
|
||||
badge: 'SAFE',
|
||||
pill: '租号前先验实名与资料',
|
||||
tone: 'green',
|
||||
image_url: '',
|
||||
},
|
||||
{
|
||||
eyebrow: '高效筛选',
|
||||
title: '按区服、段位、哈夫币快速找号',
|
||||
badge: 'FAST',
|
||||
pill: '支持扫码号与账密号',
|
||||
tone: 'orange',
|
||||
image_url: '',
|
||||
},
|
||||
]
|
||||
|
||||
export async function fetchMobileHomeConfig() {
|
||||
const { data } = await apiClient.get<ApiResponse<MobileHomeConfig>>('/mobile-home-config')
|
||||
return mergeHomeConfig(data.data)
|
||||
}
|
||||
|
||||
export function mergeHomeConfig(config?: Partial<MobileHomeConfig>): MobileHomeConfig {
|
||||
const announcements =
|
||||
config?.announcements
|
||||
?.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||||
.filter(Boolean) || []
|
||||
const banners =
|
||||
config?.banners
|
||||
?.map((item) => {
|
||||
const banner = isBannerLike(item) ? item : ({} as Partial<HomeBannerSlide>)
|
||||
return {
|
||||
eyebrow: banner.eyebrow?.trim() || '',
|
||||
title: banner.title?.trim() || '',
|
||||
badge: banner.badge?.trim() || '',
|
||||
pill: banner.pill?.trim() || '',
|
||||
tone: normalizeBannerTone(banner.tone),
|
||||
image_url: banner.image_url?.trim() || '',
|
||||
}
|
||||
})
|
||||
.filter((item) => item.title || item.image_url) || []
|
||||
|
||||
return {
|
||||
announcements: announcements.length ? announcements : defaultHomeAnnouncements,
|
||||
banners: banners.length ? banners : defaultHomeBanners,
|
||||
publish_options: mergeListingPublishOptions(config?.publish_options),
|
||||
}
|
||||
}
|
||||
|
||||
function isBannerLike(value: unknown): value is Partial<HomeBannerSlide> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function normalizeBannerTone(tone?: string) {
|
||||
if (tone === 'green' || tone === 'orange') return tone
|
||||
return 'blue'
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
import type { ListingReviewStatus, ListingStatus } from '@/shared/types/status'
|
||||
|
||||
export interface Listing {
|
||||
id: number
|
||||
account_id: number
|
||||
owner_id: number
|
||||
owner_phone?: string
|
||||
owner_nickname?: string
|
||||
title: string
|
||||
description: string
|
||||
game_name: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rank_level: string
|
||||
haf_coin_amount: number
|
||||
asset_summary?: Record<string, unknown>
|
||||
screenshot_urls: string[]
|
||||
cover_url: string
|
||||
price: number
|
||||
deposit_amount: number
|
||||
is_accelerated_sale?: boolean
|
||||
in_transaction: boolean
|
||||
status: ListingStatus
|
||||
review_status: ListingReviewStatus
|
||||
review_reason: string
|
||||
published_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ListingPayload {
|
||||
title: string
|
||||
description: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rank_level: string
|
||||
haf_coin_amount: number
|
||||
asset_summary?: Record<string, unknown>
|
||||
screenshot_urls: string[]
|
||||
price: number
|
||||
deposit_amount: number
|
||||
}
|
||||
|
||||
export interface PublicListingQuery {
|
||||
page?: number
|
||||
page_size?: number
|
||||
keyword?: string
|
||||
sort?: string
|
||||
zone?: string
|
||||
server?: string
|
||||
region?: string
|
||||
login_method?: string
|
||||
rank?: string
|
||||
insurance?: string
|
||||
stamina?: string
|
||||
load?: string
|
||||
skin_group?: string
|
||||
skin_name?: string
|
||||
min_coin?: number
|
||||
max_coin?: number
|
||||
min_price?: number
|
||||
max_price?: number
|
||||
min_deposit?: number
|
||||
max_deposit?: number
|
||||
min_total?: number
|
||||
max_total?: number
|
||||
min_fire_level?: number
|
||||
max_fire_level?: number
|
||||
min_secret_kd?: number
|
||||
max_secret_kd?: number
|
||||
[key: `resource_${string}_min`]: number | undefined
|
||||
[key: `resource_${string}_max`]: number | undefined
|
||||
}
|
||||
|
||||
export interface PublicListingPage {
|
||||
items: Listing[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
zone_counts: Record<string, number>
|
||||
}
|
||||
|
||||
export async function fetchListings(query: PublicListingQuery = {}) {
|
||||
const page = await fetchListingsPage(query)
|
||||
return page.items
|
||||
}
|
||||
|
||||
export async function fetchListingsPage(query: PublicListingQuery = {}) {
|
||||
const params = Object.fromEntries(
|
||||
Object.entries(query).filter(([, value]) => value !== '' && value !== undefined && value !== null)
|
||||
)
|
||||
const { data } = await apiClient.get<ApiResponse<Partial<PublicListingPage>>>('/listings', { params })
|
||||
return normalizePublicListingPage(data.data, query)
|
||||
}
|
||||
|
||||
function normalizePublicListingPage(data: Partial<PublicListingPage>, query: PublicListingQuery): PublicListingPage {
|
||||
const items = Array.isArray(data.items) ? data.items : []
|
||||
return {
|
||||
items,
|
||||
total: Number.isFinite(Number(data.total)) ? Number(data.total) : items.length,
|
||||
page: Number.isFinite(Number(data.page)) ? Number(data.page) : Number(query.page || 1),
|
||||
page_size: Number.isFinite(Number(data.page_size)) ? Number(data.page_size) : Number(query.page_size || items.length || 20),
|
||||
zone_counts: data.zone_counts && typeof data.zone_counts === 'object' ? data.zone_counts : {},
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchListing(id: string | number) {
|
||||
const { data } = await apiClient.get<ApiResponse<Listing>>(`/listings/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchSellerListings() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Listing[] }>>('/seller/listings')
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export async function createListing(payload: ListingPayload) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>('/listings', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function submitListingReview(id: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/listings/${id}/submit-review`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function offlineListing(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ offline: boolean }>>(`/listings/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchPendingReviewListings() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Listing[] }>>('/admin/listings/pending')
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export interface AdminListingQuery {
|
||||
owner_id?: string
|
||||
status?: ListingStatus | ''
|
||||
review_status?: ListingReviewStatus | ''
|
||||
limit?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface AdminListingPage {
|
||||
items: Listing[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export async function fetchAdminListings(query: AdminListingQuery = {}) {
|
||||
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
|
||||
const { data } = await apiClient.get<ApiResponse<AdminListingPage>>('/admin/listings', { params })
|
||||
return normalizeAdminListingPage(data.data, query)
|
||||
}
|
||||
|
||||
function normalizeAdminListingPage(data: Partial<AdminListingPage>, query: AdminListingQuery): AdminListingPage {
|
||||
const items = Array.isArray(data.items) ? data.items : []
|
||||
return {
|
||||
items,
|
||||
total: Number.isFinite(Number(data.total)) ? Number(data.total) : items.length,
|
||||
page: Number.isFinite(Number(data.page)) ? Number(data.page) : Number(query.page || 1),
|
||||
page_size: Number.isFinite(Number(data.page_size)) ? Number(data.page_size) : Number(query.page_size || items.length || 10),
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAdminListing(id: string | number) {
|
||||
const { data } = await apiClient.get<ApiResponse<Listing>>(`/admin/listings/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function adminOfflineListing(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/admin/listings/${id}/offline`, { reason })
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function adminMarkListingAbnormal(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/admin/listings/${id}/mark-abnormal`, { reason })
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function approveListing(id: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/admin/listings/${id}/approve`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function rejectListing(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/admin/listings/${id}/reject`, { reason })
|
||||
return data.data
|
||||
}
|
||||
Reference in New Issue
Block a user