feat(portal-shell): clean widget design tokens and fix lint:tokens (P1-6)
516 mechanical replacements across 25 widget files: - spacing xs/sm/md/lg/xl to numeric 1/2/3/4/6 - text-heading-3 to text-lg font-semibold - bg-danger to bg-destructive - border border dedup Fix .eslintrc.tokens.js to use typescript-eslint parser (was importing uninstalled @typescript-eslint/parser). lint:tokens now passes.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* audit-logs(admin / main)
|
||||
@@ -34,8 +34,7 @@ const RESOURCE_OPTIONS = [
|
||||
"homework",
|
||||
] as const;
|
||||
|
||||
const inputCls =
|
||||
"rounded-md border border bg-card px-sm py-xs text-sm text-foreground";
|
||||
const inputCls = "rounded-md border bg-card px-2 py-1 text-sm text-foreground";
|
||||
|
||||
export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
const rawPageSize = props.props.pageSize;
|
||||
@@ -81,10 +80,10 @@ export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
const selectedLog = logs.find((l) => l.id === selectedId);
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">审计日志</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">审计日志</h3>
|
||||
|
||||
<div className="mt-sm flex flex-wrap gap-2">
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={userIdFilter}
|
||||
@@ -122,17 +121,17 @@ export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{logs.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无审计日志</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无审计日志</p>
|
||||
) : (
|
||||
<div className="mt-sm overflow-x-auto">
|
||||
<div className="mt-2 overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">时间</th>
|
||||
<th className="py-xs text-left">用户</th>
|
||||
<th className="py-xs text-left">操作</th>
|
||||
<th className="py-xs text-left">资源</th>
|
||||
<th className="py-xs text-left">IP</th>
|
||||
<th className="py-1 text-left">时间</th>
|
||||
<th className="py-1 text-left">用户</th>
|
||||
<th className="py-1 text-left">操作</th>
|
||||
<th className="py-1 text-left">资源</th>
|
||||
<th className="py-1 text-left">IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -146,16 +145,16 @@ export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
isSelected ? "bg-primary-subtle" : "bg-background"
|
||||
}`}
|
||||
>
|
||||
<td className="py-xs text-muted-foreground">
|
||||
<td className="py-1 text-muted-foreground">
|
||||
{log.timestamp}
|
||||
</td>
|
||||
<td className="py-xs text-foreground">{log.userName}</td>
|
||||
<td className="py-xs text-foreground">{log.action}</td>
|
||||
<td className="py-xs text-foreground">
|
||||
<td className="py-1 text-foreground">{log.userName}</td>
|
||||
<td className="py-1 text-foreground">{log.action}</td>
|
||||
<td className="py-1 text-foreground">
|
||||
{log.resource}
|
||||
{log.resourceId ? ` / ${log.resourceId}` : ""}
|
||||
</td>
|
||||
<td className="py-xs text-muted-foreground">{log.ip}</td>
|
||||
<td className="py-1 text-muted-foreground">{log.ip}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -165,15 +164,15 @@ export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
)}
|
||||
|
||||
{selectedLog ? (
|
||||
<div className="mt-sm rounded-md bg-background p-2">
|
||||
<div className="mt-2 rounded-md bg-background p-2">
|
||||
<p className="text-xs text-muted-foreground">详情</p>
|
||||
<pre className="mt-xs overflow-x-auto whitespace-pre-wrap break-words text-xs text-foreground">
|
||||
<pre className="mt-1 overflow-x-auto whitespace-pre-wrap break-words text-xs text-foreground">
|
||||
{selectedLog.details || "(无详细信息)"}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-sm flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
共 {total} 条,第 {offset + 1} - {rangeEnd} 条
|
||||
</span>
|
||||
@@ -182,7 +181,7 @@ export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
type="button"
|
||||
disabled={!hasPrev}
|
||||
onClick={() => setOffset(Math.max(0, offset - pageSize))}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-foreground"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
@@ -190,7 +189,7 @@ export default function AuditLogs(props: PluginProps): React.ReactElement {
|
||||
type="button"
|
||||
disabled={!hasNext}
|
||||
onClick={() => setOffset(offset + pageSize)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-foreground"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* invitation-codes(admin / main)
|
||||
@@ -30,8 +30,7 @@ const STATUS_LABELS: Record<CodeStatus, string> = {
|
||||
|
||||
const ROLE_OPTIONS = ["teacher", "student", "parent"] as const;
|
||||
|
||||
const inputCls =
|
||||
"rounded-md border border bg-card px-sm py-xs text-sm text-foreground";
|
||||
const inputCls = "rounded-md border bg-card px-2 py-1 text-sm text-foreground";
|
||||
const labelCls = "text-xs text-muted-foreground";
|
||||
|
||||
export default function InvitationCodes(
|
||||
@@ -98,17 +97,17 @@ export default function InvitationCodes(
|
||||
const codes = data ?? [];
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">邀请码</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">邀请码</h3>
|
||||
{status.length > 0 ? (
|
||||
<p className="mt-xs text-xs text-muted-foreground">{status}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{status}</p>
|
||||
) : null}
|
||||
|
||||
{/* 生成新邀请码 */}
|
||||
<div className="mt-sm rounded-md bg-background p-2">
|
||||
<div className="mt-2 rounded-md bg-background p-2">
|
||||
<p className={labelCls}>生成新邀请码</p>
|
||||
<div className="mt-xs flex flex-wrap items-end gap-2">
|
||||
<div className="space-y-xs">
|
||||
<div className="mt-1 flex flex-wrap items-end gap-2">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>角色</label>
|
||||
<select
|
||||
value={newRole}
|
||||
@@ -122,7 +121,7 @@ export default function InvitationCodes(
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>可用次数</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -132,7 +131,7 @@ export default function InvitationCodes(
|
||||
className={`${inputCls} w-24`}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>有效期(小时)</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -145,7 +144,7 @@ export default function InvitationCodes(
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreate}
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
生成
|
||||
</button>
|
||||
@@ -153,7 +152,7 @@ export default function InvitationCodes(
|
||||
</div>
|
||||
|
||||
{/* 筛选 */}
|
||||
<div className="mt-md flex items-center gap-2">
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<label className={labelCls}>状态</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
@@ -171,18 +170,18 @@ export default function InvitationCodes(
|
||||
|
||||
{/* 邀请码列表 */}
|
||||
{codes.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无邀请码</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无邀请码</p>
|
||||
) : (
|
||||
<div className="mt-sm overflow-x-auto">
|
||||
<div className="mt-2 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">邀请码</th>
|
||||
<th className="py-xs text-left">角色</th>
|
||||
<th className="py-xs text-left">状态</th>
|
||||
<th className="py-xs text-left">使用/上限</th>
|
||||
<th className="py-xs text-left">过期时间</th>
|
||||
<th className="py-xs text-left">操作</th>
|
||||
<th className="py-1 text-left">邀请码</th>
|
||||
<th className="py-1 text-left">角色</th>
|
||||
<th className="py-1 text-left">状态</th>
|
||||
<th className="py-1 text-left">使用/上限</th>
|
||||
<th className="py-1 text-left">过期时间</th>
|
||||
<th className="py-1 text-left">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -191,23 +190,21 @@ export default function InvitationCodes(
|
||||
const canRevoke = c.status === "active";
|
||||
return (
|
||||
<tr key={c.id} className="border-b border">
|
||||
<td className="py-xs font-mono text-foreground">
|
||||
{c.code}
|
||||
</td>
|
||||
<td className="py-xs text-foreground">{c.role}</td>
|
||||
<td className="py-xs text-foreground">{c.status}</td>
|
||||
<td className="py-xs text-foreground">
|
||||
<td className="py-1 font-mono text-foreground">{c.code}</td>
|
||||
<td className="py-1 text-foreground">{c.role}</td>
|
||||
<td className="py-1 text-foreground">{c.status}</td>
|
||||
<td className="py-1 text-foreground">
|
||||
{c.usedCount} / {c.maxUses}
|
||||
</td>
|
||||
<td className="py-xs text-muted-foreground">
|
||||
<td className="py-1 text-muted-foreground">
|
||||
{c.expiresAt}
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(c.code)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
@@ -216,7 +213,7 @@ export default function InvitationCodes(
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => handleRevoke(c.id)}
|
||||
className="rounded-md bg-danger px-sm py-xs text-xs text-primary-foreground"
|
||||
className="rounded-md bg-destructive px-2 py-1 text-xs text-primary-foreground"
|
||||
>
|
||||
撤销
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* plugin-manager(admin / main)
|
||||
@@ -84,14 +84,14 @@ function RegistryTab(): React.ReactElement {
|
||||
items.map((item) => (
|
||||
<div
|
||||
key={item.pluginId}
|
||||
className="rounded-xl border border bg-background p-4"
|
||||
className="rounded-xl border bg-background p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-body text-foreground">
|
||||
{item.displayName}
|
||||
{item.isBuiltin ? (
|
||||
<span className="ml-xs text-xs text-muted-foreground">
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
内置
|
||||
</span>
|
||||
) : null}
|
||||
@@ -100,12 +100,12 @@ function RegistryTab(): React.ReactElement {
|
||||
{item.pluginId} · v{item.version} · {item.category} ·{" "}
|
||||
{item.defaultSlot}
|
||||
</p>
|
||||
<p className="mt-xs text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-xs text-sm text-foreground">
|
||||
<label className="flex items-center gap-1 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.isActive}
|
||||
@@ -120,14 +120,14 @@ function RegistryTab(): React.ReactElement {
|
||||
? setEditingId(null)
|
||||
: handleStartEdit(item)
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
>
|
||||
{editingId === item.pluginId ? "取消" : "编辑 props"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{editingId === item.pluginId ? (
|
||||
<div className="mt-sm space-y-xs">
|
||||
<div className="mt-2 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
defaultProps (JSON)
|
||||
</label>
|
||||
@@ -135,7 +135,7 @@ function RegistryTab(): React.ReactElement {
|
||||
value={draftProps}
|
||||
onChange={(e) => setDraftProps(e.target.value)}
|
||||
rows={6}
|
||||
className="w-full rounded-md border border bg-card p-2 font-mono text-xs text-foreground"
|
||||
className="w-full rounded-md border bg-card p-2 font-mono text-xs text-foreground"
|
||||
/>
|
||||
{draftError.length > 0 ? (
|
||||
<p className="text-xs text-muted-foreground">{draftError}</p>
|
||||
@@ -143,7 +143,7 @@ function RegistryTab(): React.ReactElement {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveProps(item.pluginId)}
|
||||
className="rounded-md bg-primary px-md py-xs text-xs text-primary-foreground"
|
||||
className="rounded-md bg-primary px-3 py-1 text-xs text-primary-foreground"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
@@ -199,7 +199,7 @@ function MappingTab(): React.ReactElement {
|
||||
|
||||
const draftList = Object.values(drafts);
|
||||
const selectCls =
|
||||
"rounded-md border border bg-card px-sm py-xs text-sm text-foreground";
|
||||
"rounded-md border bg-card px-2 py-1 text-sm text-foreground";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -226,17 +226,17 @@ function MappingTab(): React.ReactElement {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">插件</th>
|
||||
<th className="py-xs text-left">启用</th>
|
||||
<th className="py-xs text-left">Slot</th>
|
||||
<th className="py-xs text-left">排序</th>
|
||||
<th className="py-1 text-left">插件</th>
|
||||
<th className="py-1 text-left">启用</th>
|
||||
<th className="py-1 text-left">Slot</th>
|
||||
<th className="py-1 text-left">排序</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{draftList.map((m) => (
|
||||
<tr key={m.pluginId} className="border-b border">
|
||||
<td className="py-xs text-foreground">{m.pluginId}</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1 text-foreground">{m.pluginId}</td>
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={m.isEnabled}
|
||||
@@ -245,17 +245,17 @@ function MappingTab(): React.ReactElement {
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="text"
|
||||
value={m.slot}
|
||||
onChange={(e) =>
|
||||
updateDraft(m.pluginId, { slot: e.target.value })
|
||||
}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="number"
|
||||
value={m.sortOrder}
|
||||
@@ -264,7 +264,7 @@ function MappingTab(): React.ReactElement {
|
||||
sortOrder: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -276,7 +276,7 @@ function MappingTab(): React.ReactElement {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
保存映射
|
||||
</button>
|
||||
@@ -313,7 +313,7 @@ function LayoutTab(): React.ReactElement {
|
||||
}
|
||||
|
||||
const selectCls =
|
||||
"rounded-md border border bg-card px-sm py-xs text-sm text-foreground";
|
||||
"rounded-md border bg-card px-2 py-1 text-sm text-foreground";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -356,11 +356,11 @@ function LayoutTab(): React.ReactElement {
|
||||
>
|
||||
<p className="text-body text-foreground">{tpl.displayName}</p>
|
||||
<p className="text-xs text-muted-foreground">{tpl.description}</p>
|
||||
<p className="mt-xs text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
可用 Slot:{tpl.availableSlots.join("、")}
|
||||
</p>
|
||||
{isCurrent ? (
|
||||
<p className="mt-xs text-xs text-foreground">当前默认</p>
|
||||
<p className="mt-1 text-xs text-foreground">当前默认</p>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
@@ -402,12 +402,12 @@ function UserLayoutTab(): React.ReactElement {
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
placeholder="输入用户 ID"
|
||||
className="w-64 rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-64 rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
className="rounded-md bg-danger px-md py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-destructive px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
重置布局
|
||||
</button>
|
||||
@@ -423,16 +423,16 @@ export default function PluginManager(_props: PluginProps): React.ReactElement {
|
||||
const [tab, setTab] = useState<TabKey>("registry");
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">插件管理</h3>
|
||||
<div className="mt-sm flex gap-2 border-b border">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">插件管理</h3>
|
||||
<div className="mt-2 flex gap-2 border-b border">
|
||||
{(Object.keys(TAB_LABELS) as TabKey[]).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => setTab(key)}
|
||||
aria-pressed={tab === key}
|
||||
className={`rounded-md px-sm py-xs text-sm ${
|
||||
className={`rounded-md px-2 py-1 text-sm ${
|
||||
tab === key
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card text-foreground"
|
||||
@@ -442,7 +442,7 @@ export default function PluginManager(_props: PluginProps): React.ReactElement {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-sm">
|
||||
<div className="mt-2">
|
||||
{tab === "registry" ? <RegistryTab /> : null}
|
||||
{tab === "mapping" ? <MappingTab /> : null}
|
||||
{tab === "layout" ? <LayoutTab /> : null}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* rbac-manager(admin / main)
|
||||
@@ -79,21 +79,21 @@ export default function RbacManager(_props: PluginProps): React.ReactElement {
|
||||
|
||||
if (roles.length === 0) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">角色权限</h3>
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无角色数据</p>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">角色权限</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无角色数据</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">角色权限</h3>
|
||||
<div className="mt-sm flex gap-4">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">角色权限</h3>
|
||||
<div className="mt-2 flex gap-4">
|
||||
{/* 左侧角色列表 */}
|
||||
<div className="w-64 flex-shrink-0">
|
||||
<p className="text-sm text-muted-foreground">角色</p>
|
||||
<ul className="mt-xs space-y-xs">
|
||||
<ul className="mt-1 space-y-1">
|
||||
{roles.map((role) => {
|
||||
const isSelected = role.id === effectiveRoleId;
|
||||
return (
|
||||
@@ -102,7 +102,7 @@ export default function RbacManager(_props: PluginProps): React.ReactElement {
|
||||
type="button"
|
||||
onClick={() => setSelectedRoleId(role.id)}
|
||||
aria-pressed={isSelected}
|
||||
className={`w-full rounded-md border px-sm py-xs text-left text-sm ${
|
||||
className={`w-full rounded-md border px-2 py-1 text-left text-sm ${
|
||||
isSelected
|
||||
? "border-accent bg-primary-subtle text-foreground"
|
||||
: "border bg-background text-foreground"
|
||||
@@ -124,11 +124,11 @@ export default function RbacManager(_props: PluginProps): React.ReactElement {
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">权限</th>
|
||||
<th className="py-1 text-left">权限</th>
|
||||
{roles.map((role) => (
|
||||
<th
|
||||
key={role.id}
|
||||
className={`py-xs text-center ${
|
||||
className={`py-1 text-center ${
|
||||
role.id === effectiveRoleId
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground"
|
||||
@@ -142,7 +142,7 @@ export default function RbacManager(_props: PluginProps): React.ReactElement {
|
||||
<tbody>
|
||||
{permissions.map((perm) => (
|
||||
<tr key={perm.id} className="border-b border">
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<p className="text-foreground">{perm.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{perm.resource} / {perm.action}
|
||||
@@ -153,7 +153,7 @@ export default function RbacManager(_props: PluginProps): React.ReactElement {
|
||||
rolePermissionMap.get(role.id)?.has(perm.id) ?? false;
|
||||
const isBusy = busyRoleId === role.id;
|
||||
return (
|
||||
<td key={role.id} className="py-xs text-center">
|
||||
<td key={role.id} className="py-1 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* school-settings(admin / main)
|
||||
@@ -18,7 +18,7 @@ import type { PluginProps } from "@/lib/types";
|
||||
|
||||
const TERM_OPTIONS = ["第一学期", "第二学期", "暑假", "寒假"] as const;
|
||||
const inputCls =
|
||||
"w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground";
|
||||
"w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground";
|
||||
const labelCls = "text-xs text-muted-foreground";
|
||||
|
||||
export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
@@ -70,22 +70,22 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
|
||||
if (!draft) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">学校设置</h3>
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无学校数据</p>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">学校设置</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无学校数据</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">学校设置</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">学校设置</h3>
|
||||
{status.length > 0 ? (
|
||||
<p className="mt-xs text-xs text-muted-foreground">{status}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{status}</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-sm grid grid-cols-2 gap-4">
|
||||
<div className="space-y-xs">
|
||||
<div className="mt-2 grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>学校名称</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -94,7 +94,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>联系电话</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -103,7 +103,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>联系邮箱</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -112,7 +112,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>当前学年</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -124,7 +124,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>当前学期</label>
|
||||
<select
|
||||
value={draft.currentTerm}
|
||||
@@ -138,7 +138,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>学期开始</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -147,7 +147,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>学期结束</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -156,7 +156,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-xs">
|
||||
<div className="space-y-1">
|
||||
<label className={labelCls}>学校地址</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -168,7 +168,7 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{showAdvanced ? (
|
||||
<div className="mt-md rounded-md bg-background p-2">
|
||||
<div className="mt-3 rounded-md bg-background p-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
高级配置:学区划分、学段映射、教研室组织等由 config-service
|
||||
角色-插件映射控制。
|
||||
@@ -176,18 +176,18 @@ export default function SchoolSettings(props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-md flex justify-end gap-2">
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => data && setDraft({ ...data })}
|
||||
className="rounded-md border border bg-card px-md py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-3 py-1 text-sm text-foreground"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* user-management(admin / main)
|
||||
@@ -87,13 +87,13 @@ export default function UserManagement(
|
||||
const rangeEnd = Math.min(offset + PAGE_SIZE, total);
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">用户管理</h3>
|
||||
<div className="mt-sm flex gap-2">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">用户管理</h3>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<select
|
||||
value={roleFilter}
|
||||
onChange={handleRoleFilterChange}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
aria-label="按角色筛选"
|
||||
>
|
||||
<option value="">全部角色</option>
|
||||
@@ -106,17 +106,17 @@ export default function UserManagement(
|
||||
</div>
|
||||
|
||||
{users.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无用户</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无用户</p>
|
||||
) : (
|
||||
<div className="mt-sm overflow-x-auto">
|
||||
<div className="mt-2 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">姓名</th>
|
||||
<th className="py-xs text-left">邮箱</th>
|
||||
<th className="py-xs text-left">角色</th>
|
||||
<th className="py-xs text-left">状态</th>
|
||||
<th className="py-xs text-left">创建时间</th>
|
||||
<th className="py-1 text-left">姓名</th>
|
||||
<th className="py-1 text-left">邮箱</th>
|
||||
<th className="py-1 text-left">角色</th>
|
||||
<th className="py-1 text-left">状态</th>
|
||||
<th className="py-1 text-left">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -124,14 +124,14 @@ export default function UserManagement(
|
||||
const isBusy = busyId === u.id;
|
||||
return (
|
||||
<tr key={u.id} className="border-b border">
|
||||
<td className="py-xs text-foreground">{u.name}</td>
|
||||
<td className="py-xs text-foreground">{u.email}</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1 text-foreground">{u.name}</td>
|
||||
<td className="py-1 text-foreground">{u.email}</td>
|
||||
<td className="py-1">
|
||||
<select
|
||||
value={u.role}
|
||||
disabled={isBusy}
|
||||
onChange={(e) => handleRoleChange(u.id, e.target.value)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
aria-label="修改角色"
|
||||
>
|
||||
{ROLE_OPTIONS.map((r) => (
|
||||
@@ -141,14 +141,14 @@ export default function UserManagement(
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<select
|
||||
value={u.status}
|
||||
disabled={isBusy}
|
||||
onChange={(e) =>
|
||||
handleStatusChange(u.id, e.target.value)
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
aria-label="修改状态"
|
||||
>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
@@ -158,7 +158,7 @@ export default function UserManagement(
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="py-xs text-muted-foreground">
|
||||
<td className="py-1 text-muted-foreground">
|
||||
{u.createdAt}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -169,7 +169,7 @@ export default function UserManagement(
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-sm flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
共 {total} 条,第 {offset + 1} - {rangeEnd} 条
|
||||
</span>
|
||||
@@ -178,7 +178,7 @@ export default function UserManagement(
|
||||
type="button"
|
||||
disabled={!hasPrev}
|
||||
onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-foreground"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
@@ -186,7 +186,7 @@ export default function UserManagement(
|
||||
type="button"
|
||||
disabled={!hasNext}
|
||||
onClick={() => setOffset(offset + PAGE_SIZE)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-foreground"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* child-overview(parent / main)
|
||||
@@ -41,17 +41,17 @@ export default function ChildOverview(_props: PluginProps): React.ReactElement {
|
||||
|
||||
if (children.length === 0) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">孩子总览</h3>
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无孩子信息</p>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">孩子总览</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无孩子信息</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">孩子总览</h3>
|
||||
<div className="mt-sm grid grid-cols-2 gap-4">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">孩子总览</h3>
|
||||
<div className="mt-2 grid grid-cols-2 gap-4">
|
||||
{children.map((child) => {
|
||||
const isSelected = child.id === selectedChildId;
|
||||
return (
|
||||
@@ -77,12 +77,12 @@ export default function ChildOverview(_props: PluginProps): React.ReactElement {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-sm space-y-xs">
|
||||
<div className="mt-2 space-y-1">
|
||||
<p className="text-sm text-muted-foreground">最近成绩</p>
|
||||
{child.recentGrades.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">暂无成绩</p>
|
||||
) : (
|
||||
<ul className="space-y-xs">
|
||||
<ul className="space-y-1">
|
||||
{child.recentGrades.map((g) => (
|
||||
<li
|
||||
key={g.subject}
|
||||
@@ -95,7 +95,7 @@ export default function ChildOverview(_props: PluginProps): React.ReactElement {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-sm flex justify-between border-t border pt-xs text-xs">
|
||||
<div className="mt-2 flex justify-between border-t border pt-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
出勤:
|
||||
{formatRate(child.attendance.present, child.attendance.total)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* leave-approval(parent / main)
|
||||
@@ -89,13 +89,13 @@ export default function LeaveApproval(_props: PluginProps): React.ReactElement {
|
||||
const requests = data ?? [];
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-heading-3 text-foreground">请假审批</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">请假审批</h3>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
aria-label="按状态筛选"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
@@ -106,25 +106,22 @@ export default function LeaveApproval(_props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{childId.length > 0 ? (
|
||||
<p className="mt-xs text-xs text-muted-foreground">已按选中孩子过滤</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">已按选中孩子过滤</p>
|
||||
) : null}
|
||||
{rejectError.length > 0 ? (
|
||||
<p className="mt-xs text-xs text-muted-foreground">{rejectError}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{rejectError}</p>
|
||||
) : null}
|
||||
|
||||
{requests.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无请假申请</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无请假申请</p>
|
||||
) : (
|
||||
<ul className="mt-sm space-y-4">
|
||||
<ul className="mt-2 space-y-4">
|
||||
{requests.map((req) => {
|
||||
const isPending = req.status === "pending";
|
||||
const isBusy = busyId === req.id;
|
||||
const reasonValue = rejectReasons[req.id] ?? "";
|
||||
return (
|
||||
<li
|
||||
key={req.id}
|
||||
className="rounded-xl border border bg-background p-4"
|
||||
>
|
||||
<li key={req.id} className="rounded-xl border bg-background p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-body text-foreground">{req.childName}</p>
|
||||
@@ -133,18 +130,18 @@ export default function LeaveApproval(_props: PluginProps): React.ReactElement {
|
||||
{req.endDate}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-md bg-muted px-sm py-xs text-xs text-foreground">
|
||||
<span className="rounded-md bg-muted px-2 py-1 text-xs text-foreground">
|
||||
{STATUS_LABELS[req.status] ?? req.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-sm text-sm text-muted-foreground">
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{req.reason}
|
||||
</p>
|
||||
<p className="mt-xs text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
提交于 {req.createdAt}
|
||||
</p>
|
||||
{isPending ? (
|
||||
<div className="mt-sm space-y-xs">
|
||||
<div className="mt-2 space-y-1">
|
||||
<input
|
||||
type="text"
|
||||
value={reasonValue}
|
||||
@@ -155,14 +152,14 @@ export default function LeaveApproval(_props: PluginProps): React.ReactElement {
|
||||
}))
|
||||
}
|
||||
placeholder="拒绝原因(拒绝时必填)"
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => handleApprove(req.id)}
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
批准
|
||||
</button>
|
||||
@@ -170,7 +167,7 @@ export default function LeaveApproval(_props: PluginProps): React.ReactElement {
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => handleReject(req.id)}
|
||||
className="rounded-md bg-danger px-md py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-destructive px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* child-selector(sidebar / side)
|
||||
@@ -39,7 +39,7 @@ export default function ChildSelector(_props: PluginProps): React.ReactElement {
|
||||
<select
|
||||
value={currentChildId}
|
||||
onChange={(e) => handleSelect(e.target.value)}
|
||||
className="mt-xs w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="mt-1 w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
aria-label="选择孩子"
|
||||
>
|
||||
<option value="">请选择孩子</option>
|
||||
@@ -50,7 +50,7 @@ export default function ChildSelector(_props: PluginProps): React.ReactElement {
|
||||
))}
|
||||
</select>
|
||||
{current ? (
|
||||
<p className="mt-xs text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{current.grade} · {current.className}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* class-selector(sidebar / side)
|
||||
@@ -38,7 +38,7 @@ export default function ClassSelector(_props: PluginProps): React.ReactElement {
|
||||
<select
|
||||
value={currentClassId}
|
||||
onChange={(e) => handleSelect(e.target.value)}
|
||||
className="mt-xs w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="mt-1 w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
aria-label="选择班级"
|
||||
>
|
||||
<option value="">请选择班级</option>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* quick-actions(sidebar / side)
|
||||
@@ -45,13 +45,13 @@ export default function QuickActions(props: PluginProps): React.ReactElement {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">快捷操作</p>
|
||||
<ul className="mt-xs space-y-sm">
|
||||
<ul className="mt-1 space-y-2">
|
||||
{actions.map((item) => (
|
||||
<li key={item.path}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleClick(item.path)}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-left text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-left text-sm text-foreground"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* term-switcher(sidebar / side)
|
||||
@@ -42,7 +42,7 @@ export default function TermSwitcher(_props: PluginProps): React.ReactElement {
|
||||
<select
|
||||
value={selectedTermId}
|
||||
onChange={(e) => handleSelect(e.target.value)}
|
||||
className="mt-xs w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="mt-1 w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
aria-label="选择学期"
|
||||
>
|
||||
<option value="">请选择学期</option>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* ai-tutor(student / main)
|
||||
@@ -70,13 +70,13 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
const canSend = !sending && input.trim().length > 0;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">AI 辅导</h3>
|
||||
<div className="mt-sm flex">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">AI 辅导</h3>
|
||||
<div className="mt-2 flex">
|
||||
{/* 左侧会话列表 */}
|
||||
<div className="flex w-64 flex-col border-r border pr-sm">
|
||||
<div className="flex w-64 flex-col border-r border pr-2">
|
||||
<p className="text-sm text-muted-foreground">会话列表</p>
|
||||
<ul className="mt-xs space-y-sm">
|
||||
<ul className="mt-1 space-y-2">
|
||||
{sessions.map((session) => {
|
||||
const active = activeSessionId === session.id;
|
||||
return (
|
||||
@@ -84,7 +84,7 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className={`w-full rounded-md px-sm py-xs text-left ${active ? "bg-primary" : "bg-background"}`}
|
||||
className={`w-full rounded-md px-2 py-1 text-left ${active ? "bg-primary" : "bg-background"}`}
|
||||
>
|
||||
<span
|
||||
className={`block text-sm ${active ? "text-primary-foreground" : "text-foreground"}`}
|
||||
@@ -92,7 +92,7 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
{session.title}
|
||||
</span>
|
||||
<span
|
||||
className={`mt-xs block text-xs ${active ? "text-primary-foreground" : "text-muted-foreground"}`}
|
||||
className={`mt-1 block text-xs ${active ? "text-primary-foreground" : "text-muted-foreground"}`}
|
||||
>
|
||||
{session.lastMessage}
|
||||
</span>
|
||||
@@ -109,8 +109,8 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{/* 右侧消息区 */}
|
||||
<div className="flex flex-1 flex-col pl-sm">
|
||||
<div className="flex-1 space-y-sm">
|
||||
<div className="flex flex-1 flex-col pl-2">
|
||||
<div className="flex-1 space-y-2">
|
||||
{messages.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
请输入问题开始与 AI 辅导对话
|
||||
@@ -121,8 +121,8 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
key={msg.id}
|
||||
className={
|
||||
msg.role === "user"
|
||||
? "ml-sm rounded-md bg-primary px-sm py-xs text-sm text-primary-foreground"
|
||||
: "rounded-md bg-background px-sm py-xs text-sm text-foreground"
|
||||
? "ml-2 rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground"
|
||||
: "rounded-md bg-background px-2 py-1 text-sm text-foreground"
|
||||
}
|
||||
>
|
||||
{msg.content}
|
||||
@@ -135,7 +135,7 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{/* 输入区 */}
|
||||
<div className="mt-sm flex">
|
||||
<div className="mt-2 flex">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
@@ -146,7 +146,7 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
}
|
||||
}}
|
||||
placeholder="输入你的问题..."
|
||||
className="flex-1 rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="flex-1 rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -154,8 +154,8 @@ export default function AiTutor(props: PluginProps): React.ReactElement {
|
||||
onClick={() => void handleSend()}
|
||||
className={
|
||||
canSend
|
||||
? "ml-sm rounded-md bg-primary px-sm py-xs text-sm text-primary-foreground"
|
||||
: "ml-sm rounded-md bg-muted px-sm py-xs text-sm text-muted-foreground"
|
||||
? "ml-2 rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground"
|
||||
: "ml-2 rounded-md bg-muted px-2 py-1 text-sm text-muted-foreground"
|
||||
}
|
||||
>
|
||||
发送
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* elective-selector(student / main)
|
||||
@@ -64,8 +64,8 @@ export default function ElectiveSelector(
|
||||
|
||||
if (!termId) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">选课</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">选课</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择学期</p>
|
||||
</section>
|
||||
);
|
||||
@@ -74,31 +74,31 @@ export default function ElectiveSelector(
|
||||
const courses = data ?? [];
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">选课</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">选课</h3>
|
||||
{courses.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无可选课程</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无可选课程</p>
|
||||
) : (
|
||||
<ul className="mt-sm space-y-4">
|
||||
<ul className="mt-2 space-y-4">
|
||||
{courses.map((course) => {
|
||||
const enrolled = enrolledIds[course.id] === true;
|
||||
const full = course.enrolled >= course.capacity;
|
||||
return (
|
||||
<li
|
||||
key={course.id}
|
||||
className="rounded-xl border border bg-background p-2"
|
||||
className="rounded-xl border bg-background p-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-body text-foreground">
|
||||
{course.name}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-sm rounded-md px-xs py-xs text-xs ${CATEGORY_BADGE[course.category]}`}
|
||||
className={`ml-2 rounded-md px-1 py-1 text-xs ${CATEGORY_BADGE[course.category]}`}
|
||||
>
|
||||
{course.category}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-xs flex flex-col text-sm text-muted-foreground">
|
||||
<div className="mt-1 flex flex-col text-sm text-muted-foreground">
|
||||
<span>教师:{course.teacher}</span>
|
||||
<span>
|
||||
容量:{course.enrolled}/{course.capacity}
|
||||
@@ -107,7 +107,7 @@ export default function ElectiveSelector(
|
||||
<span>时间:{course.schedule}</span>
|
||||
<span>学分:{course.credits}</span>
|
||||
</div>
|
||||
<div className="mt-xs">
|
||||
<div className="mt-1">
|
||||
{enrolled ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -115,8 +115,8 @@ export default function ElectiveSelector(
|
||||
onClick={() => void handleDrop(course.id)}
|
||||
className={
|
||||
dropping
|
||||
? "rounded-md bg-muted px-sm py-xs text-xs text-muted-foreground"
|
||||
: "rounded-md bg-danger px-sm py-xs text-xs text-primary-foreground"
|
||||
? "rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground"
|
||||
: "rounded-md bg-destructive px-2 py-1 text-xs text-primary-foreground"
|
||||
}
|
||||
>
|
||||
退课
|
||||
@@ -128,8 +128,8 @@ export default function ElectiveSelector(
|
||||
onClick={() => void handleEnroll(course.id)}
|
||||
className={
|
||||
enrolling || full
|
||||
? "rounded-md bg-muted px-sm py-xs text-xs text-muted-foreground"
|
||||
: "rounded-md bg-primary px-sm py-xs text-xs text-primary-foreground"
|
||||
? "rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground"
|
||||
: "rounded-md bg-primary px-2 py-1 text-xs text-primary-foreground"
|
||||
}
|
||||
>
|
||||
{full ? "已满" : "选课"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* error-book(student / main)
|
||||
@@ -46,9 +46,9 @@ export default function ErrorBook(props: PluginProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">错题本</h3>
|
||||
<div className="mt-sm flex items-center">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">错题本</h3>
|
||||
<div className="mt-2 flex items-center">
|
||||
<label
|
||||
htmlFor="error-book-subject"
|
||||
className="text-sm text-muted-foreground"
|
||||
@@ -59,7 +59,7 @@ export default function ErrorBook(props: PluginProps): React.ReactElement {
|
||||
id="error-book-subject"
|
||||
value={subjectFilter}
|
||||
onChange={(e) => setSubjectFilter(e.target.value)}
|
||||
className="ml-sm rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="ml-2 rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
<option value="">全部科目</option>
|
||||
{subjects.map((s) => (
|
||||
@@ -71,31 +71,28 @@ export default function ErrorBook(props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{filteredItems.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无错题数据</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无错题数据</p>
|
||||
) : (
|
||||
<ul className="mt-sm space-y-4">
|
||||
<ul className="mt-2 space-y-4">
|
||||
{filteredItems.map((item) => {
|
||||
const mastered = masteredIds[item.id] === true;
|
||||
return (
|
||||
<li
|
||||
key={item.id}
|
||||
className="rounded-xl border border bg-background p-2"
|
||||
>
|
||||
<li key={item.id} className="rounded-xl border bg-background p-2">
|
||||
<div className="flex items-center">
|
||||
<span className="rounded-md bg-muted px-sm py-xs text-xs text-foreground">
|
||||
<span className="rounded-md bg-muted px-2 py-1 text-xs text-foreground">
|
||||
{item.subject}
|
||||
</span>
|
||||
<span className="ml-sm text-xs text-muted-foreground">
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
错误 {item.errorCount} 次
|
||||
</span>
|
||||
<span className="ml-sm text-xs text-muted-foreground">
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
最后错误:{item.lastErrorAt}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-xs text-body text-foreground">
|
||||
<p className="mt-1 text-body text-foreground">
|
||||
{item.question}
|
||||
</p>
|
||||
<div className="mt-xs flex flex-col">
|
||||
<div className="mt-1 flex flex-col">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
我的答案:{item.myAnswer}
|
||||
</span>
|
||||
@@ -109,8 +106,8 @@ export default function ErrorBook(props: PluginProps): React.ReactElement {
|
||||
onClick={() => void handleMarkMastered(item.id)}
|
||||
className={
|
||||
mastered
|
||||
? "mt-xs rounded-md bg-muted px-sm py-xs text-xs text-muted-foreground"
|
||||
: "mt-xs rounded-md bg-primary px-sm py-xs text-xs text-primary-foreground"
|
||||
? "mt-1 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground"
|
||||
: "mt-1 rounded-md bg-primary px-2 py-1 text-xs text-primary-foreground"
|
||||
}
|
||||
>
|
||||
{mastered ? "已掌握" : "标记已掌握"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* learning-path(student / main)
|
||||
@@ -35,7 +35,7 @@ function statusClass(status: LearningNodeStatus): string {
|
||||
case "locked":
|
||||
return "bg-muted text-muted-foreground";
|
||||
case "available":
|
||||
return "border border bg-card text-foreground";
|
||||
return "border bg-card text-foreground";
|
||||
case "in_progress":
|
||||
return "bg-warning text-foreground";
|
||||
case "completed":
|
||||
@@ -69,8 +69,8 @@ export default function LearningPath(_props: PluginProps): React.ReactElement {
|
||||
|
||||
if (!subjectId) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">学习路径</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">学习路径</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择科目</p>
|
||||
</section>
|
||||
);
|
||||
@@ -81,44 +81,44 @@ export default function LearningPath(_props: PluginProps): React.ReactElement {
|
||||
const progress = path?.progress ?? 0;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">学习路径</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">学习路径</h3>
|
||||
|
||||
<div className="mt-sm">
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">整体进度</span>
|
||||
<span className="text-sm text-foreground">{progress}%</span>
|
||||
</div>
|
||||
<div className="mt-xs h-xs w-full rounded-md bg-muted">
|
||||
<div className="mt-1 h-1 w-full rounded-md bg-muted">
|
||||
<div
|
||||
className="h-xs rounded-md bg-primary"
|
||||
className="h-1 rounded-md bg-primary"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{nodes.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无学习路径数据</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无学习路径数据</p>
|
||||
) : (
|
||||
<ol className="mt-md space-y-4">
|
||||
<ol className="mt-3 space-y-4">
|
||||
{nodes.map((node) => {
|
||||
const clickable = node.status !== "locked";
|
||||
return (
|
||||
<li key={node.id} className="flex items-start">
|
||||
<span
|
||||
className={`flex w-64 items-center justify-center rounded-md px-sm py-xs text-xs ${statusClass(
|
||||
className={`flex w-64 items-center justify-center rounded-md px-2 py-1 text-xs ${statusClass(
|
||||
node.status,
|
||||
)}`}
|
||||
>
|
||||
{TYPE_LABEL[node.type]}
|
||||
</span>
|
||||
<div className="ml-sm flex-1">
|
||||
<div className="ml-2 flex-1">
|
||||
<div className="flex items-center">
|
||||
<span className="text-body text-foreground">
|
||||
{node.title}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-sm rounded-md px-xs py-xs text-xs ${statusClass(
|
||||
className={`ml-2 rounded-md px-1 py-1 text-xs ${statusClass(
|
||||
node.status,
|
||||
)}`}
|
||||
>
|
||||
@@ -126,7 +126,7 @@ export default function LearningPath(_props: PluginProps): React.ReactElement {
|
||||
</span>
|
||||
</div>
|
||||
{node.dependencies.length > 0 && (
|
||||
<p className="mt-xs text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
依赖:{node.dependencies.join("、")}
|
||||
</p>
|
||||
)}
|
||||
@@ -134,7 +134,7 @@ export default function LearningPath(_props: PluginProps): React.ReactElement {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNavigate(node)}
|
||||
className="mt-xs rounded-md border border bg-card px-sm py-xs text-xs text-foreground"
|
||||
className="mt-1 rounded-md border bg-card px-2 py-1 text-xs text-foreground"
|
||||
>
|
||||
进入学习
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* lesson-plan-editor(teacher / main)
|
||||
@@ -43,8 +43,8 @@ export default function LessonPlanEditor(
|
||||
|
||||
if (!classId) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">备课画布</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">备课画布</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择班级</p>
|
||||
</section>
|
||||
);
|
||||
@@ -101,19 +101,19 @@ export default function LessonPlanEditor(
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-heading-3 text-foreground">备课画布</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">备课画布</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNew}
|
||||
className="rounded-md bg-primary px-sm py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
新建备课
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-sm flex gap-4">
|
||||
<ul className="w-64 shrink-0 space-y-sm">
|
||||
<div className="mt-2 flex gap-4">
|
||||
<ul className="w-64 shrink-0 space-y-2">
|
||||
{plans.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">暂无备课记录</li>
|
||||
) : (
|
||||
@@ -122,7 +122,7 @@ export default function LessonPlanEditor(
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(p)}
|
||||
className={`w-full rounded-md border border px-sm py-xs text-left text-sm ${
|
||||
className={`w-full rounded-md border px-2 py-1 text-left text-sm ${
|
||||
draft.id === p.id
|
||||
? "bg-muted text-foreground"
|
||||
: "bg-card text-foreground"
|
||||
@@ -135,7 +135,7 @@ export default function LessonPlanEditor(
|
||||
)}
|
||||
</ul>
|
||||
<div className="flex-1 space-y-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">标题</span>
|
||||
<input
|
||||
type="text"
|
||||
@@ -143,50 +143,50 @@ export default function LessonPlanEditor(
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, title: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
placeholder="请输入备课标题"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">教学目标</span>
|
||||
<textarea
|
||||
value={draft.objectives}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, objectives: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
rows={3}
|
||||
placeholder="请输入教学目标"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">教学内容</span>
|
||||
<textarea
|
||||
value={draft.content}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, content: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
rows={4}
|
||||
placeholder="请输入教学内容"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col space-y-xs">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">教学资源</span>
|
||||
<ul className="space-y-xs">
|
||||
<ul className="space-y-1">
|
||||
{draft.resources.map((r, i) => (
|
||||
<li key={i} className="flex gap-xs">
|
||||
<li key={i} className="flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={r}
|
||||
onChange={(e) => handleResourceChange(i, e.target.value)}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
placeholder="资源名称或链接"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleResourceRemove(i)}
|
||||
className="rounded-md bg-muted px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md bg-muted px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
@@ -196,7 +196,7 @@ export default function LessonPlanEditor(
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResourceAdd}
|
||||
className="self-start rounded-md bg-muted px-sm py-xs text-sm text-foreground"
|
||||
className="self-start rounded-md bg-muted px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
添加资源
|
||||
</button>
|
||||
@@ -205,7 +205,7 @@ export default function LessonPlanEditor(
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !draft.title.trim()}
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground disabled:opacity-50"
|
||||
className="rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
{saving ? "保存中" : "保存"}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* question-bank(teacher / main)
|
||||
@@ -66,8 +66,8 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
|
||||
if (!bankId) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">题库管理</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">题库管理</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择题库</p>
|
||||
</section>
|
||||
);
|
||||
@@ -98,25 +98,25 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-heading-3 text-foreground">题库管理</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">题库管理</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowForm((v) => !v)}
|
||||
className="rounded-md bg-primary px-sm py-xs text-sm text-primary-foreground"
|
||||
className="rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
{showForm ? "收起新建" : "新建题目"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm flex gap-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<div className="mt-2 flex gap-4">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">题型</span>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{QUESTION_TYPES.map((t) => (
|
||||
@@ -126,12 +126,12 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">难度</span>
|
||||
<select
|
||||
value={difficultyFilter}
|
||||
onChange={(e) => setDifficultyFilter(e.target.value)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{DIFFICULTIES.map((d) => (
|
||||
@@ -144,16 +144,16 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{showForm ? (
|
||||
<div className="mt-sm space-y-4 rounded-xl bg-muted p-4">
|
||||
<div className="mt-2 space-y-4 rounded-xl bg-muted p-4">
|
||||
<div className="flex gap-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">题型</span>
|
||||
<select
|
||||
value={newQuestion.type}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, type: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
{QUESTION_TYPES.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
@@ -162,14 +162,14 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">难度</span>
|
||||
<select
|
||||
value={newQuestion.difficulty}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, difficulty: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
{DIFFICULTIES.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
@@ -179,26 +179,26 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">题干</span>
|
||||
<textarea
|
||||
value={newQuestion.content}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, content: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
rows={3}
|
||||
placeholder="请输入题干"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">答案</span>
|
||||
<textarea
|
||||
value={newQuestion.answer}
|
||||
onChange={(e) =>
|
||||
setNewQuestion((q) => ({ ...q, answer: e.target.value }))
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
rows={2}
|
||||
placeholder="请输入答案"
|
||||
/>
|
||||
@@ -207,38 +207,38 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={!newQuestion.content.trim()}
|
||||
className="rounded-md bg-primary px-md py-xs text-sm text-primary-foreground disabled:opacity-50"
|
||||
className="rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ul className="mt-sm space-y-4">
|
||||
<ul className="mt-2 space-y-4">
|
||||
{questions.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">暂无题目</li>
|
||||
) : (
|
||||
questions.map((q) => (
|
||||
<li key={q.id} className="rounded-md border border p-2">
|
||||
<div className="flex flex-wrap gap-xs">
|
||||
<span className="rounded-md bg-primary px-xs py-xs text-xs text-primary-foreground">
|
||||
<li key={q.id} className="rounded-md border p-2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="rounded-md bg-primary px-1 py-1 text-xs text-primary-foreground">
|
||||
{TYPE_LABELS[q.type] ?? q.type}
|
||||
</span>
|
||||
<span className="rounded-md bg-muted px-xs py-xs text-xs text-foreground">
|
||||
<span className="rounded-md bg-muted px-1 py-1 text-xs text-foreground">
|
||||
{DIFFICULTY_LABELS[q.difficulty] ?? q.difficulty}
|
||||
</span>
|
||||
{q.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-md bg-muted px-xs py-xs text-xs text-muted-foreground"
|
||||
className="rounded-md bg-muted px-1 py-1 text-xs text-muted-foreground"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-xs text-body text-foreground">{q.content}</p>
|
||||
<p className="mt-1 text-body text-foreground">{q.content}</p>
|
||||
{q.options.length > 0 ? (
|
||||
<ul className="mt-xs space-y-xs">
|
||||
<ul className="mt-1 space-y-1">
|
||||
{q.options.map((opt, i) => (
|
||||
<li key={i} className="text-sm text-muted-foreground">
|
||||
{String.fromCharCode(65 + i)}. {opt}
|
||||
@@ -246,7 +246,7 @@ export default function QuestionBank(_props: PluginProps): React.ReactElement {
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<p className="mt-xs text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
答案:{q.answer}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* scheduling-rules(teacher / main)
|
||||
@@ -44,8 +44,8 @@ export default function SchedulingRules(
|
||||
|
||||
if (!classId) {
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">排课规则</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">排课规则</h3>
|
||||
<p className="text-sm text-muted-foreground">请先选择班级</p>
|
||||
</section>
|
||||
);
|
||||
@@ -85,20 +85,20 @@ export default function SchedulingRules(
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">排课规则</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">排课规则</h3>
|
||||
{rules.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无排课规则</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无排课规则</p>
|
||||
) : (
|
||||
<table className="mt-sm w-full text-sm">
|
||||
<table className="mt-2 w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border text-muted-foreground">
|
||||
<th className="py-xs text-left">星期</th>
|
||||
<th className="py-xs text-left">节次</th>
|
||||
<th className="py-xs text-left">科目</th>
|
||||
<th className="py-xs text-left">教师</th>
|
||||
<th className="py-xs text-left">教室</th>
|
||||
<th className="py-xs text-left">操作</th>
|
||||
<th className="py-1 text-left">星期</th>
|
||||
<th className="py-1 text-left">节次</th>
|
||||
<th className="py-1 text-left">科目</th>
|
||||
<th className="py-1 text-left">教师</th>
|
||||
<th className="py-1 text-left">教室</th>
|
||||
<th className="py-1 text-left">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -107,7 +107,7 @@ export default function SchedulingRules(
|
||||
if (isEditing && draft) {
|
||||
return (
|
||||
<tr key={rule.id} className="border-b border">
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<select
|
||||
value={draft.dayOfWeek}
|
||||
onChange={(e) =>
|
||||
@@ -115,7 +115,7 @@ export default function SchedulingRules(
|
||||
d ? { ...d, dayOfWeek: Number(e.target.value) } : d,
|
||||
)
|
||||
}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
{DAY_NAMES.map((name, i) => (
|
||||
<option key={name} value={i + 1}>
|
||||
@@ -124,7 +124,7 @@ export default function SchedulingRules(
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="text"
|
||||
value={draft.periods}
|
||||
@@ -133,10 +133,10 @@ export default function SchedulingRules(
|
||||
d ? { ...d, periods: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="text"
|
||||
value={draft.subject}
|
||||
@@ -145,10 +145,10 @@ export default function SchedulingRules(
|
||||
d ? { ...d, subject: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="text"
|
||||
value={draft.teacherId}
|
||||
@@ -157,10 +157,10 @@ export default function SchedulingRules(
|
||||
d ? { ...d, teacherId: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1">
|
||||
<input
|
||||
type="text"
|
||||
value={draft.room}
|
||||
@@ -169,23 +169,23 @@ export default function SchedulingRules(
|
||||
d ? { ...d, room: e.target.value } : d,
|
||||
)
|
||||
}
|
||||
className="w-full rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-full rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-xs">
|
||||
<div className="flex gap-xs">
|
||||
<td className="py-1">
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="rounded-md bg-primary px-sm py-xs text-xs text-primary-foreground disabled:opacity-50"
|
||||
className="rounded-md bg-primary px-2 py-1 text-xs text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="rounded-md bg-muted px-sm py-xs text-xs text-foreground"
|
||||
className="rounded-md bg-muted px-2 py-1 text-xs text-foreground"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@@ -196,18 +196,18 @@ export default function SchedulingRules(
|
||||
}
|
||||
return (
|
||||
<tr key={rule.id} className="border-b border">
|
||||
<td className="py-xs text-foreground">
|
||||
<td className="py-1 text-foreground">
|
||||
{dayName(rule.dayOfWeek)}
|
||||
</td>
|
||||
<td className="py-xs text-foreground">{rule.periods}</td>
|
||||
<td className="py-xs text-foreground">{rule.subject}</td>
|
||||
<td className="py-xs text-foreground">{rule.teacherId}</td>
|
||||
<td className="py-xs text-foreground">{rule.room}</td>
|
||||
<td className="py-xs">
|
||||
<td className="py-1 text-foreground">{rule.periods}</td>
|
||||
<td className="py-1 text-foreground">{rule.subject}</td>
|
||||
<td className="py-1 text-foreground">{rule.teacherId}</td>
|
||||
<td className="py-1 text-foreground">{rule.room}</td>
|
||||
<td className="py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEdit(rule)}
|
||||
className="rounded-md bg-muted px-sm py-xs text-xs text-foreground"
|
||||
className="rounded-md bg-muted px-2 py-1 text-xs text-foreground"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* textbook-manager(teacher / main)
|
||||
@@ -41,41 +41,41 @@ export default function TextbookManager(
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<h3 className="text-heading-3 text-foreground">教材管理</h3>
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">教材管理</h3>
|
||||
|
||||
<div className="mt-sm flex gap-4">
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<div className="mt-2 flex gap-4">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">科目 ID</span>
|
||||
<input
|
||||
type="text"
|
||||
value={subjectInput}
|
||||
onChange={(e) => setSubjectInput(e.target.value)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
placeholder="可选,输入科目 ID"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col space-y-xs">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<span className="text-sm text-muted-foreground">年级</span>
|
||||
<input
|
||||
type="text"
|
||||
value={gradeInput}
|
||||
onChange={(e) => setGradeInput(e.target.value)}
|
||||
className="rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
placeholder="可选,如:三年级"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleApply}
|
||||
className="self-end rounded-md bg-primary px-md py-xs text-sm text-primary-foreground"
|
||||
className="self-end rounded-md bg-primary px-3 py-1 text-sm text-primary-foreground"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-md flex gap-4">
|
||||
<ul className="flex-1 space-y-sm">
|
||||
<div className="mt-3 flex gap-4">
|
||||
<ul className="flex-1 space-y-2">
|
||||
{textbooks.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">暂无教材数据</li>
|
||||
) : (
|
||||
@@ -86,8 +86,8 @@ export default function TextbookManager(
|
||||
onClick={() => setSelectedId(t.id)}
|
||||
className={
|
||||
selectedId === t.id
|
||||
? "w-full rounded-md border border bg-muted p-2 text-left"
|
||||
: "w-full rounded-md border border bg-card p-2 text-left"
|
||||
? "w-full rounded-md border bg-muted p-2 text-left"
|
||||
: "w-full rounded-md border bg-card p-2 text-left"
|
||||
}
|
||||
>
|
||||
<p className="text-body text-foreground">{t.title}</p>
|
||||
@@ -105,13 +105,13 @@ export default function TextbookManager(
|
||||
|
||||
<div className="flex-1">
|
||||
{selected ? (
|
||||
<div className="rounded-xl border border bg-card p-4">
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<h4 className="text-body text-foreground">{selected.title}</h4>
|
||||
<p className="mt-xs text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{selected.author} · {selected.publisher}
|
||||
</p>
|
||||
<h5 className="mt-md text-sm text-foreground">章节列表</h5>
|
||||
<ol className="mt-xs space-y-xs">
|
||||
<h5 className="mt-3 text-sm text-foreground">章节列表</h5>
|
||||
<ol className="mt-1 space-y-1">
|
||||
{selected.chapters.map((c, i) => (
|
||||
<li key={c.id} className="text-sm text-foreground">
|
||||
{i + 1}. {c.title}
|
||||
@@ -120,7 +120,7 @@ export default function TextbookManager(
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border bg-card p-4 text-sm text-muted-foreground">
|
||||
<div className="rounded-xl border bg-card p-4 text-sm text-muted-foreground">
|
||||
点击左侧教材查看章节
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* global-search(topbar / top)
|
||||
@@ -49,10 +49,10 @@ export default function GlobalSearch(_props: PluginProps): React.ReactElement {
|
||||
onFocus={() => setOpen(true)}
|
||||
placeholder="搜索学生、班级、考试…"
|
||||
aria-label="全局搜索"
|
||||
className="w-72 rounded-md border border bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="w-72 rounded-md border bg-card px-2 py-1 text-sm text-foreground"
|
||||
/>
|
||||
{open && keyword.length > 0 ? (
|
||||
<ul className="absolute right-0 z-50 mt-sm w-72 rounded-xl border border bg-card p-2 shadow-md">
|
||||
<ul className="absolute right-0 z-50 mt-2 w-72 rounded-xl border bg-card p-2 shadow-md">
|
||||
{results.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">暂无结果</li>
|
||||
) : (
|
||||
@@ -61,7 +61,7 @@ export default function GlobalSearch(_props: PluginProps): React.ReactElement {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(item)}
|
||||
className="flex w-full flex-col border-b border py-xs text-left"
|
||||
className="flex w-full flex-col border-b border py-1 text-left"
|
||||
>
|
||||
<span className="text-sm text-foreground">{item.title}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -44,12 +44,12 @@ export default function LocaleSwitcher(
|
||||
aria-label={t("common.locale.label")}
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="rounded-md bg-card px-sm py-xs text-sm text-foreground"
|
||||
className="rounded-md bg-card px-2 py-1 text-sm text-foreground"
|
||||
>
|
||||
{locale}
|
||||
</button>
|
||||
{open ? (
|
||||
<ul className="absolute right-0 z-50 mt-sm w-40 rounded-xl border border bg-card p-2 shadow-md">
|
||||
<ul className="absolute right-0 z-50 mt-2 w-40 rounded-xl border bg-card p-2 shadow-md">
|
||||
{LOCALES.map((item) => (
|
||||
<li key={item.value}>
|
||||
<button
|
||||
@@ -57,8 +57,8 @@ export default function LocaleSwitcher(
|
||||
onClick={() => handleSelect(item.value)}
|
||||
className={
|
||||
item.value === locale
|
||||
? "w-full rounded-md bg-primary px-sm py-xs text-left text-sm text-primary-foreground"
|
||||
: "w-full rounded-md px-sm py-xs text-left text-sm text-foreground"
|
||||
? "w-full rounded-md bg-primary px-2 py-1 text-left text-sm text-primary-foreground"
|
||||
: "w-full rounded-md px-2 py-1 text-left text-sm text-foreground"
|
||||
}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* notification-bell(topbar / top)
|
||||
@@ -30,24 +30,24 @@ export default function NotificationBell(
|
||||
aria-label="通知"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="relative rounded-md bg-card px-sm py-xs text-foreground"
|
||||
className="relative rounded-md bg-card px-2 py-1 text-foreground"
|
||||
>
|
||||
<span aria-hidden>🔔</span>
|
||||
{items.length > 0 ? (
|
||||
<span className="absolute -right-xs -top-xs rounded-full bg-danger px-xs text-xs text-primary-foreground">
|
||||
<span className="absolute -right-xs -top-xs rounded-full bg-destructive px-1 text-xs text-primary-foreground">
|
||||
{items.length}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
{open ? (
|
||||
<ul className="absolute right-0 z-50 mt-sm w-64 rounded-xl border border bg-card p-2 shadow-md">
|
||||
<ul className="absolute right-0 z-50 mt-2 w-64 rounded-xl border bg-card p-2 shadow-md">
|
||||
{items.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">暂无通知</li>
|
||||
) : (
|
||||
items.map((n) => (
|
||||
<li
|
||||
key={n.id}
|
||||
className="border-b border py-xs text-sm text-foreground"
|
||||
className="border-b border py-1 text-sm text-foreground"
|
||||
>
|
||||
{n.title}
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* user-menu(topbar / top)
|
||||
@@ -30,7 +30,7 @@ export default function UserMenu(_props: PluginProps): React.ReactElement {
|
||||
aria-label="用户菜单"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex items-center gap-2 rounded-md bg-card px-sm py-xs text-foreground"
|
||||
className="flex items-center gap-2 rounded-md bg-card px-2 py-1 text-foreground"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
@@ -41,12 +41,12 @@ export default function UserMenu(_props: PluginProps): React.ReactElement {
|
||||
<span className="text-sm">{displayName}</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<ul className="absolute right-0 z-50 mt-sm w-56 rounded-xl border border bg-card p-2 shadow-md">
|
||||
<li className="border-b border py-xs">
|
||||
<ul className="absolute right-0 z-50 mt-2 w-56 rounded-xl border bg-card p-2 shadow-md">
|
||||
<li className="border-b border py-1">
|
||||
<p className="text-sm text-foreground">{displayName}</p>
|
||||
<p className="text-xs text-muted-foreground">{displayEmail}</p>
|
||||
</li>
|
||||
<li className="py-xs text-sm text-muted-foreground">
|
||||
<li className="py-1 text-sm text-muted-foreground">
|
||||
角色:{displayRole}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* notifications-widget(universal / main)
|
||||
@@ -23,20 +23,20 @@ function TypeBadge({ type }: { type: string }): React.ReactElement {
|
||||
const label = TYPE_LABEL[type] ?? type;
|
||||
if (type === "urgent") {
|
||||
return (
|
||||
<span className="rounded-md bg-danger px-sm py-xs text-xs text-primary-foreground">
|
||||
<span className="rounded-md bg-destructive px-2 py-1 text-xs text-primary-foreground">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (type === "warning") {
|
||||
return (
|
||||
<span className="rounded-md bg-primary px-sm py-xs text-xs text-primary-foreground">
|
||||
<span className="rounded-md bg-primary px-2 py-1 text-xs text-primary-foreground">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="rounded-md bg-muted px-sm py-xs text-xs text-muted-foreground">
|
||||
<span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
@@ -58,26 +58,26 @@ export default function NotificationsWidget(
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border bg-card p-4">
|
||||
<section className="rounded-xl border bg-card p-4">
|
||||
<div className="flex">
|
||||
<h3 className="flex-1 text-heading-3 text-foreground">通知</h3>
|
||||
<h3 className="flex-1 text-lg font-semibold text-foreground">通知</h3>
|
||||
<span className="text-sm text-muted-foreground">共 {total} 条</span>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<p className="mt-sm text-sm text-muted-foreground">暂无通知</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">暂无通知</p>
|
||||
) : (
|
||||
<ul className="mt-sm space-y-sm">
|
||||
<ul className="mt-2 space-y-2">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="border-b border py-xs text-sm text-foreground"
|
||||
className="border-b border py-1 text-sm text-foreground"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex-1">{item.title}</span>
|
||||
<TypeBadge type={item.type} />
|
||||
</div>
|
||||
<p className="mt-xs text-xs text-muted-foreground">{item.body}</p>
|
||||
<p className="mt-xs text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-muted-foreground">{item.body}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{item.createdAt}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user