Files
Mycontent/soul-admin/src/components/modules/user/SetVipModal.tsx
卡若 64db4927ea fix: 注册 user-rules 和 shensheshou 路由,修复规则配置保存失败
- 将 /api/db/user-rules 的 GET/POST/PUT/DELETE 路由注册到 db 组
- 将 /api/admin/shensheshou/* 的 4 个路由注册到 admin 组
- 包含上次对话的头像 URL 修复(normalizeImageUrl 全栈)

Made-with: Cursor
2026-03-15 19:31:07 +08:00

282 lines
9.6 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.

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