v0.16.0: 后台体验升级——i18n(默认英文)/浅深主题/字号放大/关站维护页/个人设置(头像·语言·主题·密码·MCP Token)/权限管理(菜单+MCP·API)/询盘抽屉翻倍+PHP序列化数据清洗(22.7万行)+去裸JSON
This commit is contained in:
+22
-20
@@ -8,6 +8,7 @@ import { EditOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons"
|
||||
import { File as FileIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { adminListMedia, adminUpdateMedia, adminUploadMedia } from "~/server/admin";
|
||||
import { useAdminT } from "~/admin-i18n";
|
||||
import { AdminEmpty, Forbidden, PageHeader, fmtDateTime, guardAdmin, tablePagination, type AdminSearch } from "~/components/admin/common";
|
||||
import type { media } from "~/db/schema";
|
||||
|
||||
@@ -30,6 +31,7 @@ type MediaRow = typeof media.$inferSelect;
|
||||
|
||||
/** # 函数 媒体库页面组件 */
|
||||
function MediaPage() {
|
||||
const t = useAdminT();
|
||||
const data = Route.useLoaderData();
|
||||
const search = Route.useSearch();
|
||||
const navigate = Route.useNavigate();
|
||||
@@ -57,7 +59,7 @@ function MediaPage() {
|
||||
async function handleSave(values: { altText?: string; title?: string }) {
|
||||
if (!editing) return;
|
||||
await adminUpdateMedia({ data: { id: editing.id, altText: values.altText, title: values.title } });
|
||||
message.success("已保存");
|
||||
message.success(t("已保存"));
|
||||
setEditing(null);
|
||||
await router.invalidate();
|
||||
}
|
||||
@@ -72,12 +74,12 @@ function MediaPage() {
|
||||
return (
|
||||
<Card size="small">
|
||||
<PageHeader
|
||||
title="媒体库"
|
||||
title={t("媒体库")}
|
||||
extra={
|
||||
<Space>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="搜索文件路径"
|
||||
placeholder={t("搜索文件路径")}
|
||||
prefix={<SearchOutlined aria-hidden />}
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
@@ -97,17 +99,17 @@ function MediaPage() {
|
||||
const CHUNK = 0x8000;
|
||||
for (let i = 0; i < buf.length; i += CHUNK) bin += String.fromCharCode(...buf.subarray(i, i + CHUNK));
|
||||
const res = await adminUploadMedia({ data: { filename: f.name, mime: f.type || "", dataBase64: btoa(bin) } });
|
||||
message.success(`已上传 ${f.name}(#${res.id})`);
|
||||
message.success(t("已上传 {name}(#{id})", { name: f.name, id: res.id }));
|
||||
onSuccess?.(res);
|
||||
await router.invalidate();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : "上传失败");
|
||||
message.error(e instanceof Error ? e.message : t("上传失败"));
|
||||
onError?.(e as Error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" icon={<UploadOutlined aria-hidden />}>
|
||||
上传媒体
|
||||
{t("上传媒体")}
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
@@ -124,18 +126,18 @@ function MediaPage() {
|
||||
columns={[
|
||||
{ title: "ID", dataIndex: "id", width: 70 },
|
||||
{
|
||||
title: "预览",
|
||||
title: t("预览"),
|
||||
dataIndex: "url",
|
||||
width: 88,
|
||||
render: (v: string, r) =>
|
||||
r.mimeType.startsWith("image") ? (
|
||||
<Image src={v} alt={r.altText || r.path} width={56} height={56} style={{ objectFit: "cover", borderRadius: 4 }} />
|
||||
) : (
|
||||
<FileIcon size={36} aria-hidden color="#bfbfbf" />
|
||||
<FileIcon size={36} aria-hidden color="var(--ant-color-text-quaternary)" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "路径",
|
||||
title: t("路径"),
|
||||
dataIndex: "path",
|
||||
render: (v: string, r) => (
|
||||
<Typography.Link href={r.url} target="_blank" style={{ wordBreak: "break-all" }}>
|
||||
@@ -143,29 +145,29 @@ function MediaPage() {
|
||||
</Typography.Link>
|
||||
),
|
||||
},
|
||||
{ title: "类型", dataIndex: "mimeType", width: 140 },
|
||||
{ title: "尺寸", key: "dim", width: 110, render: (_, r) => (r.width ? `${r.width} x ${r.height}` : "-") },
|
||||
{ title: "大小", dataIndex: "size", width: 90, render: (v: number | null) => fmtSize(v) },
|
||||
{ title: "引用", dataIndex: "refCount", width: 70 },
|
||||
{ title: "替代文本", dataIndex: "altText", ellipsis: true },
|
||||
{ title: "上传时间", dataIndex: "createdAt", width: 160, render: (v: string) => fmtDateTime(v) },
|
||||
{ title: t("类型"), dataIndex: "mimeType", width: 140 },
|
||||
{ title: t("尺寸"), key: "dim", width: 110, render: (_, r) => (r.width ? `${r.width} x ${r.height}` : "-") },
|
||||
{ title: t("大小"), dataIndex: "size", width: 90, render: (v: number | null) => fmtSize(v) },
|
||||
{ title: t("引用数"), dataIndex: "refCount", width: 70 },
|
||||
{ title: t("替代文本"), dataIndex: "altText", ellipsis: true },
|
||||
{ title: t("上传时间"), dataIndex: "createdAt", width: 160, render: (v: string) => fmtDateTime(v) },
|
||||
{
|
||||
title: "操作",
|
||||
title: t("操作"),
|
||||
key: "actions",
|
||||
width: 70,
|
||||
render: (_, r) => (
|
||||
<Button size="small" type="text" icon={<EditOutlined aria-label="编辑" />} onClick={() => openEdit(r)} />
|
||||
<Button size="small" type="text" icon={<EditOutlined aria-label={t("编辑")} />} onClick={() => openEdit(r)} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal title={`编辑媒体 #${editing?.id ?? ""}`} open={editing !== null} onCancel={() => setEditing(null)} onOk={() => form.submit()} destroyOnHidden>
|
||||
<Modal title={t("编辑媒体 #{id}", { id: editing?.id ?? "" })} open={editing !== null} onCancel={() => setEditing(null)} onOk={() => form.submit()} destroyOnHidden>
|
||||
<Form form={form} layout="vertical" onFinish={(v) => void handleSave(v)}>
|
||||
<Form.Item name="altText" label="替代文本(alt)">
|
||||
<Form.Item name="altText" label={t("替代文本(alt)")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="title" label="标题">
|
||||
<Form.Item name="title" label={t("标题")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
Reference in New Issue
Block a user