- 将 /api/db/user-rules 的 GET/POST/PUT/DELETE 路由注册到 db 组 - 将 /api/admin/shensheshou/* 的 4 个路由注册到 admin 组 - 包含上次对话的头像 URL 修复(normalizeImageUrl 全栈) Made-with: Cursor
282 lines
9.6 KiB
TypeScript
282 lines
9.6 KiB
TypeScript
import toast from '@/utils/toast'
|
||
import { useState, useEffect } from 'react'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogFooter,
|
||
} from '@/components/ui/dialog'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Label } from '@/components/ui/label'
|
||
import { Switch } from '@/components/ui/switch'
|
||
import { Crown, Save, X } from 'lucide-react'
|
||
import { get, put } from '@/api/client'
|
||
|
||
interface SetVipModalProps {
|
||
open: boolean
|
||
onClose: () => void
|
||
userId: string | null
|
||
userNickname?: string
|
||
onSaved?: () => void
|
||
}
|
||
|
||
interface VipRole {
|
||
id: number
|
||
name: string
|
||
sort: number
|
||
}
|
||
|
||
interface VipForm {
|
||
isVip: boolean
|
||
vipExpireDate: string
|
||
vipSort: number | ''
|
||
vipRole: string
|
||
vipRoleCustom: string
|
||
vipName: string
|
||
vipProject: string
|
||
vipContact: string
|
||
vipBio: string
|
||
}
|
||
|
||
const DEFAULT_FORM: VipForm = {
|
||
isVip: false,
|
||
vipExpireDate: '',
|
||
vipSort: '',
|
||
vipRole: '',
|
||
vipRoleCustom: '',
|
||
vipName: '',
|
||
vipProject: '',
|
||
vipContact: '',
|
||
vipBio: '',
|
||
}
|
||
|
||
export function SetVipModal({
|
||
open,
|
||
onClose,
|
||
userId,
|
||
userNickname = '',
|
||
onSaved,
|
||
}: SetVipModalProps) {
|
||
const [form, setForm] = useState<VipForm>(DEFAULT_FORM)
|
||
const [roles, setRoles] = useState<VipRole[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
setForm(DEFAULT_FORM)
|
||
return
|
||
}
|
||
let cancelled = false
|
||
setLoading(true)
|
||
Promise.all([
|
||
get<{ success?: boolean; data?: VipRole[]; roles?: VipRole[] }>('/api/db/vip-roles'),
|
||
userId ? get<{ success?: boolean; user?: Record<string, unknown> }>(`/api/db/users?id=${encodeURIComponent(userId)}`) : Promise.resolve(null),
|
||
]).then(([rolesRes, userRes]) => {
|
||
if (cancelled) return
|
||
const rolesList = rolesRes?.data || rolesRes?.roles || []
|
||
setRoles(rolesList as VipRole[])
|
||
const u = userRes?.user || null
|
||
if (u) {
|
||
const vipRole = String(u.vipRole ?? '')
|
||
const inRoles = rolesList.some((r: VipRole) => r.name === vipRole)
|
||
setForm({
|
||
isVip: !!(u.isVip ?? false),
|
||
vipExpireDate: u.vipExpireDate ? String(u.vipExpireDate).slice(0, 10) : '',
|
||
vipSort: typeof u.vipSort === 'number' ? u.vipSort : '',
|
||
vipRole: inRoles ? vipRole : (vipRole ? '__custom__' : ''),
|
||
vipRoleCustom: inRoles ? '' : vipRole,
|
||
vipName: String(u.vipName ?? ''),
|
||
vipProject: String(u.vipProject ?? ''),
|
||
vipContact: String(u.vipContact ?? ''),
|
||
vipBio: String(u.vipBio ?? ''),
|
||
})
|
||
} else {
|
||
setForm(DEFAULT_FORM)
|
||
}
|
||
}).catch((e) => {
|
||
if (!cancelled) console.error('Load error:', e)
|
||
}).finally(() => {
|
||
if (!cancelled) setLoading(false)
|
||
})
|
||
return () => { cancelled = true }
|
||
}, [open, userId])
|
||
|
||
async function handleSave() {
|
||
if (!userId) return
|
||
if (form.isVip && !form.vipExpireDate.trim()) {
|
||
toast.error('开启 VIP 时请填写有效到期日')
|
||
return
|
||
}
|
||
if (form.isVip && form.vipExpireDate.trim()) {
|
||
const d = new Date(form.vipExpireDate)
|
||
if (isNaN(d.getTime())) {
|
||
toast.error('到期日格式无效,请使用 YYYY-MM-DD')
|
||
return
|
||
}
|
||
}
|
||
setSaving(true)
|
||
try {
|
||
const roleValue = form.vipRole === '__custom__' ? form.vipRoleCustom.trim() : form.vipRole
|
||
const payload: Record<string, unknown> = {
|
||
id: userId,
|
||
isVip: form.isVip,
|
||
vipExpireDate: form.isVip ? form.vipExpireDate : undefined,
|
||
vipSort: form.vipSort === '' ? undefined : form.vipSort,
|
||
vipRole: roleValue || undefined,
|
||
vipName: form.vipName || undefined,
|
||
vipProject: form.vipProject || undefined,
|
||
vipContact: form.vipContact || undefined,
|
||
vipBio: form.vipBio || undefined,
|
||
}
|
||
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload)
|
||
if (data?.success) {
|
||
toast.success('VIP 设置已保存')
|
||
onSaved?.()
|
||
onClose()
|
||
} else {
|
||
toast.error('保存失败: ' + (data as { error?: string })?.error)
|
||
}
|
||
} catch (e) {
|
||
console.error('Save VIP error:', e)
|
||
toast.error('保存失败')
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
if (!open) return null
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={() => onClose()}>
|
||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-white flex items-center gap-2">
|
||
<Crown className="w-5 h-5 text-amber-400" />
|
||
设置 VIP - {userNickname || userId}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
{loading ? (
|
||
<div className="py-8 text-center text-gray-400">加载中...</div>
|
||
) : (
|
||
<div className="space-y-4 py-4">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-gray-300">VIP 会员</Label>
|
||
<Switch
|
||
checked={form.isVip}
|
||
onCheckedChange={(checked) => setForm((f) => ({ ...f, isVip: checked }))}
|
||
/>
|
||
</div>
|
||
{form.isVip && (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">
|
||
到期日 (YYYY-MM-DD) <span className="text-amber-400">*</span>
|
||
</Label>
|
||
<Input
|
||
type="date"
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
value={form.vipExpireDate}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipExpireDate: e.target.value }))}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">排序</Label>
|
||
<Input
|
||
type="number"
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder="数字越小越靠前,留空按时间"
|
||
value={form.vipSort === '' ? '' : form.vipSort}
|
||
onChange={(e) => {
|
||
const v = e.target.value
|
||
setForm((f) => ({ ...f, vipSort: v === '' ? '' : parseInt(v, 10) || 0 }))
|
||
}}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">角色</Label>
|
||
<select
|
||
className="w-full bg-[#0a1628] border border-gray-700 text-white rounded-md px-3 py-2"
|
||
value={form.vipRole}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipRole: e.target.value }))}
|
||
>
|
||
<option value="">请选择或下方手动填写</option>
|
||
{roles.map((r) => (
|
||
<option key={r.id} value={r.name}>{r.name}</option>
|
||
))}
|
||
<option value="__custom__">其他(手动填写)</option>
|
||
</select>
|
||
{form.vipRole === '__custom__' && (
|
||
<Input
|
||
className="bg-[#0a1628] border-gray-700 text-white mt-1"
|
||
placeholder="输入自定义角色"
|
||
value={form.vipRoleCustom}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipRoleCustom: e.target.value }))}
|
||
/>
|
||
)}
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">VIP 展示名</Label>
|
||
<Input
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder="创业老板排行展示名"
|
||
value={form.vipName}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipName: 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={form.vipProject}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipProject: 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={form.vipContact}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipContact: 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={form.vipBio}
|
||
onChange={(e) => setForm((f) => ({ ...f, vipBio: e.target.value }))}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button
|
||
variant="outline"
|
||
onClick={onClose}
|
||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||
>
|
||
<X className="w-4 h-4 mr-2" />
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleSave}
|
||
disabled={saving || loading}
|
||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||
>
|
||
<Save className="w-4 h-4 mr-2" />
|
||
{saving ? '保存中...' : '保存'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|