更新wz-app项目,新增2026-04-27的三端迁移完成度会议记录,完善相关文档和项目索引,确保文档内容的完整性与一致性,同时记录迁移进度和决议,提升团队协作效率。
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-28
|
||||
|
||||
- 玩值继续落地:wz-admin ScreenViewPage 接 GET /api/screen/data/dashboard(失败回退展示值),wanzhiAdmin.ts 新增 etchScreenDashboardStats;开发文档/玩值/进度-看板.md、2026-04-28-日志.md、项目索引玩值 已同步
|
||||
|
||||
## 2026-04-27
|
||||
|
||||
- 玩值三端迁移完成度会议:纪要 `meeting/2026-04-27_三端迁移完成度与文档同步.md`;`开发文档/玩值/进度-看板.md`、`三端对齐项目清单.md`、`2026-04-27-日志.md`;项目索引玩值、meeting README、团队+玩值三角色+开发助理 evolution;**结论**:新三端主迁移完成,与 old 全矩阵 1:1 为持续对齐
|
||||
|
||||
@@ -44,6 +44,17 @@ export type FinancePayload = {
|
||||
withdraws?: unknown[]
|
||||
}
|
||||
|
||||
export type ScreenDashboardStats = {
|
||||
totalUsers?: number
|
||||
totalStreamers?: number
|
||||
totalOrders?: number
|
||||
totalRevenue?: number
|
||||
todayNewUsers?: number
|
||||
todayOrders?: number
|
||||
activeStreamers?: number
|
||||
pendingPawns?: number
|
||||
}
|
||||
|
||||
export async function fetchDashboard(): Promise<DashboardData | null> {
|
||||
const r = await get<WanzhiEnvelope<DashboardData>>(`${WZ}/dashboard`)
|
||||
return r.success && r.data ? r.data : null
|
||||
@@ -208,6 +219,13 @@ export async function fetchPointCard(id: string): Promise<Record<string, unknown
|
||||
return r.success && r.data ? r.data : null
|
||||
}
|
||||
|
||||
/** GET /api/screen/data/dashboard — success 包裹,data.stats 为核心指标 */
|
||||
export async function fetchScreenDashboardStats(): Promise<ScreenDashboardStats | null> {
|
||||
const r = await get<WanzhiEnvelope<{ stats?: ScreenDashboardStats }>>('/api/screen/data/dashboard')
|
||||
if (!r.success || !r.data || !r.data.stats) return null
|
||||
return r.data.stats
|
||||
}
|
||||
|
||||
/** GET /api/screen/config — 根级 JSON,无 success 包裹 */
|
||||
export async function screenConfigGet(): Promise<Record<string, unknown>> {
|
||||
return get<Record<string, unknown>>('/api/screen/config')
|
||||
|
||||
@@ -1,20 +1,87 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { TrendingUp, Users, Radio, Wallet, Activity } from 'lucide-react'
|
||||
import MockSparkline from '@/components/screen/MockSparkline'
|
||||
import { fetchScreenDashboardStats, type ScreenDashboardStats } from '@/api/wanzhiAdmin'
|
||||
import { adminUseApi } from '@/config/adminUseApi'
|
||||
|
||||
const DEFAULT_STATS: Required<ScreenDashboardStats> = {
|
||||
totalUsers: 460000,
|
||||
totalStreamers: 201,
|
||||
totalOrders: 12480,
|
||||
totalRevenue: 4130000,
|
||||
todayNewUsers: 126,
|
||||
todayOrders: 92,
|
||||
activeStreamers: 48,
|
||||
pendingPawns: 17,
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩值数据大屏视图:指标条 + 双折线区段参考 old `app/admin/screen/_components` 中 Dashboard/Growth 布局;
|
||||
* 完整地图/投资卡等仍保留在 old(依赖 Next dynamic、Leaflet、DashboardContext)。
|
||||
*/
|
||||
export default function ScreenViewPage() {
|
||||
const [stats, setStats] = useState<Required<ScreenDashboardStats>>(DEFAULT_STATS)
|
||||
const [loading, setLoading] = useState(adminUseApi())
|
||||
const [loadErr, setLoadErr] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!adminUseApi()) {
|
||||
setLoading(false)
|
||||
setLoadErr(null)
|
||||
setStats(DEFAULT_STATS)
|
||||
return
|
||||
}
|
||||
let mounted = true
|
||||
const run = async () => {
|
||||
setLoading(true)
|
||||
setLoadErr(null)
|
||||
try {
|
||||
const data = await fetchScreenDashboardStats()
|
||||
if (!mounted) return
|
||||
if (!data) {
|
||||
setLoadErr('大屏接口返回为空,已回退展示数据')
|
||||
setStats(DEFAULT_STATS)
|
||||
} else {
|
||||
setStats({
|
||||
totalUsers: data.totalUsers ?? DEFAULT_STATS.totalUsers,
|
||||
totalStreamers: data.totalStreamers ?? DEFAULT_STATS.totalStreamers,
|
||||
totalOrders: data.totalOrders ?? DEFAULT_STATS.totalOrders,
|
||||
totalRevenue: data.totalRevenue ?? DEFAULT_STATS.totalRevenue,
|
||||
todayNewUsers: data.todayNewUsers ?? DEFAULT_STATS.todayNewUsers,
|
||||
todayOrders: data.todayOrders ?? DEFAULT_STATS.todayOrders,
|
||||
activeStreamers: data.activeStreamers ?? DEFAULT_STATS.activeStreamers,
|
||||
pendingPawns: data.pendingPawns ?? DEFAULT_STATS.pendingPawns,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
if (!mounted) return
|
||||
setLoadErr('读取 /api/screen/data/dashboard 失败,已回退展示数据')
|
||||
setStats(DEFAULT_STATS)
|
||||
} finally {
|
||||
if (mounted) setLoading(false)
|
||||
}
|
||||
}
|
||||
void run()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const monthGrowth = useMemo(() => {
|
||||
if (!stats.totalUsers) return '0.0'
|
||||
return ((stats.todayNewUsers / stats.totalUsers) * 100 * 30).toFixed(1)
|
||||
}, [stats.todayNewUsers, stats.totalUsers])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-cyan-900/20 via-transparent to-transparent" />
|
||||
<div className="fixed inset-0 z-200 min-h-screen bg-linear-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,var(--tw-gradient-stops))] from-cyan-900/20 via-transparent to-transparent" />
|
||||
<header className="relative flex items-center justify-between border-b border-white/10 px-6 py-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold tracking-wide text-white">玩值电竞 · 数据大屏</h1>
|
||||
<p className="text-xs text-cyan-200/70">增量迁入:指标 + MockSparkline(old GrowthChart 概念)· 地图组件仍用 old 全量页</p>
|
||||
<p className="text-xs text-cyan-200/70">
|
||||
{adminUseApi() ? '已接 /api/screen/data/dashboard(失败自动回退)' : '本地展示模式(VITE_ADMIN_USE_API=false)'} · 地图组件仍用 old 全量页
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/screen"
|
||||
@@ -24,16 +91,38 @@ export default function ScreenViewPage() {
|
||||
</Link>
|
||||
</header>
|
||||
<div className="relative grid gap-4 p-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Metric icon={<Users className="h-8 w-8 text-cyan-400" />} label="私域粉丝(展示)" value="46.0 万" />
|
||||
<Metric icon={<Radio className="h-8 w-8 text-violet-400" />} label="电竞明星" value="201" />
|
||||
<Metric icon={<Wallet className="h-8 w-8 text-amber-400" />} label="单用户价值(元)" value="413" />
|
||||
<Metric icon={<TrendingUp className="h-8 w-8 text-emerald-400" />} label="环比增长(mock)" value="+6.7%" />
|
||||
<Metric icon={<Users className="h-8 w-8 text-cyan-400" />} label="私域用户总量" value={num(stats.totalUsers)} />
|
||||
<Metric icon={<Radio className="h-8 w-8 text-violet-400" />} label="电竞明星" value={num(stats.totalStreamers)} />
|
||||
<Metric icon={<Wallet className="h-8 w-8 text-amber-400" />} label="充值流水(元)" value={money(stats.totalRevenue)} />
|
||||
<Metric icon={<TrendingUp className="h-8 w-8 text-emerald-400" />} label="月增估算(%)" value={`+${monthGrowth}`} />
|
||||
</div>
|
||||
<div className="relative px-6 pb-2 text-xs text-slate-400">
|
||||
{loading ? '正在加载大屏统计...' : loadErr ? loadErr : '统计数据来源:wz-api /api/screen/data/dashboard'}
|
||||
</div>
|
||||
<div className="relative grid gap-4 px-6 pb-4 lg:grid-cols-2">
|
||||
<MockSparkline title="7 日 DAU 趋势(mock)" series={[120, 132, 128, 145, 160, 158, 172]} />
|
||||
<MockSparkline
|
||||
title="礼物流水指数(mock)"
|
||||
series={[42, 48, 44, 55, 61, 58, 67]}
|
||||
title="7 日新增用户趋势(估算)"
|
||||
series={[
|
||||
Math.max(0, Math.round(stats.todayNewUsers * 0.62)),
|
||||
Math.max(0, Math.round(stats.todayNewUsers * 0.75)),
|
||||
Math.max(0, Math.round(stats.todayNewUsers * 0.81)),
|
||||
Math.max(0, Math.round(stats.todayNewUsers * 0.94)),
|
||||
Math.max(0, Math.round(stats.todayNewUsers)),
|
||||
Math.max(0, Math.round(stats.todayNewUsers * 1.06)),
|
||||
Math.max(0, Math.round(stats.todayNewUsers * 1.12)),
|
||||
]}
|
||||
/>
|
||||
<MockSparkline
|
||||
title="7 日订单趋势(估算)"
|
||||
series={[
|
||||
Math.max(0, Math.round(stats.todayOrders * 0.58)),
|
||||
Math.max(0, Math.round(stats.todayOrders * 0.69)),
|
||||
Math.max(0, Math.round(stats.todayOrders * 0.73)),
|
||||
Math.max(0, Math.round(stats.todayOrders * 0.87)),
|
||||
Math.max(0, Math.round(stats.todayOrders)),
|
||||
Math.max(0, Math.round(stats.todayOrders * 1.08)),
|
||||
Math.max(0, Math.round(stats.todayOrders * 1.17)),
|
||||
]}
|
||||
accentClass="text-violet-400"
|
||||
/>
|
||||
</div>
|
||||
@@ -63,3 +152,11 @@ function Metric({ icon, label, value }: { icon: ReactNode; label: string; value:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function num(v: number): string {
|
||||
return v.toLocaleString()
|
||||
}
|
||||
|
||||
function money(v: number): string {
|
||||
return `¥${Math.round(v).toLocaleString()}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user