Files
hfb_sys/distribution-admin/src/api.ts
T
yml2213 329d0f75bb feat(distribution): 分站前台按独立商城呈现并支持首页文案自定义
分站前台原本对顾客暴露分销身份,且登录等页面写死了主站品牌,
与「一手独立商城」的定位不符;同时首页文案全部硬编码,站长无法按自己口径调整。

去分销身份(顾客可见处):
- 头部品牌区、首页主视觉、移动端认证卡去掉「官方分销/官方分销认证/官方分销站」标识
- 会员徽标「分销专享」→「会员专享」,「所属商城」兜底「专属分销站/商城」→「官方商城」
- 订单页说明改为「当前为【X】商城,仅展示在本站产生的订单记录」,保留订单按站隔离的必要说明
- 无公告时的兜底跑马灯、轮播角标去掉分销措辞
- 清理随之失效的 pc-brand-badge / mobile-brand-badge 样式

品牌跟随站点配置(即可自定义):
- PC/移动登录页、移动注册页、加桌面引导页的品牌改为 站点名称 + Logo(无 Logo 取站名首字),
  不再写死主站「大锤商行」;主站行为不变(新增 store getter brandMark:分站取站名首字,主站仍为「锤」)

首页文案可配置:
- 新增迁移 000076 与 distribution_sites.storefront_text_json
  (tagline 头部副标题 / hero_badge 主视觉徽标 / hero_desc 主视觉描述 / card_sub 移动端卡片副标题)
- 分销后台「品牌装修 → 首页文案」可编辑,占位提示即默认值,留空或整段清空回落内置默认
- 贯通 UpdateSiteBranding → 公开站点信息接口(含 Redis 缓存失效)→ 前台 store 逐字段合并默认值
- 装修预览同步展示站长填写的副标题与徽标

测试:
- 后端补装修字段写入/公开接口透出/清空回落断言
- 前端新增 storefrontText 单测(默认合并、覆盖、空白与非法类型回落)
2026-10-01 09:24:56 +08:00

794 lines
21 KiB
TypeScript

