mirror of
https://github.com/2234839/web-font.git
synced 2026-04-29 21:00:45 +08:00
重构上传组件为独立文件,移除 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:
parent
9da270733d
commit
3163e99534
@ -1,5 +1,5 @@
|
||||
pnpm dev 可以同时启动前后端,并且都会修改代码后自动重载
|
||||
|
||||
- pnpm docker_push 发布当前项目的docker镜像
|
||||
## 浏览器测试(vite-plugin-pilot)
|
||||
已安装。`npx pilot run '代码'` 执行 JS(返回结果+日志+快照)、`npx pilot page` 页面状态
|
||||
`npx pilot help` 查看pilot所有功能
|
||||
|
||||
@ -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",
|
||||
|
||||
146
src/App.tsx
146
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() {
|
||||
</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
219
src/UploadSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"txikijs"
|
||||
]
|
||||
},
|
||||
"compilerOptions": {},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
|
||||
@ -4,9 +4,6 @@
|
||||
"lib": [
|
||||
"ES2023","DOM"
|
||||
],
|
||||
"typeRoots": [
|
||||
"node_modules/@txikijs/types"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
|
||||
@ -7,6 +7,5 @@ export default defineConfig({
|
||||
clean: true,
|
||||
bundle: true,
|
||||
noExternal: [/.*/],
|
||||
external: ["@txikijs/types"],
|
||||
outDir: "dist_backend",
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user