mirror of
https://github.com/2234839/web-font.git
synced 2026-06-04 00:08:11 +08:00
- 新增 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>
118 lines
5.2 KiB
Vue
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>
|