chore: 停止上传开发文档并同步代码
- 从仓库索引移除 开发文档/(本地保留) - 忽略 wechat/info.log 与 soul-api-linux - 同步小程序/管理端/API改动 Made-with: Cursor
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
endpoint、bucket、accessKey 等,用于图片/文件上传
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user