import axios from 'axios'
const TOKEN_KEY = 'hfb_dist_access_token'
export function getStoredToken(): string | null {
try {
return localStorage.getItem(TOKEN_KEY)
} catch {
return null
}
}
export function setStoredToken(token: string): void {
try {
localStorage.setItem(TOKEN_KEY, token)
} catch {
// ignore local storage errors
}
}
export function clearStoredToken(): void {
try {
localStorage.removeItem(TOKEN_KEY)
} catch {
// ignore local storage errors
}
}
const api = axios.create({ baseURL: '/api', withCredentials: true })
api.interceptors.request.use((config) => {
const token = getStoredToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
api.interceptors.response.use(
(resp) => resp,
(error) => {
if (error?.response?.status === 401) {
clearStoredToken()
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
window.location.href = '/login'
}
}
return Promise.reject(error)
},
)
export interface ApiResponse<T> {
data: T
code?: string | number
message?: string
}
export function extract<T>(resp: { data: ApiResponse<T> }): T {
return resp.data.data
}
export function formatMoney(cent: number | undefined | null): string {
return `¥${((cent ?? 0) / 100).toFixed(2)}`
}
export function formatRatio(ratio: number | undefined | null): string {
const value = Number(ratio || 0)
if (!Number.isFinite(value) || value <= 0) return '--'
return `1:${value.toFixed(1)}`
}
export interface CurrentMember {
admin_id: number
username: string
nickname: string
is_superadmin: boolean
site_id?: number
site_code?: string
site_name?: string
domain?: string
member_type?: string
}
export interface LoginResult {
token: string
refresh_token?: string
member: CurrentMember
}
export async function login(username: string, password: string): Promise<LoginResult> {
const resp = await api.post<ApiResponse<LoginResult>>('/distribution/auth/login', {
username,
password,
})
const res = extract(resp)
if (res.token) {
setStoredToken(res.token)
}
return res
}
export async function getMe(): Promise<CurrentMember> {
const resp = await api.get<ApiResponse<CurrentMember>>('/distribution/auth/me')
return extract(resp)
}
export async function logout(): Promise<void> {
try {
await api.post('/distribution/auth/logout')
} finally {
clearStoredToken()
}
}
export interface Site {
id: number
site_code: string
site_name: string
domain: string
status: string
display_account_enabled: boolean
tax_enabled: boolean
tax_rate_bps: number
settlement_hold_days?: number
logo_url?: string
favicon_url?: string
banners_json?: string
announcements_json?: string
footer_info_json?: string
aw_recycle_json?: string
cooperation_feedback_json?: string
storefront_text_json?: string
listing_qrcode_url?: string
receive_ratio_delta?: number
receive_cost_delta?: number
admin_user_id?: number
admin_username?: string
admin_nickname?: string
member_type?: string
created_at: string
}
export interface Rule {
id: number
site_id?: number
mode: string
version: number
status: string
platform_receive_ratio: number
platform_sell_ratio: number
distributor_receive_ratio: number
distributor_sell_ratio: number
distributor_a_ratio: number
distributor_b_ratio: number
service_fee_value: number
service_fee_unit: string
payment_fee_bps: number
tax_enabled: boolean
tax_rate_bps: number
effective_from: string
effective_to?: string
}
export interface DistributionOrder {
order_id: number
order_no: string
site_id: number
status: string
handoff_status?: string
refund_status: string
settlement_status: string
distributor_settlement_status?: string
release_at?: string
distribution_mode: string
gross_paid_amount_cent?: number
refund_amount_cent?: number
net_paid_amount_cent: number
payment_fee_cent: number
service_fee_cent: number
markup_amount_cent: number
tax_amount_cent: number
split_base_amount_cent: number
rounding_adjustment_cent: number
created_at: string
// 商品与租客字段
listing_no?: string
title?: string
game_name?: string
server_region?: string
login_platform?: string
rent_duration_hours?: number
rented_at?: string
renter_phone?: string
renter_nickname?: string
owner_rent_amount_cent?: number
base_price_cent?: number
shelf_markup_cent?: number
deposit_amount_cent?: number
// 商户本站收益拆解字段
distributor_earning_cent?: number
distributor_role?: string
distributor_markup_cent?: number
distributor_split_cent?: number
}
export interface OrderPage {
items: DistributionOrder[]
total: number
page: number
page_size: number
}
export interface AccountDisplay {
id: number
site_id: number
account_id: number
listing_id: number
display_status: string
markup_amount_cent?: number
shelf_ratio_delta?: number
base_ratio?: number
site_ratio?: number
listing_no?: string
title?: string
game_name?: string
server_region?: string
login_platform?: string
rank_level?: string
haf_coin_amount?: number
price_cent?: number
site_price_cent?: number
deposit_amount_cent?: number
listing_status?: string
in_transaction?: boolean
listing_site_id?: number | null
source_channel?: string
}
export interface SyncSiteAccountsRequest {
listing_ids?: number[]
clean_stale_listings?: boolean
default_shelf_ratio_delta?: number
}
export interface SyncSiteAccountsResult {
synced_count: number
skipped_count: number
updated_count?: number
reenabled_count?: number
disabled_stale_count?: number
total_active: number
}
export interface PublicDistributionListing {
listing_id: number
listing_no: string
account_id: number
title: string
game_name: string
server_region: string
login_platform: string
rank_level: string
haf_coin_amount: number
price_cent: number
deposit_amount_cent: number
in_transaction: boolean
site_code: string
site_name: string
}
export interface PublicDistributionListingsResult {
items: PublicDistributionListing[]
total: number
page: number
page_size: number
}
export async function fetchPublicSiteListings(
siteCode: string,
page = 1,
pageSize = 24,
): Promise<PublicDistributionListingsResult> {
const resp = await api.get<ApiResponse<PublicDistributionListingsResult>>(
`/distribution/sites/${siteCode}/listings`,
{
params: { page, page_size: pageSize },
},
)
return extract(resp)
}
export interface PaymentChannel {
id: number
site_id: number
config_id: number
priority: number
status: string
}
export interface SettlementBatch {
id: number
site_id: number
batch_no: string
period_start: string
period_end: string
total_amount_cent: number
status: string
}
export interface SettlementItem {
id: number
order_id: number
party_role: string
amount_cent: number
status: string
}
export interface OnboardSitePayload {
site_code: string
site_name: string
domain: string
admin_username: string
admin_password: string
admin_nickname?: string
member_type?: string
display_account_enabled?: boolean
tax_enabled?: boolean
tax_rate_bps?: number
settlement_hold_days?: number
}
export interface OnboardSiteResult {
site: Site
rule: Rule
member: CurrentMember
}
export async function onboardSite(payload: OnboardSitePayload): Promise<OnboardSiteResult> {
const resp = await api.post<ApiResponse<OnboardSiteResult>>('/admin/distribution/sites/onboard', payload)
return extract(resp)
}
export async function listSites(): Promise<Site[]> {
const resp = await api.get<ApiResponse<{ items: Site[] }>>('/admin/distribution/sites')
return extract(resp).items ?? []
}
export async function updateSite(
siteID: number,
payload: Partial<
Pick<
Site,
| 'site_name'
| 'domain'
| 'status'
| 'display_account_enabled'
| 'tax_enabled'
| 'tax_rate_bps'
| 'settlement_hold_days'
| 'listing_qrcode_url'
| 'receive_ratio_delta'
| 'receive_cost_delta'
>
>
): Promise<Site> {
const resp = await api.put<ApiResponse<Site>>(`/admin/distribution/sites/${siteID}`, payload)
return extract(resp)
}
export async function deleteSite(siteID: number): Promise<{ deleted: boolean }> {
const resp = await api.delete<ApiResponse<{ deleted: boolean }>>(`/admin/distribution/sites/${siteID}`)
return extract(resp)
}
export async function resetSiteAdminPassword(siteID: number, newPassword: string): Promise<{ message: string }> {
const resp = await api.put<ApiResponse<{ message: string }>>(`/admin/distribution/sites/${siteID}/admin-password`, {
new_password: newPassword,
})
return extract(resp)
}
export interface DistributionSiteDefaults {
default_settlement_hold_days: number
}
// fetchDistributionSiteDefaults 读取新建站点的默认结算冷却天数,供开通表单预填。
export async function fetchDistributionSiteDefaults(): Promise<DistributionSiteDefaults> {
const resp = await api.get<ApiResponse<DistributionSiteDefaults>>(
'/admin/distribution/site-defaults'
)
return extract(resp)
}
export async function listRules(siteID?: number, mode?: string): Promise<Rule[]> {
const resp = await api.get<ApiResponse<{ items: Rule[] }>>('/admin/distribution/rules', {
params: { site_id: siteID || undefined, mode: mode || undefined },
})
return extract(resp).items ?? []
}
export interface CreateRulePayload {
site_id?: number
mode: string
distributor_receive_ratio?: number
platform_sell_ratio?: number
distributor_sell_ratio?: number
platform_receive_ratio?: number
distributor_a_ratio?: number
distributor_b_ratio?: number
service_fee_value: number
service_fee_unit: string
payment_fee_bps: number
tax_enabled: boolean
tax_rate_bps: number
effective_from?: string
}
export async function createRule(payload: CreateRulePayload): Promise<Rule> {
const resp = await api.post<ApiResponse<Rule>>('/admin/distribution/rules', payload)
return extract(resp)
}
export async function updateRuleStatus(id: number, status: string): Promise<Rule> {
const resp = await api.put<ApiResponse<Rule>>(`/admin/distribution/rules/${id}/status`, { status })
return extract(resp)
}
export async function listOrders(params: {
kind?: string
role?: string
status?: string
siteID?: number
page?: number
pageSize?: number
keyword?: string
}): Promise<OrderPage> {
const resp = await api.get<ApiResponse<OrderPage>>('/admin/distribution/orders', {
params: {
kind: params.kind || undefined,
role: params.role || undefined,
status: params.status || undefined,
keyword: params.keyword || undefined,
site_id: params.siteID || undefined,
page: params.page || 1,
page_size: params.pageSize || 20,
},
})
return extract(resp)
}
export async function listAccountDisplays(siteID: number, status?: string): Promise<AccountDisplay[]> {
const resp = await api.get<ApiResponse<{ items: AccountDisplay[] }>>('/admin/distribution/accounts', {
params: { site_id: siteID, status: status || undefined },
})
return extract(resp).items ?? []
}
export async function updateAccountDisplay(id: number, displayStatus: string): Promise<AccountDisplay> {
const resp = await api.put<ApiResponse<AccountDisplay>>(`/admin/distribution/accounts/${id}/display`, {
display_status: displayStatus,
})
return extract(resp)
}
export async function syncSiteAccounts(siteID: number, req?: SyncSiteAccountsRequest): Promise<SyncSiteAccountsResult> {
const resp = await api.post<ApiResponse<SyncSiteAccountsResult>>(`/admin/distribution/sites/${siteID}/sync-accounts`, req ?? {})
return extract(resp)
}
export async function listPaymentChannels(siteID: number): Promise<PaymentChannel[]> {
const resp = await api.get<ApiResponse<{ items: PaymentChannel[] }>>('/admin/distribution/payment-channels', {
params: { site_id: siteID },
})
return extract(resp).items ?? []
}
export async function bindPaymentChannel(siteID: number, configID: number, priority: number): Promise<PaymentChannel> {
const resp = await api.post<ApiResponse<PaymentChannel>>(
'/admin/distribution/payment-channels',
{ config_id: configID, priority },
{ params: { site_id: siteID } },
)
return extract(resp)
}
export async function unbindPaymentChannel(id: number): Promise<void> {
await api.delete(`/admin/distribution/payment-channels/${id}`)
}
export async function listSettlementBatches(siteID: number): Promise<SettlementBatch[]> {
const resp = await api.get<ApiResponse<{ items: SettlementBatch[] }>>('/admin/distribution/settlement-batches', {
params: { site_id: siteID },
})
return extract(resp).items ?? []
}
export async function createSettlementBatch(siteID: number, periodStart: string, periodEnd: string): Promise<SettlementBatch> {
const resp = await api.post<ApiResponse<SettlementBatch>>('/admin/distribution/settlement-batches', {
site_id: siteID,
period_start: periodStart,
period_end: periodEnd,
})
return extract(resp)
}
export async function settleBatch(id: number): Promise<SettlementBatch> {
const resp = await api.post<ApiResponse<SettlementBatch>>(`/admin/distribution/settlement-batches/${id}/settle`)
return extract(resp)
}
export async function cancelSettlementBatch(id: number): Promise<SettlementBatch> {
const resp = await api.post<ApiResponse<SettlementBatch>>(`/admin/distribution/settlement-batches/${id}/cancel`)
return extract(resp)
}
export async function listSettlementItems(batchID: number): Promise<SettlementItem[]> {
const resp = await api.get<ApiResponse<{ items: SettlementItem[] }>>(`/admin/distribution/settlement-batches/${batchID}/items`)
return extract(resp).items ?? []
}
export interface ReceivedListing {
listing_id: number
listing_no: string
site_id: number
account_id: number
owner_id: number
owner_phone?: string
owner_nickname?: string
title: string
game_name: string
server_region: string
login_platform: string
rank_level: string
haf_coin_amount: number
seller_ratio?: number
seller_total_price_cent?: number
buyer_ratio?: number
price_cent: number
deposit_amount_cent: number
status: string
review_status: string
review_reason: string
published_at?: string
created_at: string
}
export interface ReceivedListingPage {
items: ReceivedListing[]
total: number
page: number
page_size: number
}
export async function fetchReceivedListings(params: {
siteID?: number
page?: number
pageSize?: number
status?: string
reviewStatus?: string
gameName?: string
keyword?: string
}): Promise<ReceivedListingPage> {
const resp = await api.get<ApiResponse<ReceivedListingPage>>('/admin/distribution/received-listings', {
params: {
site_id: params.siteID,
page: params.page || 1,
page_size: params.pageSize || 20,
status: params.status || undefined,
review_status: params.reviewStatus || undefined,
game_name: params.gameName || undefined,
keyword: params.keyword || undefined,
},
})
return extract(resp)
}
export async function updateReceivedListingStatus(id: number, status: string): Promise<void> {
await api.put(`/admin/distribution/received-listings/${id}/status`, { status })
}
export interface SiteWallet {
site_id: number
available_balance_cent: number
frozen_balance_cent: number
pending_release_cent: number
total_settled_cent: number
total_withdrawn_cent: number
settlement_hold_days?: number
}
export async function fetchSiteWallet(siteID: number): Promise<SiteWallet> {
const resp = await api.get<ApiResponse<SiteWallet>>(`/admin/distribution/sites/${siteID}/wallet`)
return extract(resp)
}
export interface SiteWithdrawal {
id: number
withdraw_no: string
site_id: number
amount_cent: number
fee_cent: number
actual_amount_cent: number
account_type: string
account_name: string
account_no: string
bank_name?: string
bank_branch?: string
status: string
review_remark?: string
payment_proof_url?: string
payment_remark?: string
paid_at?: string
created_at: string
}
export interface SiteWithdrawalPage {
items: SiteWithdrawal[]
total: number
page: number
page_size: number
}
export async function fetchSiteWithdrawals(siteID: number, page = 1, pageSize = 20): Promise<SiteWithdrawalPage> {
const resp = await api.get<ApiResponse<SiteWithdrawalPage>>(`/admin/distribution/sites/${siteID}/withdrawals`, {
params: { page, page_size: pageSize },
})
return extract(resp)
}
export interface ApplySiteWithdrawalPayload {
site_id?: number
amount_cent: number
account_type: 'alipay' | 'bank'
account_name: string
account_no: string
bank_name?: string
bank_branch?: string
}
export async function applySiteWithdrawal(payload: ApplySiteWithdrawalPayload): Promise<SiteWithdrawal> {
const resp = await api.post<ApiResponse<SiteWithdrawal>>(`/admin/distribution/sites/${payload.site_id}/withdrawals`, payload)
return extract(resp)
}
export interface SiteBrandingPayload {
logo_url?: string
favicon_url?: string
banners_json?: string
announcements_json?: string
footer_info_json?: string
aw_recycle_json?: string
cooperation_feedback_json?: string
storefront_text_json?: string
listing_qrcode_url?: string
}
export async function uploadSiteImage(file: File, scene: string): Promise<string> {
const form = new FormData()
form.append('file', file)
form.append('scene', scene)
const resp = await api.post<ApiResponse<{ url: string }>>('/admin/files/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return extract(resp).url
}
export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise<Site> {
const resp = await api.put<ApiResponse<Site>>(`/admin/distribution/sites/${siteID}/branding`, payload)
return extract(resp)
}
export async function updateAccountMarkup(id: number, shelfRatioDelta: number): Promise<AccountDisplay> {
const resp = await api.put<ApiResponse<AccountDisplay>>(`/admin/distribution/accounts/${id}/markup`, {
shelf_ratio_delta: shelfRatioDelta,
})
return extract(resp)
}
export interface BatchUpdateAccountMarkupItem {
id?: number
listing_id?: number
shelf_ratio_delta: number
}
export interface BatchUpdateAccountMarkupRequest {
listing_ids?: number[]
display_ids?: number[]
shelf_ratio_delta?: number
items?: BatchUpdateAccountMarkupItem[]
}
export async function batchUpdateAccountMarkup(
siteID: number,
reqOrListingIDs: BatchUpdateAccountMarkupRequest | number[],
shelfRatioDelta?: number,
): Promise<void> {
const payload: BatchUpdateAccountMarkupRequest = Array.isArray(reqOrListingIDs)
? { listing_ids: reqOrListingIDs, shelf_ratio_delta: shelfRatioDelta ?? 0 }
: reqOrListingIDs
await api.post(`/admin/distribution/accounts/batch-markup`, payload, {
params: { site_id: siteID },
})
}
export async function batchUpdateAccountDisplay(
siteID: number,
displayIDs: number[],
targetStatus: 'enabled' | 'disabled',
): Promise<void> {
await api.post(
`/admin/distribution/accounts/batch-display`,
{
display_ids: displayIDs,
target_status: targetStatus,
},
{
params: { site_id: siteID },
},
)
}
export interface DistributionAnalytics {
today_received_count: number
total_received_count: number
active_listing_count: number
today_order_count: number
today_gmv_cent: number
today_profit_cent: number
month_profit_cent: number
available_balance_cent: number
trend_days: {
date: string
received_count: number
order_count: number
gmv_cent: number
profit_cent: number
}[]
}
export async function fetchDashboardAnalytics(siteID: number): Promise<DistributionAnalytics> {
const resp = await api.get<ApiResponse<DistributionAnalytics>>(`/admin/distribution/sites/${siteID}/analytics`)
return extract(resp)
}
export interface SiteSMSConfig {
enabled: boolean
provider: string
access_key_id: string
masked_secret: string
has_secret: boolean
endpoint: string
sign_name: string
login_template_code: string
}
export interface UpdateSiteSMSConfigPayload {
enabled?: boolean
provider?: string
access_key_id?: string
access_key_secret?: string
endpoint?: string
sign_name?: string
login_template_code?: string
}
export interface TestSiteSMSConfigPayload {
phone: string
enabled?: boolean
provider?: string
access_key_id?: string
access_key_secret?: string
endpoint?: string
sign_name?: string
login_template_code?: string
}
export async function fetchSiteSMSConfig(siteID: number): Promise<SiteSMSConfig> {
const resp = await api.get<ApiResponse<SiteSMSConfig>>(`/admin/distribution/sites/${siteID}/sms-config`)
return extract(resp)
}
export async function updateSiteSMSConfig(siteID: number, payload: UpdateSiteSMSConfigPayload): Promise<SiteSMSConfig> {
const resp = await api.put<ApiResponse<SiteSMSConfig>>(`/admin/distribution/sites/${siteID}/sms-config`, payload)
return extract(resp)
}
export async function testSiteSMSConfig(siteID: number, payload: TestSiteSMSConfigPayload): Promise<{ message: string }> {
const resp = await api.post<ApiResponse<{ message: string }>>(`/admin/distribution/sites/${siteID}/sms-config/test`, payload)
return extract(resp)
}