重构上传组件为独立文件,移除 txikijs 依赖,调整布局顺序

- 提取 UploadSection 为 src/UploadSection.tsx,复用 FileUploader 组件和 useUpload hook
- 移除 @txikijs/types 包及所有相关配置引用
- 布局顺序调整:选择字体 → 预览 → CSS代码 → 上传 → 原理/SDK文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
崮生(子虚) 2026-04-08 20:55:53 +08:00
parent 9da270733d
commit 3163e99534
7 changed files with 225 additions and 153 deletions

View File

@ -1,5 +1,5 @@
pnpm dev 可以同时启动前后端,并且都会修改代码后自动重载
- pnpm docker_push 发布当前项目的docker镜像
## 浏览器测试vite-plugin-pilot
已安装。`npx pilot run '代码'` 执行 JS返回结果+日志+快照)、`npx pilot page` 页面状态
`npx pilot help` 查看pilot所有功能

View File

@ -20,7 +20,6 @@
"web-streams-polyfill": "^4.2.0"
},
"devDependencies": {
"@txikijs/types": "^26.4.0",
"@types/node": "^25.5.2",
"tsup": "^8.5.1",
"typescript": "^6.0.2",

View File

@ -1,5 +1,6 @@
import { createMemo, createSignal, onMount, Show, For } from "solid-js";
import { fetchFonts, fetchConfig, uploadFont, type FontInfo, type ServerConfig } from "./api";
import { fetchFonts, fetchConfig, type FontInfo, type ServerConfig } from "./api";
import UploadSection from "./UploadSection";
const s = {
wrap: {
@ -228,6 +229,8 @@ function App() {
</section>
</Show>
<UploadSection config={serverConfig()} onUploaded={refreshFonts} />
<section style={{ ...s.section, "font-size": "12px", color: "#aaa", "line-height": "1.8" }}>
<p><b></b> text URL </p>
<p><b></b> CSS text </p>
@ -249,149 +252,8 @@ function App() {
});
<\/script>`}</pre>
</section>
<UploadSection config={serverConfig()} onUploaded={refreshFonts} />
</div>
);
}
function UploadSection(props: { config: ServerConfig; onUploaded: () => void }) {
const [tempFile, set_tempFile] = createSignal<File | null>(null);
const [adminFile, set_adminFile] = createSignal<File | null>(null);
const [adminApiKey, set_adminApiKey] = createSignal("");
const [uploading, set_uploading] = createSignal(false);
const [msg, set_msg] = createSignal<{ ok: boolean; text: string } | null>(null);
function showMsg(ok: boolean, text: string) {
set_msg({ ok, text });
setTimeout(() => set_msg(null), 3000);
}
async function handleTempUpload() {
const file = tempFile();
if (!file) return;
set_uploading(true);
const result = await uploadFont(file, "temp");
set_uploading(false);
if (result.success) {
showMsg(true, "上传成功");
set_tempFile(null);
props.onUploaded();
} else {
showMsg(false, result.error ?? "上传失败");
}
}
async function handleAdminUpload() {
const file = adminFile();
if (!file) return;
set_uploading(true);
const result = await uploadFont(file, "admin", adminApiKey());
set_uploading(false);
if (result.success) {
showMsg(true, "上传成功");
set_adminFile(null);
props.onUploaded();
} else {
showMsg(false, result.error ?? "上传失败");
}
}
const canUpload = () => props.config.enableTempUpload || props.config.adminUploadEnabled;
return (
<Show when={canUpload()}>
<section style={s.section}>
<label style={{ ...s.label, "font-size": "14px", "font-weight": 500, "margin-bottom": "12px" }}></label>
<Show when={msg()}>
{(m) => (
<div
style={{
padding: "8px 12px",
"margin-bottom": "12px",
"border-radius": "6px",
"font-size": "13px",
background: m().ok ? "#f0faf0" : "#fef2f2",
color: m().ok ? "#166534" : "#b91c1c",
border: `1px solid ${m().ok ? "#bbf7d0" : "#fecaca"}`,
}}
>
{m().text}
</div>
)}
</Show>
<Show when={props.config.enableTempUpload}>
<div style={s.card}>
<div style={{ "font-size": "14px", "font-weight": 500, "margin-bottom": "4px" }}></div>
<div style={{ "font-size": "12px", color: "#999", "margin-bottom": "12px" }}>
10
</div>
<div style={{ display: "flex", "gap": "8px", "align-items": "center" }}>
<label style={{ ...s.btn, display: "inline-flex", "align-items": "center", cursor: "pointer" }}>
<input
type="file"
accept=".ttf,.otf,.woff,.woff2"
style={{ display: "none" }}
onChange={(e) => set_tempFile(e.target.files?.[0] ?? null)}
/>
</label>
<span style={{ "font-size": "13px", color: "#666" }}>
{tempFile()?.name ?? "未选择文件"}
</span>
<button
style={{ ...s.btn, opacity: tempFile() && !uploading() ? 1 : 0.5, cursor: tempFile() && !uploading() ? "pointer" : "not-allowed" }}
disabled={!tempFile() || uploading()}
onClick={handleTempUpload}
>
{uploading() ? "..." : "上传"}
</button>
</div>
</div>
</Show>
<Show when={props.config.adminUploadEnabled}>
<div style={s.card}>
<div style={{ "font-size": "14px", "font-weight": 500, "margin-bottom": "4px" }}></div>
<div style={{ "font-size": "12px", color: "#999", "margin-bottom": "12px" }}>
API Key
</div>
<input
type="text"
autocomplete="off"
style={{ ...s.input, width: "100%", "margin-bottom": "10px" }}
value={adminApiKey()}
onInput={(e) => set_adminApiKey(e.target.value)}
placeholder="API Key"
/>
<div style={{ display: "flex", "gap": "8px", "align-items": "center" }}>
<label style={{ ...s.btn, display: "inline-flex", "align-items": "center", cursor: "pointer" }}>
<input
type="file"
accept=".ttf,.otf,.woff,.woff2"
style={{ display: "none" }}
onChange={(e) => set_adminFile(e.target.files?.[0] ?? null)}
/>
</label>
<span style={{ "font-size": "13px", color: "#666" }}>
{adminFile()?.name ?? "未选择文件"}
</span>
<button
style={{ ...s.btn, opacity: adminFile() && adminApiKey() && !uploading() ? 1 : 0.5, cursor: adminFile() && adminApiKey() && !uploading() ? "pointer" : "not-allowed" }}
disabled={!adminFile() || !adminApiKey() || uploading()}
onClick={handleAdminUpload}
>
{uploading() ? "..." : "上传"}
</button>
</div>
</div>
</Show>
</section>
</Show>
);
}
export default App;

219
src/UploadSection.tsx Normal file
View File

@ -0,0 +1,219 @@
import { createSignal, Show } from "solid-js";
import { uploadFont, type UploadResult, type ServerConfig } from "./api";
const ACCEPT = ".ttf,.otf,.woff,.woff2";
const btn = {
padding: "6px 20px",
"font-size": "14px",
border: "1px solid #d9d9d9",
"border-radius": "6px",
cursor: "pointer",
background: "#fff",
color: "#333",
} as const;
const card = {
padding: "16px",
border: "1px solid #e8e8e8",
"border-radius": "8px",
"margin-bottom": "16px",
} as const;
const label = {
display: "block",
"font-size": "13px",
color: "#555",
"margin-bottom": "6px",
} as const;
const input = {
padding: "6px 12px",
"font-size": "14px",
border: "1px solid #d9d9d9",
"border-radius": "6px",
outline: "none",
"box-sizing": "border-box",
} as const;
const section = {
"margin-bottom": "28px",
} as const;
/** 通用文件上传行:选择文件 + 文件名 + 上传按钮 */
function FileUploader(props: {
accept?: string;
disabled?: boolean;
uploading?: boolean;
onFileSelect: (file: File) => void;
fileName: string | undefined;
onUpload: () => void;
}) {
return (
<div style={{ display: "flex", "gap": "8px", "align-items": "center" }}>
<label style={{ ...btn, display: "inline-flex", "align-items": "center", cursor: "pointer" }}>
<input
type="file"
accept={props.accept ?? ACCEPT}
style={{ display: "none" }}
onChange={(e) => e.target.files?.[0] && props.onFileSelect(e.target.files[0])}
/>
</label>
<span style={{ "font-size": "13px", color: "#666" }}>
{props.fileName ?? "未选择文件"}
</span>
<button
style={{ ...btn, opacity: !props.disabled && !props.uploading ? 1 : 0.5, cursor: !props.disabled && !props.uploading ? "pointer" : "not-allowed" }}
disabled={props.disabled || props.uploading}
onClick={props.onUpload}
>
{props.uploading ? "..." : "上传"}
</button>
</div>
);
}
function useUpload(onSuccess: () => void) {
const [file, set_file] = createSignal<File | null>(null);
const [apiKey, set_apiKey] = createSignal("");
const [uploading, set_uploading] = createSignal(false);
const [msg, set_msg] = createSignal<{ ok: boolean; text: string } | null>(null);
function showMsg(ok: boolean, text: string) {
set_msg({ ok, text });
setTimeout(() => set_msg(null), 3000);
}
async function upload(mode: "temp" | "admin", key?: string) {
const f = file();
if (!f) return;
set_uploading(true);
const result: UploadResult = await uploadFont(f, mode, key);
set_uploading(false);
if (result.success) {
showMsg(true, "上传成功");
set_file(null);
onSuccess();
} else {
showMsg(false, result.error ?? "上传失败");
}
}
return { file, set_file, apiKey, set_apiKey, uploading, msg, showMsg, upload };
}
/** 游客上传区域 */
function TempUploadCard(props: {
uploading: boolean;
msg: { ok: boolean; text: string } | null;
onFileSelect: (file: File) => void;
fileName: string | undefined;
onUpload: () => void;
}) {
return (
<div style={card}>
<div style={{ "font-size": "14px", "font-weight": 500, "margin-bottom": "4px" }}></div>
<div style={{ "font-size": "12px", color: "#999", "margin-bottom": "12px" }}>
10
</div>
<FileUploader
disabled={!props.fileName}
uploading={props.uploading}
onFileSelect={props.onFileSelect}
fileName={props.fileName}
onUpload={props.onUpload}
/>
</div>
);
}
/** 管理员上传区域 */
function AdminUploadCard(props: {
uploading: boolean;
msg: { ok: boolean; text: string } | null;
apiKey: string;
onApiKeyInput: (value: string) => void;
onFileSelect: (file: File) => void;
fileName: string | undefined;
onUpload: () => void;
}) {
return (
<div style={card}>
<div style={{ "font-size": "14px", "font-weight": 500, "margin-bottom": "4px" }}></div>
<div style={{ "font-size": "12px", color: "#999", "margin-bottom": "12px" }}>
API Key
</div>
<input
type="text"
autocomplete="off"
style={{ ...input, width: "100%", "margin-bottom": "10px" }}
value={props.apiKey}
onInput={(e) => props.onApiKeyInput(e.target.value)}
placeholder="API Key"
/>
<FileUploader
disabled={!props.fileName || !props.apiKey}
uploading={props.uploading}
onFileSelect={props.onFileSelect}
fileName={props.fileName}
onUpload={props.onUpload}
/>
</div>
);
}
export default function UploadSection(props: { config: ServerConfig; onUploaded: () => void }) {
const temp = useUpload(props.onUploaded);
const admin = useUpload(props.onUploaded);
const canUpload = () => props.config.enableTempUpload || props.config.adminUploadEnabled;
return (
<Show when={canUpload()}>
<section style={section}>
<label style={{ ...label, "font-size": "14px", "font-weight": 500, "margin-bottom": "12px" }}></label>
<Show when={temp.msg()}>
{(m) => (
<div
style={{
padding: "8px 12px",
"margin-bottom": "12px",
"border-radius": "6px",
"font-size": "13px",
background: m().ok ? "#f0faf0" : "#fef2f2",
color: m().ok ? "#166534" : "#b91c1c",
border: `1px solid ${m().ok ? "#bbf7d0" : "#fecaca"}`,
}}
>
{m().text}
</div>
)}
</Show>
<Show when={props.config.enableTempUpload}>
<TempUploadCard
uploading={temp.uploading()}
msg={temp.msg()}
onFileSelect={(f) => temp.set_file(f)}
fileName={temp.file()?.name}
onUpload={() => temp.upload("temp")}
/>
</Show>
<Show when={props.config.adminUploadEnabled}>
<AdminUploadCard
uploading={admin.uploading()}
msg={admin.msg()}
apiKey={admin.apiKey()}
onApiKeyInput={(v) => admin.set_apiKey(v)}
onFileSelect={(f) => admin.set_file(f)}
fileName={admin.file()?.name}
onUpload={() => admin.upload("admin", admin.apiKey())}
/>
</Show>
</section>
</Show>
);
}

View File

@ -1,10 +1,6 @@
{
"files": [],
"compilerOptions": {
"types": [
"txikijs"
]
},
"compilerOptions": {},
"references": [
{
"path": "./tsconfig.app.json"

View File

@ -4,9 +4,6 @@
"lib": [
"ES2023","DOM"
],
"typeRoots": [
"node_modules/@txikijs/types"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */

View File

@ -7,6 +7,5 @@ export default defineConfig({
clean: true,
bundle: true,
noExternal: [/.*/],
external: ["@txikijs/types"],
outDir: "dist_backend",
});