Files
soul/app/admin/match/page.tsx
卡若 4dd2f9f4a7 feat: 完善后台管理+搜索功能+分销系统
主要更新:
- 后台菜单精简(9项→6项)
- 新增搜索功能(敏感信息过滤)
- 分销绑定和提现系统完善
- 数据库初始化API(自动修复表结构)
- 用户管理:显示绑定关系详情
- 小程序:上下章导航优化、匹配页面重构
- 修复hydration和数据类型问题
2026-01-25 19:37:59 +08:00

518 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect, Suspense } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Settings, Save, RefreshCw, Edit3, Plus, Trash2, Users, Zap, DollarSign } from "lucide-react"
interface MatchType {
id: string
label: string
matchLabel: string
icon: string
matchFromDB: boolean
showJoinAfterMatch: boolean
price: number
enabled: boolean
}
interface MatchConfig {
matchTypes: MatchType[]
freeMatchLimit: number
matchPrice: number
settings: {
enableFreeMatches: boolean
enablePaidMatches: boolean
maxMatchesPerDay: number
}
}
const DEFAULT_CONFIG: MatchConfig = {
matchTypes: [
{ id: 'partner', label: '创业合伙', matchLabel: '创业伙伴', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false, price: 1, enabled: true },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: false, showJoinAfterMatch: true, price: 1, enabled: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '商业顾问', icon: '❤️', matchFromDB: false, showJoinAfterMatch: true, price: 1, enabled: true },
{ id: 'team', label: '团队招募', matchLabel: '加入项目', icon: '🎮', matchFromDB: false, showJoinAfterMatch: true, price: 1, enabled: true }
],
freeMatchLimit: 3,
matchPrice: 1,
settings: {
enableFreeMatches: true,
enablePaidMatches: true,
maxMatchesPerDay: 10
}
}
function MatchConfigContent() {
const [config, setConfig] = useState<MatchConfig>(DEFAULT_CONFIG)
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [showTypeModal, setShowTypeModal] = useState(false)
const [editingType, setEditingType] = useState<MatchType | null>(null)
const [formData, setFormData] = useState({
id: '',
label: '',
matchLabel: '',
icon: '⭐',
matchFromDB: false,
showJoinAfterMatch: true,
price: 1,
enabled: true
})
// 加载配置
const loadConfig = async () => {
setIsLoading(true)
try {
const res = await fetch('/api/db/config?key=match_config')
const data = await res.json()
if (data.success && data.config) {
setConfig({ ...DEFAULT_CONFIG, ...data.config })
}
} catch (error) {
console.error('加载匹配配置失败:', error)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadConfig()
}, [])
// 保存配置
const handleSave = async () => {
setIsSaving(true)
try {
const res = await fetch('/api/db/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: 'match_config',
config,
description: '匹配功能配置'
})
})
const data = await res.json()
if (data.success) {
alert('配置保存成功!')
} else {
alert('保存失败: ' + (data.error || '未知错误'))
}
} catch (error) {
console.error('保存配置失败:', error)
alert('保存失败')
} finally {
setIsSaving(false)
}
}
// 编辑匹配类型
const handleEditType = (type: MatchType) => {
setEditingType(type)
setFormData({
id: type.id,
label: type.label,
matchLabel: type.matchLabel,
icon: type.icon,
matchFromDB: type.matchFromDB,
showJoinAfterMatch: type.showJoinAfterMatch,
price: type.price,
enabled: type.enabled
})
setShowTypeModal(true)
}
// 添加匹配类型
const handleAddType = () => {
setEditingType(null)
setFormData({
id: '',
label: '',
matchLabel: '',
icon: '⭐',
matchFromDB: false,
showJoinAfterMatch: true,
price: 1,
enabled: true
})
setShowTypeModal(true)
}
// 保存匹配类型
const handleSaveType = () => {
if (!formData.id || !formData.label) {
alert('请填写类型ID和名称')
return
}
const newTypes = [...config.matchTypes]
if (editingType) {
// 更新
const index = newTypes.findIndex(t => t.id === editingType.id)
if (index !== -1) {
newTypes[index] = { ...formData }
}
} else {
// 新增
if (newTypes.some(t => t.id === formData.id)) {
alert('类型ID已存在')
return
}
newTypes.push({ ...formData })
}
setConfig({ ...config, matchTypes: newTypes })
setShowTypeModal(false)
}
// 删除匹配类型
const handleDeleteType = (typeId: string) => {
if (!confirm('确定要删除这个匹配类型吗?')) return
const newTypes = config.matchTypes.filter(t => t.id !== typeId)
setConfig({ ...config, matchTypes: newTypes })
}
// 切换类型启用状态
const handleToggleType = (typeId: string) => {
const newTypes = config.matchTypes.map(t =>
t.id === typeId ? { ...t, enabled: !t.enabled } : t
)
setConfig({ ...config, matchTypes: newTypes })
}
const icons = ['⭐', '👥', '❤️', '🎮', '💼', '🚀', '💡', '🎯', '🔥', '✨']
return (
<div className="p-8 max-w-6xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
<Settings className="w-6 h-6 text-[#38bdac]" />
</h2>
<p className="text-gray-400 mt-1"></p>
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={loadConfig}
disabled={isLoading}
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
>
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
<Button
onClick={handleSave}
disabled={isSaving}
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? '保存中...' : '保存配置'}
</Button>
</div>
</div>
{/* 基础设置 */}
<Card className="bg-[#0f2137] border-gray-700/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Zap className="w-5 h-5 text-yellow-400" />
</CardTitle>
<CardDescription className="text-gray-400">
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* 每日免费次数 */}
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
type="number"
min="0"
max="100"
className="bg-[#0a1628] border-gray-700 text-white"
value={config.freeMatchLimit}
onChange={(e) => setConfig({ ...config, freeMatchLimit: parseInt(e.target.value) || 0 })}
/>
<p className="text-xs text-gray-500"></p>
</div>
{/* 付费匹配价格 */}
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
type="number"
min="0.01"
step="0.01"
className="bg-[#0a1628] border-gray-700 text-white"
value={config.matchPrice}
onChange={(e) => setConfig({ ...config, matchPrice: parseFloat(e.target.value) || 1 })}
/>
<p className="text-xs text-gray-500"></p>
</div>
{/* 每日最大次数 */}
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
type="number"
min="1"
max="100"
className="bg-[#0a1628] border-gray-700 text-white"
value={config.settings.maxMatchesPerDay}
onChange={(e) => setConfig({
...config,
settings: { ...config.settings, maxMatchesPerDay: parseInt(e.target.value) || 10 }
})}
/>
<p className="text-xs text-gray-500"></p>
</div>
</div>
<div className="flex gap-8 pt-4 border-t border-gray-700/50">
<div className="flex items-center gap-3">
<Switch
checked={config.settings.enableFreeMatches}
onCheckedChange={(checked) => setConfig({
...config,
settings: { ...config.settings, enableFreeMatches: checked }
})}
/>
<Label className="text-gray-300"></Label>
</div>
<div className="flex items-center gap-3">
<Switch
checked={config.settings.enablePaidMatches}
onCheckedChange={(checked) => setConfig({
...config,
settings: { ...config.settings, enablePaidMatches: checked }
})}
/>
<Label className="text-gray-300"></Label>
</div>
</div>
</CardContent>
</Card>
{/* 匹配类型管理 */}
<Card className="bg-[#0f2137] border-gray-700/50">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="text-white flex items-center gap-2">
<Users className="w-5 h-5 text-[#38bdac]" />
</CardTitle>
<CardDescription className="text-gray-400">
</CardDescription>
</div>
<Button
onClick={handleAddType}
size="sm"
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
>
<Plus className="w-4 h-4 mr-1" />
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400">ID</TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-right text-gray-400"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{config.matchTypes.map((type) => (
<TableRow key={type.id} className="hover:bg-[#0a1628] border-gray-700/50">
<TableCell>
<span className="text-2xl">{type.icon}</span>
</TableCell>
<TableCell className="font-mono text-gray-300">{type.id}</TableCell>
<TableCell className="text-white font-medium">{type.label}</TableCell>
<TableCell className="text-gray-300">{type.matchLabel}</TableCell>
<TableCell>
<Badge className="bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/20 border-0">
¥{type.price}
</Badge>
</TableCell>
<TableCell>
{type.matchFromDB ? (
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0"></Badge>
) : (
<Badge variant="outline" className="text-gray-500 border-gray-600"></Badge>
)}
</TableCell>
<TableCell>
<Switch
checked={type.enabled}
onCheckedChange={() => handleToggleType(type.id)}
/>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditType(type)}
className="text-gray-400 hover:text-[#38bdac] hover:bg-[#38bdac]/10"
>
<Edit3 className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteType(type.id)}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* 编辑类型弹窗 */}
<Dialog open={showTypeModal} onOpenChange={setShowTypeModal}>
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg">
<DialogHeader>
<DialogTitle className="text-white flex items-center gap-2">
{editingType ? <Edit3 className="w-5 h-5 text-[#38bdac]" /> : <Plus className="w-5 h-5 text-[#38bdac]" />}
{editingType ? '编辑匹配类型' : '添加匹配类型'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-gray-300">ID</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="如: partner"
value={formData.id}
onChange={(e) => setFormData({ ...formData, id: e.target.value })}
disabled={!!editingType}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<div className="flex gap-1 flex-wrap">
{icons.map((icon) => (
<button
key={icon}
type="button"
className={`w-8 h-8 text-lg rounded ${formData.icon === icon ? 'bg-[#38bdac]/30 ring-1 ring-[#38bdac]' : 'bg-[#0a1628]'}`}
onClick={() => setFormData({ ...formData, icon })}
>
{icon}
</button>
))}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="如: 创业合伙"
value={formData.label}
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white"
placeholder="如: 创业伙伴"
value={formData.matchLabel}
onChange={(e) => setFormData({ ...formData, matchLabel: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-gray-300"></Label>
<Input
type="number"
min="0.01"
step="0.01"
className="bg-[#0a1628] border-gray-700 text-white"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 1 })}
/>
</div>
<div className="flex gap-6 pt-2">
<div className="flex items-center gap-3">
<Switch
checked={formData.matchFromDB}
onCheckedChange={(checked) => setFormData({ ...formData, matchFromDB: checked })}
/>
<Label className="text-gray-300 text-sm"></Label>
</div>
<div className="flex items-center gap-3">
<Switch
checked={formData.showJoinAfterMatch}
onCheckedChange={(checked) => setFormData({ ...formData, showJoinAfterMatch: checked })}
/>
<Label className="text-gray-300 text-sm"></Label>
</div>
<div className="flex items-center gap-3">
<Switch
checked={formData.enabled}
onCheckedChange={(checked) => setFormData({ ...formData, enabled: checked })}
/>
<Label className="text-gray-300 text-sm"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowTypeModal(false)}
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
>
</Button>
<Button
onClick={handleSaveType}
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
>
<Save className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
export default function MatchConfigPage() {
return (
<Suspense fallback={null}>
<MatchConfigContent />
</Suspense>
)
}