服务层 (Services)
4个 Service,TypeScript 实现 | 文件位于
web/src/services/
架构概览
services/
├── api-service.ts ← HTTP 请求统一封装(axios)
├── resource-service.ts ← 资源加载(笔顺/字源/音频,纯本地)
├── resource-cache.ts ← LRU 缓存
└── reward-service.ts ← 奖励发放(EXP/积分/徽章)api-service.ts — API 服务
文件: web/src/services/api-service.ts
职责
- 基于 axios 的统一 HTTP 请求封装
- 响应拦截器自动解包
{ code, msg, data }格式 - 切换后端只需修改
baseURL
API
typescript
export const api = {
get<T>(url, params?): Promise<T>
post<T>(url, data?): Promise<T>
put<T>(url, data?): Promise<T>
delete<T>(url): Promise<T>
}配置
baseURL:import.meta.env.VITE_API_BASE_URL || "/api/v1"timeout: 10000ms- 响应拦截器:
code !== 0时自动 reject,code === 0时返回data - 所有环境(开发/生产)均启用 Mock API(mockjs 拦截 axios 请求)
使用示例
typescript
// Store 中调用
const data = await api.get<UserData>("/users/me");
await api.put("/users/me", userData);
const chars = await api.get<{ list: Character[] }>("/characters", { pageSize: 500 });resource-service.ts — 资源服务
文件: web/src/services/resource-service.ts
职责
- Hanzi Writer 笔顺数据加载(本地 JSON,import.meta.glob + 请求去重)
- 字源 SVG 加载(本地 public/data/ 目录)
- 汉字音频播放(本地 MP3 → TTS 降级)
- 字源索引缓存管理(localStorage → 内存 → 本地文件)
核心函数
| 函数 | 说明 |
|---|---|
getHanziWriterData(char) | 获取笔顺数据(LRU缓存 + 请求去重,同一汉字并发复用 Promise) |
getEtymologySVGUrl(char, stage) | 获取字源 SVG 本地 URL |
fetchEtymologyIndex() | 获取字源索引(localStorage → 内存 → 本地文件) |
playAudio(char, pronunciation) | 播放汉字音频(本地 MP3 → TTS 降级) |
getAudioUrl(pronunciation) | 获取音频本地 URL |
checkEtymologyAvailable(char, stage) | 预检查字源 SVG 可用性(Image 预加载,3s 超时) |
fetchEtymologyIndexRaw() | 获取字源索引原始 JSON(供 character.vue) |
isEtymologyAvailableByIndex(char, script) | 通过缓存判断字源可用性(零延迟) |
资源加载策略
| 资源 | 策略 |
|---|---|
| HanziWriter | 本地 JSON (import.meta.glob) + LRU 缓存 + 请求去重 |
| EtymologySVG | 本地 public/data/etymology-svg/ |
| 字源索引 | localStorage → 内存缓存 → local index.json |
| HanziAudio | 本地 public/data/hanzi-audio/ → TTS 降级 |
resource-cache.ts — 资源缓存
文件: web/src/services/resource-cache.ts
职责
- LRU 缓存策略,最大容量 50 个条目
- 缓存 Hanzi Writer 笔顺数据等高频资源
- 持久化到 localStorage(
tzxq_resource_cache),500ms 防抖写入
方法
| 方法 | 说明 |
|---|---|
resourceCache.get(key) | 获取缓存值(命中时移到最新位置) |
resourceCache.set(key, value) | 设置缓存值(满时淘汰最旧条目) |
resourceCache.has(key) | 检查缓存键是否存在 |
resourceCache.clear() | 清空所有缓存 |
resourceCache.size() | 获取当前缓存条目数 |
reward-service.ts — 奖励服务
文件: web/src/services/reward-service.ts
职责
- 所有模块的经验值(EXP)/ 积分(Points)/ 徽章发放统一入口
- 确保奖励规则一致、去重、可追溯
奖励常量
| 常量 | EXP | 积分 | 触发场景 |
|---|---|---|---|
WRITE_COMPLETE | 10 | 1 | 完成一次书写练习 |
WRITE_PERFECT | 10 | 1 | 书写满分 |
FIRST_LEARN | 5 | 1 | 首次学会某个汉字 |
RECOGNITION_PASS | 5 | 0 | 认读通过 |
RECOGNITION_FAIL | 1 | 0 | 认读未通过 |
REVIEW_CHAR | 5 | 1 | 完成单个汉字复习全流程 |
DAILY_REVIEW_FULL | 50 | 5 | 每日复习完成率 100% |
DAILY_REVIEW_80 | 30 | 3 | 每日复习完成率 ≥ 80% |
BADGE_UNLOCK | 50 | 5 | 解锁新徽章 |
核心函数
| 函数 | 调用入口 |
|---|---|
awardWriteReward(char, score, isFirstLearn?) | 书写模块 |
awardRecognitionReward(char, score) | 认读模块 |
awardReviewCharReward(char) | 复习模块(带去重) |
awardDailyReviewReward(percent) | 每日复习完成 |
awardGameReward(gameType, level, stars, levelConfig) | 3 款游戏 |
awardBadgeReward(badgeId) | 徽章解锁 |
resetReviewDedup() | 新会话开始时 |
数据流
页面组件 → Pinia Store → api-service.ts (axios)
↓ (所有环境)
Mock API (mockjs 拦截)
↓
mock/data/index.ts (JSON 数据)
↓
localStorage (tzxq_mock_db)资源流:
页面组件 → resource-service.ts → 本地 JSON/文件 → TTS 降级
↓
resource-cache.ts (LRU 缓存,localStorage 持久化)