chore: 停止上传开发文档并同步代码

- 从仓库索引移除 开发文档/(本地保留)
- 忽略 wechat/info.log 与 soul-api-linux
- 同步小程序/管理端/API改动

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 15:25:26 +08:00
parent c6904e4a32
commit f9d5e85b4e
350 changed files with 2588 additions and 64437 deletions

View File

@@ -85,12 +85,13 @@ export function DashboardPage() {
const [detailUserId, setDetailUserId] = useState<string | null>(null)
const [showDetailModal, setShowDetailModal] = useState(false)
const [trackPeriod, setTrackPeriod] = useState<string>('today')
const [trackPeriod, setTrackPeriod] = useState<string>('week')
const [trackStats, setTrackStats] = useState<{
total: number
byModule: Record<string, { action: string; target: string; module: string; page: string; count: number }[]>
} | null>(null)
const [trackLoading, setTrackLoading] = useState(false)
const [ordersExpanded, setOrdersExpanded] = useState(false)
const showError = (err: unknown) => {
const e = err as Error & { status?: number; name?: string }
@@ -159,7 +160,7 @@ export function DashboardPage() {
const ordersData = await get<OrdersRes>('/api/admin/orders?page=1&pageSize=20&status=paid', init)
const orders = ordersData?.orders ?? []
const paid = orders.filter((p) => ['paid', 'completed', 'success'].includes(p.status || ''))
setPurchases(paid.slice(0, 5))
setPurchases(paid.slice(0, 10))
} catch {
setPurchases([])
}
@@ -383,7 +384,7 @@ export function DashboardPage() {
) : (
<>
{purchases
.slice(0, 5)
.slice(0, ordersExpanded ? 10 : 4)
.map((p) => {
const referrer: UserRow | undefined = p.referrerId
? users.find((u) => u.id === p.referrerId)
@@ -476,6 +477,16 @@ export function DashboardPage() {
<p className="text-gray-500"></p>
</div>
)}
{purchases.length > 4 && (
<button
type="button"
onClick={() => setOrdersExpanded(!ordersExpanded)}
className="w-full py-2 text-xs text-gray-400 hover:text-[#38bdac] transition-colors flex items-center justify-center gap-1"
>
<ChevronRight className={`w-3.5 h-3.5 transition-transform ${ordersExpanded ? 'rotate-90' : 'rotate-270'}`} />
{ordersExpanded ? '收起' : `展开更多(共 ${purchases.length} 条)`}
</button>
)}
</>
)}
</div>
@@ -591,10 +602,30 @@ export function DashboardPage() {
{items
.sort((a, b) => b.count - a.count)
.slice(0, 8)
.map((item, i) => (
.map((item, i) => {
const targetLabels: Record<string, string> = {
'开始匹配': '开始匹配', 'mentor': '导师顾问', 'team': '团队招募',
'investor': '资源对接', '充值': '充值', '退款': '退款',
'wallet': '钱包', '设置': '设置', 'VIP': 'VIP会员',
'推广': '推广中心', '目录': '目录', '搜索': '搜索',
'匹配': '找伙伴', 'settings': '设置', 'expired': '已过期',
'active': '活跃', 'converted': '已转化', 'fill_profile': '完善资料',
'register': '注册', 'purchase': '购买', 'btn_click': '按钮点击',
'nav_click': '导航点击', 'card_click': '卡片点击', 'tab_click': '标签切换',
'rule_trigger': '规则触发', 'view_chapter': '浏览章节',
'链接卡若': '链接卡若', '更多分享': '更多分享', '分享朋友圈文案': '分享朋友圈',
'选择金额10': '选择金额10元',
}
const actionLabels: Record<string, string> = {
'btn_click': '按钮点击', 'nav_click': '导航点击', 'card_click': '卡片点击',
'tab_click': '标签切换', 'purchase': '购买', 'register': '注册',
'rule_trigger': '规则触发', 'view_chapter': '浏览章节',
}
const label = targetLabels[item.target] || item.target || actionLabels[item.action] || item.action
return (
<div key={i} className="flex items-center justify-between text-xs">
<span className="text-gray-300 truncate mr-2" title={`${item.action}: ${item.target}`}>
{item.target || item.action}
{label}
</span>
<div className="flex items-center gap-2 shrink-0">
<div className="w-16 h-1.5 bg-gray-700 rounded-full overflow-hidden">
@@ -606,7 +637,7 @@ export function DashboardPage() {
<span className="text-gray-400 w-8 text-right">{item.count}</span>
</div>
</div>
))}
)})}
</div>
</div>
)

View File

@@ -11,7 +11,6 @@ import {
RefreshCw,
CheckCircle,
XCircle,
Calendar,
DollarSign,
Link2,
Eye,
@@ -22,7 +21,7 @@ import { ReferralSettingsPage } from '@/pages/referral-settings/ReferralSettings
import { Pagination } from '@/components/ui/Pagination'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
@@ -554,275 +553,128 @@ export function DistributionPage() {
) : (
<>
{activeTab === 'overview' && overview && (
<div className="space-y-6">
<div className="grid grid-cols-4 gap-4">
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-white mt-1">{overview.todayClicks}</p>
<p className="text-xs text-gray-500 mt-0.5"></p>
<div className="space-y-5">
{/* 今日核心指标 - 一行紧凑 */}
<div className="grid grid-cols-6 gap-3">
{[
{ label: '今日点击', value: overview.todayClicks, icon: Eye, color: 'blue' },
{ label: '独立用户', value: overview.todayUniqueVisitors ?? 0, icon: Users, color: 'cyan' },
{ label: '人均点击', value: (overview.todayClickRate ?? 0).toFixed(1), icon: TrendingUp, color: 'amber' },
{ label: '今日绑定', value: overview.todayBindings, icon: Link2, color: 'green' },
{ label: '今日转化', value: overview.todayConversions, icon: CheckCircle, color: 'purple' },
{ label: '今日佣金', value: `¥${overview.todayEarnings.toFixed(2)}`, icon: DollarSign, color: 'teal', isMoney: true },
].map((item) => (
<Card key={item.label} className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-2">
<p className="text-gray-400 text-xs">{item.label}</p>
<item.icon className={`w-4 h-4 text-${item.color}-400 opacity-60`} />
</div>
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
<Eye className="w-6 h-6 text-blue-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-white mt-1">{overview.todayUniqueVisitors ?? 0}</p>
<p className="text-xs text-gray-500 mt-0.5">访</p>
</div>
<div className="w-12 h-12 rounded-xl bg-cyan-500/20 flex items-center justify-center">
<Users className="w-6 h-6 text-cyan-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-white mt-1">
{(overview.todayClickRate ?? 0).toFixed(2)}
</p>
<p className="text-xs text-gray-500 mt-0.5">/</p>
</div>
<div className="w-12 h-12 rounded-xl bg-amber-500/20 flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-amber-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-white mt-1">{overview.todayBindings}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
<Link2 className="w-6 h-6 text-green-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-white mt-1">{overview.todayConversions}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-purple-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-[#38bdac] mt-1">
¥{overview.todayEarnings.toFixed(2)}
</p>
</div>
<div className="w-12 h-12 rounded-xl bg-[#38bdac]/20 flex items-center justify-center">
<DollarSign className="w-6 h-6 text-[#38bdac]" />
</div>
</div>
</CardContent>
</Card>
<p className={`text-xl font-bold ${item.isMoney ? 'text-[#38bdac]' : 'text-white'}`}>{item.value}</p>
</CardContent>
</Card>
))}
</div>
{/* 每篇文章今日点击 */}
{(overview.todayClicksByPage?.length ?? 0) > 0 && (
<Card className="bg-[#0f2137] border-gray-700/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Eye className="w-5 h-5 text-[#38bdac]" />
/
</CardTitle>
<p className="text-gray-400 text-sm mt-1"></p>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-700 text-left text-gray-400">
<th className="pb-3 pr-4">/</th>
<th className="pb-3 pr-4 text-right"></th>
<th className="pb-3 text-right"></th>
</tr>
</thead>
<tbody>
{[...(overview.todayClicksByPage ?? [])]
.sort((a, b) => b.clicks - a.clicks)
.map((row, i) => (
<tr key={i} className="border-b border-gray-700/50">
<td className="py-2 pr-4 text-white font-mono">{row.page || '(未区分)'}</td>
<td className="py-2 pr-4 text-right text-white">{row.clicks}</td>
<td className="py-2 text-right text-gray-400">
{overview.todayClicks > 0
? ((row.clicks / overview.todayClicks) * 100).toFixed(1)
: 0}
%
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 文章点击 + 提醒 并排 */}
<div className="grid grid-cols-3 gap-4">
{/* 文章点击表 */}
<Card className="bg-[#0f2137] border-gray-700/50 col-span-2">
<CardContent className="p-4">
<h4 className="text-sm font-medium text-white mb-3 flex items-center gap-1.5">
<Eye className="w-4 h-4 text-[#38bdac]" />
</h4>
{(overview.todayClicksByPage?.length ?? 0) > 0 ? (
<div className="space-y-1.5 max-h-[200px] overflow-y-auto">
{[...(overview.todayClicksByPage ?? [])].sort((a, b) => b.clicks - a.clicks).map((row, i) => (
<div key={i} className="flex items-center justify-between text-xs py-1 border-b border-gray-700/30 last:border-0">
<span className="text-gray-300 truncate mr-2">{row.page || '(未区分)'}</span>
<div className="flex items-center gap-2 shrink-0">
<span className="text-white font-medium">{row.clicks}</span>
<span className="text-gray-500 w-12 text-right">{overview.todayClicks > 0 ? ((row.clicks / overview.todayClicks) * 100).toFixed(1) : 0}%</span>
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500 text-xs"></p>
)}
</CardContent>
</Card>
)}
<div className="grid grid-cols-2 gap-4">
<Card className="bg-orange-500/10 border-orange-500/30">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center">
<Clock className="w-6 h-6 text-orange-400" />
{/* 提醒卡片 */}
<div className="space-y-3">
<Card className="bg-orange-500/10 border-orange-500/30">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-orange-400 shrink-0" />
<div>
<p className="text-orange-300 text-xs font-medium"></p>
<p className="text-xl font-bold text-white">{overview.expiringBindings} </p>
<p className="text-orange-300/50 text-[10px]">7</p>
</div>
</div>
<div className="flex-1">
<p className="text-orange-300 font-medium"></p>
<p className="text-2xl font-bold text-white">{overview.expiringBindings} </p>
<p className="text-orange-300/60 text-sm">7</p>
</CardContent>
</Card>
<Card className="bg-blue-500/10 border-blue-500/30 cursor-pointer" onClick={() => setActiveTab('withdrawals')}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Wallet className="w-5 h-5 text-blue-400 shrink-0" />
<div>
<p className="text-blue-300 text-xs font-medium"></p>
<p className="text-xl font-bold text-white">{overview.pendingWithdrawals} </p>
<p className="text-blue-300/50 text-[10px]"> ¥{overview.pendingWithdrawAmount.toFixed(2)} </p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-blue-500/10 border-blue-500/30">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
<Wallet className="w-6 h-6 text-blue-400" />
</div>
<div className="flex-1">
<p className="text-blue-300 font-medium"></p>
<p className="text-2xl font-bold text-white">{overview.pendingWithdrawals} </p>
<p className="text-blue-300/60 text-sm">
¥{overview.pendingWithdrawAmount.toFixed(2)}
</p>
</div>
<Button
onClick={() => setActiveTab('withdrawals')}
variant="outline"
className="border-blue-500/50 text-blue-400 hover:bg-blue-500/20"
>
</Button>
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-2 gap-6">
<Card className="bg-[#0f2137] border-gray-700/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Calendar className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-white">{overview.monthClicks}</p>
</div>
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-white">{overview.monthBindings}</p>
</div>
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-white">{overview.monthConversions}</p>
</div>
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-[#38bdac]">
¥{overview.monthEarnings.toFixed(2)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-white">
{overview.totalClicks.toLocaleString()}
</p>
</div>
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-white">
{overview.totalBindings.toLocaleString()}
</p>
</div>
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-white">{overview.totalConversions}</p>
</div>
<div className="p-4 bg-white/5 rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-xl font-bold text-[#38bdac]">
¥{overview.totalEarnings.toFixed(2)}
</p>
</div>
</div>
<div className="mt-4 p-4 bg-[#38bdac]/10 rounded-lg flex items-center justify-between">
<span className="text-gray-300"></span>
<span className="text-[#38bdac] font-bold text-xl">
{overview.conversionRate}%
</span>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
</div>
{/* 本月/累计/推广 合并成一张表 */}
<Card className="bg-[#0f2137] border-gray-700/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Users className="w-5 h-5 text-[#38bdac]" />
广
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-4 gap-4">
<div className="p-4 bg-white/5 rounded-lg text-center">
<p className="text-3xl font-bold text-white">{overview.totalDistributors}</p>
<p className="text-gray-400 text-sm mt-1">广</p>
</div>
<div className="p-4 bg-white/5 rounded-lg text-center">
<p className="text-3xl font-bold text-green-400">{overview.activeDistributors}</p>
<p className="text-gray-400 text-sm mt-1"></p>
</div>
<div className="p-4 bg-white/5 rounded-lg text-center">
<p className="text-3xl font-bold text-[#38bdac]">90%</p>
<p className="text-gray-400 text-sm mt-1"></p>
</div>
<div className="p-4 bg-white/5 rounded-lg text-center">
<p className="text-3xl font-bold text-orange-400">30</p>
<p className="text-gray-400 text-sm mt-1"></p>
</div>
<CardContent className="p-4">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-700 text-gray-400 text-xs">
<th className="pb-2 text-left font-normal"></th>
<th className="pb-2 text-right font-normal"></th>
<th className="pb-2 text-right font-normal"></th>
</tr>
</thead>
<tbody className="text-white">
<tr className="border-b border-gray-700/30">
<td className="py-2.5 text-gray-300"></td>
<td className="py-2.5 text-right font-medium">{overview.monthClicks}</td>
<td className="py-2.5 text-right font-medium">{overview.totalClicks.toLocaleString()}</td>
</tr>
<tr className="border-b border-gray-700/30">
<td className="py-2.5 text-gray-300"></td>
<td className="py-2.5 text-right font-medium">{overview.monthBindings}</td>
<td className="py-2.5 text-right font-medium">{overview.totalBindings.toLocaleString()}</td>
</tr>
<tr className="border-b border-gray-700/30">
<td className="py-2.5 text-gray-300"></td>
<td className="py-2.5 text-right font-medium">{overview.monthConversions}</td>
<td className="py-2.5 text-right font-medium">{overview.totalConversions}</td>
</tr>
<tr className="border-b border-gray-700/30">
<td className="py-2.5 text-gray-300"></td>
<td className="py-2.5 text-right font-medium text-[#38bdac]">¥{overview.monthEarnings.toFixed(2)}</td>
<td className="py-2.5 text-right font-medium text-[#38bdac]">¥{overview.totalEarnings.toFixed(2)}</td>
</tr>
<tr>
<td className="py-2.5 text-gray-300"></td>
<td className="py-2.5 text-right"></td>
<td className="py-2.5 text-right font-medium text-[#38bdac]">{overview.conversionRate}%</td>
</tr>
</tbody>
</table>
<div className="flex items-center gap-6 mt-4 pt-3 border-t border-gray-700/30 text-xs">
<span className="text-gray-400">广 <span className="text-white font-medium ml-1">{overview.totalDistributors}</span></span>
<span className="text-gray-400"> <span className="text-green-400 font-medium ml-1">{overview.activeDistributors}</span></span>
<span className="text-gray-400"> <span className="text-[#38bdac] font-medium ml-1">90%</span></span>
<span className="text-gray-400"> <span className="text-orange-400 font-medium ml-1">30</span></span>
</div>
</CardContent>
</Card>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, CardContent } from '@/components/ui/card'
import {
@@ -23,8 +23,8 @@ interface CKBPlanStats {
ckbApiUrl: string
}
const typeLabels: Record<string, string> = { partner: '找伙伴', investor: '资源对接', mentor: '导师顾问', team: '团队招募' }
const typeIcons: Record<string, string> = { partner: '⭐', investor: '👥', mentor: '❤️', team: '🎮' }
const typeLabels: Record<string, string> = { partner: '找伙伴', investor: '资源对接', mentor: '导师顾问', team: '团队招募', join: '加入', match: '匹配' }
const typeIcons: Record<string, string> = { partner: '⭐', investor: '👥', mentor: '❤️', team: '🎮', join: '📋', match: '🔗' }
interface Props {
onSwitchTab?: (tabId: string) => void
@@ -69,129 +69,116 @@ export function CKBStatsTab({ onSwitchTab, onOpenCKB }: Props = {}) {
useEffect(() => { loadStats() }, [loadStats])
const v = (n: number | undefined) => isLoading ? '—' : String(n ?? 0)
const avgMatch = stats?.uniqueUsers ? (stats.totalMatches / stats.uniqueUsers).toFixed(1) : '0'
return (
<div className="space-y-8">
{/* ===== 区块一:找伙伴核心数据 ===== */}
<div>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Users className="w-5 h-5 text-[#38bdac]" />
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-5">
<Card className="bg-gradient-to-br from-[#0f2137] to-[#162d4a] border-gray-700/40 cursor-pointer hover:border-[#38bdac]/60 transition-all" onClick={() => onSwitchTab?.('partner')}>
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-4xl font-bold text-white">{v(stats?.totalMatches)}</p>
<p className="text-[#38bdac] text-xs mt-3 flex items-center gap-1"><ExternalLink className="w-3 h-3" /> </p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-[#0f2137] to-[#162d4a] border-gray-700/40 cursor-pointer hover:border-yellow-500/60 transition-all" onClick={() => onSwitchTab?.('partner')}>
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-4xl font-bold text-white">{v(stats?.todayMatches)}</p>
<p className="text-yellow-400/60 text-xs mt-3 flex items-center gap-1"><Zap className="w-3 h-3" /> </p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-[#0f2137] to-[#162d4a] border-gray-700/40 cursor-pointer hover:border-blue-500/60 transition-all" onClick={() => navigate('/users')}>
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-4xl font-bold text-white">{v(stats?.uniqueUsers)}</p>
<p className="text-blue-400/60 text-xs mt-3 flex items-center gap-1"><ExternalLink className="w-3 h-3" /> </p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/40">
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-3xl font-bold text-white">{isLoading ? '—' : (stats?.uniqueUsers ? (stats.totalMatches / stats.uniqueUsers).toFixed(1) : '0')}</p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/40">
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-3xl font-bold text-white">{v(stats?.paidMatchCount)}</p>
</CardContent>
</Card>
</div>
<div className="space-y-6">
{/* 核心指标:一行紧凑卡片 */}
<div className="grid grid-cols-5 gap-3">
<Card className="bg-[#0f2137] border-gray-700/40 cursor-pointer hover:border-[#38bdac]/60 transition-all" onClick={() => onSwitchTab?.('partner')}>
<CardContent className="p-4">
<p className="text-gray-400 text-xs"></p>
<p className="text-2xl font-bold text-white mt-1">{v(stats?.totalMatches)}</p>
<p className="text-[#38bdac] text-[10px] mt-1 flex items-center gap-0.5"><ExternalLink className="w-2.5 h-2.5" /> </p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/40">
<CardContent className="p-4">
<p className="text-gray-400 text-xs"></p>
<p className="text-2xl font-bold text-white mt-1">{v(stats?.todayMatches)}</p>
<p className="text-yellow-400/60 text-[10px] mt-1 flex items-center gap-0.5"><Zap className="w-2.5 h-2.5" /> </p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/40 cursor-pointer hover:border-blue-500/60 transition-all" onClick={() => navigate('/users')}>
<CardContent className="p-4">
<p className="text-gray-400 text-xs"></p>
<p className="text-2xl font-bold text-white mt-1">{v(stats?.uniqueUsers)}</p>
<p className="text-blue-400/60 text-[10px] mt-1 flex items-center gap-0.5"><Users className="w-2.5 h-2.5" /> </p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/40">
<CardContent className="p-4">
<p className="text-gray-400 text-xs"></p>
<p className="text-2xl font-bold text-white mt-1">{isLoading ? '—' : avgMatch}</p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/40">
<CardContent className="p-4">
<p className="text-gray-400 text-xs"></p>
<p className="text-2xl font-bold text-white mt-1">{v(stats?.paidMatchCount)}</p>
</CardContent>
</Card>
</div>
{/* 类型分布 */}
{stats?.byType && stats.byType.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-white mb-4"></h3>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{stats.byType.map(item => {
const pct = stats.totalMatches > 0 ? ((item.count / stats.totalMatches) * 100) : 0
return (
<div key={item.matchType} className="bg-[#0f2137] border border-gray-700/40 rounded-xl p-5">
<div className="flex items-center gap-2 mb-3">
<span className="text-2xl">{typeIcons[item.matchType] || '📊'}</span>
<span className="text-gray-300 font-medium">{typeLabels[item.matchType] || item.matchType}</span>
</div>
<p className="text-3xl font-bold text-white mb-2">{item.count}</p>
<div className="w-full h-2 bg-gray-700/50 rounded-full overflow-hidden">
<div className="h-full bg-[#38bdac] rounded-full transition-all" style={{ width: `${Math.min(pct, 100)}%` }} />
</div>
<p className="text-gray-500 text-xs mt-1.5">{pct.toFixed(1)}%</p>
</div>
)
})}
</div>
</div>
)}
{/* ===== 区块二AI 获客数据 ===== */}
<div>
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Link2 className="w-5 h-5 text-orange-400" /> AI
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-5 mb-6">
<Card className="bg-[#0f2137] border-orange-500/20 cursor-pointer hover:border-orange-500/50 transition-colors" onClick={() => onOpenCKB?.('submitted')}>
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2">线</p>
<p className="text-3xl font-bold text-white">{isLoading ? '—' : (ckbStats?.ckbTotal ?? 0)}</p>
<p className="text-orange-400/60 text-xs mt-2"> </p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-orange-500/20 cursor-pointer hover:border-orange-500/50 transition-colors" onClick={() => onOpenCKB?.('contact')}>
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2"></p>
<p className="text-3xl font-bold text-white">{isLoading ? '—' : (ckbStats?.withContact ?? 0)}</p>
<p className="text-orange-400/60 text-xs mt-2"> </p>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-orange-500/20 cursor-pointer hover:border-orange-500/50 transition-colors" onClick={() => onOpenCKB?.('test')}>
<CardContent className="p-6">
<p className="text-gray-400 text-sm mb-2">AI </p>
<p className="text-xl font-bold text-orange-400"> </p>
<p className="text-gray-500 text-xs mt-2"> · · API </p>
</CardContent>
</Card>
</div>
{/* CKB 各类型提交统计 */}
{ckbStats?.byType && ckbStats.byType.length > 0 && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
{ckbStats.byType.map(item => (
<div key={item.matchType} className="bg-[#0a1628] border border-gray-700/30 rounded-lg p-4 flex items-center gap-3">
<span className="text-xl">{typeIcons[item.matchType] || '📋'}</span>
<div>
<p className="text-gray-400 text-xs">{typeLabels[item.matchType] || item.matchType}</p>
<p className="text-xl font-bold text-white">{item.total}</p>
</div>
{/* 类型分布 + AI 获客:并排两列 */}
<div className="grid grid-cols-2 gap-4">
{/* 左列:匹配类型分布 */}
<Card className="bg-[#0f2137] border-gray-700/40">
<CardContent className="p-4">
<h4 className="text-sm font-medium text-white mb-3"></h4>
{stats?.byType && stats.byType.length > 0 ? (
<div className="space-y-2">
{stats.byType.map(item => {
const pct = stats.totalMatches > 0 ? ((item.count / stats.totalMatches) * 100) : 0
return (
<div key={item.matchType} className="flex items-center gap-3">
<span className="text-lg shrink-0">{typeIcons[item.matchType] || '📊'}</span>
<div className="flex-1 min-w-0">
<div className="flex justify-between text-xs mb-0.5">
<span className="text-gray-300">{typeLabels[item.matchType] || item.matchType}</span>
<span className="text-gray-500">{item.count} ({pct.toFixed(0)}%)</span>
</div>
<div className="w-full h-1.5 bg-gray-700/50 rounded-full overflow-hidden">
<div className="h-full bg-[#38bdac] rounded-full" style={{ width: `${Math.min(pct, 100)}%` }} />
</div>
</div>
</div>
)
})}
</div>
))}
</div>
)}
) : (
<p className="text-gray-500 text-xs"></p>
)}
</CardContent>
</Card>
{/* 右列AI 获客概览 */}
<Card className="bg-[#0f2137] border-orange-500/20">
<CardContent className="p-4">
<h4 className="text-sm font-medium text-white mb-3 flex items-center gap-1.5">
<Link2 className="w-4 h-4 text-orange-400" /> AI
</h4>
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="bg-[#0a1628] rounded-lg p-3 cursor-pointer hover:border-orange-500/50 border border-transparent transition-colors" onClick={() => onOpenCKB?.('submitted')}>
<p className="text-gray-400 text-xs">线</p>
<p className="text-xl font-bold text-white">{isLoading ? '—' : (ckbStats?.ckbTotal ?? 0)}</p>
</div>
<div className="bg-[#0a1628] rounded-lg p-3 cursor-pointer hover:border-orange-500/50 border border-transparent transition-colors" onClick={() => onOpenCKB?.('contact')}>
<p className="text-gray-400 text-xs"></p>
<p className="text-xl font-bold text-white">{isLoading ? '—' : (ckbStats?.withContact ?? 0)}</p>
</div>
</div>
{ckbStats?.byType && ckbStats.byType.length > 0 && (
<div className="space-y-1.5">
{ckbStats.byType.map(item => (
<div key={item.matchType} className="flex items-center gap-2 text-xs">
<span>{typeIcons[item.matchType] || '📋'}</span>
<span className="text-gray-400">{typeLabels[item.matchType] || item.matchType}</span>
<span className="ml-auto text-white font-medium">{item.total}</span>
</div>
))}
</div>
)}
<button
type="button"
onClick={() => onOpenCKB?.('test')}
className="mt-3 w-full text-xs text-orange-400 hover:text-orange-300 text-center py-1.5 bg-orange-500/10 rounded"
>
AI
</button>
</CardContent>
</Card>
</div>
{/* 接口联通测试已移到右上角「存客宝」按钮面板 */}
</div>
)
}

View File

@@ -34,7 +34,6 @@ import {
Smartphone,
ShieldCheck,
Link2,
FileText,
Cloud,
} from 'lucide-react'
import { get, post } from '@/api/client'
@@ -63,6 +62,7 @@ interface FeatureConfig {
matchEnabled: boolean
referralEnabled: boolean
searchEnabled: boolean
aboutEnabled: boolean
}
interface MpConfig {
@@ -70,14 +70,15 @@ interface MpConfig {
withdrawSubscribeTmplId?: string
mchId?: string
minWithdraw?: number
auditMode?: boolean
}
interface OssConfig {
endpoint?: string
bucket?: string
region?: string
accessKeyId?: string
accessKeySecret?: string
bucket?: string
region?: string
}
const defaultMpConfig: MpConfig = {
@@ -104,10 +105,19 @@ const defaultSettings: LocalSettings = {
ckbLeadApiKey: '',
}
const defaultOssConfig: OssConfig = {
endpoint: '',
accessKeyId: '',
accessKeySecret: '',
bucket: '',
region: '',
}
const defaultFeatures: FeatureConfig = {
matchEnabled: true,
referralEnabled: true,
searchEnabled: true,
aboutEnabled: true,
}
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
@@ -121,7 +131,7 @@ export function SettingsPage() {
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMpConfig)
const [ossConfig, setOssConfig] = useState<OssConfig>({})
const [ossConfig, setOssConfig] = useState<OssConfig>(defaultOssConfig)
const [isSaving, setIsSaving] = useState(false)
const [loading, setLoading] = useState(true)
const [dialogOpen, setDialogOpen] = useState(false)
@@ -205,6 +215,30 @@ export function SettingsPage() {
saveFeatureConfigOnly(next, () => setFeatureConfig(prev))
}
const [auditModeSaving, setAuditModeSaving] = useState(false)
const handleAuditModeSwitch = async (checked: boolean) => {
const prev = mpConfig
const next = { ...prev, auditMode: checked }
setMpConfig(next)
setAuditModeSaving(true)
try {
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
mp_config: next,
})
if (!res || (res as { success?: boolean }).success === false) {
setMpConfig(prev)
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
return
}
showResult('已保存', checked ? '审核模式已开启,小程序将隐藏所有支付入口。' : '审核模式已关闭,支付功能已恢复。')
} catch (error) {
setMpConfig(prev)
showResult('保存失败', error instanceof Error ? error.message : String(error), true)
} finally {
setAuditModeSaving(false)
}
}
const handleSave = async () => {
setIsSaving(true)
try {
@@ -224,15 +258,13 @@ export function SettingsPage() {
mchId: mpConfig.mchId || '',
minWithdraw: typeof mpConfig.minWithdraw === 'number' ? mpConfig.minWithdraw : 10,
},
ossConfig: Object.keys(ossConfig).length
? {
endpoint: ossConfig.endpoint ?? '',
bucket: ossConfig.bucket ?? '',
region: ossConfig.region ?? '',
accessKeyId: ossConfig.accessKeyId ?? '',
accessKeySecret: ossConfig.accessKeySecret ?? '',
}
: undefined,
ossConfig: {
endpoint: ossConfig.endpoint || '',
accessKeyId: ossConfig.accessKeyId || '',
accessKeySecret: ossConfig.accessKeySecret || '',
bucket: ossConfig.bucket || '',
region: ossConfig.region || '',
},
})
if (!res || (res as { success?: boolean }).success === false) {
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
@@ -299,7 +331,7 @@ export function SettingsPage() {
value="api-docs"
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
>
<FileText className="w-4 h-4 mr-2" />
<BookOpen className="w-4 h-4 mr-2" />
API
</TabsTrigger>
</TabsList>
@@ -570,6 +602,120 @@ export function SettingsPage() {
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Cloud className="w-5 h-5 text-[#38bdac]" />
OSS
</CardTitle>
<CardDescription className="text-gray-400">
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-gray-300">Endpoint</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="oss-cn-hangzhou.aliyuncs.com"
value={ossConfig.endpoint ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, endpoint: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">Region</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="oss-cn-hangzhou"
value={ossConfig.region ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, region: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">AccessKey ID</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="LTAI5t..."
value={ossConfig.accessKeyId ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, accessKeyId: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">AccessKey Secret</Label>
<Input
type="password"
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="********"
value={ossConfig.accessKeySecret ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, accessKeySecret: e.target.value }))
}
/>
</div>
<div className="space-y-2 col-span-2">
<Label className="text-gray-300">Bucket </Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="my-soul-bucket"
value={ossConfig.bucket ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, bucket: e.target.value }))
}
/>
</div>
</div>
<div className={`p-3 rounded-lg ${ossConfig.endpoint && ossConfig.bucket && ossConfig.accessKeyId ? 'bg-green-500/10 border border-green-500/30' : 'bg-amber-500/10 border border-amber-500/30'}`}>
<p className={`text-xs ${ossConfig.endpoint && ossConfig.bucket && ossConfig.accessKeyId ? 'text-green-300' : 'text-amber-300'}`}>
{ossConfig.endpoint && ossConfig.bucket && ossConfig.accessKeyId
? `✅ OSS 已配置(${ossConfig.bucket}.${ossConfig.endpoint}),上传将自动使用云端存储`
: '⚠ 未配置 OSS当前上传存储在本地服务器。填写以上信息并保存后自动启用云端存储'}
</p>
</div>
</CardContent>
</Card>
<Card className={`bg-[#0f2137] shadow-xl ${mpConfig.auditMode ? 'border-amber-500/50 border-2' : 'border-gray-700/50'}`}>
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-amber-400" />
</CardTitle>
<CardDescription className="text-gray-400">
</CardDescription>
</CardHeader>
<CardContent>
<div className={`flex items-center justify-between p-4 rounded-lg border ${mpConfig.auditMode ? 'bg-amber-500/10 border-amber-500/30' : 'bg-[#0a1628] border-gray-700/50'}`}>
<div className="space-y-1">
<div className="flex items-center gap-2">
<ShieldCheck className={`w-4 h-4 ${mpConfig.auditMode ? 'text-amber-400' : 'text-gray-400'}`} />
<Label htmlFor="audit-mode" className="text-white font-medium cursor-pointer">
{mpConfig.auditMode ? '审核模式(已开启)' : '审核模式(已关闭)'}
</Label>
</div>
<p className="text-xs text-gray-400 ml-6">
{mpConfig.auditMode
? '当前已隐藏所有支付、VIP、充值、收益等入口审核员看不到任何付费内容'
: '关闭状态小程序正常显示所有功能含支付、VIP 等)'}
</p>
</div>
<Switch
id="audit-mode"
checked={mpConfig.auditMode ?? false}
disabled={auditModeSaving}
onCheckedChange={handleAuditModeSwitch}
/>
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
@@ -624,7 +770,7 @@ export function SettingsPage() {
</Label>
</div>
<p className="text-xs text-gray-400 ml-6"></p>
<p className="text-xs text-gray-400 ml-6"></p>
</div>
<Switch
id="search-enabled"
@@ -633,6 +779,23 @@ export function SettingsPage() {
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
/>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Settings className="w-4 h-4 text-[#38bdac]" />
<Label htmlFor="about-enabled" className="text-white font-medium cursor-pointer">
</Label>
</div>
<p className="text-xs text-gray-400 ml-6">访</p>
</div>
<Switch
id="about-enabled"
checked={featureConfig.aboutEnabled}
disabled={featureSwitchSaving}
onCheckedChange={(checked) => handleFeatureSwitch('aboutEnabled', checked)}
/>
</div>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/30">
<p className="text-xs text-blue-300">
@@ -641,78 +804,6 @@ export function SettingsPage() {
</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Cloud className="w-5 h-5 text-[#38bdac]" />
OSS
</CardTitle>
<CardDescription className="text-gray-400">
endpointbucketaccessKey /
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-gray-300">Endpoint</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="oss-cn-hangzhou.aliyuncs.com"
value={ossConfig.endpoint ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, endpoint: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">Bucket</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="bucket 名称"
value={ossConfig.bucket ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, bucket: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">Region</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="oss-cn-hangzhou"
value={ossConfig.region ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, region: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">AccessKey ID</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="AccessKey ID"
value={ossConfig.accessKeyId ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, accessKeyId: e.target.value }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300">AccessKey Secret</Label>
<Input
type="password"
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="AccessKey Secret"
value={ossConfig.accessKeySecret ?? ''}
onChange={(e) =>
setOssConfig((prev) => ({ ...prev, accessKeySecret: e.target.value }))
}
/>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>