This commit is contained in:
+26
-22
@@ -27,30 +27,42 @@ function Toast({ message, onClose }) {
|
|||||||
return <div className="fixed bottom-5 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-xl bg-ink px-4 py-3 text-sm text-white shadow-xl"><Check size={16} />{message}</div>
|
return <div className="fixed bottom-5 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-xl bg-ink px-4 py-3 text-sm text-white shadow-xl"><Check size={16} />{message}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Login() {
|
function PickupPanel({ className = '' }) {
|
||||||
const login = useAppStore((s) => s.login)
|
const [pickup, setPickup] = useState({ hash: '', code: '' })
|
||||||
const [form, setForm] = useState({ username: '', password: '' })
|
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [pickup, setPickup] = useState({ hash: '', code: '' })
|
|
||||||
const [pickupError, setPickupError] = useState('')
|
|
||||||
const [pickupBusy, setPickupBusy] = useState(false)
|
|
||||||
const retrieve = async () => {
|
const retrieve = async () => {
|
||||||
const hash = pickup.hash.trim()
|
const hash = pickup.hash.trim()
|
||||||
setPickupError('')
|
setError('')
|
||||||
if (hash.length !== 8) return setPickupError('请输入 8 位文件码')
|
if (hash.length !== 8) return setError('请输入 8 位文件码')
|
||||||
if (pickup.code && pickup.code.length !== 6) return setPickupError('取件码必须是 6 位数字')
|
if (pickup.code && pickup.code.length !== 6) return setError('取件码必须是 6 位数字')
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (pickup.code) query.set('code', pickup.code)
|
if (pickup.code) query.set('code', pickup.code)
|
||||||
setPickupBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
await api(`/api/pickup/${encodeURIComponent(hash)}${query.size ? `?${query}` : ''}`)
|
await api(`/api/pickup/${encodeURIComponent(hash)}${query.size ? `?${query}` : ''}`)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = `/uf/${encodeURIComponent(hash)}${query.size ? `?${query}` : ''}`
|
link.href = `/uf/${encodeURIComponent(hash)}${query.size ? `?${query}` : ''}`
|
||||||
document.body.appendChild(link); link.click(); link.remove()
|
document.body.appendChild(link); link.click(); link.remove()
|
||||||
} catch (error) { setPickupError(error.message) }
|
} catch (error) { setError(error.message) }
|
||||||
finally { setPickupBusy(false) }
|
finally { setBusy(false) }
|
||||||
}
|
}
|
||||||
|
return <form className={`card p-5 ${className}`} onSubmit={(event) => { event.preventDefault(); retrieve() }}>
|
||||||
|
<div className="mb-3 flex items-center gap-2 text-sm font-semibold"><ArrowDownToLine size={17} className="text-leaf" />收到分享?直接取件</div>
|
||||||
|
<div className="grid gap-2 sm:grid-cols-[1fr_130px_auto]">
|
||||||
|
<input className="field" maxLength={8} placeholder="8 位文件码" value={pickup.hash} onChange={(event) => { setPickup({ ...pickup, hash: event.target.value }); setError('') }} />
|
||||||
|
<input className="field" inputMode="numeric" maxLength={6} placeholder="取件码(可选)" value={pickup.code} onChange={(event) => { setPickup({ ...pickup, code: event.target.value.replace(/\D/g, '') }); setError('') }} />
|
||||||
|
<button className="btn-secondary" disabled={busy}>{busy ? <LoaderCircle className="animate-spin" size={17} /> : '下载'}</button>
|
||||||
|
</div>
|
||||||
|
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const login = useAppStore((s) => s.login)
|
||||||
|
const [form, setForm] = useState({ username: '', password: '' })
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
const submit = async (event) => {
|
const submit = async (event) => {
|
||||||
event.preventDefault(); setBusy(true); setError('')
|
event.preventDefault(); setBusy(true); setError('')
|
||||||
try { await login(form.username, form.password) }
|
try { await login(form.username, form.password) }
|
||||||
@@ -63,15 +75,7 @@ function Login() {
|
|||||||
<p className="mb-4 text-sm font-medium uppercase tracking-[.24em] text-leaf">Send less. Share better.</p>
|
<p className="mb-4 text-sm font-medium uppercase tracking-[.24em] text-leaf">Send less. Share better.</p>
|
||||||
<h1 className="max-w-xl text-5xl font-semibold leading-[1.08] tracking-[-.045em] sm:text-6xl">把文件交给链接,<br />把空间留给重要的事。</h1>
|
<h1 className="max-w-xl text-5xl font-semibold leading-[1.08] tracking-[-.045em] sm:text-6xl">把文件交给链接,<br />把空间留给重要的事。</h1>
|
||||||
<p className="mt-6 max-w-lg text-base leading-7 text-ink/55">限时保存、下载次数与取件码,让每一次分享都清晰可控。</p>
|
<p className="mt-6 max-w-lg text-base leading-7 text-ink/55">限时保存、下载次数与取件码,让每一次分享都清晰可控。</p>
|
||||||
<div className="card mt-10 max-w-lg p-5">
|
<PickupPanel className="mt-10 max-w-lg" />
|
||||||
<div className="mb-3 flex items-center gap-2 text-sm font-semibold"><ArrowDownToLine size={17} className="text-leaf" />收到分享?直接取件</div>
|
|
||||||
<div className="grid gap-2 sm:grid-cols-[1fr_130px_auto]">
|
|
||||||
<input className="field" maxLength={8} placeholder="8 位文件码" value={pickup.hash} onChange={(e) => { setPickup({ ...pickup, hash: e.target.value }); setPickupError('') }} />
|
|
||||||
<input className="field" inputMode="numeric" maxLength={6} placeholder="取件码(可选)" value={pickup.code} onChange={(e) => { setPickup({ ...pickup, code: e.target.value.replace(/\D/g, '') }); setPickupError('') }} />
|
|
||||||
<button className="btn-secondary" disabled={pickupBusy} onClick={retrieve}>{pickupBusy ? <LoaderCircle className="animate-spin" size={17} /> : '下载'}</button>
|
|
||||||
</div>
|
|
||||||
{pickupError && <p className="mt-2 text-sm text-red-600">{pickupError}</p>}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<section className="mx-auto w-full max-w-md py-8 lg:justify-self-end">
|
<section className="mx-auto w-full max-w-md py-8 lg:justify-self-end">
|
||||||
<form className="card p-7 sm:p-9" onSubmit={submit}>
|
<form className="card p-7 sm:p-9" onSubmit={submit}>
|
||||||
@@ -232,7 +236,7 @@ function Dashboard() {
|
|||||||
const notify = (message) => setToast(message)
|
const notify = (message) => setToast(message)
|
||||||
return <div className="noise min-h-screen"><header className="sticky top-0 z-20 border-b border-line/80 bg-paper/90 backdrop-blur-xl"><div className="mx-auto flex max-w-6xl items-center justify-between px-5 py-3.5"><div className="flex items-center gap-2.5"><div className="grid size-9 place-items-center rounded-xl bg-leaf text-white"><FileArchive size={19} /></div><span className="font-semibold">ufile</span></div><div className="flex items-center gap-3"><span className="hidden text-sm text-ink/50 sm:block">{user.username}</span><button className="btn-secondary !p-2.5" title="退出登录" onClick={logout}><LogOut size={16} /></button></div></div></header>
|
return <div className="noise min-h-screen"><header className="sticky top-0 z-20 border-b border-line/80 bg-paper/90 backdrop-blur-xl"><div className="mx-auto flex max-w-6xl items-center justify-between px-5 py-3.5"><div className="flex items-center gap-2.5"><div className="grid size-9 place-items-center rounded-xl bg-leaf text-white"><FileArchive size={19} /></div><span className="font-semibold">ufile</span></div><div className="flex items-center gap-3"><span className="hidden text-sm text-ink/50 sm:block">{user.username}</span><button className="btn-secondary !p-2.5" title="退出登录" onClick={logout}><LogOut size={16} /></button></div></div></header>
|
||||||
<main className="mx-auto max-w-6xl px-5 py-7"><div className="mb-7 flex w-fit max-w-full gap-1 overflow-x-auto rounded-xl bg-ink/5 p-1"><button className={`flex shrink-0 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${tab === 'files' ? 'bg-white shadow-sm' : 'text-ink/50'}`} onClick={() => setTab('files')}><FileUp size={16} />文件空间</button>{user.canManageFiles && <button className={`flex shrink-0 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${tab === 'managed-files' ? 'bg-white shadow-sm' : 'text-ink/50'}`} onClick={() => setTab('managed-files')}><Files size={16} />文件管理</button>}{user.canManageUsers && <button className={`flex shrink-0 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${tab === 'users' ? 'bg-white shadow-sm' : 'text-ink/50'}`} onClick={() => setTab('users')}><Users size={16} />用户管理</button>}</div>
|
<main className="mx-auto max-w-6xl px-5 py-7"><div className="mb-7 flex w-fit max-w-full gap-1 overflow-x-auto rounded-xl bg-ink/5 p-1"><button className={`flex shrink-0 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${tab === 'files' ? 'bg-white shadow-sm' : 'text-ink/50'}`} onClick={() => setTab('files')}><FileUp size={16} />文件空间</button>{user.canManageFiles && <button className={`flex shrink-0 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${tab === 'managed-files' ? 'bg-white shadow-sm' : 'text-ink/50'}`} onClick={() => setTab('managed-files')}><Files size={16} />文件管理</button>}{user.canManageUsers && <button className={`flex shrink-0 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${tab === 'users' ? 'bg-white shadow-sm' : 'text-ink/50'}`} onClick={() => setTab('users')}><Users size={16} />用户管理</button>}</div>
|
||||||
{tab === 'files' ? <div className="mx-auto max-w-2xl"><UploadPanel /></div> : tab === 'managed-files' ? <FileManagementPanel notify={notify} /> : <UsersPanel notify={notify} />}
|
{tab === 'files' ? <div className="mx-auto max-w-2xl space-y-5"><PickupPanel /><UploadPanel /></div> : tab === 'managed-files' ? <FileManagementPanel notify={notify} /> : <UsersPanel notify={notify} />}
|
||||||
</main>{toast && <Toast message={toast} onClose={() => setToast('')} />}
|
</main>{toast && <Toast message={toast} onClose={() => setToast('')} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user