diff --git a/CLAUDE.md b/CLAUDE.md index 3a498ee..3dce32c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ pnpm dev 可以同时启动前后端,并且都会修改代码后自动重载 - +- pnpm docker_push 发布当前项目的docker镜像 ## 浏览器测试(vite-plugin-pilot) 已安装。`npx pilot run '代码'` 执行 JS(返回结果+日志+快照)、`npx pilot page` 页面状态 `npx pilot help` 查看pilot所有功能 diff --git a/package.json b/package.json index f4192c9..a2cf334 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 7614ae4..16eea6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { + + 原理:服务端根据 text 参数裁剪字体,只返回所需字符的子集。相同 URL 的请求会被浏览器自动缓存。 基础用法:将 CSS 复制到你的页面,修改 text 参数中的文字即可: @@ -249,149 +252,8 @@ function App() { }); <\/script>`} - - ); } -function UploadSection(props: { config: ServerConfig; onUploaded: () => void }) { - const [tempFile, set_tempFile] = createSignal(null); - const [adminFile, set_adminFile] = createSignal(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 ( - - - 上传字体 - - - {(m) => ( - - {m().text} - - )} - - - - - 临时上传 - - 最多保留 10 个文件,超出后自动删除最早上传的 - - - - 选择文件 - set_tempFile(e.target.files?.[0] ?? null)} - /> - - - {tempFile()?.name ?? "未选择文件"} - - - {uploading() ? "..." : "上传"} - - - - - - - - 管理员上传 - - 永久保存,需要 API Key 认证 - - set_adminApiKey(e.target.value)} - placeholder="API Key" - /> - - - 选择文件 - set_adminFile(e.target.files?.[0] ?? null)} - /> - - - {adminFile()?.name ?? "未选择文件"} - - - {uploading() ? "..." : "上传"} - - - - - - - ); -} - export default App; diff --git a/src/UploadSection.tsx b/src/UploadSection.tsx new file mode 100644 index 0000000..e252dc4 --- /dev/null +++ b/src/UploadSection.tsx @@ -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 ( + + + 选择文件 + e.target.files?.[0] && props.onFileSelect(e.target.files[0])} + /> + + + {props.fileName ?? "未选择文件"} + + + {props.uploading ? "..." : "上传"} + + + ); +} + +function useUpload(onSuccess: () => void) { + const [file, set_file] = createSignal(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 ( + + 游客上传 + + 临时文件,最多保留 10 个,超出后自动删除最早上传的 + + + + ); +} + +/** 管理员上传区域 */ +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 ( + + 管理员上传 + + 永久保存,需要 API Key 认证 + + props.onApiKeyInput(e.target.value)} + placeholder="API Key" + /> + + + ); +} + +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 ( + + + 上传字体 + + + {(m) => ( + + {m().text} + + )} + + + + temp.set_file(f)} + fileName={temp.file()?.name} + onUpload={() => temp.upload("temp")} + /> + + + + admin.set_apiKey(v)} + onFileSelect={(f) => admin.set_file(f)} + fileName={admin.file()?.name} + onUpload={() => admin.upload("admin", admin.apiKey())} + /> + + + + ); +} diff --git a/tsconfig.json b/tsconfig.json index f049896..28a48e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,6 @@ { "files": [], - "compilerOptions": { - "types": [ - "txikijs" - ] - }, + "compilerOptions": {}, "references": [ { "path": "./tsconfig.app.json" diff --git a/tsconfig.node.json b/tsconfig.node.json index cac66fd..51cc05e 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,9 +4,6 @@ "lib": [ "ES2023","DOM" ], - "typeRoots": [ - "node_modules/@txikijs/types" - ], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ diff --git a/tsup.config.ts b/tsup.config.ts index 53acdb3..97d52a0 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -7,6 +7,5 @@ export default defineConfig({ clean: true, bundle: true, noExternal: [/.*/], - external: ["@txikijs/types"], outDir: "dist_backend", });
原理:服务端根据 text 参数裁剪字体,只返回所需字符的子集。相同 URL 的请求会被浏览器自动缓存。
基础用法:将 CSS 复制到你的页面,修改 text 参数中的文字即可: