v0.11.0 后台 CRM 化:媒体上传/单页面 Elementor/修订/分类标签/菜单/用户/审计/设置/Dashboard v2 + dusuniot-cms MCP 服务器

This commit is contained in:
bigcode
2026-11-04 06:10:04 +08:00
parent 2251a25e12
commit b16c33c08b
15 changed files with 2053 additions and 152 deletions
+40 -12
View File
@@ -3,11 +3,11 @@
* 表格 + 缩略图预览;编辑替代文本(alt)与标题。
*/
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { App, Button, Card, Form, Image, Input, Modal, Space, Table, Typography } from "antd";
import { EditOutlined, SearchOutlined } from "@ant-design/icons";
import { App, Button, Card, Form, Image, Input, Modal, Space, Table, Typography, Upload } from "antd";
import { EditOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
import { File as FileIcon } from "lucide-react";
import { useState } from "react";
import { adminListMedia, adminUpdateMedia } from "~/server/admin";
import { adminListMedia, adminUpdateMedia, adminUploadMedia } from "~/server/admin";
import { AdminEmpty, Forbidden, PageHeader, fmtDateTime, guardAdmin, tablePagination, type AdminSearch } from "~/components/admin/common";
import type { media } from "~/db/schema";
@@ -74,15 +74,43 @@ function MediaPage() {
<PageHeader
title="媒体库"
extra={
<Input
allowClear
placeholder="搜索文件路径"
prefix={<SearchOutlined aria-hidden />}
value={q}
onChange={(e) => setQ(e.target.value)}
onPressEnter={() => updateSearch({ q })}
style={{ width: 240 }}
/>
<Space>
<Input
allowClear
placeholder="搜索文件路径"
prefix={<SearchOutlined aria-hidden />}
value={q}
onChange={(e) => setQ(e.target.value)}
onPressEnter={() => updateSearch({ q })}
style={{ width: 240 }}
/>
{/* 上传走 base64 JSON(server fn RPC),单文件 ≤20MB;成功后刷新列表 */}
<Upload
multiple
showUploadList={false}
customRequest={async ({ file, onSuccess, onError }) => {
try {
const f = file as File;
const buf = new Uint8Array(await f.arrayBuffer());
// 分块 btoa:直接 btoa(二进制串) 在大文件下栈溢出,按 0x8000 分块
let bin = "";
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})`);
onSuccess?.(res);
await router.invalidate();
} catch (e) {
message.error(e instanceof Error ? e.message : "上传失败");
onError?.(e as Error);
}
}}
>
<Button type="primary" icon={<UploadOutlined aria-hidden />}>
上传媒体
</Button>
</Upload>
</Space>
}
/>
<Table