web-font/src/UploadSection.vue
崮生(子虚) b4a7a820eb feat: AI Typography Skill 系统 + 中英双语 i18n + Before/After Demo
- 新增 skills/chinese-web-typography.md 作为提供给 AI agent 的排版智能文件
- 重写 README.md(中文默认),新增 README.en.md(英文版),互相引用
- 新增 TypographyDemo.vue 前后对比组件,展示中文字体效果差异
- 新增 src/i18n.ts 轻量国际化方案,所有组件文案支持中英切换
- 后端 /api/fonts 返回 temporary 字段标识临时字体
- 升级至 v1.8.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:41:47 +08:00

118 lines
5.2 KiB
Vue

<script setup lang="ts">
import { ref, computed } from "vue";
import { uploadFont, type UploadResult, type ServerConfig } from "./api";
import { t } from "./i18n";
const ACCEPT = ".ttf,.otf,.woff,.woff2";
const props = defineProps<{
config: ServerConfig;
onUploaded: () => void;
}>();
function useUpload(onSuccess: () => void) {
const file = ref<File | null>(null);
const apiKey = ref("");
const uploading = ref(false);
const msg = ref<{ ok: boolean; text: string } | null>(null);
function showMsg(ok: boolean, text: string) {
msg.value = { ok, text };
setTimeout(() => { msg.value = null; }, 3000);
}
async function upload(mode: "temp" | "admin", key?: string) {
const f = file.value;
if (!f) return;
uploading.value = true;
const result: UploadResult = await uploadFont(f, mode, key);
uploading.value = false;
if (result.success) {
showMsg(true, t("uploadSuccess"));
file.value = null;
onSuccess();
} else {
showMsg(false, result.error ?? t("uploadFailed"));
}
}
return { file, apiKey, uploading, msg, upload };
}
const temp = useUpload(() => props.onUploaded());
const admin = useUpload(() => props.onUploaded());
const canUpload = computed(() => props.config.enableTempUpload || props.config.adminUploadEnabled);
function onFileSelect(e: Event, target: ReturnType<typeof useUpload>) {
const f = (e.target as HTMLInputElement).files?.[0];
if (f) target.file.value = f;
}
</script>
<template>
<section v-if="canUpload" style="margin-bottom: 28px">
<label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 12px">{{ t('uploadFont') }}</label>
<div style="font-size: 12px; color: #e6a700; margin-bottom: 12px">{{ t('uploadTip') }}</div>
<div
v-if="temp.msg.value"
:style="{
padding: '8px 12px',
marginBottom: '12px',
borderRadius: '6px',
fontSize: '13px',
background: temp.msg.value.ok ? '#f0faf0' : '#fef2f2',
color: temp.msg.value.ok ? '#166534' : '#b91c1c',
border: `1px solid ${temp.msg.value.ok ? '#bbf7d0' : '#fecaca'}`,
}"
>
{{ temp.msg.value.text }}
</div>
<div v-if="config.enableTempUpload" style="padding: 16px; border: 1px solid #e8e8e8; border-radius: 8px; margin-bottom: 16px">
<div style="font-size: 14px; font-weight: 500; margin-bottom: 4px">{{ t('guestUpload') }}</div>
<div style="font-size: 12px; color: #999; margin-bottom: 12px">{{ t('guestUploadDesc') }}</div>
<div style="display: flex; gap: 8px; align-items: center">
<label style="padding: 6px 20px; font-size: 14px; border: 1px solid #d9d9d9; border-radius: 6px; cursor: pointer; background: #fff; color: #333; display: inline-flex; align-items: center">
{{ t('selectFile') }}
<input type="file" :accept="ACCEPT" style="display: none" @change="onFileSelect($event, temp)" />
</label>
<span style="font-size: 13px; color: #666">{{ temp.file.value?.name ?? t('noFile') }}</span>
<button
:disabled="!temp.file.value || temp.uploading.value"
:style="{ padding: '6px 20px', fontSize: '14px', border: '1px solid #d9d9d9', borderRadius: '6px', cursor: temp.file.value && !temp.uploading.value ? 'pointer' : 'not-allowed', background: '#fff', color: '#333', opacity: temp.file.value && !temp.uploading.value ? 1 : 0.5 }"
@click="temp.upload('temp')"
>
{{ temp.uploading.value ? '...' : t('upload') }}
</button>
</div>
</div>
<div v-if="config.adminUploadEnabled" style="padding: 16px; border: 1px solid #e8e8e8; border-radius: 8px; margin-bottom: 16px">
<div style="font-size: 14px; font-weight: 500; margin-bottom: 4px">{{ t('adminUpload') }}</div>
<div style="font-size: 12px; color: #999; margin-bottom: 12px">{{ t('adminUploadDesc') }}</div>
<input
type="text"
autocomplete="off"
v-model="admin.apiKey.value"
placeholder="API Key"
style="padding: 6px 12px; font-size: 14px; border: 1px solid #d9d9d9; border-radius: 6px; outline: none; box-sizing: border-box; width: 100%; margin-bottom: 10px"
/>
<div style="display: flex; gap: 8px; align-items: center">
<label style="padding: 6px 20px; font-size: 14px; border: 1px solid #d9d9d9; border-radius: 6px; cursor: pointer; background: #fff; color: #333; display: inline-flex; align-items: center">
{{ t('selectFile') }}
<input type="file" :accept="ACCEPT" style="display: none" @change="onFileSelect($event, admin)" />
</label>
<span style="font-size: 13px; color: #666">{{ admin.file.value?.name ?? t('noFile') }}</span>
<button
:disabled="!admin.file.value || !admin.apiKey.value || admin.uploading.value"
:style="{ padding: '6px 20px', fontSize: '14px', border: '1px solid #d9d9d9', borderRadius: '6px', cursor: admin.file.value && admin.apiKey.value && !admin.uploading.value ? 'pointer' : 'not-allowed', background: '#fff', color: '#333', opacity: admin.file.value && admin.apiKey.value && !admin.uploading.value ? 1 : 0.5 }"
@click="admin.upload('admin', admin.apiKey.value)"
>
{{ admin.uploading.value ? '...' : t('upload') }}
</button>
</div>
</div>
</section>
</template>