mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 09:14:11 +05:30
polish: refine admin ui controls
This commit is contained in:
@@ -15,6 +15,7 @@ import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -22,6 +23,13 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
|
||||
@@ -43,6 +51,32 @@ interface AnnouncementFormData {
|
||||
endAt: Date | string | null;
|
||||
}
|
||||
|
||||
const audienceLabels: Record<AnnouncementAudience, string> = {
|
||||
PUBLIC: "公开",
|
||||
USERS: "全部用户",
|
||||
ADMINS: "全部管理员",
|
||||
SPECIFIC_USER: "指定用户",
|
||||
};
|
||||
|
||||
const displayTypeLabels: Record<AnnouncementDisplayType, string> = {
|
||||
INLINE: "普通公告",
|
||||
BIG: "大公告",
|
||||
POPUP: "弹窗公告",
|
||||
};
|
||||
|
||||
function getAudienceLabel(value: unknown) {
|
||||
return audienceLabels[value as AnnouncementAudience] ?? "选择范围";
|
||||
}
|
||||
|
||||
function getDisplayTypeLabel(value: unknown) {
|
||||
return displayTypeLabels[value as AnnouncementDisplayType] ?? "选择展示方式";
|
||||
}
|
||||
|
||||
function getTargetUserLabel(users: AnnouncementOptionUser[], value: unknown) {
|
||||
if (!value) return "不指定";
|
||||
return users.find((user) => user.id === value)?.email ?? "选择用户";
|
||||
}
|
||||
|
||||
function toDateTimeLocalValue(value: Date | string | null) {
|
||||
if (!value) {
|
||||
return "";
|
||||
@@ -94,48 +128,55 @@ export function AnnouncementForm({
|
||||
<DialogTrigger render={<Button variant={triggerVariant} size="sm" />}>
|
||||
{triggerLabel ?? "编辑"}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="flex max-h-[min(90dvh,38rem)] flex-col overflow-hidden p-0 sm:max-w-[30rem]">
|
||||
<DialogHeader className="border-b border-border/60 px-4 py-3 pr-10">
|
||||
<DialogTitle>编辑公告</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form action={handleSubmit} className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<DialogBody className="flex-1 px-4 py-3">
|
||||
<form action={handleSubmit} className="space-y-3 text-[12px] leading-5 [&_[data-slot=input]]:!h-8 [&_[data-slot=input]]:!min-h-8 [&_[data-slot=input]]:!px-2.5 [&_[data-slot=input]]:!text-xs [&_[data-slot=label]]:!text-xs [&_[data-slot=select-trigger]]:!h-8 [&_[data-slot=select-trigger]]:!min-h-8 [&_[data-slot=select-trigger]]:!px-2.5 [&_[data-slot=select-trigger]]:!text-xs [&_textarea]:!text-xs">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`title-${announcement.id}`}>标题</Label>
|
||||
<Input id={`title-${announcement.id}`} name="title" defaultValue={announcement.title} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`audience-${announcement.id}`}>目标范围</Label>
|
||||
<select
|
||||
id={`audience-${announcement.id}`}
|
||||
<Select
|
||||
name="audience"
|
||||
defaultValue={announcement.audience}
|
||||
onChange={(event) => setAudience(event.target.value as AnnouncementAudience)}
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
value={audience}
|
||||
onValueChange={(value) => setAudience((value ?? "USERS") as AnnouncementAudience)}
|
||||
>
|
||||
<option value="PUBLIC">公开(登录/注册页可见)</option>
|
||||
<option value="USERS">全部用户</option>
|
||||
<option value="ADMINS">全部管理员</option>
|
||||
<option value="SPECIFIC_USER">指定用户</option>
|
||||
</select>
|
||||
<SelectTrigger id={`audience-${announcement.id}`} className="w-full">
|
||||
<SelectValue>{(value) => getAudienceLabel(value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="PUBLIC">公开(登录/注册页可见)</SelectItem>
|
||||
<SelectItem value="USERS">全部用户</SelectItem>
|
||||
<SelectItem value="ADMINS">全部管理员</SelectItem>
|
||||
<SelectItem value="SPECIFIC_USER">指定用户</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`displayType-${announcement.id}`}>展示方式</Label>
|
||||
<select
|
||||
id={`displayType-${announcement.id}`}
|
||||
<Select
|
||||
name="displayType"
|
||||
defaultValue={announcement.displayType}
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
>
|
||||
<option value="INLINE">普通公告</option>
|
||||
<option value="BIG">大公告</option>
|
||||
<option value="POPUP">弹窗公告</option>
|
||||
</select>
|
||||
<SelectTrigger id={`displayType-${announcement.id}`} className="w-full">
|
||||
<SelectValue>{(value) => getDisplayTypeLabel(value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="INLINE">普通公告</SelectItem>
|
||||
<SelectItem value="BIG">大公告</SelectItem>
|
||||
<SelectItem value="POPUP">弹窗公告</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`dismissible-${announcement.id}`}>允许用户关闭</Label>
|
||||
<BooleanToggle
|
||||
id={`dismissible-${announcement.id}`}
|
||||
@@ -144,29 +185,33 @@ export function AnnouncementForm({
|
||||
trueLabel="允许"
|
||||
falseLabel="不允许"
|
||||
ariaLabel="允许用户关闭"
|
||||
size="compact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`targetUserId-${announcement.id}`}>指定用户(可选)</Label>
|
||||
<select
|
||||
id={`targetUserId-${announcement.id}`}
|
||||
<Select
|
||||
name="targetUserId"
|
||||
defaultValue={announcement.targetUserId ?? ""}
|
||||
disabled={audience !== "SPECIFIC_USER"}
|
||||
className="h-10 w-full px-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<option value="">不指定</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger id={`targetUserId-${announcement.id}`} className="w-full">
|
||||
<SelectValue>{(value) => getTargetUserLabel(users, value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="">不指定</SelectItem>
|
||||
{users.map((user) => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`body-${announcement.id}`}>内容</Label>
|
||||
<Textarea
|
||||
id={`body-${announcement.id}`}
|
||||
@@ -177,8 +222,8 @@ export function AnnouncementForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`startAt-${announcement.id}`}>开始时间(可选)</Label>
|
||||
<Input
|
||||
id={`startAt-${announcement.id}`}
|
||||
@@ -187,7 +232,7 @@ export function AnnouncementForm({
|
||||
defaultValue={toDateTimeLocalValue(announcement.startAt)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`endAt-${announcement.id}`}>结束时间(可选)</Label>
|
||||
<Input
|
||||
id={`endAt-${announcement.id}`}
|
||||
@@ -198,7 +243,7 @@ export function AnnouncementForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`sendNotification-${announcement.id}`}>同步发送站内通知</Label>
|
||||
<BooleanToggle
|
||||
id={`sendNotification-${announcement.id}`}
|
||||
@@ -207,13 +252,15 @@ export function AnnouncementForm({
|
||||
trueLabel="发送"
|
||||
falseLabel="不发送"
|
||||
ariaLabel="同步发送站内通知"
|
||||
size="compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PendingSubmitButton className="w-full" pendingLabel="保存中...">
|
||||
<PendingSubmitButton className="h-8 w-full" pendingLabel="保存中...">
|
||||
保存修改
|
||||
</PendingSubmitButton>
|
||||
</form>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -239,50 +286,65 @@ export function CreateAnnouncementButton({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen) {
|
||||
setAudience("USERS");
|
||||
}
|
||||
setOpen(nextOpen);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger render={<Button />}>发布公告</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="flex max-h-[min(90dvh,38rem)] flex-col overflow-hidden p-0 sm:max-w-[30rem]">
|
||||
<DialogHeader className="border-b border-border/60 px-4 py-3 pr-10">
|
||||
<DialogTitle>发布公告</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form action={handleSubmit} className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<DialogBody className="flex-1 px-4 py-3">
|
||||
<form action={handleSubmit} className="space-y-3 text-[12px] leading-5 [&_[data-slot=input]]:!h-8 [&_[data-slot=input]]:!min-h-8 [&_[data-slot=input]]:!px-2.5 [&_[data-slot=input]]:!text-xs [&_[data-slot=label]]:!text-xs [&_[data-slot=select-trigger]]:!h-8 [&_[data-slot=select-trigger]]:!min-h-8 [&_[data-slot=select-trigger]]:!px-2.5 [&_[data-slot=select-trigger]]:!text-xs [&_textarea]:!text-xs">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-title">标题</Label>
|
||||
<Input id="create-announcement-title" name="title" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-audience">目标范围</Label>
|
||||
<select
|
||||
id="create-announcement-audience"
|
||||
<Select
|
||||
name="audience"
|
||||
defaultValue="USERS"
|
||||
onChange={(event) => setAudience(event.target.value as AnnouncementAudience)}
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
value={audience}
|
||||
onValueChange={(value) => setAudience((value ?? "USERS") as AnnouncementAudience)}
|
||||
>
|
||||
<option value="PUBLIC">公开(登录/注册页可见)</option>
|
||||
<option value="USERS">全部用户</option>
|
||||
<option value="ADMINS">全部管理员</option>
|
||||
<option value="SPECIFIC_USER">指定用户</option>
|
||||
</select>
|
||||
<SelectTrigger id="create-announcement-audience" className="w-full">
|
||||
<SelectValue>{(value) => getAudienceLabel(value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="PUBLIC">公开(登录/注册页可见)</SelectItem>
|
||||
<SelectItem value="USERS">全部用户</SelectItem>
|
||||
<SelectItem value="ADMINS">全部管理员</SelectItem>
|
||||
<SelectItem value="SPECIFIC_USER">指定用户</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-displayType">展示方式</Label>
|
||||
<select
|
||||
id="create-announcement-displayType"
|
||||
<Select
|
||||
name="displayType"
|
||||
defaultValue="INLINE"
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
>
|
||||
<option value="INLINE">普通公告</option>
|
||||
<option value="BIG">大公告</option>
|
||||
<option value="POPUP">弹窗公告</option>
|
||||
</select>
|
||||
<SelectTrigger id="create-announcement-displayType" className="w-full">
|
||||
<SelectValue>{(value) => getDisplayTypeLabel(value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="INLINE">普通公告</SelectItem>
|
||||
<SelectItem value="BIG">大公告</SelectItem>
|
||||
<SelectItem value="POPUP">弹窗公告</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-dismissible">允许用户关闭</Label>
|
||||
<BooleanToggle
|
||||
id="create-announcement-dismissible"
|
||||
@@ -291,45 +353,49 @@ export function CreateAnnouncementButton({
|
||||
trueLabel="允许"
|
||||
falseLabel="不允许"
|
||||
ariaLabel="允许用户关闭"
|
||||
size="compact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-targetUserId">指定用户(可选)</Label>
|
||||
<select
|
||||
id="create-announcement-targetUserId"
|
||||
<Select
|
||||
name="targetUserId"
|
||||
defaultValue=""
|
||||
disabled={audience !== "SPECIFIC_USER"}
|
||||
className="h-10 w-full px-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<option value="">不指定</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger id="create-announcement-targetUserId" className="w-full">
|
||||
<SelectValue>{(value) => getTargetUserLabel(users, value)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
<SelectItem value="">不指定</SelectItem>
|
||||
{users.map((user) => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.email}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-body">内容</Label>
|
||||
<Textarea id="create-announcement-body" name="body" rows={5} required />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-startAt">开始时间(可选)</Label>
|
||||
<Input id="create-announcement-startAt" name="startAt" type="datetime-local" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-endAt">结束时间(可选)</Label>
|
||||
<Input id="create-announcement-endAt" name="endAt" type="datetime-local" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="create-announcement-sendNotification">同步发送站内通知</Label>
|
||||
<BooleanToggle
|
||||
id="create-announcement-sendNotification"
|
||||
@@ -338,13 +404,15 @@ export function CreateAnnouncementButton({
|
||||
trueLabel="发送"
|
||||
falseLabel="不发送"
|
||||
ariaLabel="同步发送站内通知"
|
||||
size="compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PendingSubmitButton className="w-full" pendingLabel="发布中...">
|
||||
<PendingSubmitButton className="h-8 w-full" pendingLabel="发布中...">
|
||||
发布
|
||||
</PendingSubmitButton>
|
||||
</form>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user