Files
soul/app/admin/match/page.tsx

518 lines
19 KiB
TypeScript
Raw Normal View History

"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>
)
}