From f6ad7c34ee8ff0133a1f611d17f75d566c00cc66 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 9 Sep 2026 14:44:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(distribution):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=88=86=E9=94=80=E5=90=8E=E5=8F=B0=E4=B8=BASaaS=E5=95=86?= =?UTF-8?q?=E6=88=B7=E5=B7=A5=E4=BD=9C=E5=8F=B0=E5=B9=B6=E5=85=A8=E9=9D=A2?= =?UTF-8?q?=E5=95=86=E4=B8=9A=E5=8C=96=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 商品货架 (Accounts.tsx): 支持期望售价/固定金额/百分比多模式实时联动加价与批量调价,高亮100%独享溢价 - 订单中心 (Orders.tsx): 透传账号游戏租客信息,新增收益对账抽屉透明拆解底价、手续费、分成与自主溢价 - 结算与规则 (Settlements.tsx & Rules.tsx): 纠正角色倒错,商户视角展现周期对账单与分成合作政策 - 店铺装修 (SiteSettings.tsx): 废除手写JSON,提供横幅/公告/页脚可视化列表编辑器与前台实时模拟器 - 数据大盘 (Dashboard.tsx): 对齐最新订单字段,大字展示分销商净收益 - 后端数据层 (dto.go, repository.go, handler.go): 聚合计算分销商订单收益,支持商品与租客搜索 --- backend/internal/modules/distribution/dto.go | 76 +- .../internal/modules/distribution/handler.go | 9 +- .../modules/distribution/repository.go | 120 +++- distribution-admin/src/AppShell.tsx | 40 +- distribution-admin/src/api.ts | 24 + distribution-admin/src/pages/Accounts.tsx | 499 +++++++++++-- distribution-admin/src/pages/Dashboard.tsx | 66 +- distribution-admin/src/pages/Orders.tsx | 649 ++++++++++++++--- distribution-admin/src/pages/Rules.tsx | 264 +++++-- distribution-admin/src/pages/Settlements.tsx | 315 +++++++-- distribution-admin/src/pages/SiteSettings.tsx | 659 ++++++++++++++---- distribution-admin/vite.config.ts | 1 + 12 files changed, 2257 insertions(+), 465 deletions(-) diff --git a/backend/internal/modules/distribution/dto.go b/backend/internal/modules/distribution/dto.go index c3eb05c..be74a68 100644 --- a/backend/internal/modules/distribution/dto.go +++ b/backend/internal/modules/distribution/dto.go @@ -91,36 +91,58 @@ type UpdateRuleStatusRequest struct { } type DistributionOrderDTO struct { - OrderID uint64 `json:"order_id"` - OrderNo string `json:"order_no"` - SiteID uint64 `json:"site_id"` - ListingID uint64 `json:"listing_id"` - AccountID uint64 `json:"account_id"` - OwnerID uint64 `json:"owner_id"` - RenterID uint64 `json:"renter_id"` - Status string `json:"status"` - HandoffStatus string `json:"handoff_status"` - RefundStatus string `json:"refund_status"` - SettlementStatus string `json:"settlement_status"` - DistributionMode string `json:"distribution_mode"` - GrossPaidAmountCent int64 `json:"gross_paid_amount_cent"` - RefundAmountCent int64 `json:"refund_amount_cent"` - NetPaidAmountCent int64 `json:"net_paid_amount_cent"` - PaymentFeeCent int64 `json:"payment_fee_cent"` - ServiceFeeCent int64 `json:"service_fee_cent"` - MarkupAmountCent int64 `json:"markup_amount_cent"` - TaxAmountCent int64 `json:"tax_amount_cent"` - SplitBaseAmountCent int64 `json:"split_base_amount_cent"` - RoundingAdjustmentCent int64 `json:"rounding_adjustment_cent"` - CreatedAt time.Time `json:"created_at"` + OrderID uint64 `json:"order_id"` + OrderNo string `json:"order_no"` + SiteID uint64 `json:"site_id"` + ListingID uint64 `json:"listing_id"` + AccountID uint64 `json:"account_id"` + OwnerID uint64 `json:"owner_id"` + RenterID uint64 `json:"renter_id"` + Status string `json:"status"` + HandoffStatus string `json:"handoff_status"` + RefundStatus string `json:"refund_status"` + SettlementStatus string `json:"settlement_status"` + DistributionMode string `json:"distribution_mode"` + GrossPaidAmountCent int64 `json:"gross_paid_amount_cent"` + RefundAmountCent int64 `json:"refund_amount_cent"` + NetPaidAmountCent int64 `json:"net_paid_amount_cent"` + PaymentFeeCent int64 `json:"payment_fee_cent"` + ServiceFeeCent int64 `json:"service_fee_cent"` + MarkupAmountCent int64 `json:"markup_amount_cent"` + TaxAmountCent int64 `json:"tax_amount_cent"` + SplitBaseAmountCent int64 `json:"split_base_amount_cent"` + RoundingAdjustmentCent int64 `json:"rounding_adjustment_cent"` + CreatedAt time.Time `json:"created_at"` + + // 关联业务与分销商收益透传字段 + ListingNo string `json:"listing_no,omitempty"` + Title string `json:"title,omitempty"` + GameName string `json:"game_name,omitempty"` + ServerRegion string `json:"server_region,omitempty"` + LoginPlatform string `json:"login_platform,omitempty"` + RentDurationHours int `json:"rent_duration_hours,omitempty"` + RentedAt *time.Time `json:"rented_at,omitempty"` + RenterPhone string `json:"renter_phone,omitempty"` + RenterNickname string `json:"renter_nickname,omitempty"` + OwnerRentAmountCent int64 `json:"owner_rent_amount_cent,omitempty"` + BasePriceCent int64 `json:"base_price_cent,omitempty"` + ShelfMarkupCent int64 `json:"shelf_markup_cent,omitempty"` + DepositAmountCent int64 `json:"deposit_amount_cent,omitempty"` + + // 当前分销商在该订单中的收益拆解 + DistributorEarningCent int64 `json:"distributor_earning_cent"` + DistributorRole string `json:"distributor_role,omitempty"` + DistributorMarkupCent int64 `json:"distributor_markup_cent"` + DistributorSplitCent int64 `json:"distributor_split_cent"` } type DistributionOrderListQuery struct { - SiteID uint64 - Kind string - Status string - Page int - Size int + SiteID uint64 + Kind string + Status string + Keyword string + Page int + Size int } type DistributionOrderPage struct { diff --git a/backend/internal/modules/distribution/handler.go b/backend/internal/modules/distribution/handler.go index b9fa2f2..4e075c4 100644 --- a/backend/internal/modules/distribution/handler.go +++ b/backend/internal/modules/distribution/handler.go @@ -143,7 +143,14 @@ func (h *Handler) ListOrders(c *gin.Context) { } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{SiteID: scoped, Kind: c.Query("kind"), Status: c.Query("status"), Page: page, Size: size}) + result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{ + SiteID: scoped, + Kind: c.Query("kind"), + Status: c.Query("status"), + Keyword: c.Query("keyword"), + Page: page, + Size: size, + }) if err != nil { writeError(c, err) return diff --git a/backend/internal/modules/distribution/repository.go b/backend/internal/modules/distribution/repository.go index 3b2e37f..786602c 100644 --- a/backend/internal/modules/distribution/repository.go +++ b/backend/internal/modules/distribution/repository.go @@ -417,13 +417,22 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList if size > 100 { size = 100 } - base := r.db.WithContext(ctx).Table("rental_orders ro").Joins("JOIN distribution_order_financials f ON f.order_id = ro.id") + base := r.db.WithContext(ctx).Table("rental_orders ro"). + Joins("JOIN distribution_order_financials f ON f.order_id = ro.id"). + Joins("LEFT JOIN rental_listings rl ON rl.id = ro.listing_id"). + Joins("LEFT JOIN game_accounts ga ON ga.id = ro.account_id"). + Joins("LEFT JOIN users u ON u.id = ro.renter_id") + if query.SiteID > 0 { base = base.Where("ro.distribution_site_id = ?", query.SiteID) } if query.Status != "" { base = base.Where("ro.status = ?", query.Status) } + if strings.TrimSpace(query.Keyword) != "" { + kw := "%" + strings.TrimSpace(query.Keyword) + "%" + base = base.Where("(ro.order_no LIKE ? OR rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ?)", kw, kw, kw, kw) + } switch strings.TrimSpace(query.Kind) { case "sold": base = base.Where("EXISTS (SELECT 1 FROM distribution_order_parties p WHERE p.order_id = ro.id AND p.party_role IN ('distributor_sell','distributor_b'))") @@ -438,16 +447,99 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList return nil, err } items := make([]DistributionOrderDTO, 0) - selectSQL := "ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at" + selectSQL := ` + ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, + ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, + f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, + f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at, + COALESCE(rl.listing_no, '') AS listing_no, + COALESCE(ga.title, '') AS title, + COALESCE(ga.game_name, '') AS game_name, + COALESCE(ga.server_region, '') AS server_region, + COALESCE(ga.login_platform, '') AS login_platform, + ro.estimated_duration_hours AS rent_duration_hours, + ro.rented_at AS rented_at, + COALESCE(u.phone, '') AS renter_phone, + COALESCE(u.nickname, '') AS renter_nickname, + COALESCE(ro.owner_rent_amount_cent, 0) AS owner_rent_amount_cent, + COALESCE(ro.base_price_cent, 0) AS base_price_cent, + COALESCE(ro.shelf_markup_cent, 0) AS shelf_markup_cent, + COALESCE(ro.deposit_amount_cent, 0) AS deposit_amount_cent + ` if err := base.Select(selectSQL).Order("ro.id DESC").Offset((page - 1) * size).Limit(size).Scan(&items).Error; err != nil { return nil, err } + + if len(items) > 0 { + orderIDs := make([]uint64, len(items)) + for i, it := range items { + orderIDs[i] = it.OrderID + } + var parties []model.DistributionOrderParty + pq := r.db.WithContext(ctx).Where("order_id IN ?", orderIDs) + if query.SiteID > 0 { + pq = pq.Where("site_id = ?", query.SiteID) + } + if err := pq.Find(&parties).Error; err == nil { + partiesByOrder := make(map[uint64][]model.DistributionOrderParty) + for _, p := range parties { + partiesByOrder[p.OrderID] = append(partiesByOrder[p.OrderID], p) + } + for i := range items { + plist := partiesByOrder[items[i].OrderID] + var totalEarning int64 + var markupCent int64 + var splitCent int64 + var roles []string + for _, p := range plist { + totalEarning += p.IncomeAmountCent + if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" { + roles = append(roles, "sell") + markupCent += items[i].ShelfMarkupCent + splitCent += (p.IncomeAmountCent - items[i].ShelfMarkupCent) + } else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" { + roles = append(roles, "receive") + splitCent += p.IncomeAmountCent + } + } + items[i].DistributorEarningCent = totalEarning + items[i].DistributorMarkupCent = markupCent + items[i].DistributorSplitCent = splitCent + items[i].DistributorRole = strings.Join(roles, ",") + } + } + } + return &DistributionOrderPage{Items: items, Total: total, Page: page, PageSize: size}, nil } func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*DistributionOrderDTO, error) { var item DistributionOrderDTO - err := r.db.WithContext(ctx).Table("rental_orders ro").Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").Select("ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at").Where("ro.id = ?", id).Scan(&item).Error + selectSQL := ` + ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, + ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, + f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, + f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at, + COALESCE(rl.listing_no, '') AS listing_no, + COALESCE(ga.title, '') AS title, + COALESCE(ga.game_name, '') AS game_name, + COALESCE(ga.server_region, '') AS server_region, + COALESCE(ga.login_platform, '') AS login_platform, + ro.estimated_duration_hours AS rent_duration_hours, + ro.rented_at AS rented_at, + COALESCE(u.phone, '') AS renter_phone, + COALESCE(u.nickname, '') AS renter_nickname, + COALESCE(ro.owner_rent_amount_cent, 0) AS owner_rent_amount_cent, + COALESCE(ro.base_price_cent, 0) AS base_price_cent, + COALESCE(ro.shelf_markup_cent, 0) AS shelf_markup_cent, + COALESCE(ro.deposit_amount_cent, 0) AS deposit_amount_cent + ` + err := r.db.WithContext(ctx).Table("rental_orders ro"). + Joins("JOIN distribution_order_financials f ON f.order_id = ro.id"). + Joins("LEFT JOIN rental_listings rl ON rl.id = ro.listing_id"). + Joins("LEFT JOIN game_accounts ga ON ga.id = ro.account_id"). + Joins("LEFT JOIN users u ON u.id = ro.renter_id"). + Select(selectSQL).Where("ro.id = ?", id).Scan(&item).Error if err != nil { return nil, err } @@ -457,6 +549,28 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri if err := r.ensureSiteAccess(ctx, adminID, item.SiteID); err != nil { return nil, err } + var parties []model.DistributionOrderParty + if err := r.db.WithContext(ctx).Where("order_id = ? AND site_id = ?", item.OrderID, item.SiteID).Find(&parties).Error; err == nil { + var totalEarning int64 + var markupCent int64 + var splitCent int64 + var roles []string + for _, p := range parties { + totalEarning += p.IncomeAmountCent + if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" { + roles = append(roles, "sell") + markupCent += item.ShelfMarkupCent + splitCent += (p.IncomeAmountCent - item.ShelfMarkupCent) + } else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" { + roles = append(roles, "receive") + splitCent += p.IncomeAmountCent + } + } + item.DistributorEarningCent = totalEarning + item.DistributorMarkupCent = markupCent + item.DistributorSplitCent = splitCent + item.DistributorRole = strings.Join(roles, ",") + } return &item, nil } diff --git a/distribution-admin/src/AppShell.tsx b/distribution-admin/src/AppShell.tsx index 4c127bb..e5ccbb0 100644 --- a/distribution-admin/src/AppShell.tsx +++ b/distribution-admin/src/AppShell.tsx @@ -30,24 +30,34 @@ import Login from './pages/Login' import Storefront from './pages/Storefront' import './style.css' -type MenuItem = Required['items'][number] & { superadminOnly?: boolean } - -const ALL_MENU_ITEMS: MenuItem[] = [ - { key: '/', icon: , label: 仪表盘 }, - { key: '/sites', icon: , label: 分销网站, superadminOnly: true }, - { key: '/receive', icon: , label: 分销收号 }, - { key: '/sold', icon: , label: 售出账号 }, - { key: '/accounts', icon: , label: 商品展示与加价 }, - { key: '/wallet', icon: , label: 资金与提现 }, - { key: '/settlements', icon: , label: 结算管理 }, - { key: '/settings', icon: , label: 品牌装修 }, - { key: '/payments', icon: , label: 支付通道 }, - { key: '/rules', icon: , label: 比例与税费 }, -] +type MenuItem = Required['items'][number] function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: () => void }) { const location = useLocation() - const menuItems = ALL_MENU_ITEMS.filter((item) => !item.superadminOnly || member.is_superadmin) + const menuItems: MenuItem[] = [ + { key: '/', icon: , label: 仪表盘 }, + ...(member.is_superadmin + ? [{ key: '/sites', icon: , label: 分销站点管理 }] + : []), + { key: '/receive', icon: , label: 分销收号 }, + { key: '/sold', icon: , label: 售出账号订单 }, + { key: '/accounts', icon: , label: 商品货架与加价 }, + { key: '/wallet', icon: , label: 资金与提现 }, + { + key: '/settlements', + icon: , + label: {member.is_superadmin ? '结算批次管理' : '周期对账单'}, + }, + { key: '/settings', icon: , label: 品牌装修 }, + ...(member.is_superadmin + ? [{ key: '/payments', icon: , label: 支付通道配置 }] + : []), + { + key: '/rules', + icon: , + label: {member.is_superadmin ? '比例与税费规则' : '合作政策与权益'}, + }, + ] const selectedKey = menuItems.some((item) => item.key === location.pathname) ? location.pathname diff --git a/distribution-admin/src/api.ts b/distribution-admin/src/api.ts index 4644db4..193150e 100644 --- a/distribution-admin/src/api.ts +++ b/distribution-admin/src/api.ts @@ -149,9 +149,12 @@ export interface DistributionOrder { order_no: string site_id: number status: string + handoff_status?: string refund_status: string settlement_status: 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 @@ -160,6 +163,27 @@ export interface DistributionOrder { 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 { diff --git a/distribution-admin/src/pages/Accounts.tsx b/distribution-admin/src/pages/Accounts.tsx index 5f2f213..9d62c8e 100644 --- a/distribution-admin/src/pages/Accounts.tsx +++ b/distribution-admin/src/pages/Accounts.tsx @@ -15,6 +15,9 @@ import { Alert, Tooltip, Divider, + Radio, + Row, + Col, } from 'antd' import { SyncOutlined, @@ -89,12 +92,20 @@ export default function Accounts() { const [txFilter, setTxFilter] = useState('all') const [keyword, setKeyword] = useState('') - // Markup modal state + // Multi-mode Pricing modal state const [markupModalVisible, setMarkupModalVisible] = useState(false) const [targetAccount, setTargetAccount] = useState(null) + const [pricingMode, setPricingMode] = useState<'target_price' | 'add_amount' | 'percent' | 'ratio_delta'>('target_price') + const [targetPriceYuan, setTargetPriceYuan] = useState(0) + const [addAmountYuan, setAddAmountYuan] = useState(0) + const [percentMarkup, setPercentMarkup] = useState(0) const [markupRatio, setMarkupRatio] = useState(0) const [markupSaving, setMarkupSaving] = useState(false) + // Batch pricing state + const [batchMode, setBatchMode] = useState<'percent' | 'add_amount' | 'ratio_delta'>('percent') + const [batchValue, setBatchValue] = useState(10) + async function load(site?: number) { if (!site) return setLoading(true) @@ -204,42 +215,154 @@ export default function Accounts() { }) } + function roundRatio(value: number): number { + if (value <= 0 || isNaN(value) || !isFinite(value)) return 0 + return Math.round(value * 10) / 10 + } + + function calcDeltaFromTargetPrice(hafCoinAmount: number, basePriceCent: number, targetPriceYuan: number): number { + if (basePriceCent <= 0 || targetPriceYuan <= 0) return 0 + const coinWan = hafCoinAmount / 10000 + const basePriceYuan = basePriceCent / 100 + if (targetPriceYuan <= basePriceYuan) return 0 + const baseRatio = roundRatio(coinWan / basePriceYuan) + if (baseRatio <= 0) return 0 + const siteRatio = roundRatio(coinWan / targetPriceYuan) + const delta = roundRatio(baseRatio - siteRatio) + return Math.max(0, delta) + } + + function calcPreviewPrice(hafCoinAmount: number, basePriceCent: number, delta: number): { priceCent: number; siteRatio: number; markupCent: number } { + const coinWan = hafCoinAmount / 10000 + const basePriceYuan = basePriceCent / 100 + const baseRatio = roundRatio(coinWan / basePriceYuan) + if (delta <= 0 || baseRatio <= 0) { + return { priceCent: basePriceCent, siteRatio: baseRatio, markupCent: 0 } + } + let siteRatio = roundRatio(baseRatio - delta) + if (siteRatio <= 0.01) siteRatio = 0.01 + const priceYuan = coinWan / siteRatio + const priceCent = Math.max(basePriceCent, Math.round(priceYuan * 100)) + return { + priceCent, + siteRatio, + markupCent: priceCent - basePriceCent, + } + } + function handleOpenMarkup(item?: AccountDisplay) { if (item) { setTargetAccount(item) - setMarkupRatio(item.shelf_ratio_delta ?? 0) + const currentDelta = item.shelf_ratio_delta ?? 0 + setMarkupRatio(currentDelta) + const basePriceYuan = (item.price_cent || 0) / 100 + const currentPriceYuan = (item.site_price_cent || item.price_cent || 0) / 100 + setTargetPriceYuan(currentPriceYuan) + setAddAmountYuan(Math.max(0, Number((currentPriceYuan - basePriceYuan).toFixed(2)))) + setPercentMarkup( + basePriceYuan > 0 + ? Number((((currentPriceYuan - basePriceYuan) / basePriceYuan) * 100).toFixed(1)) + : 0 + ) + setPricingMode('target_price') } else { if (selectedRowKeys.length === 0) { message.warning('请先勾选需要批量加价的账号商品') return } setTargetAccount(null) - setMarkupRatio(0) + setBatchMode('percent') + setBatchValue(10) } setMarkupModalVisible(true) } + function onTargetPriceChange(val: number | null) { + const nextTarget = Math.max(0, val ?? 0) + setTargetPriceYuan(nextTarget) + if (!targetAccount) return + const basePriceYuan = (targetAccount.price_cent || 0) / 100 + const addYuan = Math.max(0, Number((nextTarget - basePriceYuan).toFixed(2))) + setAddAmountYuan(addYuan) + setPercentMarkup(basePriceYuan > 0 ? Number(((addYuan / basePriceYuan) * 100).toFixed(1)) : 0) + const delta = calcDeltaFromTargetPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextTarget) + setMarkupRatio(delta) + } + + function onAddAmountChange(val: number | null) { + const nextAdd = Math.max(0, val ?? 0) + setAddAmountYuan(nextAdd) + if (!targetAccount) return + const basePriceYuan = (targetAccount.price_cent || 0) / 100 + const nextTarget = Number((basePriceYuan + nextAdd).toFixed(2)) + setTargetPriceYuan(nextTarget) + setPercentMarkup(basePriceYuan > 0 ? Number(((nextAdd / basePriceYuan) * 100).toFixed(1)) : 0) + const delta = calcDeltaFromTargetPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextTarget) + setMarkupRatio(delta) + } + + function onPercentMarkupChange(val: number | null) { + const nextPct = Math.max(0, val ?? 0) + setPercentMarkup(nextPct) + if (!targetAccount) return + const basePriceYuan = (targetAccount.price_cent || 0) / 100 + const nextTarget = Number((basePriceYuan * (1 + nextPct / 100)).toFixed(2)) + setTargetPriceYuan(nextTarget) + setAddAmountYuan(Number((nextTarget - basePriceYuan).toFixed(2))) + const delta = calcDeltaFromTargetPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextTarget) + setMarkupRatio(delta) + } + + function onMarkupRatioChange(val: number | null) { + const nextDelta = Math.max(0, val ?? 0) + setMarkupRatio(nextDelta) + if (!targetAccount) return + const preview = calcPreviewPrice(targetAccount.haf_coin_amount || 0, targetAccount.price_cent || 0, nextDelta) + const previewYuan = Number((preview.priceCent / 100).toFixed(2)) + const basePriceYuan = (targetAccount.price_cent || 0) / 100 + setTargetPriceYuan(previewYuan) + const addYuan = Number((preview.markupCent / 100).toFixed(2)) + setAddAmountYuan(addYuan) + setPercentMarkup(basePriceYuan > 0 ? Number(((addYuan / basePriceYuan) * 100).toFixed(1)) : 0) + } + async function handleSaveMarkup() { if (!siteID) return - if (markupRatio < 0) { - message.error('提价调减比例不能小于0') - return - } setMarkupSaving(true) try { if (targetAccount) { await updateAccountMarkup(targetAccount.id, markupRatio) - message.success('商品加价比例设置成功') + message.success(`商品《${targetAccount.title || targetAccount.listing_no}》加价成功!`) } else { - const listingIDs = items - .filter((it) => selectedRowKeys.includes(it.id)) - .map((it) => it.listing_id) - if (listingIDs.length === 0) { + const selectedItems = items.filter((it) => selectedRowKeys.includes(it.id)) + if (selectedItems.length === 0) { message.warning('请先勾选要加价的商品') return } - await batchUpdateAccountMarkup(siteID, listingIDs, markupRatio) - message.success(`已成功为选中的 ${listingIDs.length} 个商品批量设置提价!`) + + if (batchMode === 'ratio_delta') { + const listingIDs = selectedItems.map((it) => it.listing_id) + await batchUpdateAccountMarkup(siteID, listingIDs, batchValue) + } else if (batchMode === 'percent') { + await Promise.all( + selectedItems.map((it) => { + const basePriceYuan = (it.price_cent || 0) / 100 + const targetYuan = basePriceYuan * (1 + batchValue / 100) + const delta = calcDeltaFromTargetPrice(it.haf_coin_amount || 0, it.price_cent || 0, targetYuan) + return updateAccountMarkup(it.id, delta) + }) + ) + } else if (batchMode === 'add_amount') { + await Promise.all( + selectedItems.map((it) => { + const basePriceYuan = (it.price_cent || 0) / 100 + const targetYuan = basePriceYuan + batchValue + const delta = calcDeltaFromTargetPrice(it.haf_coin_amount || 0, it.price_cent || 0, targetYuan) + return updateAccountMarkup(it.id, delta) + }) + ) + } + message.success(`已成功为选中的 ${selectedItems.length} 个商品批量设置提价!`) setSelectedRowKeys([]) } setMarkupModalVisible(false) @@ -742,63 +865,315 @@ export default function Accounts() { /> - {/* Markup Modal */} + {/* Rich Multi-mode Markup Modal */} + + {targetAccount ? `商品加价设置 - ${targetAccount.title || targetAccount.listing_no}` : `批量调价 (${selectedRowKeys.length} 个账号商品)`} + } open={markupModalVisible} onCancel={() => setMarkupModalVisible(false)} onOk={handleSaveMarkup} confirmLoading={markupSaving} - okText="保存提价" + okText="保存并同步货架" + cancelText="取消" + width={580} destroyOnClose > - - - 分站提价是通过调小「1 元兑换哈夫币」的兑换比例来提高销售价。比例越小售价越高; - 提价多挣的差价收益 100% 归本分站所有。 - -
- 提价调减比例: - setMarkupRatio(val ?? 0)} - addonAfter="比例点" - placeholder="0" - /> -
- {targetAccount && ( -
- 主站基准比例 {targetAccount.base_ratio || '-'} → 分站提价后比例{' '} - {Math.max((targetAccount.base_ratio || 0) - markupRatio, 0.1).toFixed(1)} - ,预计售价{' '} - - {markupRatio > 0 && targetAccount.base_ratio - ? `¥${( - (targetAccount.haf_coin_amount || 0) / - 10000 / - Math.max(targetAccount.base_ratio - markupRatio, 0.1) - ).toFixed(2)}` - : formatMoney(targetAccount.site_price_cent || targetAccount.price_cent)} - - (加价溢价全额归本站独享) -
+ + + + {targetAccount ? ( + <> +
+ + + + {targetAccount.title || targetAccount.listing_no} + + + + 游戏区服: + {targetAccount.game_name || '-'} / {targetAccount.server_region || '-'} + + + 哈夫币资产: + + {(targetAccount.haf_coin_amount || 0).toLocaleString()} 币 + + ({((targetAccount.haf_coin_amount || 0) / 10000).toFixed(1)} 万) + + + + + 主站基准价: + ¥{((targetAccount.price_cent || 0) / 100).toFixed(2)} + + + 主站基准比例: + {targetAccount.base_ratio || '-'} 点 + + +
+ +
+ + 加价模式选择: + + setPricingMode(e.target.value)} + buttonStyle="solid" + style={{ width: '100%' }} + > + 期望售价 + 固定加价 + 百分比上浮 + 比例调减 + +
+ +
+ {pricingMode === 'target_price' && ( +
+
+ 最终售价(元): +
+ + + 输入最终期望标价,系统自动换算对应比例点 + +
+ )} + + {pricingMode === 'add_amount' && ( +
+
+ 加价金额(元): +
+ + + 在主站基准价上直接增加的纯利润金额 + +
+ )} + + {pricingMode === 'percent' && ( +
+
+ 上浮百分比(%): +
+ + + 按基准价百分比上浮,例如 10% 或 20% + +
+ )} + + {pricingMode === 'ratio_delta' && ( +
+
+ 调减比例点(点): +
+ + + 高级模式:直接调减 1 元兑换哈夫币比例,比例越小售价越高 + +
+ )} +
+ + {/* Real-time Earnings Comparison Card */} +
+
+ 预计本站标价: + + ¥{targetPriceYuan.toFixed(2)} + +
+
+ 本站 100% 独享溢价: + + +¥{addAmountYuan.toFixed(2)} + +
+ +
+ 兑换比例换算: + + 基准 {targetAccount.base_ratio || '-'} → 标价{' '} + {Math.max((targetAccount.base_ratio || 0) - markupRatio, 0.01).toFixed(1)}{' '} + (调减 {markupRatio.toFixed(1)} 点) + +
+
+ + ) : ( + <> +
+ + 批量加价模式(已勾选 {selectedRowKeys.length} 件商品): + + setBatchMode(e.target.value)} + buttonStyle="solid" + style={{ width: '100%' }} + > + + 按百分比批量上浮 (推荐) + + + 固定加价金额 + + + 统一调减比例点 + + +
+ +
+ {batchMode === 'percent' && ( +
+ + 统一上浮百分比(%): + + setBatchValue(v ?? 0)} + placeholder="例如 10 或 15" + /> + + 所有勾选商品将按各自底价自动上浮 {batchValue}%,并自动为每件商品精确匹配比例点 + +
+ )} + + {batchMode === 'add_amount' && ( +
+ + 统一加价金额(元): + + setBatchValue(v ?? 0)} + placeholder="例如 5.00" + /> + + 所有勾选商品统一定价加价 ¥{batchValue.toFixed(2)},溢价收益全额入账本站 + +
+ )} + + {batchMode === 'ratio_delta' && ( +
+ + 统一调减比例点数: + + setBatchValue(v ?? 0)} + placeholder="例如 0.5" + /> + + 统一在各商品原比例基础上调减 {batchValue} 点 + +
+ )} +
+ +
+ 💡 已勾选 {selectedRowKeys.length} 件商品,批量调价后差价收益将 100% 归本分销商账户独享! +
+ )}
diff --git a/distribution-admin/src/pages/Dashboard.tsx b/distribution-admin/src/pages/Dashboard.tsx index 1e2c8b6..34ef219 100644 --- a/distribution-admin/src/pages/Dashboard.tsx +++ b/distribution-admin/src/pages/Dashboard.tsx @@ -267,13 +267,65 @@ export default function Dashboard() { dataSource={recentOrders} pagination={false} columns={[ - { title: '订单号', dataIndex: 'order_no', render: (v: string) => {v} }, - { title: '状态', dataIndex: 'status', render: (v: string) => {v} }, - { title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) }, - { title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) }, - { title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) }, - { title: '结算状态', dataIndex: 'settlement_status', render: (v: string) => {v} }, - { title: '创建时间', dataIndex: 'created_at' }, + { + title: '订单号', + dataIndex: 'order_no', + width: 170, + render: (v: string) => {v}, + }, + { + title: '租赁账号与游戏', + minWidth: 180, + render: (_: unknown, row: DistributionOrder) => ( +
+ + {row.title || (row.listing_no ? `商品 #${row.listing_no}` : `订单 #${row.order_id}`)} + + {row.game_name && {row.game_name}} + {row.server_region && {row.server_region}} +
+ ), + }, + { + title: '实付金额', + width: 120, + render: (_: unknown, row: DistributionOrder) => ( + + {formatMoney(row.net_paid_amount_cent || row.gross_paid_amount_cent || 0)} + + ), + }, + { + title: '本站净收益', + width: 140, + render: (_: unknown, row: DistributionOrder) => ( + + +{formatMoney(row.distributor_earning_cent || 0)} + + ), + }, + { + title: '订单状态', + dataIndex: 'status', + width: 110, + render: (v: string) => {v}, + }, + { + title: '结算状态', + dataIndex: 'settlement_status', + width: 110, + render: (v: string) => {v === 'settled' ? '已结算' : '待结算'}, + }, + { + title: '下单时间', + dataIndex: 'created_at', + width: 160, + render: (v: string) => ( + + {v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-'} + + ), + }, ]} /> diff --git a/distribution-admin/src/pages/Orders.tsx b/distribution-admin/src/pages/Orders.tsx index 04ec35d..7490c7c 100644 --- a/distribution-admin/src/pages/Orders.tsx +++ b/distribution-admin/src/pages/Orders.tsx @@ -11,8 +11,22 @@ import { Input, Button, Modal, + Drawer, + Descriptions, + Divider, + Row, + Col, + Statistic, + Alert, } from 'antd' -import { SearchOutlined, CheckCircleOutlined, StopOutlined } from '@ant-design/icons' +import { + SearchOutlined, + CheckCircleOutlined, + StopOutlined, + FileTextOutlined, + DollarCircleOutlined, + ReloadOutlined, +} from '@ant-design/icons' import { formatMoney, listOrders, @@ -26,20 +40,26 @@ import { } from '../api' import { useMember } from '../MemberContext' -const ORDER_STATUSES = [ - 'pending_payment', - 'pending_handoff', - 'renting', - 'overdue', - 'pending_checkout_confirm', - 'pending_checkout_accept', - 'checkout_disputing', - 'disputing', - 'abnormal', - 'completed', - 'refunded', - 'cancelled', -] +const ORDER_STATUS_MAP: Record = { + pending_payment: { label: '待支付', color: 'orange' }, + pending_handoff: { label: '待上号交付', color: 'processing' }, + renting: { label: '在租中', color: 'cyan' }, + overdue: { label: '已逾期', color: 'volcano' }, + pending_checkout_confirm: { label: '待结单确认', color: 'blue' }, + pending_checkout_accept: { label: '待号主验收', color: 'blue' }, + checkout_disputing: { label: '结单争议中', color: 'magenta' }, + disputing: { label: '维权中', color: 'magenta' }, + abnormal: { label: '异常', color: 'error' }, + completed: { label: '已完成', color: 'success' }, + refunded: { label: '已退款', color: 'purple' }, + cancelled: { label: '已取消', color: 'default' }, +} + +const SETTLEMENT_STATUS_MAP: Record = { + settled: { label: '已结算入账', color: 'success' }, + pending: { label: '履约中待结算', color: 'warning' }, + failed: { label: '结算异常', color: 'error' }, +} const REVIEW_STATUS_LABELS: Record = { pending: { label: '待审核', color: 'orange' }, @@ -55,20 +75,25 @@ const LISTING_STATUS_LABELS: Record = draft: { label: '草稿', color: 'warning' }, } +function maskPhone(phone?: string): string { + if (!phone || phone.length < 7) return phone || '-' + return phone.slice(0, 3) + '****' + phone.slice(-4) +} + export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { const member = useMember() const [sites, setSites] = useState([]) const [siteID, setSiteID] = useState(member?.site_id) - - // Orders Tab State + + // Orders State const [orders, setOrders] = useState([]) const [orderTotal, setOrderTotal] = useState(0) const [orderPage, setOrderPage] = useState(1) const [orderLoading, setOrderLoading] = useState(false) - const [status, setStatus] = useState('') + const [orderStatus, setOrderStatus] = useState('') const [orderKeyword, setOrderKeyword] = useState('') - // Received Listings Tab State + // Received Listings Tab State (for kind === 'receive') const [activeTab, setActiveTab] = useState<'received_listings' | 'orders'>('received_listings') const [receivedListings, setReceivedListings] = useState([]) const [recvTotal, setRecvTotal] = useState(0) @@ -78,51 +103,61 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { const [recvReviewStatus, setRecvReviewStatus] = useState('') const [recvKeyword, setRecvKeyword] = useState('') + // Order Details & Earnings Drawer State + const [selectedOrder, setSelectedOrder] = useState(null) + const [drawerVisible, setDrawerVisible] = useState(false) + const pageSize = 20 - const loadOrders = useCallback(async (targetPage: number) => { - if (!siteID) return - setOrderLoading(true) - try { - const result = await listOrders({ - kind, - status, - siteID, - page: targetPage, - pageSize, - keyword: orderKeyword || undefined - }) - setOrders(result.items) - setOrderTotal(result.total) - setOrderPage(result.page) - } catch { - message.error('订单加载失败') - } finally { - setOrderLoading(false) - } - }, [kind, status, siteID, orderKeyword]) + const loadOrders = useCallback( + async (targetPage: number) => { + if (!siteID) return + setOrderLoading(true) + try { + const result = await listOrders({ + kind, + status: orderStatus || undefined, + siteID, + page: targetPage, + pageSize, + keyword: orderKeyword || undefined, + }) + setOrders(result.items) + setOrderTotal(result.total) + setOrderPage(result.page) + } catch { + message.error('订单加载失败') + } finally { + setOrderLoading(false) + } + }, + [kind, orderStatus, siteID, orderKeyword] + ) - const loadReceivedListings = useCallback(async (targetPage: number) => { - if (!siteID) return - setRecvLoading(true) - try { - const result = await fetchReceivedListings({ - siteID, - page: targetPage, - pageSize, - status: recvStatus || undefined, - reviewStatus: recvReviewStatus || undefined, - keyword: recvKeyword || undefined, - }) - setReceivedListings(result.items) - setRecvTotal(result.total) - setRecvPage(result.page) - } catch { - message.error('分销收号资产加载失败') - } finally { - setRecvLoading(false) - } - }, [siteID, recvStatus, recvReviewStatus, recvKeyword]) + const loadReceivedListings = useCallback( + async (targetPage: number) => { + if (!siteID) return + setRecvLoading(true) + try { + const result = await fetchReceivedListings({ + siteID, + page: targetPage, + pageSize, + status: recvStatus || undefined, + reviewStatus: recvReviewStatus || undefined, + keyword: recvKeyword || undefined, + }) + setReceivedListings(result.items) + setRecvTotal(result.total) + setRecvPage(result.page) + } catch { + message.error('分销收号资产加载失败') + } finally { + setRecvLoading(false) + } + }, + [siteID, recvStatus, recvReviewStatus, recvKeyword] + ) useEffect(() => { async function init() { @@ -165,7 +200,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { } else { void loadOrders(1) } - }, [kind, activeTab, loadOrders, loadReceivedListings]) + }, [kind, activeTab, siteID, loadOrders, loadReceivedListings]) async function handleToggleListingStatus(item: ReceivedListing) { const nextStatus = item.status === 'offline' ? 'published' : 'offline' @@ -186,17 +221,161 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { }) } + const orderColumns = [ + { + title: '订单号 / 时间', + width: 190, + render: (_: unknown, row: DistributionOrder) => ( +
+ + {row.order_no} + +
+ {row.created_at ? new Date(row.created_at).toLocaleString('zh-CN', { hour12: false }) : '-'} +
+
+ ), + }, + { + title: '商品与游戏', + minWidth: 220, + render: (_: unknown, row: DistributionOrder) => ( +
+ + {row.title || (row.listing_no ? `商品 #${row.listing_no}` : `订单商品 #${row.order_id}`)} + + + {row.game_name && {row.game_name}} + {row.server_region && {row.server_region}} + {row.login_platform && {row.login_platform}} + {row.rent_duration_hours ? 租期: {row.rent_duration_hours}h : null} + +
+ ), + }, + { + title: '租客', + width: 140, + render: (_: unknown, row: DistributionOrder) => ( +
+
{row.renter_nickname || '平台租客'}
+ + {maskPhone(row.renter_phone)} + +
+ ), + }, + { + title: '实付租金 / 押金', + width: 140, + render: (_: unknown, row: DistributionOrder) => { + const netPaid = row.net_paid_amount_cent || row.gross_paid_amount_cent || 0 + return ( +
+ + {formatMoney(netPaid)} + + {row.deposit_amount_cent ? ( +
押金: {formatMoney(row.deposit_amount_cent)}
+ ) : null} +
+ ) + }, + }, + { + title: '本站净收益', + width: 180, + render: (_: unknown, row: DistributionOrder) => { + const earning = row.distributor_earning_cent ?? 0 + const roleLabel = + row.distributor_role === 'seller' + ? '售出方' + : row.distributor_role === 'receiver' + ? '收号方' + : row.distributor_role === 'two_party_distributor' + ? '分销方' + : '本站收益' + return ( +
+ + +{formatMoney(earning)} + +
+ 0 ? 'green' : 'default'} style={{ fontSize: 11 }}> + {roleLabel} + +
+ {row.distributor_markup_cent && row.distributor_markup_cent > 0 ? ( +
+ 含自主溢价: +{formatMoney(row.distributor_markup_cent)} +
+ ) : null} +
+ ) + }, + }, + { + title: '订单履约状态', + width: 120, + render: (_: unknown, row: DistributionOrder) => { + const conf = ORDER_STATUS_MAP[row.status] || { label: row.status, color: 'default' } + return ( +
+ {conf.label} + {row.refund_status && row.refund_status !== 'none' && ( +
+ + 退款: {row.refund_status} + +
+ )} +
+ ) + }, + }, + { + title: '结算状态', + width: 110, + render: (_: unknown, row: DistributionOrder) => { + const conf = SETTLEMENT_STATUS_MAP[row.settlement_status] || { + label: row.settlement_status, + color: 'default', + } + return {conf.label} + }, + }, + { + title: '操作', + width: 110, + render: (_: unknown, row: DistributionOrder) => ( + + ), + }, + ] + const title = kind === 'sold' ? '售出账号订单' : kind === 'receive' ? '分销收号管理' : '全部分销订单' return ( - +
- {title} + + {title} + {kind === 'receive' ? '查看由当前分销网站上传的号源资产及审核流转状态,并跟踪对应的收号分成订单' - : '查看分销订单交易及财务明细'} + : '查看分销订单履约进展与收益拆解对账单(本站独享自主溢价与提成明细)'}
{member?.is_superadmin && sites.length > 1 && ( @@ -228,9 +407,7 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { style={{ width: 140 }} value={recvReviewStatus || undefined} options={Object.entries(REVIEW_STATUS_LABELS).map(([k, v]) => ({ label: v.label, value: k }))} - onChange={(val) => { - setRecvReviewStatus(val ?? '') - }} + onChange={(val) => setRecvReviewStatus(val ?? '')} /> setRecvKeyword(e.target.value)} onPressEnter={() => void loadReceivedListings(1)} /> - + {v}, + render: (v: string) => ( + + {v} + + ), }, { title: '账号标题与游戏', @@ -377,37 +567,50 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { label: `收号分成订单 (${orderTotal})`, children: ( - + } + style={{ width: 280 }} + allowClear + value={orderKeyword} + onChange={(e) => setOrderKeyword(e.target.value)} + onPressEnter={() => void loadOrders(1)} + /> + +
`共 ${tot} 笔分成订单`, onChange: (p) => void loadOrders(p), }} - columns={[ - { title: '订单号', dataIndex: 'order_no' }, - { title: '订单状态', dataIndex: 'status', render: (v: string) => {v} }, - { title: '退款状态', dataIndex: 'refund_status' }, - { title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) }, - { title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) }, - { title: '服务费', dataIndex: 'service_fee_cent', render: (v: number) => formatMoney(v) }, - { title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) }, - { title: '尾差', dataIndex: 'rounding_adjustment_cent', render: (v: number) => formatMoney(v) }, - { title: '结算', dataIndex: 'settlement_status' }, - { title: '创建时间', dataIndex: 'created_at' }, - ]} /> ), @@ -417,35 +620,269 @@ export default function Orders({ kind }: { kind: 'all' | 'sold' | 'receive' }) { ) : ( - + } + style={{ width: 280 }} + allowClear + value={orderKeyword} + onChange={(e) => setOrderKeyword(e.target.value)} + onPressEnter={() => void loadOrders(1)} + /> + +
void loadOrders(p) }} - columns={[ - { title: '订单号', dataIndex: 'order_no' }, - { title: '订单状态', dataIndex: 'status', render: (v: string) => {v} }, - { title: '退款状态', dataIndex: 'refund_status' }, - { title: '净支付', dataIndex: 'net_paid_amount_cent', render: (v: number) => formatMoney(v) }, - { title: '手续费', dataIndex: 'payment_fee_cent', render: (v: number) => formatMoney(v) }, - { title: '服务费', dataIndex: 'service_fee_cent', render: (v: number) => formatMoney(v) }, - { title: '分成基数', dataIndex: 'split_base_amount_cent', render: (v: number) => formatMoney(v) }, - { title: '尾差', dataIndex: 'rounding_adjustment_cent', render: (v: number) => formatMoney(v) }, - { title: '结算', dataIndex: 'settlement_status' }, - { title: '创建时间', dataIndex: 'created_at' }, - ]} + columns={orderColumns} + pagination={{ + current: orderPage, + pageSize, + total: orderTotal, + showTotal: (tot) => `共 ${tot} 笔分销订单`, + onChange: (p) => void loadOrders(p), + }} /> )} + + {/* Order Details & Earnings Drawer */} + + + 订单收益对账单 + {selectedOrder && ( + + ({selectedOrder.order_no}) + + )} + + } + open={drawerVisible} + onClose={() => { + setDrawerVisible(false) + setSelectedOrder(null) + }} + width={620} + destroyOnClose + > + {selectedOrder && ( + + {/* Top Stat Summary */} +
+ +
+ + + + + + +
结算状态
+ + {SETTLEMENT_STATUS_MAP[selectedOrder.settlement_status]?.label || selectedOrder.settlement_status} + + + + + + {/* Product & Rental Info */} + + {ORDER_STATUS_MAP[selectedOrder.status]?.label || selectedOrder.status} + + ), + }, + { + key: 'created_at', + label: '下单时间', + span: 2, + children: selectedOrder.created_at + ? new Date(selectedOrder.created_at).toLocaleString('zh-CN', { hour12: false }) + : '-', + }, + ]} + /> + + {/* Clear Transparent Accounting Breakdown Card */} +
+ + 订单清算与商户收益对账单 + +
+
+ 1. 租客实付租金总额: + + ¥ + {( + (selectedOrder.net_paid_amount_cent || selectedOrder.gross_paid_amount_cent || 0) / + 100 + ).toFixed(2)} + +
+
+ 2. 减:号主底价成本(全额归号主,不分润): + + - ¥{((selectedOrder.owner_rent_amount_cent || 0) / 100).toFixed(2)} + +
+
+ 3. 减:支付通道手续费(千分之五): + + - ¥{((selectedOrder.payment_fee_cent || 0) / 100).toFixed(2)} + +
+
+ 4. 可分润主站提价利润池: + ¥{((selectedOrder.split_base_amount_cent || 0) / 100).toFixed(2)} +
+ + + +
+ + 本分销站角色: + {selectedOrder.distributor_role === 'seller' + ? '售出方 (享 40% 提成分润 + 100% 自主加价溢价)' + : selectedOrder.distributor_role === 'receiver' + ? '收号方 (享 35% 提成分润)' + : '分销合作方'} + +
+ +
+ + ✦ 分站自主加价溢价(100% 独享收益): + + + + ¥{((selectedOrder.distributor_markup_cent || 0) / 100).toFixed(2)} + +
+ +
+ + ✦ 提价利润分账分成: + + + + ¥{((selectedOrder.distributor_split_cent || 0) / 100).toFixed(2)} + +
+ + + +
+ 本单应收净利润合计: + + + ¥{((selectedOrder.distributor_earning_cent || 0) / 100).toFixed(2)} + +
+
+
+ + + + )} + ) } diff --git a/distribution-admin/src/pages/Rules.tsx b/distribution-admin/src/pages/Rules.tsx index f2131cd..5309845 100644 --- a/distribution-admin/src/pages/Rules.tsx +++ b/distribution-admin/src/pages/Rules.tsx @@ -1,12 +1,20 @@ -import { useEffect, useState } from 'react' -import { Card, Table, Typography, Space, Select, Tag, message } from 'antd' +import { useEffect, useState, useCallback } from 'react' +import { Card, Table, Typography, Space, Select, Tag, message, Row, Col, Divider, Alert, Button } from 'antd' +import { + SafetyCertificateOutlined, + DollarCircleOutlined, + ThunderboltOutlined, + CheckCircleOutlined, + LockOutlined, +} from '@ant-design/icons' import { listRules, listSites, updateRuleStatus, getMe, type Rule, type Site } from '../api' import { useMember } from '../MemberContext' const MODE_LABELS: Record = { - two_party_distributor_receive: '两方-分销收号', - two_party_platform_receive: '两方-分销卖出', - three_party: '三方', + two_party_distributor_receive: '分销收号模式 (主站售出 / 散户在本站上传)', + two_party_platform_receive: '分销卖号模式 (分站售出 / 主站共享号源池)', + three_party: '分销跨站协同模式 (分站A收号 + 分站B卖出)', + site_self_sell: '分站自收自卖模式', } export default function Rules() { @@ -16,16 +24,16 @@ export default function Rules() { const [rules, setRules] = useState([]) const [loading, setLoading] = useState(false) - async function load(site?: number) { + const load = useCallback(async (site?: number) => { setLoading(true) try { setRules(await listRules(site)) } catch { - message.error('规则加载失败') + message.error('分成规则加载失败') } finally { setLoading(false) } - } + }, []) useEffect(() => { async function init() { @@ -56,7 +64,7 @@ export default function Rules() { } } void init() - }, [member]) + }, [member, load]) async function toggleStatus(rule: Rule) { const next = rule.status === 'active' ? 'disabled' : 'active' @@ -69,57 +77,231 @@ export default function Rules() { } } + const isSuperadmin = member?.is_superadmin + return ( - - 比例与税费规则 - - {member?.is_superadmin && sites.length > 1 && ( + +
+
+ + {isSuperadmin ? '分成规则配置与管理' : '分销商合作协议与分成政策'} + + + {isSuperadmin + ? '主站超管可维护各交易模式的分账比例、服务费与税费政策' + : '查看当前分销站签约的收益权益、分成政策与资金结算恒等式说明'} + +
+ {isSuperadmin && sites.length > 1 && (
MODE_LABELS[v] ?? v }, - { title: '版本', dataIndex: 'version' }, - { title: '状态', dataIndex: 'status', render: (v: string) => {v} }, { - title: '比例', - render: (_: unknown, r: Rule) => { - const parts: string[] = [] - if (r.distributor_receive_ratio > 0) parts.push(`分销收 ${r.distributor_receive_ratio}%`) - if (r.platform_sell_ratio > 0) parts.push(`主站卖 ${r.platform_sell_ratio}%`) - if (r.distributor_sell_ratio > 0) parts.push(`分销卖 ${r.distributor_sell_ratio}%`) - if (r.platform_receive_ratio > 0) parts.push(`主站收 ${r.platform_receive_ratio}%`) - if (r.distributor_a_ratio > 0) parts.push(`分销A ${r.distributor_a_ratio}%`) - if (r.distributor_b_ratio > 0) parts.push(`分销B ${r.distributor_b_ratio}%`) - return parts.join(' / ') || '-' - }, - }, - { title: '服务费', render: (_: unknown, r: Rule) => `${r.service_fee_value}%` }, - { title: '手续费', dataIndex: 'payment_fee_bps', render: (v: number) => `${v / 100}%` }, - { title: '税费', render: (_: unknown, r: Rule) => (r.tax_enabled ? `${r.tax_rate_bps / 100}%` : '关闭') }, - { title: '生效时间', dataIndex: 'effective_from' }, - { - title: '操作', - render: (_: unknown, r: Rule) => ( - toggleStatus(r)}>{r.status === 'active' ? '停用' : '启用'} + title: '业务交易模式', + dataIndex: 'mode', + render: (v: string) => ( +
+ {MODE_LABELS[v] ?? v} +
模式标识: {v}
+
), }, + { + title: '版本号', + dataIndex: 'version', + width: 90, + render: (v: number) => v{v}, + }, + { + title: '状态', + dataIndex: 'status', + width: 100, + render: (v: string) => ( + + {v === 'active' ? '生效中' : '已停用'} + + ), + }, + { + title: '分成权益比例', + render: (_: unknown, r: Rule) => { + const parts: string[] = [] + if (r.distributor_receive_ratio > 0) parts.push(`分销收号: ${r.distributor_receive_ratio}%`) + if (r.distributor_sell_ratio > 0) parts.push(`分销卖出: ${r.distributor_sell_ratio}%`) + if (r.platform_receive_ratio > 0) parts.push(`主站收号: ${r.platform_receive_ratio}%`) + if (r.platform_sell_ratio > 0) parts.push(`主站卖出: ${r.platform_sell_ratio}%`) + if (r.distributor_a_ratio > 0) parts.push(`分销A: ${r.distributor_a_ratio}%`) + if (r.distributor_b_ratio > 0) parts.push(`分销B: ${r.distributor_b_ratio}%`) + return ( + + {parts.map((p, idx) => ( + + {p} + + ))} + + ) + }, + }, + { + title: '平台服务费', + width: 110, + render: (_: unknown, r: Rule) => {r.service_fee_value}%, + }, + { + title: '通道手续费', + width: 110, + dataIndex: 'payment_fee_bps', + render: (v: number) => {(v / 100).toFixed(2)}%, + }, + { + title: '生效时间', + dataIndex: 'effective_from', + width: 160, + render: (v: string) => ( + + {v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-'} + + ), + }, + ...(isSuperadmin + ? [ + { + title: '操作', + width: 100, + render: (_: unknown, r: Rule) => ( + + ), + }, + ] + : []), ]} /> diff --git a/distribution-admin/src/pages/Settlements.tsx b/distribution-admin/src/pages/Settlements.tsx index 4c19ce5..b270591 100644 --- a/distribution-admin/src/pages/Settlements.tsx +++ b/distribution-admin/src/pages/Settlements.tsx @@ -1,5 +1,30 @@ -import { useEffect, useState } from 'react' -import { Card, Table, Typography, Space, Select, Tag, DatePicker, Button, message, Modal } from 'antd' +import { useEffect, useState, useCallback } from 'react' +import { + Card, + Table, + Typography, + Space, + Select, + Tag, + DatePicker, + Button, + message, + Modal, + Alert, + Descriptions, + Divider, + Statistic, + Row, + Col, + Empty, +} from 'antd' +import { + DollarCircleOutlined, + FileTextOutlined, + CheckCircleOutlined, + ClockCircleOutlined, + PlusOutlined, +} from '@ant-design/icons' import { createSettlementBatch, formatMoney, @@ -15,6 +40,21 @@ import { import { useMember } from '../MemberContext' import type { Dayjs } from 'dayjs' +const STATUS_LABELS: Record = { + settled: { label: '已打款结算', color: 'success' }, + draft: { label: '待审核出账', color: 'warning' }, + processing: { label: '结算处理中', color: 'processing' }, +} + +const PARTY_ROLE_LABELS: Record = { + seller: '卖出提成分账 (40%)', + distributor_b: '卖出提成分账 (40%)', + receiver: '收号提成分账 (35%)', + distributor_a: '收号提成分账 (35%)', + two_party_distributor: '分销商分润', + platform: '平台服务费', +} + export default function Settlements() { const member = useMember() const [sites, setSites] = useState([]) @@ -24,17 +64,19 @@ export default function Settlements() { const [range, setRange] = useState<[Dayjs | null, Dayjs | null] | null>(null) const [itemsFor, setItemsFor] = useState(null) const [items, setItems] = useState([]) + const [itemsLoading, setItemsLoading] = useState(false) + const [creating, setCreating] = useState(false) - async function load(id: number) { + const load = useCallback(async (id: number) => { setLoading(true) try { setBatches(await listSettlementBatches(id)) } catch { - message.error('结算批次加载失败') + message.error('结算账单加载失败') } finally { setLoading(false) } - } + }, []) useEffect(() => { async function init() { @@ -66,107 +108,284 @@ export default function Settlements() { } } void init() - }, [member]) + }, [member, load]) async function handleCreate() { if (!siteID || !range?.[0] || !range?.[1]) { message.warning('请选择站点和结算周期') return } + setCreating(true) try { const batch = await createSettlementBatch(siteID, range[0].toISOString(), range[1].toISOString()) - message.success(`批次 ${batch.batch_no} 已创建,合计 ${formatMoney(batch.total_amount_cent)}`) + message.success(`结算批次 ${batch.batch_no} 已创建,合计金额 ${formatMoney(batch.total_amount_cent)}`) void load(siteID) } catch (error: unknown) { const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message message.error(respMessage ?? '批次创建失败') + } finally { + setCreating(false) } } async function handleSettle(batch: SettlementBatch) { - try { - await settleBatch(batch.id) - message.success(`批次 ${batch.batch_no} 已结算`) - if (siteID) void load(siteID) - } catch (error: unknown) { - const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message - message.error(respMessage ?? '结算失败') - } + Modal.confirm({ + title: '确认打款结算?', + content: `即将对批次【${batch.batch_no}】执行结算操作,打款金额 ${formatMoney(batch.total_amount_cent)} 将计入商户可用余额。`, + onOk: async () => { + try { + await settleBatch(batch.id) + message.success(`批次 ${batch.batch_no} 已成功结算入账!`) + if (siteID) void load(siteID) + } catch (error: unknown) { + const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message + message.error(respMessage ?? '结算失败') + } + }, + }) } async function showItems(batch: SettlementBatch) { + setItemsFor(batch) + setItemsLoading(true) try { - setItemsFor(batch) setItems(await listSettlementItems(batch.id)) } catch { - message.error('结算明细加载失败') + message.error('结算对账明细加载失败') + } finally { + setItemsLoading(false) } } + const isSuperadmin = member?.is_superadmin + return ( - - 结算管理 - - {member?.is_superadmin && sites.length > 1 && ( + +
+
+ + {isSuperadmin ? '结算管理与批次出账' : '周期结算单与财务对账'} + + + {isSuperadmin + ? '主站财务管理员可按周期创建结算批次并执行打款审核' + : '查看主站定期出具的结算账单,核对订单分成明细与资金入账凭证'} + +
+ {isSuperadmin && sites.length > 1 && (
+ ), + }} columns={[ - { title: '批次号', dataIndex: 'batch_no' }, - { title: '周期开始', dataIndex: 'period_start' }, - { title: '周期结束', dataIndex: 'period_end' }, - { title: '合计金额', dataIndex: 'total_amount_cent', render: (v: number) => formatMoney(v) }, { - title: '状态', + title: '账单批次号', + dataIndex: 'batch_no', + width: 200, + render: (v: string) => {v}, + }, + { + title: '结算周期开始', + dataIndex: 'period_start', + width: 170, + render: (v: string) => ( + + {v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-'} + + ), + }, + { + title: '结算周期截止', + dataIndex: 'period_end', + width: 170, + render: (v: string) => ( + + {v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-'} + + ), + }, + { + title: '账单合计收益', + dataIndex: 'total_amount_cent', + width: 160, + render: (v: number) => ( + + +{formatMoney(v)} + + ), + }, + { + title: '结算状态', dataIndex: 'status', - render: (v: string) => {v === 'settled' ? '已结算' : '草稿'}, + width: 130, + render: (v: string) => { + const conf = STATUS_LABELS[v] || { label: v, color: 'default' } + return {conf.label} + }, }, { title: '操作', + width: 160, render: (_: unknown, row: SettlementBatch) => ( - showItems(row)}>明细 - {row.status !== 'settled' && handleSettle(row)}>结算} + + {isSuperadmin && row.status !== 'settled' && ( + + )} ), }, ]} /> + + {/* Itemized Settlement Details Modal */} + + 结算账单对账明细 · {itemsFor?.batch_no ?? ''} + + } footer={null} + width={680} onCancel={() => setItemsFor(null)} + destroyOnClose > -
formatMoney(v) }, - { title: '状态', dataIndex: 'status' }, - ]} - /> + {itemsFor && ( + +
+ +
+ + + +
结算状态
+ + {STATUS_LABELS[itemsFor.status]?.label || itemsFor.status} + + + +
包含明细笔数
+ {items.length} 笔 + + + + +
订单 #{v}, + }, + { + title: '分润角色 / 收益类型', + dataIndex: 'party_role', + render: (v: string) => ( + {PARTY_ROLE_LABELS[v] || v} + ), + }, + { + title: '分成到账金额', + dataIndex: 'amount_cent', + render: (v: number) => ( + + +{formatMoney(v)} + + ), + }, + { + title: '入账状态', + dataIndex: 'status', + render: (v: string) => ( + + {v === 'settled' ? '已入账钱包' : v || '待打款'} + + ), + }, + ]} + /> + + )} ) diff --git a/distribution-admin/src/pages/SiteSettings.tsx b/distribution-admin/src/pages/SiteSettings.tsx index cc09b54..67e87e6 100644 --- a/distribution-admin/src/pages/SiteSettings.tsx +++ b/distribution-admin/src/pages/SiteSettings.tsx @@ -12,14 +12,20 @@ import { Col, Divider, Image, - Alert, Tag, + Tabs, + Collapse, } from 'antd' import { SaveOutlined, PictureOutlined, NotificationOutlined, EyeOutlined, + PlusOutlined, + DeleteOutlined, + InfoCircleOutlined, + SkinOutlined, + GlobalOutlined, } from '@ant-design/icons' import { listSites, @@ -30,6 +36,23 @@ import { } from '../api' import { useMember } from '../MemberContext' +interface BannerItem { + image_url: string + title?: string + link?: string +} + +interface AnnouncementItem { + title: string + content?: string +} + +interface FooterInfo { + copyright?: string + icp?: string + contact?: string +} + export default function SiteSettings() { const member = useMember() const [sites, setSites] = useState([]) @@ -38,7 +61,16 @@ export default function SiteSettings() { const [saving, setSaving] = useState(false) const [currentSite, setCurrentSite] = useState(null) - const [form] = Form.useForm() + // Visual state for customizer + const [logoUrl, setLogoUrl] = useState('') + const [faviconUrl, setFaviconUrl] = useState('') + const [banners, setBanners] = useState([]) + const [announcements, setAnnouncements] = useState([]) + const [footerInfo, setFooterInfo] = useState({ + copyright: '', + icp: '', + contact: '', + }) const loadSite = useCallback(async (id: number) => { setLoading(true) @@ -48,20 +80,33 @@ export default function SiteSettings() { const found = rows.find((s) => s.id === id) if (found) { setCurrentSite(found) - form.setFieldsValue({ - logo_url: found.logo_url || '', - favicon_url: found.favicon_url || '', - banners_json: found.banners_json || '[]', - announcements_json: found.announcements_json || '[]', - footer_info_json: found.footer_info_json || '{}', - }) + setLogoUrl(found.logo_url || '') + setFaviconUrl(found.favicon_url || '') + + try { + setBanners(found.banners_json ? JSON.parse(found.banners_json) : []) + } catch { + setBanners([]) + } + + try { + setAnnouncements(found.announcements_json ? JSON.parse(found.announcements_json) : []) + } catch { + setAnnouncements([]) + } + + try { + setFooterInfo(found.footer_info_json ? JSON.parse(found.footer_info_json) : {}) + } catch { + setFooterInfo({}) + } } } catch { message.error('加载站点设置失败') } finally { setLoading(false) } - }, [form]) + }, []) useEffect(() => { async function init() { @@ -95,21 +140,59 @@ export default function SiteSettings() { void init() }, [member, loadSite]) - async function handleSave(values: SiteBrandingPayload) { - if (!siteID) return - // Validate JSON formatting - try { - if (values.banners_json) JSON.parse(values.banners_json) - if (values.announcements_json) JSON.parse(values.announcements_json) - if (values.footer_info_json) JSON.parse(values.footer_info_json) - } catch { - message.error('轮播图、公告或页脚配置的 JSON 格式不正确,请检查!') - return - } + function handleAddBanner() { + setBanners([ + ...banners, + { + image_url: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=1200&q=80', + title: '热门推荐活动', + link: '', + }, + ]) + } + function handleUpdateBanner(index: number, field: keyof BannerItem, value: string) { + const next = [...banners] + next[index] = { ...next[index], [field]: value } + setBanners(next) + } + + function handleDeleteBanner(index: number) { + setBanners(banners.filter((_, i) => i !== index)) + } + + function handleAddAnnouncement() { + setAnnouncements([ + ...announcements, + { + title: '平台全新上线精选商品,免押畅玩!', + content: '', + }, + ]) + } + + function handleUpdateAnnouncement(index: number, field: keyof AnnouncementItem, value: string) { + const next = [...announcements] + next[index] = { ...next[index], [field]: value } + setAnnouncements(next) + } + + function handleDeleteAnnouncement(index: number) { + setAnnouncements(announcements.filter((_, i) => i !== index)) + } + + async function handleSave() { + if (!siteID) return setSaving(true) try { - const updated = await updateSiteBranding(siteID, values) + const payload: SiteBrandingPayload = { + logo_url: logoUrl.trim(), + favicon_url: faviconUrl.trim(), + banners_json: JSON.stringify(banners), + announcements_json: JSON.stringify(announcements), + footer_info_json: JSON.stringify(footerInfo), + } + const updated = await updateSiteBranding(siteID, payload) setCurrentSite(updated) message.success('分销站点品牌与装修配置保存成功!') } catch (error: unknown) { @@ -120,39 +203,21 @@ export default function SiteSettings() { } } - const logoUrl = Form.useWatch('logo_url', form) - const bannersJsonStr = Form.useWatch('banners_json', form) - const announcementsJsonStr = Form.useWatch('announcements_json', form) - - let banners: { image_url: string; title?: string }[] = [] - try { - if (bannersJsonStr) banners = JSON.parse(bannersJsonStr) - } catch { - // ignore json error in preview - } - - let announcements: { title: string; content?: string }[] = [] - try { - if (announcementsJsonStr) announcements = JSON.parse(announcementsJsonStr) - } catch { - // ignore - } - return ( - +
- 品牌与站点装修 + 品牌与站点可视化装修 - 自定义当前分销站前台 Logo、浏览器图标、轮播横幅、站点公告及页脚备案信息。 + 可视化配置当前分销独立站的 Logo、首页轮播海报、跑马灯公告及页脚客服信息。
{member?.is_superadmin && sites.length > 1 && ( - - -
- - - - - + {/* Left Visual Form Customizer */} + + + +
+ 站点 Logo 图片链接: + setLogoUrl(e.target.value)} + allowClear + style={{ marginTop: 6 }} + /> + + 建议尺寸 240x60 像素,透明底色 PNG 或 SVG 格式 + +
- +
+ 浏览器标签页 Favicon 图标: + setFaviconUrl(e.target.value)} + allowClear + style={{ marginTop: 6 }} + /> + + 浏览器选项卡显示的小图标,建议 32x32 像素 + +
+ + ), + }, + { + key: 'banners', + label: `首页轮播横幅 (${banners.length})`, + children: ( + +
+ + 为分销商城首页配置吸睛的促销横幅(支持自动轮播展示)。 + + +
- - - 首页轮播图配置 (Banners JSON) -
- } - name="banners_json" - extra='格式:[{"image_url": "图片URL", "title": "标题", "link": "点击跳转链接"}]' - > - - + {banners.length === 0 ? ( +
+ 暂未添加轮播横幅,点击上方按钮新增 +
+ ) : ( + banners.map((item, idx) => ( +
+
+ 横幅 #{idx + 1} + +
+ +
+
+ {item.image_url ? ( + + ) : ( +
无图片
+ )} +
+ + + + handleUpdateBanner(idx, 'image_url', e.target.value)} + /> + handleUpdateBanner(idx, 'title', e.target.value)} + /> + handleUpdateBanner(idx, 'link', e.target.value)} + /> + + + + + )) + )} + + ), + }, + { + key: 'announcements', + label: `跑马灯公告 (${announcements.length})`, + children: ( + +
+ + 在商城顶部轮播滚动的最新活动或服务保障公告。 + + +
- - - 站点跑马灯公告 (Announcements JSON) -
- } - name="announcements_json" - extra='格式:[{"title": "公告标题", "content": "详细内容"}]' - > - - + {announcements.length === 0 ? ( +
+ 暂无公告,点击上方按钮新增 +
+ ) : ( + announcements.map((item, idx) => ( +
+
+ 公告 #{idx + 1} + +
+ + handleUpdateAnnouncement(idx, 'title', e.target.value)} + /> + handleUpdateAnnouncement(idx, 'content', e.target.value)} + /> + +
+ )) + )} + + ), + }, + { + key: 'footer', + label: '页脚与客服备案', + children: ( + +
+ 商城版权文案: + setFooterInfo({ ...footerInfo, copyright: e.target.value })} + style={{ marginTop: 6 }} + /> +
- - - +
+ ICP 备案编号: + setFooterInfo({ ...footerInfo, icp: e.target.value })} + style={{ marginTop: 6 }} + /> +
- - - - +
+ 官方客服微信 / QQ: + setFooterInfo({ ...footerInfo, contact: e.target.value })} + style={{ marginTop: 6 }} + /> +
+
+ ), + }, + ]} + /> + + + + {/* Advanced Raw JSON Inspector (Collapsed) */} + + {JSON.stringify( + { + banners, + announcements, + footer_info: footerInfo, + }, + null, + 2 + )} + + ), + }, + ]} + /> -
- -
- {/* Header Preview */} -
-
- {logoUrl ? ( - - ) : ( - - {currentSite?.site_name || '分销站点名称'} - - )} + {/* Right Live Visual Simulation Card */} +
+ +
+ {/* Simulated Browser Bar */} +
+
+
+
+
+
+
+ https://{currentSite?.domain || `${currentSite?.site_code || 'demo'}.hfb.com`}
- 分销前台顶栏
- {/* Announcement Preview */} + {/* Simulated Header Nav */} +
+ {logoUrl ? ( + Logo + ) : ( + + {currentSite?.site_name || 'HFB 分销商城'} + + )} + 分销前台 +
+ + {/* Simulated Announcements Marquee */} {announcements.length > 0 && ( -
- - - {announcements[0].title} - - } - type="warning" - banner - style={{ borderRadius: 4, padding: '4px 12px' }} - /> +
+ + + {announcements[0]?.title} +
)} - {/* Banner Preview */} -
-
轮播图预览:
- {banners.length > 0 && banners[0].image_url ? ( -
- Banner { - (e.target as HTMLImageElement).src = 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=600&auto=format&fit=crop&q=60' - }} - /> -
+ {/* Simulated Banner Carousel */} +
+ {banners.length > 0 && banners[0]?.image_url ? ( + Banner ) : ( -
- 暂无轮播图配置 +
+ 暂无轮播横幅,请在左侧添加 +
+ )} + {banners.length > 0 && banners[0]?.title && ( +
+ {banners[0].title}
)}
- {/* Content Placeholder */} -
- [ 游戏商品列表展示区域 ] + {/* Simulated Account Listings Grid Preview */} +
+ + 精选租号货架 + +
+
+ 三角洲全金典藏号 +
¥35.00 /小时
+
+
+ 无畏契约全套幻化 +
¥28.00 /小时
+
+
- {/* Footer Preview */} -
-
{currentSite?.site_name || '分销站点'} · 技术支持由 HFB 平台提供
+ {/* Simulated Footer */} +
+
{footerInfo.copyright || `© 2026 ${currentSite?.site_name || '分销网'}`}
+ {footerInfo.icp &&
{footerInfo.icp}
} + {footerInfo.contact &&
{footerInfo.contact}
}
diff --git a/distribution-admin/vite.config.ts b/distribution-admin/vite.config.ts index 7cdb5fd..45b01b3 100644 --- a/distribution-admin/vite.config.ts +++ b/distribution-admin/vite.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ plugins: [react()], server: { port: distributionPort, + allowedHosts: true, proxy: { '/api': { target: apiProxyTarget,