主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
518 lines
19 KiB
TypeScript
518 lines
19 KiB
TypeScript
"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>
|
||
)
|
||
}
|