update at 2026-02-09 16:09:44
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
`apiserver/` 提供微信小程序用的远端渲染接口:
|
`apiserver/` 提供微信小程序用的远端渲染接口:
|
||||||
- 小程序只上传参数(字体 ID、文字、字号、颜色等)
|
- 小程序只上传参数(字体 ID、文字、字号、颜色等)
|
||||||
- 服务端读取 `fonts.json` + `fonts/`,生成 SVG/PNG 后返回
|
- 服务端读取字体清单 + 字体目录,生成 SVG/PNG 后返回
|
||||||
|
|
||||||
## 1. 启动
|
## 1. 启动
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ python3 apiserver/server.py \
|
|||||||
|
|
||||||
其中 `--static-root` 目录必须包含:
|
其中 `--static-root` 目录必须包含:
|
||||||
|
|
||||||
- `fonts.json`
|
- `fonts/`(统一字体目录)
|
||||||
- `fonts/`(字体文件目录)
|
- `miniprogram/assets/fonts.json`(小程序清单)
|
||||||
|
|
||||||
如果不传 `--manifest`,默认读取 `<static-root>/fonts.json`。
|
如果不传 `--manifest`,默认优先读取 `<static-root>/miniprogram/assets/fonts.json`,不存在时回退到 `<static-root>/fonts.json`。
|
||||||
|
|
||||||
## 2. API
|
## 2. API
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ python3 apiserver/server.py \
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"fontId": "其他字体/AlimamaDaoLiTi",
|
"fontId": "0001",
|
||||||
"text": "星程字体转换",
|
"text": "星程字体转换",
|
||||||
"fontSize": 120,
|
"fontSize": 120,
|
||||||
"fillColor": "#000000",
|
"fillColor": "#000000",
|
||||||
@@ -54,7 +54,7 @@ python3 apiserver/server.py \
|
|||||||
{
|
{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": {
|
"data": {
|
||||||
"fontId": "其他字体/AlimamaDaoLiTi",
|
"fontId": "0001",
|
||||||
"fontName": "AlimamaDaoLiTi",
|
"fontName": "AlimamaDaoLiTi",
|
||||||
"width": 956.2,
|
"width": 956.2,
|
||||||
"height": 144.3,
|
"height": 144.3,
|
||||||
@@ -100,7 +100,7 @@ sudo systemctl status font2svg-api
|
|||||||
|
|
||||||
## 5. 约束
|
## 5. 约束
|
||||||
|
|
||||||
- 字体解析完全基于 `fonts.json`,`fontId` 必须存在。
|
- 字体解析完全基于字体清单(默认 `miniprogram/assets/fonts.json`),字体文件统一从 `<static-root>/fonts/` 读取,`fontId` 必须存在。
|
||||||
- 服务端启用 CORS,允许小程序访问。
|
- 服务端启用 CORS,允许小程序访问。
|
||||||
- 不依赖 Flask/FastAPI,使用 Python 标准库 HTTP 服务。
|
- 不依赖 Flask/FastAPI,使用 Python 标准库 HTTP 服务。
|
||||||
- `/api/render-png` 依赖 `node + sharp`(使用 `apiserver/svg_to_png.js` 转换)。
|
- `/api/render-png` 依赖 `node + sharp`(使用 `apiserver/svg_to_png.js` 转换)。
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -164,12 +165,29 @@ class FontCatalog:
|
|||||||
class RenderHandler(BaseHTTPRequestHandler):
|
class RenderHandler(BaseHTTPRequestHandler):
|
||||||
catalog = None
|
catalog = None
|
||||||
|
|
||||||
|
def send_response(self, code, message=None):
|
||||||
|
self._response_status = code
|
||||||
|
super().send_response(code, message)
|
||||||
|
|
||||||
def _set_cors_headers(self):
|
def _set_cors_headers(self):
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
||||||
self.send_header("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
self.send_header("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||||
self.send_header("Access-Control-Max-Age", "86400")
|
self.send_header("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
|
def _log_request_timing(self, start_time):
|
||||||
|
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||||
|
status = getattr(self, "_response_status", "-")
|
||||||
|
client_ip = self.client_address[0] if self.client_address else "-"
|
||||||
|
LOGGER.info(
|
||||||
|
"请求完成 method=%s path=%s status=%s duration_ms=%.2f client=%s",
|
||||||
|
self.command,
|
||||||
|
self.path,
|
||||||
|
status,
|
||||||
|
elapsed_ms,
|
||||||
|
client_ip,
|
||||||
|
)
|
||||||
|
|
||||||
def _send_json(self, status_code, payload):
|
def _send_json(self, status_code, payload):
|
||||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
self.send_response(status_code)
|
self.send_response(status_code)
|
||||||
@@ -240,69 +258,84 @@ class RenderHandler(BaseHTTPRequestHandler):
|
|||||||
return font_info, result
|
return font_info, result
|
||||||
|
|
||||||
def do_OPTIONS(self): # noqa: N802
|
def do_OPTIONS(self): # noqa: N802
|
||||||
self.send_response(204)
|
start_time = time.perf_counter()
|
||||||
self._set_cors_headers()
|
self._response_status = None
|
||||||
self.end_headers()
|
try:
|
||||||
|
self.send_response(204)
|
||||||
|
self._set_cors_headers()
|
||||||
|
self.end_headers()
|
||||||
|
finally:
|
||||||
|
self._log_request_timing(start_time)
|
||||||
|
|
||||||
def do_GET(self): # noqa: N802
|
def do_GET(self): # noqa: N802
|
||||||
parsed = urlparse(self.path)
|
start_time = time.perf_counter()
|
||||||
if parsed.path == "/healthz":
|
self._response_status = None
|
||||||
try:
|
try:
|
||||||
data = self.catalog.health()
|
parsed = urlparse(self.path)
|
||||||
self._send_json(200, {"ok": True, "data": data})
|
if parsed.path == "/healthz":
|
||||||
except Exception as error:
|
try:
|
||||||
LOGGER.exception("健康检查失败")
|
data = self.catalog.health()
|
||||||
self._send_json(500, {"ok": False, "error": str(error)})
|
self._send_json(200, {"ok": True, "data": data})
|
||||||
return
|
except Exception as error:
|
||||||
|
LOGGER.exception("健康检查失败")
|
||||||
|
self._send_json(500, {"ok": False, "error": str(error)})
|
||||||
|
return
|
||||||
|
|
||||||
self._send_json(404, {"ok": False, "error": "Not Found"})
|
self._send_json(404, {"ok": False, "error": "Not Found"})
|
||||||
|
finally:
|
||||||
|
self._log_request_timing(start_time)
|
||||||
|
|
||||||
def do_POST(self): # noqa: N802
|
def do_POST(self): # noqa: N802
|
||||||
parsed = urlparse(self.path)
|
start_time = time.perf_counter()
|
||||||
if parsed.path not in ("/api/render-svg", "/api/render-png"):
|
self._response_status = None
|
||||||
self._send_json(404, {"ok": False, "error": "Not Found"})
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
render_params = self._parse_render_payload()
|
parsed = urlparse(self.path)
|
||||||
font_info, result = self._render_svg_core(render_params)
|
if parsed.path not in ("/api/render-svg", "/api/render-png"):
|
||||||
except KeyError as error:
|
self._send_json(404, {"ok": False, "error": "Not Found"})
|
||||||
self._send_json(404, {"ok": False, "error": str(error)})
|
return
|
||||||
return
|
|
||||||
except FileNotFoundError as error:
|
|
||||||
self._send_json(404, {"ok": False, "error": str(error)})
|
|
||||||
return
|
|
||||||
except ValueError as error:
|
|
||||||
self._send_json(400, {"ok": False, "error": str(error)})
|
|
||||||
return
|
|
||||||
except Exception as error:
|
|
||||||
LOGGER.exception("渲染失败")
|
|
||||||
self._send_json(500, {"ok": False, "error": str(error)})
|
|
||||||
return
|
|
||||||
|
|
||||||
if parsed.path == "/api/render-png":
|
|
||||||
try:
|
try:
|
||||||
png_bytes = render_png_from_svg(
|
render_params = self._parse_render_payload()
|
||||||
result["svg"],
|
font_info, result = self._render_svg_core(render_params)
|
||||||
result["width"],
|
except KeyError as error:
|
||||||
result["height"],
|
self._send_json(404, {"ok": False, "error": str(error)})
|
||||||
)
|
return
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
self._send_json(404, {"ok": False, "error": str(error)})
|
||||||
|
return
|
||||||
|
except ValueError as error:
|
||||||
|
self._send_json(400, {"ok": False, "error": str(error)})
|
||||||
|
return
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
LOGGER.exception("PNG 渲染失败")
|
LOGGER.exception("渲染失败")
|
||||||
self._send_json(500, {"ok": False, "error": str(error)})
|
self._send_json(500, {"ok": False, "error": str(error)})
|
||||||
return
|
return
|
||||||
|
|
||||||
self._send_binary(200, png_bytes, "image/png")
|
if parsed.path == "/api/render-png":
|
||||||
return
|
try:
|
||||||
|
png_bytes = render_png_from_svg(
|
||||||
|
result["svg"],
|
||||||
|
result["width"],
|
||||||
|
result["height"],
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
LOGGER.exception("PNG 渲染失败")
|
||||||
|
self._send_json(500, {"ok": False, "error": str(error)})
|
||||||
|
return
|
||||||
|
|
||||||
response_data = {
|
self._send_binary(200, png_bytes, "image/png")
|
||||||
"svg": result["svg"],
|
return
|
||||||
"width": result["width"],
|
|
||||||
"height": result["height"],
|
response_data = {
|
||||||
"fontName": result.get("fontName") or font_info.get("name") or "Unknown",
|
"svg": result["svg"],
|
||||||
"fontId": render_params["fontId"],
|
"width": result["width"],
|
||||||
}
|
"height": result["height"],
|
||||||
self._send_json(200, {"ok": True, "data": response_data})
|
"fontName": result.get("fontName") or font_info.get("name") or "Unknown",
|
||||||
|
"fontId": render_params["fontId"],
|
||||||
|
}
|
||||||
|
self._send_json(200, {"ok": True, "data": response_data})
|
||||||
|
finally:
|
||||||
|
self._log_request_timing(start_time)
|
||||||
|
|
||||||
def log_message(self, format_str, *args):
|
def log_message(self, format_str, *args):
|
||||||
LOGGER.info("%s - %s", self.address_string(), format_str % args)
|
LOGGER.info("%s - %s", self.address_string(), format_str % args)
|
||||||
@@ -321,12 +354,12 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--static-root",
|
"--static-root",
|
||||||
default=os.getenv("FONT2SVG_STATIC_ROOT", os.getcwd()),
|
default=os.getenv("FONT2SVG_STATIC_ROOT", os.getcwd()),
|
||||||
help="静态资源根目录(包含 fonts/ 与 fonts.json)",
|
help="静态资源根目录(包含 fonts/ 与配置清单)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--manifest",
|
"--manifest",
|
||||||
default=os.getenv("FONT2SVG_MANIFEST_PATH", ""),
|
default=os.getenv("FONT2SVG_MANIFEST_PATH", ""),
|
||||||
help="字体清单路径,默认使用 <static-root>/fonts.json",
|
help="字体清单路径,默认优先使用 <static-root>/miniprogram/assets/fonts.json,其次 <static-root>/fonts.json",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -336,7 +369,14 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
static_root = os.path.abspath(args.static_root)
|
static_root = os.path.abspath(args.static_root)
|
||||||
manifest_path = args.manifest.strip() or os.path.join(static_root, "fonts.json")
|
if args.manifest.strip():
|
||||||
|
manifest_path = args.manifest.strip()
|
||||||
|
else:
|
||||||
|
manifest_candidates = [
|
||||||
|
os.path.join(static_root, "miniprogram", "assets", "fonts.json"),
|
||||||
|
os.path.join(static_root, "fonts.json"),
|
||||||
|
]
|
||||||
|
manifest_path = next((item for item in manifest_candidates if os.path.isfile(item)), manifest_candidates[0])
|
||||||
|
|
||||||
server = build_server(
|
server = build_server(
|
||||||
host=args.host,
|
host=args.host,
|
||||||
|
|||||||
11
frontend/public/default.json
Normal file
11
frontend/public/default.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"inputText": "星程字体转换",
|
||||||
|
"fontSize": 50,
|
||||||
|
"textColor": "#dc2626",
|
||||||
|
"selectedFontIds": [
|
||||||
|
"0001"
|
||||||
|
],
|
||||||
|
"favoriteFontIds": [
|
||||||
|
"0001"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -17,10 +17,13 @@ miniprogram/
|
|||||||
├── pages/
|
├── pages/
|
||||||
│ ├── index/ # 首页:输入、预览、导出
|
│ ├── index/ # 首页:输入、预览、导出
|
||||||
│ └── font-picker/ # 字体选择页
|
│ └── font-picker/ # 字体选择页
|
||||||
|
├── config/
|
||||||
|
│ └── server.js # 远端地址/端口/API 路径统一配置
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── core/ # 纯算法模块
|
│ ├── core/ # 纯算法模块
|
||||||
│ └── mp/ # 小程序 API 适配层
|
│ └── mp/ # 小程序 API 适配层
|
||||||
├── assets/fonts.json # 字体清单(由脚本生成)
|
├── assets/fonts.json # 字体清单(由脚本生成)
|
||||||
|
├── assets/default.json # 首次加载默认配置(内容/颜色/字号/默认字体)
|
||||||
├── app.js / app.json / app.wxss
|
├── app.js / app.json / app.wxss
|
||||||
└── project.config.json
|
└── project.config.json
|
||||||
```
|
```
|
||||||
@@ -34,6 +37,19 @@ miniprogram/
|
|||||||
5. Nginx 配置 `/api/` 反向代理到渲染服务。
|
5. Nginx 配置 `/api/` 反向代理到渲染服务。
|
||||||
6. 编译运行。
|
6. 编译运行。
|
||||||
|
|
||||||
|
## 服务器配置(换服务器只改一处)
|
||||||
|
|
||||||
|
修改 `miniprogram/config/server.js` 中的 `SERVER_CONFIG`:
|
||||||
|
|
||||||
|
- `protocol`: `https` / `http`
|
||||||
|
- `host`: 服务器域名
|
||||||
|
- `port`: 端口(默认 443/80 可留空)
|
||||||
|
- `apiPrefix`: API 前缀(默认 `/api`)
|
||||||
|
- `fontsManifestPath`: 字体清单路径(默认 `/miniprogram/assets/fonts.json`)
|
||||||
|
- `defaultConfigPath`: 默认配置路径(默认 `/miniprogram/assets/default.json`)
|
||||||
|
|
||||||
|
`app.js` 和 API 调用会自动使用该配置生成完整 URL。
|
||||||
|
|
||||||
## 导出说明
|
## 导出说明
|
||||||
|
|
||||||
- `SVG`:受微信限制,`shareFileMessage` 需由单次点击直接触发,建议逐个字体导出。
|
- `SVG`:受微信限制,`shareFileMessage` 需由单次点击直接触发,建议逐个字体导出。
|
||||||
@@ -50,6 +66,29 @@ miniprogram/
|
|||||||
|
|
||||||
如果 `path` 是相对路径(例如 `/fonts/a.ttf`),服务端会根据静态根目录拼接到实际文件路径。
|
如果 `path` 是相对路径(例如 `/fonts/a.ttf`),服务端会根据静态根目录拼接到实际文件路径。
|
||||||
|
|
||||||
|
推荐部署结构:
|
||||||
|
- 字体目录统一放在服务器根目录:`/fonts/`
|
||||||
|
- Web 配置文件独立管理:`/fonts.json`(可选 `/default.json`)
|
||||||
|
- 小程序配置文件独立管理:`/miniprogram/assets/fonts.json`、`/miniprogram/assets/default.json`
|
||||||
|
|
||||||
|
## 首次默认配置(default.json)
|
||||||
|
|
||||||
|
- 默认配置文件与 `fonts.json` 同目录:由 `config/server.js` 自动拼接(默认是 `https://fonts.biboer.cn/miniprogram/assets/default.json`)
|
||||||
|
- 小程序会在首次加载时读取该配置(远端失败则回退本地 `miniprogram/assets/default.js`)
|
||||||
|
- 配置只在首次加载生效,后续始终使用用户本地已保存配置(选择、收藏、颜色、字号、内容)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"inputText": "星程字体转换",
|
||||||
|
"fontSize": 50,
|
||||||
|
"textColor": "#dc2626",
|
||||||
|
"selectedFontIds": ["0001"],
|
||||||
|
"favoriteFontIds": ["0001"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 调试命令(仓库根目录)
|
## 调试命令(仓库根目录)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
const { buildRuntimeConfig } = require('./config/server')
|
||||||
|
|
||||||
|
const runtimeConfig = buildRuntimeConfig()
|
||||||
|
|
||||||
App({
|
App({
|
||||||
globalData: {
|
globalData: {
|
||||||
fontsManifestUrl: 'https://fonts.biboer.cn/fonts.json',
|
...runtimeConfig,
|
||||||
fontsBaseUrl: 'https://fonts.biboer.cn',
|
|
||||||
svgRenderApiUrl: 'https://fonts.biboer.cn/api/render-svg',
|
|
||||||
apiTimeoutMs: 30000,
|
apiTimeoutMs: 30000,
|
||||||
fonts: null,
|
fonts: null,
|
||||||
|
defaultConfig: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
19
miniprogram/assets/default.js
Normal file
19
miniprogram/assets/default.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
inputText: '星程字体转换',
|
||||||
|
fontSize: 50,
|
||||||
|
textColor: '#dc2626',
|
||||||
|
selectedFontIds: [
|
||||||
|
'0001',
|
||||||
|
'0003',
|
||||||
|
'0006',
|
||||||
|
'0011',
|
||||||
|
'0015',
|
||||||
|
],
|
||||||
|
favoriteFontIds: [
|
||||||
|
'0001',
|
||||||
|
'0003',
|
||||||
|
'0006',
|
||||||
|
'0011',
|
||||||
|
'0015',
|
||||||
|
],
|
||||||
|
}
|
||||||
19
miniprogram/assets/default.json
Normal file
19
miniprogram/assets/default.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"inputText": "星程字体转换",
|
||||||
|
"fontSize": 50,
|
||||||
|
"textColor": "#dc2626",
|
||||||
|
"selectedFontIds": [
|
||||||
|
"0001",
|
||||||
|
"0003",
|
||||||
|
"0006",
|
||||||
|
"0011",
|
||||||
|
"0015"
|
||||||
|
],
|
||||||
|
"favoriteFontIds": [
|
||||||
|
"0001",
|
||||||
|
"0003",
|
||||||
|
"0006",
|
||||||
|
"0011",
|
||||||
|
"0015"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,167 +1,167 @@
|
|||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
"id": "其他字体/AlimamaDaoLiTi",
|
"id": "0001",
|
||||||
"name": "AlimamaDaoLiTi",
|
"name": "AlimamaDaoLiTi",
|
||||||
"filename": "AlimamaDaoLiTi.ttf",
|
"filename": "AlimamaDaoLiTi.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/AlimamaDaoLiTi.ttf"
|
"path": "/fonts/其他字体/AlimamaDaoLiTi.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/Hangeuljaemin4-Regular",
|
"id": "0002",
|
||||||
"name": "Hangeuljaemin4-Regular",
|
"name": "Hangeuljaemin4-Regular",
|
||||||
"filename": "Hangeuljaemin4-Regular.ttf",
|
"filename": "Hangeuljaemin4-Regular.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/Hangeuljaemin4-Regular.ttf"
|
"path": "/fonts/其他字体/Hangeuljaemin4-Regular.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/I.顏體",
|
"id": "0003",
|
||||||
"name": "I.顏體",
|
"name": "I.顏體",
|
||||||
"filename": "I.顏體.ttf",
|
"filename": "I.顏體.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/I.顏體.ttf"
|
"path": "/fonts/其他字体/I.顏體.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/XCDUANZHUANGSONGTI",
|
"id": "0004",
|
||||||
"name": "XCDUANZHUANGSONGTI",
|
"name": "XCDUANZHUANGSONGTI",
|
||||||
"filename": "XCDUANZHUANGSONGTI.ttf",
|
"filename": "XCDUANZHUANGSONGTI.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/XCDUANZHUANGSONGTI.ttf"
|
"path": "/fonts/其他字体/XCDUANZHUANGSONGTI.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/qiji-combo",
|
"id": "0005",
|
||||||
"name": "qiji-combo",
|
"name": "qiji-combo",
|
||||||
"filename": "qiji-combo.ttf",
|
"filename": "qiji-combo.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/qiji-combo.ttf"
|
"path": "/fonts/其他字体/qiji-combo.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/临海隶书",
|
"id": "0006",
|
||||||
"name": "临海隶书",
|
"name": "临海隶书",
|
||||||
"filename": "临海隶书.ttf",
|
"filename": "临海隶书.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/临海隶书.ttf"
|
"path": "/fonts/其他字体/临海隶书.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/京華老宋体_KingHwa_OldSong",
|
"id": "0007",
|
||||||
"name": "京華老宋体_KingHwa_OldSong",
|
"name": "京華老宋体_KingHwa_OldSong",
|
||||||
"filename": "京華老宋体_KingHwa_OldSong.ttf",
|
"filename": "京華老宋体_KingHwa_OldSong.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/京華老宋体_KingHwa_OldSong.ttf"
|
"path": "/fonts/其他字体/京華老宋体_KingHwa_OldSong.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/优设标题黑",
|
"id": "0008",
|
||||||
"name": "优设标题黑",
|
"name": "优设标题黑",
|
||||||
"filename": "优设标题黑.ttf",
|
"filename": "优设标题黑.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/优设标题黑.ttf"
|
"path": "/fonts/其他字体/优设标题黑.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/包图小白体",
|
"id": "0009",
|
||||||
"name": "包图小白体",
|
"name": "包图小白体",
|
||||||
"filename": "包图小白体.ttf",
|
"filename": "包图小白体.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/包图小白体.ttf"
|
"path": "/fonts/其他字体/包图小白体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/源界明朝",
|
"id": "0010",
|
||||||
"name": "源界明朝",
|
"name": "源界明朝",
|
||||||
"filename": "源界明朝.ttf",
|
"filename": "源界明朝.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/源界明朝.ttf"
|
"path": "/fonts/其他字体/源界明朝.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/演示佛系体",
|
"id": "0011",
|
||||||
"name": "演示佛系体",
|
"name": "演示佛系体",
|
||||||
"filename": "演示佛系体.ttf",
|
"filename": "演示佛系体.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/演示佛系体.ttf"
|
"path": "/fonts/其他字体/演示佛系体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/站酷快乐体",
|
"id": "0012",
|
||||||
"name": "站酷快乐体",
|
"name": "站酷快乐体",
|
||||||
"filename": "站酷快乐体.ttf",
|
"filename": "站酷快乐体.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/站酷快乐体.ttf"
|
"path": "/fonts/其他字体/站酷快乐体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/问藏书房",
|
"id": "0013",
|
||||||
"name": "问藏书房",
|
"name": "问藏书房",
|
||||||
"filename": "问藏书房.ttf",
|
"filename": "问藏书房.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/问藏书房.ttf"
|
"path": "/fonts/其他字体/问藏书房.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/霞鹜臻楷",
|
"id": "0014",
|
||||||
"name": "霞鹜臻楷",
|
"name": "霞鹜臻楷",
|
||||||
"filename": "霞鹜臻楷.ttf",
|
"filename": "霞鹜臻楷.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/霞鹜臻楷.ttf"
|
"path": "/fonts/其他字体/霞鹜臻楷.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "庞门正道/庞门正道标题体",
|
"id": "0015",
|
||||||
"name": "庞门正道标题体",
|
"name": "庞门正道标题体",
|
||||||
"filename": "庞门正道标题体.ttf",
|
"filename": "庞门正道标题体.ttf",
|
||||||
"category": "庞门正道",
|
"category": "庞门正道",
|
||||||
"path": "/fonts/庞门正道/庞门正道标题体.ttf"
|
"path": "/fonts/庞门正道/庞门正道标题体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "庞门正道-测试/庞门正道标题体",
|
"id": "0016",
|
||||||
"name": "庞门正道标题体",
|
"name": "庞门正道标题体",
|
||||||
"filename": "庞门正道标题体.ttf",
|
"filename": "庞门正道标题体.ttf",
|
||||||
"category": "庞门正道-测试",
|
"category": "庞门正道-测试",
|
||||||
"path": "/fonts/庞门正道-测试/庞门正道标题体.ttf"
|
"path": "/fonts/庞门正道-测试/庞门正道标题体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗勘亭流繁",
|
"id": "0017",
|
||||||
"name": "王漢宗勘亭流繁",
|
"name": "王漢宗勘亭流繁",
|
||||||
"filename": "王漢宗勘亭流繁.ttf",
|
"filename": "王漢宗勘亭流繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗勘亭流繁.ttf"
|
"path": "/fonts/王漢宗/王漢宗勘亭流繁.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗新潮體",
|
"id": "0018",
|
||||||
"name": "王漢宗新潮體",
|
"name": "王漢宗新潮體",
|
||||||
"filename": "王漢宗新潮體.ttf",
|
"filename": "王漢宗新潮體.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗新潮體.ttf"
|
"path": "/fonts/王漢宗/王漢宗新潮體.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗波卡體空陰",
|
"id": "0019",
|
||||||
"name": "王漢宗波卡體空陰",
|
"name": "王漢宗波卡體空陰",
|
||||||
"filename": "王漢宗波卡體空陰.ttf",
|
"filename": "王漢宗波卡體空陰.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗波卡體空陰.ttf"
|
"path": "/fonts/王漢宗/王漢宗波卡體空陰.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗細黑體繁",
|
"id": "0020",
|
||||||
"name": "王漢宗細黑體繁",
|
"name": "王漢宗細黑體繁",
|
||||||
"filename": "王漢宗細黑體繁.ttf",
|
"filename": "王漢宗細黑體繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗細黑體繁.ttf"
|
"path": "/fonts/王漢宗/王漢宗細黑體繁.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗綜藝體雙空陰",
|
"id": "0021",
|
||||||
"name": "王漢宗綜藝體雙空陰",
|
"name": "王漢宗綜藝體雙空陰",
|
||||||
"filename": "王漢宗綜藝體雙空陰.ttf",
|
"filename": "王漢宗綜藝體雙空陰.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗綜藝體雙空陰.ttf"
|
"path": "/fonts/王漢宗/王漢宗綜藝體雙空陰.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗超明體繁",
|
"id": "0022",
|
||||||
"name": "王漢宗超明體繁",
|
"name": "王漢宗超明體繁",
|
||||||
"filename": "王漢宗超明體繁.ttf",
|
"filename": "王漢宗超明體繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗超明體繁.ttf"
|
"path": "/fonts/王漢宗/王漢宗超明體繁.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗酷儷海報",
|
"id": "0023",
|
||||||
"name": "王漢宗酷儷海報",
|
"name": "王漢宗酷儷海報",
|
||||||
"filename": "王漢宗酷儷海報.ttf",
|
"filename": "王漢宗酷儷海報.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗酷儷海報.ttf"
|
"path": "/fonts/王漢宗/王漢宗酷儷海報.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗顏楷體繁",
|
"id": "0024",
|
||||||
"name": "王漢宗顏楷體繁",
|
"name": "王漢宗顏楷體繁",
|
||||||
"filename": "王漢宗顏楷體繁.ttf",
|
"filename": "王漢宗顏楷體繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
|
|||||||
@@ -1,167 +1,167 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "其他字体/AlimamaDaoLiTi",
|
"id": "0001",
|
||||||
"name": "AlimamaDaoLiTi",
|
"name": "AlimamaDaoLiTi",
|
||||||
"filename": "AlimamaDaoLiTi.ttf",
|
"filename": "AlimamaDaoLiTi.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/AlimamaDaoLiTi.ttf"
|
"path": "/fonts/其他字体/AlimamaDaoLiTi.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/Hangeuljaemin4-Regular",
|
"id": "0002",
|
||||||
"name": "Hangeuljaemin4-Regular",
|
"name": "Hangeuljaemin4-Regular",
|
||||||
"filename": "Hangeuljaemin4-Regular.ttf",
|
"filename": "Hangeuljaemin4-Regular.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/Hangeuljaemin4-Regular.ttf"
|
"path": "/fonts/其他字体/Hangeuljaemin4-Regular.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/I.顏體",
|
"id": "0003",
|
||||||
"name": "I.顏體",
|
"name": "I.顏體",
|
||||||
"filename": "I.顏體.ttf",
|
"filename": "I.顏體.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/I.顏體.ttf"
|
"path": "/fonts/其他字体/I.顏體.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/XCDUANZHUANGSONGTI",
|
"id": "0004",
|
||||||
"name": "XCDUANZHUANGSONGTI",
|
"name": "XCDUANZHUANGSONGTI",
|
||||||
"filename": "XCDUANZHUANGSONGTI.ttf",
|
"filename": "XCDUANZHUANGSONGTI.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/XCDUANZHUANGSONGTI.ttf"
|
"path": "/fonts/其他字体/XCDUANZHUANGSONGTI.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/qiji-combo",
|
"id": "0005",
|
||||||
"name": "qiji-combo",
|
"name": "qiji-combo",
|
||||||
"filename": "qiji-combo.ttf",
|
"filename": "qiji-combo.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/qiji-combo.ttf"
|
"path": "/fonts/其他字体/qiji-combo.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/临海隶书",
|
"id": "0006",
|
||||||
"name": "临海隶书",
|
"name": "临海隶书",
|
||||||
"filename": "临海隶书.ttf",
|
"filename": "临海隶书.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/临海隶书.ttf"
|
"path": "/fonts/其他字体/临海隶书.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/京華老宋体_KingHwa_OldSong",
|
"id": "0007",
|
||||||
"name": "京華老宋体_KingHwa_OldSong",
|
"name": "京華老宋体_KingHwa_OldSong",
|
||||||
"filename": "京華老宋体_KingHwa_OldSong.ttf",
|
"filename": "京華老宋体_KingHwa_OldSong.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/京華老宋体_KingHwa_OldSong.ttf"
|
"path": "/fonts/其他字体/京華老宋体_KingHwa_OldSong.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/优设标题黑",
|
"id": "0008",
|
||||||
"name": "优设标题黑",
|
"name": "优设标题黑",
|
||||||
"filename": "优设标题黑.ttf",
|
"filename": "优设标题黑.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/优设标题黑.ttf"
|
"path": "/fonts/其他字体/优设标题黑.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/包图小白体",
|
"id": "0009",
|
||||||
"name": "包图小白体",
|
"name": "包图小白体",
|
||||||
"filename": "包图小白体.ttf",
|
"filename": "包图小白体.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/包图小白体.ttf"
|
"path": "/fonts/其他字体/包图小白体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/源界明朝",
|
"id": "0010",
|
||||||
"name": "源界明朝",
|
"name": "源界明朝",
|
||||||
"filename": "源界明朝.ttf",
|
"filename": "源界明朝.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/源界明朝.ttf"
|
"path": "/fonts/其他字体/源界明朝.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/演示佛系体",
|
"id": "0011",
|
||||||
"name": "演示佛系体",
|
"name": "演示佛系体",
|
||||||
"filename": "演示佛系体.ttf",
|
"filename": "演示佛系体.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/演示佛系体.ttf"
|
"path": "/fonts/其他字体/演示佛系体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/站酷快乐体",
|
"id": "0012",
|
||||||
"name": "站酷快乐体",
|
"name": "站酷快乐体",
|
||||||
"filename": "站酷快乐体.ttf",
|
"filename": "站酷快乐体.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/站酷快乐体.ttf"
|
"path": "/fonts/其他字体/站酷快乐体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/问藏书房",
|
"id": "0013",
|
||||||
"name": "问藏书房",
|
"name": "问藏书房",
|
||||||
"filename": "问藏书房.ttf",
|
"filename": "问藏书房.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/问藏书房.ttf"
|
"path": "/fonts/其他字体/问藏书房.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "其他字体/霞鹜臻楷",
|
"id": "0014",
|
||||||
"name": "霞鹜臻楷",
|
"name": "霞鹜臻楷",
|
||||||
"filename": "霞鹜臻楷.ttf",
|
"filename": "霞鹜臻楷.ttf",
|
||||||
"category": "其他字体",
|
"category": "其他字体",
|
||||||
"path": "/fonts/其他字体/霞鹜臻楷.ttf"
|
"path": "/fonts/其他字体/霞鹜臻楷.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "庞门正道/庞门正道标题体",
|
"id": "0015",
|
||||||
"name": "庞门正道标题体",
|
"name": "庞门正道标题体",
|
||||||
"filename": "庞门正道标题体.ttf",
|
"filename": "庞门正道标题体.ttf",
|
||||||
"category": "庞门正道",
|
"category": "庞门正道",
|
||||||
"path": "/fonts/庞门正道/庞门正道标题体.ttf"
|
"path": "/fonts/庞门正道/庞门正道标题体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "庞门正道-测试/庞门正道标题体",
|
"id": "0016",
|
||||||
"name": "庞门正道标题体",
|
"name": "庞门正道标题体",
|
||||||
"filename": "庞门正道标题体.ttf",
|
"filename": "庞门正道标题体.ttf",
|
||||||
"category": "庞门正道-测试",
|
"category": "庞门正道-测试",
|
||||||
"path": "/fonts/庞门正道-测试/庞门正道标题体.ttf"
|
"path": "/fonts/庞门正道-测试/庞门正道标题体.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗勘亭流繁",
|
"id": "0017",
|
||||||
"name": "王漢宗勘亭流繁",
|
"name": "王漢宗勘亭流繁",
|
||||||
"filename": "王漢宗勘亭流繁.ttf",
|
"filename": "王漢宗勘亭流繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗勘亭流繁.ttf"
|
"path": "/fonts/王漢宗/王漢宗勘亭流繁.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗新潮體",
|
"id": "0018",
|
||||||
"name": "王漢宗新潮體",
|
"name": "王漢宗新潮體",
|
||||||
"filename": "王漢宗新潮體.ttf",
|
"filename": "王漢宗新潮體.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗新潮體.ttf"
|
"path": "/fonts/王漢宗/王漢宗新潮體.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗波卡體空陰",
|
"id": "0019",
|
||||||
"name": "王漢宗波卡體空陰",
|
"name": "王漢宗波卡體空陰",
|
||||||
"filename": "王漢宗波卡體空陰.ttf",
|
"filename": "王漢宗波卡體空陰.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗波卡體空陰.ttf"
|
"path": "/fonts/王漢宗/王漢宗波卡體空陰.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗細黑體繁",
|
"id": "0020",
|
||||||
"name": "王漢宗細黑體繁",
|
"name": "王漢宗細黑體繁",
|
||||||
"filename": "王漢宗細黑體繁.ttf",
|
"filename": "王漢宗細黑體繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗細黑體繁.ttf"
|
"path": "/fonts/王漢宗/王漢宗細黑體繁.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗綜藝體雙空陰",
|
"id": "0021",
|
||||||
"name": "王漢宗綜藝體雙空陰",
|
"name": "王漢宗綜藝體雙空陰",
|
||||||
"filename": "王漢宗綜藝體雙空陰.ttf",
|
"filename": "王漢宗綜藝體雙空陰.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗綜藝體雙空陰.ttf"
|
"path": "/fonts/王漢宗/王漢宗綜藝體雙空陰.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗超明體繁",
|
"id": "0022",
|
||||||
"name": "王漢宗超明體繁",
|
"name": "王漢宗超明體繁",
|
||||||
"filename": "王漢宗超明體繁.ttf",
|
"filename": "王漢宗超明體繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗超明體繁.ttf"
|
"path": "/fonts/王漢宗/王漢宗超明體繁.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗酷儷海報",
|
"id": "0023",
|
||||||
"name": "王漢宗酷儷海報",
|
"name": "王漢宗酷儷海報",
|
||||||
"filename": "王漢宗酷儷海報.ttf",
|
"filename": "王漢宗酷儷海報.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
"path": "/fonts/王漢宗/王漢宗酷儷海報.ttf"
|
"path": "/fonts/王漢宗/王漢宗酷儷海報.ttf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "王漢宗/王漢宗顏楷體繁",
|
"id": "0024",
|
||||||
"name": "王漢宗顏楷體繁",
|
"name": "王漢宗顏楷體繁",
|
||||||
"filename": "王漢宗顏楷體繁.ttf",
|
"filename": "王漢宗顏楷體繁.ttf",
|
||||||
"category": "王漢宗",
|
"category": "王漢宗",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// CDN 配置文件
|
// CDN 配置文件
|
||||||
// 管理所有静态资源的 CDN 地址
|
// 管理所有静态资源的 CDN 地址
|
||||||
|
|
||||||
const CDN_BASE_URL = 'https://fonts.biboer.cn';
|
const { buildRuntimeConfig } = require('./server')
|
||||||
|
const runtimeConfig = buildRuntimeConfig()
|
||||||
|
const CDN_BASE_URL = runtimeConfig.fontsBaseUrl;
|
||||||
|
|
||||||
// 图标路径配置
|
// 图标路径配置
|
||||||
const ICON_PATHS = {
|
const ICON_PATHS = {
|
||||||
@@ -30,7 +32,7 @@ const ICON_PATHS = {
|
|||||||
|
|
||||||
// 字体资源路径
|
// 字体资源路径
|
||||||
const FONT_BASE_URL = `${CDN_BASE_URL}/fonts`;
|
const FONT_BASE_URL = `${CDN_BASE_URL}/fonts`;
|
||||||
const FONTS_JSON_URL = `${CDN_BASE_URL}/fonts.json`;
|
const FONTS_JSON_URL = runtimeConfig.fontsManifestUrl;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CDN_BASE_URL,
|
CDN_BASE_URL,
|
||||||
|
|||||||
54
miniprogram/config/server.js
Normal file
54
miniprogram/config/server.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// 远端服务配置(统一修改入口)
|
||||||
|
// 更换服务器时,仅需修改这里。
|
||||||
|
|
||||||
|
const SERVER_CONFIG = {
|
||||||
|
protocol: 'https',
|
||||||
|
host: 'fonts.biboer.cn',
|
||||||
|
// 留空表示使用协议默认端口(https:443 / http:80)
|
||||||
|
port: '',
|
||||||
|
apiPrefix: '/api',
|
||||||
|
fontsManifestPath: '/miniprogram/assets/fonts.json',
|
||||||
|
defaultConfigPath: '/miniprogram/assets/default.json',
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOrigin() {
|
||||||
|
const protocol = String(SERVER_CONFIG.protocol || 'https').replace(/:$/, '')
|
||||||
|
const host = String(SERVER_CONFIG.host || '').trim()
|
||||||
|
const port = String(SERVER_CONFIG.port || '').trim()
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
throw new Error('SERVER_CONFIG.host 未配置')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDefaultPort = (protocol === 'https' && port === '443') || (protocol === 'http' && port === '80')
|
||||||
|
const portPart = !port || hasDefaultPort ? '' : `:${port}`
|
||||||
|
return `${protocol}://${host}${portPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path, fallback) {
|
||||||
|
const value = String(path || fallback || '').trim()
|
||||||
|
if (!value) {
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
return value.startsWith('/') ? value : `/${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRuntimeConfig() {
|
||||||
|
const origin = buildOrigin()
|
||||||
|
const apiPrefix = normalizePath(SERVER_CONFIG.apiPrefix, '/api').replace(/\/$/, '')
|
||||||
|
const fontsManifestPath = normalizePath(SERVER_CONFIG.fontsManifestPath, '/miniprogram/assets/fonts.json')
|
||||||
|
const defaultConfigPath = normalizePath(SERVER_CONFIG.defaultConfigPath, '/miniprogram/assets/default.json')
|
||||||
|
|
||||||
|
return {
|
||||||
|
fontsBaseUrl: origin,
|
||||||
|
fontsManifestUrl: `${origin}${fontsManifestPath}`,
|
||||||
|
defaultConfigUrl: `${origin}${defaultConfigPath}`,
|
||||||
|
svgRenderApiUrl: `${origin}${apiPrefix}/render-svg`,
|
||||||
|
pngRenderApiUrl: `${origin}${apiPrefix}/render-png`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SERVER_CONFIG,
|
||||||
|
buildRuntimeConfig,
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { loadFontsManifest } = require('../../utils/mp/font-loader')
|
const { loadFontsManifest, loadDefaultConfig } = require('../../utils/mp/font-loader')
|
||||||
const { loadAppState, saveAppState, loadFavorites, saveFavorites } = require('../../utils/mp/storage')
|
const { loadAppState, saveAppState, loadFavorites, saveFavorites } = require('../../utils/mp/storage')
|
||||||
const {
|
const {
|
||||||
shareSvgFromUserTap,
|
shareSvgFromUserTap,
|
||||||
@@ -11,8 +11,11 @@ const COLOR_PALETTE = ['#000000', '#1d4ed8', '#047857', '#b45309', '#dc2626', '#
|
|||||||
const FONT_SIZE_MIN = 20
|
const FONT_SIZE_MIN = 20
|
||||||
const FONT_SIZE_MAX = 120
|
const FONT_SIZE_MAX = 120
|
||||||
const PREVIEW_RENDER_FONT_SIZE = 120
|
const PREVIEW_RENDER_FONT_SIZE = 120
|
||||||
|
const PREVIEW_MAX_CHARS_PER_LINE = 45
|
||||||
|
const PREVIEW_CACHE_LIMIT = 300
|
||||||
const MIN_PREVIEW_IMAGE_WIDTH = 24
|
const MIN_PREVIEW_IMAGE_WIDTH = 24
|
||||||
const MAX_PREVIEW_IMAGE_WIDTH = 2400
|
const MAX_PREVIEW_IMAGE_WIDTH = 2400
|
||||||
|
const FEEDBACK_EMAIL = 'douboer@gmail.com'
|
||||||
|
|
||||||
// 临时使用本地图标 - 根据Figma annotation配置
|
// 临时使用本地图标 - 根据Figma annotation配置
|
||||||
const LOCAL_ICON_PATHS = {
|
const LOCAL_ICON_PATHS = {
|
||||||
@@ -58,6 +61,31 @@ function clampFontSize(value, fallback = PREVIEW_RENDER_FONT_SIZE) {
|
|||||||
return Math.min(FONT_SIZE_MAX, Math.max(FONT_SIZE_MIN, Math.round(base)))
|
return Math.min(FONT_SIZE_MAX, Math.max(FONT_SIZE_MIN, Math.round(base)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSelectedFontIds(fontIds) {
|
||||||
|
if (!Array.isArray(fontIds)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
fontIds
|
||||||
|
.map((id) => String(id || '').trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLegacyFontId(font) {
|
||||||
|
if (!font || typeof font !== 'object') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const category = String(font.category || '').trim()
|
||||||
|
const name = String(font.name || '').trim()
|
||||||
|
if (!category || !name) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `${category}/${name}`
|
||||||
|
}
|
||||||
|
|
||||||
function extractErrorMessage(error, fallback) {
|
function extractErrorMessage(error, fallback) {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return fallback
|
return fallback
|
||||||
@@ -84,6 +112,25 @@ function writePngBufferToTempFile(pngBuffer, fontName) {
|
|||||||
return filePath
|
return filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPreviewCacheKey(fontId, text, letterSpacing, maxCharsPerLine = PREVIEW_MAX_CHARS_PER_LINE) {
|
||||||
|
const safeFontId = String(fontId || '')
|
||||||
|
const safeText = String(text || '')
|
||||||
|
const spacingNumber = Number(letterSpacing)
|
||||||
|
const safeSpacing = Number.isFinite(spacingNumber) ? spacingNumber.toFixed(4) : '0.0000'
|
||||||
|
return [safeFontId, safeSpacing, String(maxCharsPerLine), safeText].join('::')
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreviewCache(previewCache, key, value) {
|
||||||
|
previewCache.set(key, value)
|
||||||
|
if (previewCache.size <= PREVIEW_CACHE_LIMIT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const oldestKey = previewCache.keys().next().value
|
||||||
|
if (oldestKey !== undefined) {
|
||||||
|
previewCache.delete(oldestKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatSvgNumber(value) {
|
function formatSvgNumber(value) {
|
||||||
const text = String(Number(value).toFixed(2))
|
const text = String(Number(value).toFixed(2))
|
||||||
return text.replace(/\.?0+$/, '')
|
return text.replace(/\.?0+$/, '')
|
||||||
@@ -146,9 +193,9 @@ function applyLocalStyleToFontItem(font, fontSize, color) {
|
|||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
inputText: '星程字体转换',
|
inputText: '星程字体转换',
|
||||||
fontSize: FONT_SIZE_MAX,
|
fontSize: 50,
|
||||||
letterSpacingInput: '0',
|
letterSpacingInput: '0',
|
||||||
textColor: '#000000',
|
textColor: '#dc2626',
|
||||||
colorPalette: COLOR_PALETTE,
|
colorPalette: COLOR_PALETTE,
|
||||||
selectedFonts: [], // 当前已选中的字体列表
|
selectedFonts: [], // 当前已选中的字体列表
|
||||||
fontCategories: [], // 字体分类树
|
fontCategories: [], // 字体分类树
|
||||||
@@ -160,6 +207,7 @@ Page({
|
|||||||
// 搜索功能
|
// 搜索功能
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
|
feedbackEmail: FEEDBACK_EMAIL,
|
||||||
},
|
},
|
||||||
|
|
||||||
async onLoad() {
|
async onLoad() {
|
||||||
@@ -170,6 +218,8 @@ Page({
|
|||||||
console.log('============================')
|
console.log('============================')
|
||||||
|
|
||||||
this.fontMap = new Map()
|
this.fontMap = new Map()
|
||||||
|
this.legacyFontIdMap = new Map()
|
||||||
|
this.previewCache = new Map()
|
||||||
this.generateTimer = null
|
this.generateTimer = null
|
||||||
this.categoryExpandedMap = {}
|
this.categoryExpandedMap = {}
|
||||||
|
|
||||||
@@ -177,7 +227,11 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
const favorites = loadFavorites()
|
const rawFavorites = loadFavorites()
|
||||||
|
const favorites = this.normalizeFontIdList(rawFavorites)
|
||||||
|
if (normalizeSelectedFontIds(rawFavorites).join(',') !== favorites.join(',')) {
|
||||||
|
saveFavorites(favorites)
|
||||||
|
}
|
||||||
this.setData({ favorites })
|
this.setData({ favorites })
|
||||||
this.updateFontTrees()
|
this.updateFontTrees()
|
||||||
},
|
},
|
||||||
@@ -196,35 +250,126 @@ Page({
|
|||||||
this.setData({ selectedFonts })
|
this.setData({ selectedFonts })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
buildFontMaps(fonts) {
|
||||||
|
this.fontMap = new Map()
|
||||||
|
this.legacyFontIdMap = new Map()
|
||||||
|
|
||||||
|
if (!Array.isArray(fonts)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fonts.forEach((font) => {
|
||||||
|
const fontId = String(font.id || '').trim()
|
||||||
|
if (!fontId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fontMap.set(fontId, font)
|
||||||
|
|
||||||
|
const legacyId = buildLegacyFontId(font)
|
||||||
|
if (legacyId && !this.legacyFontIdMap.has(legacyId)) {
|
||||||
|
this.legacyFontIdMap.set(legacyId, fontId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveFontId(fontId) {
|
||||||
|
const normalizedId = String(fontId || '').trim()
|
||||||
|
if (!normalizedId) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFontMap = this.fontMap instanceof Map && this.fontMap.size > 0
|
||||||
|
if (!hasFontMap) {
|
||||||
|
return normalizedId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fontMap.has(normalizedId)) {
|
||||||
|
return normalizedId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.legacyFontIdMap instanceof Map && this.legacyFontIdMap.has(normalizedId)) {
|
||||||
|
return this.legacyFontIdMap.get(normalizedId) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
|
||||||
|
normalizeFontIdList(fontIds) {
|
||||||
|
const normalized = normalizeSelectedFontIds(fontIds)
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
normalized
|
||||||
|
.map((fontId) => this.resolveFontId(fontId))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
async bootstrap() {
|
async bootstrap() {
|
||||||
wx.showLoading({ title: '加载中', mask: true })
|
wx.showLoading({ title: '加载中', mask: true })
|
||||||
try {
|
try {
|
||||||
const state = loadAppState()
|
const state = loadAppState()
|
||||||
const fonts = await loadFontsManifest()
|
const isFirstLaunch = !state || !state.updatedAt
|
||||||
const favorites = loadFavorites()
|
const [fonts, defaultConfig] = await Promise.all([
|
||||||
|
loadFontsManifest(),
|
||||||
|
loadDefaultConfig(),
|
||||||
|
])
|
||||||
|
this.buildFontMaps(fonts)
|
||||||
|
|
||||||
for (const font of fonts) {
|
const rawFavorites = loadFavorites()
|
||||||
this.fontMap.set(font.id, font)
|
const normalizedStoredFavorites = this.normalizeFontIdList(rawFavorites)
|
||||||
|
const normalizedDefaultFavorites = this.normalizeFontIdList(defaultConfig.favoriteFontIds)
|
||||||
|
const favorites = isFirstLaunch ? normalizedDefaultFavorites : normalizedStoredFavorites
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizeSelectedFontIds(rawFavorites).join(',') !== favorites.join(',') ||
|
||||||
|
(isFirstLaunch && favorites.length > 0)
|
||||||
|
) {
|
||||||
|
saveFavorites(favorites)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialInputText = isFirstLaunch
|
||||||
|
? ((typeof defaultConfig.inputText === 'string' && defaultConfig.inputText) || this.data.inputText)
|
||||||
|
: (state.inputText || this.data.inputText)
|
||||||
|
const initialFontSize = isFirstLaunch
|
||||||
|
? clampFontSize(defaultConfig.fontSize, this.data.fontSize)
|
||||||
|
: clampFontSize(state.fontSize, this.data.fontSize)
|
||||||
|
const initialLetterSpacingInput = isFirstLaunch
|
||||||
|
? (
|
||||||
|
typeof defaultConfig.letterSpacing === 'number'
|
||||||
|
? String(defaultConfig.letterSpacing)
|
||||||
|
: this.data.letterSpacingInput
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
typeof state.letterSpacing === 'number'
|
||||||
|
? String(state.letterSpacing)
|
||||||
|
: this.data.letterSpacingInput
|
||||||
|
)
|
||||||
|
const initialTextColor = isFirstLaunch
|
||||||
|
? normalizeHexColor(defaultConfig.textColor || this.data.textColor)
|
||||||
|
: normalizeHexColor(state.textColor || this.data.textColor)
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
inputText: state.inputText || this.data.inputText,
|
inputText: initialInputText,
|
||||||
fontSize: clampFontSize(state.fontSize, this.data.fontSize),
|
fontSize: initialFontSize,
|
||||||
letterSpacingInput:
|
letterSpacingInput: initialLetterSpacingInput,
|
||||||
typeof state.letterSpacing === 'number' ? String(state.letterSpacing) : this.data.letterSpacingInput,
|
textColor: initialTextColor,
|
||||||
textColor: normalizeHexColor(state.textColor || this.data.textColor),
|
|
||||||
favorites,
|
favorites,
|
||||||
})
|
})
|
||||||
this.categoryExpandedMap = state.categoryExpandedMap && typeof state.categoryExpandedMap === 'object'
|
this.categoryExpandedMap = !isFirstLaunch && state.categoryExpandedMap && typeof state.categoryExpandedMap === 'object'
|
||||||
? { ...state.categoryExpandedMap }
|
? { ...state.categoryExpandedMap }
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
// 构建字体树
|
// 构建字体树
|
||||||
this.updateFontTrees()
|
this.updateFontTrees()
|
||||||
|
|
||||||
// 如果有保存的选中字体,恢复它们
|
// 恢复选中字体(首次使用走 default.json,后续走本地用户配置)
|
||||||
if (state.selectedFontIds && state.selectedFontIds.length > 0) {
|
const rawSelectedFontIds = isFirstLaunch ? defaultConfig.selectedFontIds : state.selectedFontIds
|
||||||
const selectedFonts = state.selectedFontIds
|
const normalizedStoredSelectedIds = normalizeSelectedFontIds(rawSelectedFontIds)
|
||||||
|
const initialSelectedFontIds = this.normalizeFontIdList(normalizedStoredSelectedIds)
|
||||||
|
if (initialSelectedFontIds.length > 0) {
|
||||||
|
const selectedFonts = initialSelectedFontIds
|
||||||
.map(id => this.fontMap.get(id))
|
.map(id => this.fontMap.get(id))
|
||||||
.filter(font => font)
|
.filter(font => font)
|
||||||
.map(font => ({
|
.map(font => ({
|
||||||
@@ -240,6 +385,21 @@ Page({
|
|||||||
this.updateFontTrees()
|
this.updateFontTrees()
|
||||||
await this.generateAllPreviews()
|
await this.generateAllPreviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const migratedSelectedIdsChanged = !isFirstLaunch &&
|
||||||
|
normalizedStoredSelectedIds.join(',') !== initialSelectedFontIds.join(',')
|
||||||
|
|
||||||
|
// 首次加载后立即固化配置,后续全部以用户配置为准
|
||||||
|
if (isFirstLaunch || migratedSelectedIdsChanged) {
|
||||||
|
saveAppState({
|
||||||
|
inputText: this.data.inputText,
|
||||||
|
selectedFontIds: this.data.selectedFonts.map(font => font.id),
|
||||||
|
fontSize: Number(this.data.fontSize),
|
||||||
|
letterSpacing: Number(this.data.letterSpacingInput || 0),
|
||||||
|
textColor: this.data.textColor,
|
||||||
|
categoryExpandedMap: this.categoryExpandedMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error && error.message ? error.message : '初始化失败'
|
const message = error && error.message ? error.message : '初始化失败'
|
||||||
wx.showToast({ title: message, icon: 'none', duration: 2200 })
|
wx.showToast({ title: message, icon: 'none', duration: 2200 })
|
||||||
@@ -317,7 +477,7 @@ Page({
|
|||||||
cat.allSelected = false
|
cat.allSelected = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
favoriteFonts.sort((a, b) => {
|
favoriteFonts.sort((a, b) => {
|
||||||
const categoryCompare = String(a.category || '').localeCompare(String(b.category || ''))
|
const categoryCompare = String(a.category || '').localeCompare(String(b.category || ''))
|
||||||
if (categoryCompare !== 0) return categoryCompare
|
if (categoryCompare !== 0) return categoryCompare
|
||||||
@@ -430,7 +590,7 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 切换分类全选/取消全选
|
// 切换分类全选/取消全选
|
||||||
onToggleSelectAllInCategory(e) {
|
async onToggleSelectAllInCategory(e) {
|
||||||
const category = e.currentTarget.dataset.category
|
const category = e.currentTarget.dataset.category
|
||||||
if (!category) return
|
if (!category) return
|
||||||
|
|
||||||
@@ -438,8 +598,9 @@ Page({
|
|||||||
if (!categoryFonts || categoryFonts.fonts.length === 0) return
|
if (!categoryFonts || categoryFonts.fonts.length === 0) return
|
||||||
|
|
||||||
const allSelected = categoryFonts.allSelected
|
const allSelected = categoryFonts.allSelected
|
||||||
const selectedFonts = [...this.data.selectedFonts]
|
const previousSelectedMap = new Map(this.data.selectedFonts.map(font => [font.id, font]))
|
||||||
const selectedIdSet = new Set(selectedFonts.map(f => f.id))
|
const selectedIdSet = new Set(previousSelectedMap.keys())
|
||||||
|
const addedFontIds = []
|
||||||
|
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
// 取消全选:移除该分类下的所有字体
|
// 取消全选:移除该分类下的所有字体
|
||||||
@@ -449,6 +610,9 @@ Page({
|
|||||||
} else {
|
} else {
|
||||||
// 全选:添加该分类下的所有字体
|
// 全选:添加该分类下的所有字体
|
||||||
categoryFonts.fonts.forEach(font => {
|
categoryFonts.fonts.forEach(font => {
|
||||||
|
if (!selectedIdSet.has(font.id)) {
|
||||||
|
addedFontIds.push(font.id)
|
||||||
|
}
|
||||||
selectedIdSet.add(font.id)
|
selectedIdSet.add(font.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -456,19 +620,32 @@ Page({
|
|||||||
const newSelectedFonts = []
|
const newSelectedFonts = []
|
||||||
this.fontMap.forEach(font => {
|
this.fontMap.forEach(font => {
|
||||||
if (selectedIdSet.has(font.id)) {
|
if (selectedIdSet.has(font.id)) {
|
||||||
newSelectedFonts.push({
|
const existingFont = previousSelectedMap.get(font.id)
|
||||||
id: font.id,
|
if (existingFont) {
|
||||||
name: font.name,
|
newSelectedFonts.push({
|
||||||
category: font.category,
|
...existingFont,
|
||||||
showInPreview: true,
|
showInPreview: typeof existingFont.showInPreview === 'boolean' ? existingFont.showInPreview : true,
|
||||||
previewSrc: '',
|
})
|
||||||
})
|
} else {
|
||||||
|
newSelectedFonts.push({
|
||||||
|
id: font.id,
|
||||||
|
name: font.name,
|
||||||
|
category: font.category,
|
||||||
|
showInPreview: true,
|
||||||
|
previewSrc: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setData({ selectedFonts: newSelectedFonts })
|
this.setData({ selectedFonts: newSelectedFonts })
|
||||||
this.updateFontTrees()
|
this.updateFontTrees()
|
||||||
this.scheduleGenerate()
|
|
||||||
|
if (addedFontIds.length > 0) {
|
||||||
|
for (const fontId of addedFontIds) {
|
||||||
|
await this.generatePreviewForFont(fontId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveAppState({
|
saveAppState({
|
||||||
inputText: this.data.inputText,
|
inputText: this.data.inputText,
|
||||||
@@ -488,6 +665,26 @@ Page({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const letterSpacing = Number(this.data.letterSpacingInput || 0)
|
const letterSpacing = Number(this.data.letterSpacingInput || 0)
|
||||||
|
const cacheKey = buildPreviewCacheKey(fontId, text, letterSpacing, PREVIEW_MAX_CHARS_PER_LINE)
|
||||||
|
const cached = this.previewCache.get(cacheKey)
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
const selectedFonts = this.data.selectedFonts
|
||||||
|
const index = selectedFonts.findIndex(f => f.id === fontId)
|
||||||
|
if (index >= 0) {
|
||||||
|
selectedFonts[index].baseSvg = cached.svg
|
||||||
|
selectedFonts[index].baseWidth = cached.width
|
||||||
|
selectedFonts[index].baseHeight = cached.height
|
||||||
|
selectedFonts[index].renderFontSize = PREVIEW_RENDER_FONT_SIZE
|
||||||
|
selectedFonts[index] = applyLocalStyleToFontItem(
|
||||||
|
selectedFonts[index],
|
||||||
|
Number(this.data.fontSize),
|
||||||
|
this.data.textColor,
|
||||||
|
)
|
||||||
|
this.setData({ selectedFonts })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const result = await renderSvgByApi({
|
const result = await renderSvgByApi({
|
||||||
fontId,
|
fontId,
|
||||||
@@ -496,7 +693,12 @@ Page({
|
|||||||
fontSize: PREVIEW_RENDER_FONT_SIZE,
|
fontSize: PREVIEW_RENDER_FONT_SIZE,
|
||||||
fillColor: '#000000',
|
fillColor: '#000000',
|
||||||
letterSpacing,
|
letterSpacing,
|
||||||
maxCharsPerLine: 45,
|
maxCharsPerLine: PREVIEW_MAX_CHARS_PER_LINE,
|
||||||
|
})
|
||||||
|
setPreviewCache(this.previewCache, cacheKey, {
|
||||||
|
svg: result.svg,
|
||||||
|
width: result.width,
|
||||||
|
height: result.height,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新对应字体的预览
|
// 更新对应字体的预览
|
||||||
@@ -850,4 +1052,42 @@ Page({
|
|||||||
this.setData({ searchKeyword: keyword })
|
this.setData({ searchKeyword: keyword })
|
||||||
this.updateFontTrees()
|
this.updateFontTrees()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTapFeedbackEmail() {
|
||||||
|
const email = this.data.feedbackEmail || FEEDBACK_EMAIL
|
||||||
|
const mailtoUrl = `mailto:${email}`
|
||||||
|
|
||||||
|
// 微信环境对 mailto 支持不稳定:优先尝试跳转,失败时自动复制邮箱
|
||||||
|
if (typeof wx.openUrl === 'function') {
|
||||||
|
wx.openUrl({
|
||||||
|
url: mailtoUrl,
|
||||||
|
fail: () => {
|
||||||
|
this.copyFeedbackEmail(email)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.copyFeedbackEmail(email)
|
||||||
|
},
|
||||||
|
|
||||||
|
copyFeedbackEmail(email) {
|
||||||
|
wx.setClipboardData({
|
||||||
|
data: String(email || FEEDBACK_EMAIL),
|
||||||
|
success: () => {
|
||||||
|
wx.showModal({
|
||||||
|
title: '已复制邮箱',
|
||||||
|
content: '请打开邮件应用并粘贴收件人地址发送反馈。',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
wx.showToast({
|
||||||
|
title: '邮箱复制失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -177,7 +177,8 @@
|
|||||||
|
|
||||||
<!-- 版权说明 -->
|
<!-- 版权说明 -->
|
||||||
<view class="copyright-footer">
|
<view class="copyright-footer">
|
||||||
@版权说明:仅SVG和PNG分享,无TTF下载,如侵权,反馈:douboer@gmail.com
|
<text>@版权说明:仅SVG和PNG分享,无TTF下载,如侵权,反馈:</text>
|
||||||
|
<text class="copyright-email-link" bindtap="onTapFeedbackEmail">{{feedbackEmail}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 颜色选择器弹窗 -->
|
<!-- 颜色选择器弹窗 -->
|
||||||
|
|||||||
@@ -233,7 +233,7 @@
|
|||||||
.preview-content {
|
.preview-content {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 4rpx 0;
|
padding: 4rpx 0;
|
||||||
min-height: 80rpx;
|
min-height: 40rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -284,6 +284,11 @@
|
|||||||
padding: 8rpx 8rpx;
|
padding: 8rpx 8rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-selection {
|
||||||
|
border-left: 2rpx solid #3EE4C3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索相关样式 */
|
/* 搜索相关样式 */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { request, downloadFile, readFile } = require('./wx-promisify')
|
const { request, downloadFile, readFile } = require('./wx-promisify')
|
||||||
|
|
||||||
const localFonts = require('../../assets/fonts')
|
const localFonts = require('../../assets/fonts')
|
||||||
|
const localDefaultConfig = require('../../assets/default')
|
||||||
|
|
||||||
const fontBufferCache = new Map()
|
const fontBufferCache = new Map()
|
||||||
const MAX_FONT_CACHE = 4
|
const MAX_FONT_CACHE = 4
|
||||||
@@ -44,6 +45,70 @@ function normalizeManifest(fonts, baseUrl) {
|
|||||||
.filter((item) => item.url)
|
.filter((item) => item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDefaultConfig(config) {
|
||||||
|
const payload = config && typeof config === 'object' ? config : {}
|
||||||
|
const selectedRaw = Array.isArray(payload.selectedFontIds)
|
||||||
|
? payload.selectedFontIds
|
||||||
|
: (Array.isArray(payload.selectedFonts) ? payload.selectedFonts : [])
|
||||||
|
const favoriteRaw = Array.isArray(payload.favoriteFontIds)
|
||||||
|
? payload.favoriteFontIds
|
||||||
|
: (Array.isArray(payload.favoriteFonts) ? payload.favoriteFonts : [])
|
||||||
|
|
||||||
|
const selectedFontIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
selectedRaw
|
||||||
|
.map((item) => String(item || '').trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const favoriteFontIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
favoriteRaw
|
||||||
|
.map((item) => String(item || '').trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
selectedFontIds,
|
||||||
|
favoriteFontIds,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload.inputText === 'string') {
|
||||||
|
result.inputText = payload.inputText
|
||||||
|
}
|
||||||
|
if (typeof payload.textColor === 'string') {
|
||||||
|
result.textColor = payload.textColor
|
||||||
|
}
|
||||||
|
if (payload.fontSize !== undefined && payload.fontSize !== null && payload.fontSize !== '') {
|
||||||
|
const fontSize = Number(payload.fontSize)
|
||||||
|
if (Number.isFinite(fontSize)) {
|
||||||
|
result.fontSize = fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.letterSpacing !== undefined && payload.letterSpacing !== null && payload.letterSpacing !== '') {
|
||||||
|
const letterSpacing = Number(payload.letterSpacing)
|
||||||
|
if (Number.isFinite(letterSpacing)) {
|
||||||
|
result.letterSpacing = letterSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultConfigUrl(manifestUrl, baseUrl) {
|
||||||
|
const manifest = String(manifestUrl || '').trim()
|
||||||
|
if (manifest) {
|
||||||
|
const withoutHash = manifest.split('#')[0]
|
||||||
|
const [pathPart] = withoutHash.split('?')
|
||||||
|
const slashIndex = pathPart.lastIndexOf('/')
|
||||||
|
if (slashIndex >= 0) {
|
||||||
|
return `${pathPart.slice(0, slashIndex + 1)}default.json`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizePath('/miniprogram/assets/default.json', baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
async function loadFontsManifest(options = {}) {
|
async function loadFontsManifest(options = {}) {
|
||||||
const app = getApp()
|
const app = getApp()
|
||||||
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
|
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
|
||||||
@@ -79,6 +144,40 @@ async function loadFontsManifest(options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDefaultConfig(options = {}) {
|
||||||
|
const app = getApp()
|
||||||
|
const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl
|
||||||
|
const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl
|
||||||
|
const defaultConfigUrl = options.defaultConfigUrl ||
|
||||||
|
app.globalData.defaultConfigUrl ||
|
||||||
|
buildDefaultConfigUrl(manifestUrl, baseUrl)
|
||||||
|
|
||||||
|
if (app.globalData.defaultConfig && typeof app.globalData.defaultConfig === 'object') {
|
||||||
|
return app.globalData.defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request({
|
||||||
|
url: defaultConfigUrl,
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
throw new Error(`获取默认配置失败,状态码: ${response.statusCode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteConfig = normalizeDefaultConfig(response.data)
|
||||||
|
app.globalData.defaultConfig = remoteConfig
|
||||||
|
return remoteConfig
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('远程 default.json 加载失败,回退到本地默认配置:', error)
|
||||||
|
const fallbackConfig = normalizeDefaultConfig(localDefaultConfig)
|
||||||
|
app.globalData.defaultConfig = fallbackConfig
|
||||||
|
return fallbackConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setLruCache(key, value) {
|
function setLruCache(key, value) {
|
||||||
if (fontBufferCache.has(key)) {
|
if (fontBufferCache.has(key)) {
|
||||||
fontBufferCache.delete(key)
|
fontBufferCache.delete(key)
|
||||||
@@ -125,6 +224,7 @@ function listCategories(fonts) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadFontsManifest,
|
loadFontsManifest,
|
||||||
|
loadDefaultConfig,
|
||||||
loadFontBuffer,
|
loadFontBuffer,
|
||||||
listCategories,
|
listCategories,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,12 @@ async function renderPngByApi(payload) {
|
|||||||
? Number(app.globalData.apiTimeoutMs)
|
? Number(app.globalData.apiTimeoutMs)
|
||||||
: 30000
|
: 30000
|
||||||
const baseApiUrl = buildApiUrl()
|
const baseApiUrl = buildApiUrl()
|
||||||
const apiUrl = /\/api\/render-svg$/.test(baseApiUrl)
|
const configuredPngApiUrl = app && app.globalData ? app.globalData.pngRenderApiUrl : ''
|
||||||
? baseApiUrl.replace(/\/api\/render-svg$/, '/api/render-png')
|
const apiUrl = configuredPngApiUrl || (
|
||||||
: `${baseApiUrl.replace(/\/$/, '')}/render-png`
|
/\/api\/render-svg$/.test(baseApiUrl)
|
||||||
|
? baseApiUrl.replace(/\/api\/render-svg$/, '/api/render-png')
|
||||||
|
: `${baseApiUrl.replace(/\/$/, '')}/render-png`
|
||||||
|
)
|
||||||
|
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: apiUrl,
|
url: apiUrl,
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ function run() {
|
|||||||
const wrapped = wrapTextByChars('123456', 2)
|
const wrapped = wrapTextByChars('123456', 2)
|
||||||
assert(wrapped === '12\n34\n56', 'wrapTextByChars 结果不符合预期')
|
assert(wrapped === '12\n34\n56', 'wrapTextByChars 结果不符合预期')
|
||||||
|
|
||||||
const fontFile = findFirstFontFile(path.join(__dirname, '..', 'frontend', 'public', 'fonts'))
|
const fontRootCandidates = [
|
||||||
|
path.join(__dirname, '..', 'fonts'),
|
||||||
|
path.join(__dirname, '..', 'frontend', 'public', 'fonts'),
|
||||||
|
]
|
||||||
|
const fontRoot = fontRootCandidates.find((dir) => fs.existsSync(dir))
|
||||||
|
assert(fontRoot, '未找到字体目录(fonts 或 frontend/public/fonts)')
|
||||||
|
|
||||||
|
const fontFile = findFirstFontFile(fontRoot)
|
||||||
assert(fontFile, '未找到可用字体文件')
|
assert(fontFile, '未找到可用字体文件')
|
||||||
|
|
||||||
const buffer = fs.readFileSync(fontFile)
|
const buffer = fs.readFileSync(fontFile)
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ set -e # 遇到错误立即退出
|
|||||||
# ===== 配置区域 =====
|
# ===== 配置区域 =====
|
||||||
SERVER="user@fonts.biboer.cn" # 请替换为你的 SSH 用户名
|
SERVER="user@fonts.biboer.cn" # 请替换为你的 SSH 用户名
|
||||||
REMOTE_DIR="/home/gavin/font2svg"
|
REMOTE_DIR="/home/gavin/font2svg"
|
||||||
LOCAL_FONTS_DIR="frontend/public/fonts"
|
REMOTE_MP_ASSETS_DIR="$REMOTE_DIR/miniprogram/assets"
|
||||||
LOCAL_FONTS_JSON="frontend/public/fonts.json"
|
LOCAL_FONTS_DIR="fonts"
|
||||||
|
LOCAL_WEB_FONTS_JSON="frontend/public/fonts.json"
|
||||||
|
LOCAL_WEB_DEFAULT_JSON="frontend/public/default.json"
|
||||||
|
LOCAL_MP_FONTS_JSON="miniprogram/assets/fonts.json"
|
||||||
|
LOCAL_MP_DEFAULT_JSON="miniprogram/assets/default.json"
|
||||||
|
|
||||||
# 颜色输出
|
# 颜色输出
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -38,8 +42,13 @@ check_files() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$LOCAL_FONTS_JSON" ]; then
|
if [ ! -f "$LOCAL_WEB_FONTS_JSON" ]; then
|
||||||
log_error "fonts.json 文件不存在: $LOCAL_FONTS_JSON"
|
log_error "Web fonts.json 文件不存在: $LOCAL_WEB_FONTS_JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$LOCAL_MP_FONTS_JSON" ]; then
|
||||||
|
log_error "小程序 fonts.json 文件不存在: $LOCAL_MP_FONTS_JSON"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -49,7 +58,7 @@ check_files() {
|
|||||||
|
|
||||||
create_remote_dirs() {
|
create_remote_dirs() {
|
||||||
log_info "创建远程目录..."
|
log_info "创建远程目录..."
|
||||||
ssh $SERVER "mkdir -p $REMOTE_DIR/fonts"
|
ssh $SERVER "mkdir -p $REMOTE_DIR/fonts $REMOTE_MP_ASSETS_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
upload_fonts() {
|
upload_fonts() {
|
||||||
@@ -65,34 +74,55 @@ upload_fonts() {
|
|||||||
log_info "字体文件上传完成"
|
log_info "字体文件上传完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
upload_fonts_json() {
|
upload_web_config() {
|
||||||
log_info "上传 fonts.json..."
|
log_info "上传 Web 配置..."
|
||||||
scp "$LOCAL_FONTS_JSON" "$SERVER:$REMOTE_DIR/"
|
scp "$LOCAL_WEB_FONTS_JSON" "$SERVER:$REMOTE_DIR/fonts.json"
|
||||||
log_info "fonts.json 上传完成"
|
if [ -f "$LOCAL_WEB_DEFAULT_JSON" ]; then
|
||||||
|
scp "$LOCAL_WEB_DEFAULT_JSON" "$SERVER:$REMOTE_DIR/default.json"
|
||||||
|
fi
|
||||||
|
log_info "Web 配置上传完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_miniprogram_config() {
|
||||||
|
log_info "上传小程序配置..."
|
||||||
|
scp "$LOCAL_MP_FONTS_JSON" "$SERVER:$REMOTE_MP_ASSETS_DIR/fonts.json"
|
||||||
|
if [ -f "$LOCAL_MP_DEFAULT_JSON" ]; then
|
||||||
|
scp "$LOCAL_MP_DEFAULT_JSON" "$SERVER:$REMOTE_MP_ASSETS_DIR/default.json"
|
||||||
|
else
|
||||||
|
log_warn "未找到小程序 default.json,已跳过: $LOCAL_MP_DEFAULT_JSON"
|
||||||
|
fi
|
||||||
|
log_info "小程序配置上传完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
set_permissions() {
|
set_permissions() {
|
||||||
log_info "设置文件权限..."
|
log_info "设置文件权限..."
|
||||||
ssh $SERVER "chmod -R 755 $REMOTE_DIR"
|
ssh $SERVER "chmod -R 755 $REMOTE_DIR/fonts $REMOTE_MP_ASSETS_DIR"
|
||||||
log_info "权限设置完成"
|
log_info "权限设置完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_deployment() {
|
verify_deployment() {
|
||||||
log_info "验证部署结果..."
|
log_info "验证部署结果..."
|
||||||
|
|
||||||
# 检查 fonts.json
|
# 检查小程序 fonts.json
|
||||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/fonts.json")
|
MP_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/miniprogram/assets/fonts.json")
|
||||||
|
|
||||||
if [ "$HTTP_CODE" = "200" ]; then
|
if [ "$MP_HTTP_CODE" = "200" ]; then
|
||||||
log_info "fonts.json 可访问 ✓"
|
log_info "小程序 fonts.json 可访问 ✓"
|
||||||
else
|
else
|
||||||
log_error "fonts.json 访问失败 (HTTP $HTTP_CODE)"
|
log_error "小程序 fonts.json 访问失败 (HTTP $MP_HTTP_CODE)"
|
||||||
log_warn "请检查 Cloudflare DNS 配置和 Nginx 配置"
|
log_warn "请检查 Cloudflare DNS 配置和 Nginx 配置"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
WEB_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/fonts.json")
|
||||||
|
if [ "$WEB_HTTP_CODE" = "200" ]; then
|
||||||
|
log_info "Web fonts.json 可访问 ✓"
|
||||||
|
else
|
||||||
|
log_warn "Web fonts.json 不可访问 (HTTP $WEB_HTTP_CODE)"
|
||||||
|
fi
|
||||||
|
|
||||||
# 检查 CORS 头
|
# 检查 CORS 头
|
||||||
CORS_HEADER=$(curl -s -I "https://fonts.biboer.cn/fonts.json" | grep -i "access-control-allow-origin")
|
CORS_HEADER=$(curl -s -I "https://fonts.biboer.cn/miniprogram/assets/fonts.json" | grep -i "access-control-allow-origin")
|
||||||
|
|
||||||
if [ -n "$CORS_HEADER" ]; then
|
if [ -n "$CORS_HEADER" ]; then
|
||||||
log_info "CORS 配置正确 ✓"
|
log_info "CORS 配置正确 ✓"
|
||||||
@@ -123,11 +153,12 @@ show_summary() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "2. 测试字体加载(在小程序开发者工具控制台):"
|
echo "2. 测试字体加载(在小程序开发者工具控制台):"
|
||||||
echo " wx.request({"
|
echo " wx.request({"
|
||||||
echo " url: 'https://fonts.biboer.cn/fonts.json',"
|
echo " url: 'https://fonts.biboer.cn/miniprogram/assets/fonts.json',"
|
||||||
echo " success: (res) => console.log(res.data)"
|
echo " success: (res) => console.log(res.data)"
|
||||||
echo " })"
|
echo " })"
|
||||||
echo ""
|
echo ""
|
||||||
echo "3. 验证 CDN 缓存状态:"
|
echo "3. 验证 CDN 缓存状态:"
|
||||||
|
echo " curl -I https://fonts.biboer.cn/miniprogram/assets/fonts.json | grep cf-cache-status"
|
||||||
echo " curl -I https://fonts.biboer.cn/fonts.json | grep cf-cache-status"
|
echo " curl -I https://fonts.biboer.cn/fonts.json | grep cf-cache-status"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
@@ -151,7 +182,8 @@ main() {
|
|||||||
check_files
|
check_files
|
||||||
create_remote_dirs
|
create_remote_dirs
|
||||||
upload_fonts
|
upload_fonts
|
||||||
upload_fonts_json
|
upload_web_config
|
||||||
|
upload_miniprogram_config
|
||||||
set_permissions
|
set_permissions
|
||||||
|
|
||||||
# 可选:重启 Nginx(需要 sudo 权限)
|
# 可选:重启 Nginx(需要 sudo 权限)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
生成字体清单文件
|
生成字体清单文件
|
||||||
扫描 frontend/public/fonts/ 目录下的所有字体文件,同时生成:
|
扫描 fonts/ 目录下的所有字体文件,同时生成:
|
||||||
1. frontend/public/fonts.json
|
1. frontend/public/fonts.json
|
||||||
2. miniprogram/assets/fonts.json
|
2. miniprogram/assets/fonts.json
|
||||||
3. miniprogram/assets/fonts.js
|
3. miniprogram/assets/fonts.js
|
||||||
@@ -11,7 +11,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def scan_fonts(font_dir='frontend/public/fonts'):
|
def scan_fonts(font_dir='fonts'):
|
||||||
"""扫描字体目录,返回字体信息列表"""
|
"""扫描字体目录,返回字体信息列表"""
|
||||||
fonts = []
|
fonts = []
|
||||||
font_dir_path = Path(font_dir)
|
font_dir_path = Path(font_dir)
|
||||||
@@ -34,19 +34,43 @@ def scan_fonts(font_dir='frontend/public/fonts'):
|
|||||||
|
|
||||||
relative_path = font_file.relative_to(font_dir_path).as_posix()
|
relative_path = font_file.relative_to(font_dir_path).as_posix()
|
||||||
|
|
||||||
# 生成字体信息
|
# 先生成基础信息,id 在后续统一按序号回填
|
||||||
font_info = {
|
font_info = {
|
||||||
'id': f"{category_name}/{font_file.stem}",
|
'id': '',
|
||||||
'name': font_file.stem,
|
'name': font_file.stem,
|
||||||
'filename': font_file.name,
|
'filename': font_file.name,
|
||||||
'category': category_name,
|
'category': category_name,
|
||||||
'path': f"/fonts/{relative_path}",
|
'relativePath': relative_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
fonts.append(font_info)
|
fonts.append(font_info)
|
||||||
|
|
||||||
|
# 统一排序后分配 4 位数字 id(0001、0002...)
|
||||||
|
fonts = sorted(fonts, key=lambda x: (x['category'], x['name'], x['filename']))
|
||||||
|
for index, font in enumerate(fonts, start=1):
|
||||||
|
font['id'] = f"{index:04d}"
|
||||||
|
|
||||||
return fonts
|
return fonts
|
||||||
|
|
||||||
|
|
||||||
|
def build_manifest(fonts, path_prefix):
|
||||||
|
"""根据路径前缀构建对外清单"""
|
||||||
|
prefix = f"/{str(path_prefix or '').strip('/')}"
|
||||||
|
if prefix == '/':
|
||||||
|
prefix = ''
|
||||||
|
|
||||||
|
manifest = []
|
||||||
|
for font in fonts:
|
||||||
|
manifest.append({
|
||||||
|
'id': font['id'],
|
||||||
|
'name': font['name'],
|
||||||
|
'filename': font['filename'],
|
||||||
|
'category': font['category'],
|
||||||
|
'path': f"{prefix}/{font['relativePath']}",
|
||||||
|
})
|
||||||
|
|
||||||
|
return manifest
|
||||||
|
|
||||||
def write_fonts_json(fonts, output_file):
|
def write_fonts_json(fonts, output_file):
|
||||||
"""写入字体清单 JSON 文件"""
|
"""写入字体清单 JSON 文件"""
|
||||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||||
@@ -66,15 +90,19 @@ def write_fonts_js(fonts, output_file):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
# 扫描字体(唯一来源:frontend/public/fonts)
|
# 扫描字体(唯一来源:仓库根目录 fonts/)
|
||||||
fonts = scan_fonts('frontend/public/fonts')
|
fonts = scan_fonts('fonts')
|
||||||
|
|
||||||
print(f"找到 {len(fonts)} 个字体文件")
|
print(f"找到 {len(fonts)} 个字体文件")
|
||||||
|
|
||||||
# 同步写入 Web 与小程序清单
|
# Web 清单:统一指向根目录 fonts
|
||||||
write_fonts_json(fonts, 'frontend/public/fonts.json')
|
web_fonts = build_manifest(fonts, '/fonts')
|
||||||
write_fonts_json(fonts, 'miniprogram/assets/fonts.json')
|
write_fonts_json(web_fonts, 'frontend/public/fonts.json')
|
||||||
write_fonts_js(fonts, 'miniprogram/assets/fonts.js')
|
|
||||||
|
# 小程序清单:同样指向根目录 fonts(与 web 共用一份字体目录)
|
||||||
|
miniprogram_fonts = build_manifest(fonts, '/fonts')
|
||||||
|
write_fonts_json(miniprogram_fonts, 'miniprogram/assets/fonts.json')
|
||||||
|
write_fonts_js(miniprogram_fonts, 'miniprogram/assets/fonts.js')
|
||||||
|
|
||||||
# 统计信息
|
# 统计信息
|
||||||
categories = {}
|
categories = {}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Font2SVG - fonts.json 生成脚本
|
Font2SVG - fonts.json 生成脚本
|
||||||
扫描 frontend/public/fonts/ 目录,生成小程序所需的 fonts.json
|
扫描 fonts/ 目录,生成字体清单
|
||||||
URL 格式:https://fonts.biboer.cn/fonts/{category}/{fontname}.ttf
|
URL 格式:https://fonts.biboer.cn/fonts/{category}/{fontname}.ttf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
# 配置
|
# 配置
|
||||||
BASE_URL = "https://fonts.biboer.cn/fonts"
|
BASE_URL = "https://fonts.biboer.cn/fonts"
|
||||||
FONTS_DIR = Path(__file__).parent.parent / "frontend" / "public" / "fonts"
|
FONTS_DIR = Path(__file__).parent.parent / "fonts"
|
||||||
OUTPUT_FILE = Path(__file__).parent.parent / "frontend" / "public" / "fonts.json"
|
OUTPUT_FILE = Path(__file__).parent.parent / "frontend" / "public" / "fonts.json"
|
||||||
|
|
||||||
def scan_fonts(fonts_dir: Path) -> list:
|
def scan_fonts(fonts_dir: Path) -> list:
|
||||||
@@ -51,9 +51,9 @@ def scan_fonts(fonts_dir: Path) -> list:
|
|||||||
encoded_filename = quote(font_file.name)
|
encoded_filename = quote(font_file.name)
|
||||||
url = f"{BASE_URL}/{encoded_category}/{encoded_filename}"
|
url = f"{BASE_URL}/{encoded_category}/{encoded_filename}"
|
||||||
|
|
||||||
# 创建字体信息对象
|
# 创建字体信息对象(id 在后续统一按序号回填)
|
||||||
font_info = {
|
font_info = {
|
||||||
"id": f"{category}/{font_name}",
|
"id": "",
|
||||||
"name": font_name,
|
"name": font_name,
|
||||||
"category": category,
|
"category": category,
|
||||||
"path": url,
|
"path": url,
|
||||||
@@ -71,7 +71,10 @@ def sort_fonts(fonts: list) -> list:
|
|||||||
1. 按分类排序
|
1. 按分类排序
|
||||||
2. 同分类内按名称排序
|
2. 同分类内按名称排序
|
||||||
"""
|
"""
|
||||||
return sorted(fonts, key=lambda x: (x["category"], x["name"]))
|
sorted_fonts = sorted(fonts, key=lambda x: (x["category"], x["name"]))
|
||||||
|
for index, font in enumerate(sorted_fonts, start=1):
|
||||||
|
font["id"] = f"{index:04d}"
|
||||||
|
return sorted_fonts
|
||||||
|
|
||||||
def save_fonts_json(fonts: list, output_file: Path):
|
def save_fonts_json(fonts: list, output_file: Path):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user