Browse Source
refactor(client): 外围组件原生化并清理死代码,缩小 SDK 体积
refactor(client): 外围组件原生化并清理死代码,缩小 SDK 体积
- t-select/t-dialog/t-alert/t-tag/t-button 替换为原生 DOM,删 5 条组件 import 与 omi 直接依赖,min 版缩小约 212KB - 清理死代码:images.d.ts、失效测试、dist 残留、getMessages、死类型、showAdminPanel、未用 i18n key、t-tag 死 CSS - 修复 i18n 缺失 confirm/cancel 翻译与自绘确认框 ESC 冲突master
12 changed files with 346 additions and 539 deletions
-
45client/README.md
-
25client/src/chat.ts
-
17client/src/config.ts
-
256client/src/dom.ts
-
32client/src/i18n.ts
-
5client/src/images.d.ts
-
13client/src/index.ts
-
10client/src/logger.ts
-
261client/src/styles.ts
-
35client/src/types.ts
-
100client/tests/i18n.test.ts
-
86client/tests/utils.test.ts
@ -1,5 +0,0 @@ |
|||
/** 图片导入类型声明(Rollup @rollup/plugin-image 会将 PNG 转为 base64 data URL) */ |
|||
declare module '*.png' { |
|||
const src: string; |
|||
export default src; |
|||
} |
|||
@ -1,100 +0,0 @@ |
|||
/** |
|||
* i18n.ts 国际化模块单元测试 |
|||
*/ |
|||
import { describe, it, expect, beforeEach } from 'vitest'; |
|||
import { t, setLocale, getLocale } from '../src/i18n'; |
|||
|
|||
describe('setLocale / getLocale', () => { |
|||
beforeEach(() => { |
|||
setLocale('zh-CN'); |
|||
}); |
|||
|
|||
it('默认语言为 zh-CN', () => { |
|||
expect(getLocale()).toBe('zh-CN'); |
|||
}); |
|||
|
|||
it('切换为英文', () => { |
|||
setLocale('en'); |
|||
expect(getLocale()).toBe('en'); |
|||
}); |
|||
|
|||
it('匹配语言前缀(zh → zh-CN)', () => { |
|||
setLocale('zh'); |
|||
expect(getLocale()).toBe('zh-CN'); |
|||
}); |
|||
|
|||
it('未知语言保持默认 zh-CN', () => { |
|||
setLocale('fr'); |
|||
expect(getLocale()).toBe('zh-CN'); |
|||
}); |
|||
}); |
|||
|
|||
describe('t()', () => { |
|||
beforeEach(() => { |
|||
setLocale('zh-CN'); |
|||
}); |
|||
|
|||
it('返回中文翻译', () => { |
|||
expect(t('send')).toBe('发送'); |
|||
expect(t('close')).toBe('关闭'); |
|||
}); |
|||
|
|||
it('切换英文后返回英文翻译', () => { |
|||
setLocale('en'); |
|||
expect(t('send')).toBe('Send'); |
|||
expect(t('close')).toBe('Close'); |
|||
}); |
|||
|
|||
it('未知 key 返回 key 本身', () => { |
|||
expect(t('nonexistent_key')).toBe('nonexistent_key'); |
|||
}); |
|||
|
|||
it('支持插值参数替换', () => { |
|||
setLocale('zh-CN'); |
|||
expect(t('source_count', { n: 3 })).toBe('3 条参考来源'); |
|||
}); |
|||
|
|||
it('英文插值参数替换', () => { |
|||
setLocale('en'); |
|||
expect(t('source_count', { n: 5 })).toBe('5 source(s)'); |
|||
}); |
|||
|
|||
it('中英文字典 key 数量一致', () => { |
|||
// 验证中英文字典完整性:所有中文 key 在英文中都有对应
|
|||
setLocale('zh-CN'); |
|||
const zhKeys = Object.keys(getAllKeys('zh-CN')); |
|||
setLocale('en'); |
|||
const enKeys = Object.keys(getAllKeys('en')); |
|||
|
|||
// 每个中文 key 在英文字典中也应存在
|
|||
for (const key of zhKeys) { |
|||
setLocale('en'); |
|||
const translated = t(key); |
|||
expect(translated).not.toBe(key); // 不应回退到 key 本身
|
|||
} |
|||
}); |
|||
}); |
|||
|
|||
/** 辅助函数:获取指定语言的所有 key(通过遍历测试已知 key) */ |
|||
function getAllKeys(_locale: string): Record<string, string> { |
|||
const knownKeys = [ |
|||
'title', 'minimize', 'close', 'status_online', |
|||
'welcome_title', 'welcome_desc', |
|||
'placeholder', 'send', 'stop', 'new_message', |
|||
'copy', 'copied', 'retry', |
|||
'loading', 'stream_interrupted', |
|||
'category_all', 'source_count', |
|||
'clear', 'clear_confirm', |
|||
'history_title', 'history_empty', 'history_search', |
|||
'feedback_up', 'feedback_down', |
|||
'teaser_text', 'new_msg_announce', |
|||
'error_network', 'error_timeout', 'error_send', |
|||
'history_group_today', 'history_group_yesterday', |
|||
'history_group_week', 'history_group_earlier', |
|||
]; |
|||
const result: Record<string, string> = {}; |
|||
for (const key of knownKeys) { |
|||
result[key] = t(key); |
|||
} |
|||
return result; |
|||
} |
|||
@ -1,86 +0,0 @@ |
|||
/** |
|||
* utils.ts 工具函数单元测试 |
|||
*/ |
|||
import { describe, it, expect, vi } from 'vitest'; |
|||
import { escapeHtml, debounce, formatTime, uuid, shortUuid } from '../src/utils'; |
|||
|
|||
describe('escapeHtml', () => { |
|||
it('转义 HTML 特殊字符', () => { |
|||
expect(escapeHtml('<script>alert("xss")</script>')).toBe( |
|||
'<script>alert("xss")</script>' |
|||
); |
|||
}); |
|||
|
|||
it('转义所有五种危险字符', () => { |
|||
expect(escapeHtml('&<>"\'')).toBe('&<>"''); |
|||
}); |
|||
|
|||
it('不修改安全文本', () => { |
|||
expect(escapeHtml('Hello World 你好世界')).toBe('Hello World 你好世界'); |
|||
}); |
|||
|
|||
it('空字符串返回空字符串', () => { |
|||
expect(escapeHtml('')).toBe(''); |
|||
}); |
|||
}); |
|||
|
|||
describe('uuid', () => { |
|||
it('生成符合 UUID v4 格式的字符串', () => { |
|||
const id = uuid(); |
|||
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); |
|||
}); |
|||
|
|||
it('两次生成不同的值', () => { |
|||
expect(uuid()).not.toBe(uuid()); |
|||
}); |
|||
}); |
|||
|
|||
describe('shortUuid', () => { |
|||
it('生成长度为 8 的字符串', () => { |
|||
expect(shortUuid()).toHaveLength(8); |
|||
}); |
|||
}); |
|||
|
|||
describe('formatTime', () => { |
|||
it('格式化为 HH:mm', () => { |
|||
// 2026-01-15 14:30:00 UTC
|
|||
const ts = new Date(2026, 0, 15, 14, 30, 0).getTime(); |
|||
expect(formatTime(ts)).toBe('14:30'); |
|||
}); |
|||
|
|||
it('补零处理', () => { |
|||
const ts = new Date(2026, 0, 1, 9, 5, 0).getTime(); |
|||
expect(formatTime(ts)).toBe('09:05'); |
|||
}); |
|||
}); |
|||
|
|||
describe('debounce', () => { |
|||
it('延迟执行函数', async () => { |
|||
vi.useFakeTimers(); |
|||
const fn = vi.fn(); |
|||
const debounced = debounce(fn, 100); |
|||
|
|||
debounced(); |
|||
expect(fn).not.toHaveBeenCalled(); |
|||
|
|||
vi.advanceTimersByTime(100); |
|||
expect(fn).toHaveBeenCalledTimes(1); |
|||
|
|||
vi.useRealTimers(); |
|||
}); |
|||
|
|||
it('连续调用只执行最后一次', async () => { |
|||
vi.useFakeTimers(); |
|||
const fn = vi.fn(); |
|||
const debounced = debounce(fn, 100); |
|||
|
|||
debounced(); |
|||
debounced(); |
|||
debounced(); |
|||
|
|||
vi.advanceTimersByTime(100); |
|||
expect(fn).toHaveBeenCalledTimes(1); |
|||
|
|||
vi.useRealTimers(); |
|||
}); |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue