将项目文件整理到 refer 目录
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
Plus,
|
||||
Copy,
|
||||
Trash2,
|
||||
ArrowLeft,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Loader2,
|
||||
Check,
|
||||
AlertCircle,
|
||||
} from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { Logo } from '@/components/Logo'
|
||||
|
||||
const BRAND = '#8B5CF6'
|
||||
|
||||
function formatTime(ts: number) {
|
||||
const d = new Date(ts * 1000)
|
||||
return d.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
export function AdminUuids() {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
const [label, setLabel] = useState('')
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
|
||||
const { data: records = [], isLoading } = useQuery({
|
||||
queryKey: ['admin-uuids'],
|
||||
queryFn: () => api.listUuids(),
|
||||
})
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: () => api.createUuid(label),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin-uuids'] })
|
||||
setLabel('')
|
||||
toast('已创建新 UUID', 'success')
|
||||
},
|
||||
onError: (err: any) => toast(err?.message || '创建失败', 'error'),
|
||||
})
|
||||
|
||||
const toggle = useMutation({
|
||||
mutationFn: ({ uuid, enabled }: { uuid: string; enabled: boolean }) =>
|
||||
api.toggleUuid(uuid, enabled),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin-uuids'] }),
|
||||
onError: (err: any) => toast(err?.message || '操作失败', 'error'),
|
||||
})
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (uuid: string) => api.deleteUuid(uuid),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin-uuids'] })
|
||||
toast('已删除', 'success')
|
||||
},
|
||||
onError: (err: any) => toast(err?.message || '删除失败', 'error'),
|
||||
})
|
||||
|
||||
const handleCopy = async (uuid: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(uuid)
|
||||
setCopied(uuid)
|
||||
setTimeout(() => setCopied(null), 1500)
|
||||
} catch {
|
||||
toast('复制失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen bg-base overflow-hidden flex flex-col">
|
||||
{/* 背景光晕 */}
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
className="absolute -top-40 -left-40 h-[28rem] w-[28rem] rounded-full blur-[120px] opacity-20"
|
||||
style={{ background: `radial-gradient(circle, ${BRAND}, transparent 70%)` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-40 -right-32 h-[26rem] w-[26rem] rounded-full blur-[120px] opacity-15"
|
||||
style={{ background: 'radial-gradient(circle, hsl(var(--accent)), transparent 70%)' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 顶栏 */}
|
||||
<header className="relative z-10 flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2.5 text-foreground">
|
||||
<Logo
|
||||
size={24}
|
||||
className="shrink-0"
|
||||
style={{ color: BRAND, filter: `drop-shadow(0 0 8px ${BRAND}55)` }}
|
||||
/>
|
||||
<span className="text-sm font-semibold tracking-tight">Stock Panel</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="inline-flex items-center gap-1.5 px-3 h-9 rounded-btn text-sm text-secondary hover:text-foreground hover:bg-elevated transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回首页
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* 内容 */}
|
||||
<main className="relative z-10 flex-1 px-6 py-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="max-w-3xl mx-auto"
|
||||
>
|
||||
<div className="rounded-card border border-border bg-surface/80 backdrop-blur-sm p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">访问 UUID 管理</h1>
|
||||
<p className="mt-1 text-sm text-secondary">为合伙人创建、分发和管理访问 UUID。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建区 */}
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="备注(可选)"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
className="flex-1 px-3 py-2 rounded-input bg-base border border-border text-sm focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={() => create.mutate()}
|
||||
disabled={create.isPending}
|
||||
className="inline-flex items-center gap-2 px-4 h-10 rounded-xl bg-accent text-white text-sm font-semibold hover:bg-accent/90 disabled:opacity-60 transition-all"
|
||||
>
|
||||
{create.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-4 w-4" />
|
||||
)}
|
||||
创建 UUID
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 列表 */}
|
||||
<div className="mt-6 border border-border rounded-card overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-accent" />
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-secondary">
|
||||
暂无 UUID,点击上方按钮创建第一个。
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-elevated/50 text-secondary">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2.5 font-medium">UUID</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">备注</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">创建时间</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium">状态</th>
|
||||
<th className="text-right px-4 py-2.5 font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{records.map((r) => (
|
||||
<tr key={r.uuid} className="hover:bg-elevated/30 transition-colors">
|
||||
<td className="px-4 py-3 font-mono text-xs text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate max-w-[12rem]">{r.uuid}</span>
|
||||
<button
|
||||
onClick={() => handleCopy(r.uuid)}
|
||||
className="text-muted hover:text-accent transition-colors"
|
||||
title="复制"
|
||||
>
|
||||
{copied === r.uuid ? (
|
||||
<Check className="h-3.5 w-3.5 text-bull" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-secondary">{r.label || '—'}</td>
|
||||
<td className="px-4 py-3 text-secondary">{formatTime(r.created_at)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => toggle.mutate({ uuid: r.uuid, enabled: !r.enabled })}
|
||||
disabled={toggle.isPending}
|
||||
className="text-muted hover:text-accent transition-colors"
|
||||
title={r.enabled ? '禁用' : '启用'}
|
||||
>
|
||||
{r.enabled ? (
|
||||
<ToggleRight className="h-5 w-5 text-bull" />
|
||||
) : (
|
||||
<ToggleLeft className="h-5 w-5 text-muted" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => remove.mutate(r.uuid)}
|
||||
disabled={remove.isPending}
|
||||
className="inline-flex items-center gap-1 text-xs text-danger hover:text-danger/80 transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 text-xs text-secondary">
|
||||
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" />
|
||||
<span>提示:禁用 UUID 后,持有该 UUID 的合伙人将无法继续访问,但不会删除记录。</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user