diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..48129ca --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +## 待办事项 + +- [ ] 拆分自由输入模式和固定输入模式 +- [ ] 使用 vitest 构建组件级别的测试 +- [ ] 为汉字和拼音的转换模块构建测试 \ No newline at end of file diff --git a/package.json b/package.json index df2739e..0d8d956 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "eslint --ext .ts,.vue src", "fix": "yarn lint --fix", "prepare": "husky install", - "test": "vitest" + "test": "vitest", + "watch": "vitest --watch" }, "dependencies": { "less": "^4.1.3", @@ -19,6 +20,7 @@ "vue-router": "4" }, "devDependencies": { + "@vitest/ui": "^0.27.0", "@napi-rs/pinyin": "^1.7.0", "@rushstack/eslint-patch": "^1.1.4", "@types/node": "^18.0.0", diff --git a/src/assets/arrow-left.svg b/src/assets/arrow-left.svg new file mode 100644 index 0000000..cd3df29 --- /dev/null +++ b/src/assets/arrow-left.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/ArticleInfo.vue b/src/components/ArticleInfo.vue new file mode 100644 index 0000000..181878a --- /dev/null +++ b/src/components/ArticleInfo.vue @@ -0,0 +1,212 @@ + + + + + + + + + + + {{ shortPinyin(pinyinAnswers) }} + + + + {{ articleProgress!.currentIndex }} 字 / + {{ articleProgress!.total }} 字 + + + {{ shortText(article.name) }} + + + + + + + 更换文章 + + + + + + + 删除文章 + + + + + + diff --git a/src/components/MenuList.vue b/src/components/MenuList.vue index 5cf2167..c730ab3 100644 --- a/src/components/MenuList.vue +++ b/src/components/MenuList.vue @@ -1,5 +1,5 @@ + + + + + + + + {{ p }} + + + + + {{ char.text }} + + + + + {{ p }} + + + + + + {{ p }} + + + + {{ text }} + + + {{ p }} + + + + + + diff --git a/src/env.d.ts b/src/env.d.ts index dc76cbb..1cf69f8 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -52,17 +52,6 @@ interface Combine { progress: Progress; } -type Article = - | { - type: RawArticleName; - progress: Progress; - } - | { - type: "CUSTOM"; - name: string; - progress: Progress; - }; - interface KeyConfig { main: Char; leads: string[]; @@ -78,15 +67,3 @@ interface Settings { } type Theme = "auto" | "dark" | "light"; - -interface AppState { - currentLeadIndex: number; - currentFollowIndex: number; - currentArticleIndex: number; - progresses: Record; - localConfigs: Record; - - combines: Combine[]; - articles: Article[]; - settings: Settings; -} diff --git a/src/pages/EditArticlePage.vue b/src/pages/EditArticlePage.vue new file mode 100644 index 0000000..2b22da6 --- /dev/null +++ b/src/pages/EditArticlePage.vue @@ -0,0 +1,122 @@ + + + + + + + + 保存文章 + + + + + + + diff --git a/src/pages/ParagraphMode.vue b/src/pages/ParagraphMode.vue new file mode 100644 index 0000000..b81c09b --- /dev/null +++ b/src/pages/ParagraphMode.vue @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/PragraphMode.vue b/src/pages/PragraphMode.vue deleted file mode 100644 index 9206e11..0000000 --- a/src/pages/PragraphMode.vue +++ /dev/null @@ -1,598 +0,0 @@ - - - - - - - - - - - - - {{ shortPinyin(article.answer) }} - - - - {{ article.progress.currentIndex }} 字 / - {{ article.progress.total }} 字 - - - {{ getShortName(article.name) }} - - - - - - - - - 删除文章 - - - - - - - - {{ s }} - - - - - - - - - 保存文章 - - - - - - - - - - - - - - - diff --git a/src/router.ts b/src/router.ts index 9d74c20..ffd956b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,7 +1,7 @@ import RandomMode from "./pages/RandomMode.vue"; import LeadMode from "./pages/LeadMode.vue"; import FollowMode from "./pages/FollowMode.vue"; -import ParagraphMode from "./pages/PragraphMode.vue"; +import ParagraphMode from "./pages/ParagraphMode.vue"; import Settings from "./pages/Settings.vue"; import { RouteRecordRaw } from "vue-router"; @@ -12,6 +12,11 @@ export const routes: RouteRecordRaw[] = [ name: "随机模式", component: RandomMode, }, + { + path: "/p-mode", + name: "长句模式", + component: ParagraphMode, + }, { path: "/lead-mode", name: "声母模式", @@ -22,11 +27,7 @@ export const routes: RouteRecordRaw[] = [ name: "韵母模式", component: FollowMode, }, - { - path: "/p-mode", - name: "长句模式", - component: ParagraphMode, - }, + { path: "/settings", name: "设置", diff --git a/src/store.ts b/src/store.ts index 9d61772..ee2872b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,53 +1,175 @@ import { defineStore } from "pinia"; import { PresetConfigs, ShuangpinConfig } from "./utils/keyboard"; -import rawArticles from "./utils/article.json"; +import PresetArticles from "./utils/article.json"; +import { map } from "./utils/common"; +import { getPinyinOf, nextValidHanziIndex } from "./utils/hanzi"; +import { matchSpToPinyin } from "./utils/keyboard"; +import { TypingSummary } from "./utils/summary"; + +const defaultAppState = { + currentLeadIndex: 0, + currentFollowIndex: 0, + progresses: {} as Record, + + // 用户自定义双拼配置 + localConfigs: {} as Record, + + // 用户自定义文章 + localArticles: {} as Record, + + currentArticleIndex: 0, + isEditingArticle: false, + + isFreeMode: true, + + // 输入状态 + currentPinYinInput: [] as string[], + isValidPinYinInput: false, + + // 输入统计 + summary: new TypingSummary(), + + settings: { + enableAutoClear: true, + enableKeyHint: true, + enablePinyinHint: true, + theme: "auto", + shuangpinMode: "小鹤双拼", + }, +}; declare global { - type RawArticleName = keyof typeof rawArticles; + type RawArticleName = keyof typeof PresetArticles; + type Article = { + name: string; + text: string; + type: "CUSTOM" | "PRESET"; + }; + + type AppState = typeof defaultAppState; } const cache: Record = {}; +function getOrCreateProgress( + progresses: Record, + name: string +) { + if (!progresses[name]) { + progresses[name] = { + currentIndex: 0, + total: 0, + correctTry: 0, + totalTry: 0, + }; + } + return progresses[name]; +} + export const useStore = defineStore("app", { state: (): AppState => { - return { - currentLeadIndex: 0, - currentFollowIndex: 0, - currentArticleIndex: 0, - progresses: {}, - localConfigs: {}, - - combines: [], - articles: [], - settings: { - enableAutoClear: true, - enableKeyHint: true, - enablePinyinHint: true, - theme: "auto", - shuangpinMode: "小鹤双拼", - }, - }; + return defaultAppState; }, getters: { modes(state) { return Object.keys(PresetConfigs).concat(Object.keys(state.localConfigs)); }, + articles(state) { + return { + ...map(PresetArticles, (name, text) => [ + name, + { name, text, type: "PRESET" }, + ]), + ...map(state.localArticles, (name, text) => [ + name, + { name, text, type: "CUSTOM" }, + ]), + } as Record; + }, + articleNames(state) { + return Object.keys(PresetArticles).concat( + Object.keys(state.localArticles) + ); + }, + currentArticleName(): string { + const name = this.articleNames[this.currentArticleIndex]; + return name; + }, + currentArticle(): Article { + const article = this.articles[this.currentArticleName]; + + return article; + }, + currentParagraphs(): string[] { + const paragraphs = this.currentArticle.text.split("\n"); + + return paragraphs; + }, + currentParagraphIndex(): { + paragraphIndex: number; + textIndex: number; + } { + const ret = { + paragraphIndex: 0, // 段落索引 + textIndex: 0, // 行内索引 + }; + + let totalCount = 0; + const currentTextIndex = this.currentArticleProgress.currentIndex; + + for (let i = 0; i < this.currentParagraphs.length; i += 1) { + const textCount = this.currentParagraphs[i].length + 1; + totalCount += textCount; + if (totalCount > currentTextIndex) { + ret.paragraphIndex = i; + ret.textIndex = currentTextIndex - totalCount + textCount; // 这里需要减去缺少的末尾换行符 + break; + } + } + + return ret; + }, + currentHanzi(): string { + return ( + this.currentArticle.text[this.currentArticleProgress.currentIndex] ?? "" + ); + }, + currentPinYinHints(): string[] { + return [...new Set(getPinyinOf(this.currentHanzi))]; + }, + + currentArticleProgress(state): Progress { + const progress = getOrCreateProgress( + state.progresses, + this.currentArticleName + ); + + progress.total = this.currentArticle.text.length; + progress.currentIndex = nextValidHanziIndex( + this.currentArticle.text, + progress.currentIndex + ); + + return progress; + }, }, actions: { getProgress(name: string) { - if (!this.progresses[name]) { - this.progresses[name] = { - currentIndex: 0, - total: 0, - correctTry: 0, - totalTry: 0, - }; - } - return this.progresses[name]; + return getOrCreateProgress(this.progresses, name); }, updateProgress(name: string, progress: Progress) { this.progresses[name] = progress; }, + updateProgressWithStep(progress: Progress, step = 1) { + progress.currentIndex = nextValidHanziIndex( + this.currentArticle.text, + progress.currentIndex + step + ); + + if (progress.currentIndex >= progress.total) { + progress.currentIndex = 0; + } + }, updateProgressOnValid(lead: string, follow: string, isValid: boolean) { for (const name of [lead, follow, lead + follow]) { const progress = this.getProgress(name); @@ -61,8 +183,7 @@ export const useStore = defineStore("app", { if (progress.correctTry === 0) return 0; return progress.correctTry / progress.totalTry; }, - - mode() { + mode(): ShuangpinConfig { const name = this.$state.settings.shuangpinMode; if (!cache[name]) { const config = this.loadConfig(name); @@ -73,6 +194,10 @@ export const useStore = defineStore("app", { } return cache[name]; }, + getShuangPinHints(): string[] { + const pinyin = this.currentPinYinHints[0] ?? ""; + return (this.mode().py2sp.get(pinyin) ?? "").split(""); + }, // 配置文件 saveConfig(name: string, config: RawShuangPinConfig) { @@ -96,6 +221,48 @@ export const useStore = defineStore("app", { } return new ShuangpinConfig(name, PresetConfigs[name as ShuangpinType]); }, + + // 文章 + updateArticleProgress(step = 1) { + const progress = this.currentArticleProgress; + this.updateProgressWithStep(progress, step); + this.updateProgress(this.currentArticleName, progress); + }, + saveArticle(article: Article) { + this.localArticles[article.name] = article.text; + }, + deleteArticle(name: string) { + delete this.localArticles[name]; + this.currentArticleIndex = Math.max(0, this.currentArticleIndex - 1); + }, + resetSummary() { + this.summary = new TypingSummary(); + }, + + // 输入 + onInputSequence([lead, follow]: [string?, string?]) { + const mode = this.mode(); + + for (const answer of this.currentPinYinHints) { + const res = matchSpToPinyin(mode, lead as Char, follow as Char, answer); + this.currentPinYinInput = [res.lead, res.follow].filter((v) => !!v); + + if (!!lead && !!follow) { + this.updateProgressOnValid(res.lead, res.follow, res.valid); + } + + this.isValidPinYinInput ||= res.valid; + + if (this.isValidPinYinInput) break; + } + + const isFullInput = !!lead && !!follow; + if (isFullInput) { + this.summary.onValid(this.isValidPinYinInput); + } + + return this.isValidPinYinInput; + }, }, persist: true, }); diff --git a/src/utils/common.ts b/src/utils/common.ts index 6e1b56f..bc856fd 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -5,3 +5,21 @@ export function download(name: string, value: string) { el.download = `${name}.json`; el.click(); } + +export function map( + m: { [k in K]: V }, + func: (_: K, __: V) => [T, U] +) { + return Object.fromEntries( + Object.entries(m).map(([k, v]) => func(k as K, v as V)) + ); +} + +export function shortText(s: string, maxLength = 10, ellipsisText = "...") { + if (s.length > maxLength) { + const end = Math.max(0, maxLength - 2); + return s.slice(0, end) + (maxLength > 0 ? ellipsisText : ""); + } + + return s; +} diff --git a/src/utils/hanzi.ts b/src/utils/hanzi.ts index ee07231..aed3730 100644 --- a/src/utils/hanzi.ts +++ b/src/utils/hanzi.ts @@ -3,18 +3,18 @@ import hanziTable from "./hanzi.json"; type HanziLib = typeof hanziTable.popular; function buildTable({ pinyin, hanzi }: HanziLib) { - const h2p = new Map; - const p2h = new Map; + const h2p = new Map(); + const p2h = new Map(); for (let i = 0; i < pinyin.length; i += 1) { const h = hanzi[i]; const ps = pinyin[i]; - h2p.set(h, ps) + h2p.set(h, ps); for (const p of ps) { - if (!p2h.has(p)) p2h.set(p, []) - p2h.get(p)?.push(h) + if (!p2h.has(p)) p2h.set(p, []); + p2h.get(p)?.push(h); } } @@ -29,5 +29,13 @@ export function getPinyinOf(hanzi: string) { } export function getHanziOf(pinyin: string) { - return hanziMap.p2h.get(pinyin) ?? '' + return hanziMap.p2h.get(pinyin) ?? ""; +} + +export function nextValidHanziIndex(text: string, currentIndex: number) { + while (currentIndex < text.length && !hanziMap.h2p.has(text[currentIndex])) { + currentIndex += 1; + } + + return currentIndex; } diff --git a/src/utils/pinyin.ts b/src/utils/pinyin.ts index 7a7de5b..4af5472 100644 --- a/src/utils/pinyin.ts +++ b/src/utils/pinyin.ts @@ -58,3 +58,20 @@ export const pinyinSummary = { export function getCombineOf(p: Pinyin) { return p.lead + p.follow; } + +/** + * 生成拼音的提示文字 + * @param pinyins 提示拼音列表 + * @returns + */ +export function shortPinyin(pinyins: string[]) { + const ret = []; + let count = 0; + for (const py of pinyins) { + if (count + py.length <= 10) { + count += py.length; + ret.push(py.toUpperCase()); + } + } + return ret.join("/"); +} diff --git a/src/utils/summary.ts b/src/utils/summary.ts index 78a1b46..890dc65 100644 --- a/src/utils/summary.ts +++ b/src/utils/summary.ts @@ -1,9 +1,85 @@ -export class TypingSummary { - constructor() {} +export type TypingProgress = { + currentCorrectCount: number; // 当前正确字符总数 + currentInputCount: number; // 本次输入字符数 +}; + +export class TypeSummary { + constructor(private lastCorrectCount = 0) {} + + update(progress: TypingProgress) { + this.totalValid += progress.currentInputCount; + this.totalCorrect += Math.max( + 0, + progress.currentCorrectCount - this.lastCorrectCount + ); + this.lastCorrectCount = progress.currentCorrectCount; + } + + reset() { + this.lastCorrectCount = 0; + } onKeyPressed() { this.pressCount += 1; + this.accumTime(); + } + + addListener(el: HTMLElement) { + el.addEventListener("keypress", this.keyPressEventListener); + el.addEventListener("compositionupdate", this.keyPressEventListener); + } + removeListener(el: HTMLElement) { + el.removeEventListener("keypress", this.keyPressEventListener); + el.removeEventListener("compositionupdate", this.keyPressEventListener); + } + + /** + * 累计时间,如果间隔大于 5s,则暂停计时 + */ + private accumTime() { + const time = performance.now(); + const diff = time - this.lastTime; + + // 跳过首次闲置时间,最高只记录 5s + if (this.lastTime > 0) { + this.totalTime += Math.min(5000, diff); + } + this.lastTime = time; + } + + get hanziPerMinutes() { + if (this.totalTime === 0) return 0; + return (this.totalCorrect / this.totalTime) * 1000 * 60; + } + + get pressPerHanzi() { + if (this.totalCorrect === 0) return 0; + return this.pressCount / this.totalCorrect; + } + + get accuracy() { + if (this.totalValid === 0) return 1; + return this.totalCorrect / this.totalValid; + } + + private lastTime = 0; + private totalTime = 0; + private pressCount = 0; + + private totalValid = 0; + private totalCorrect = 0; + + private keyPressEventListener = this.onKeyPressed.bind(this); +} + +export class TypingSummary { + constructor() { + this.keyPressEventListener = this.onKeyPressed.bind(this); + } + + onKeyPressed() { + this.pressCount += 1; this.accumTime(); } @@ -12,8 +88,18 @@ export class TypingSummary { this.totalCorrect += Number(result); } + addListener(el: HTMLElement) { + el.addEventListener("keypress", this.keyPressEventListener); + el.addEventListener("compositionupdate", this.keyPressEventListener); + } + + removeListener(el: HTMLElement) { + el.removeEventListener("keypress", this.keyPressEventListener); + el.removeEventListener("compositionupdate", this.keyPressEventListener); + } + /** - * 击键间隔大于 5s,不收集时间 + * 击键间隔大于 5s,暂停累计时间 */ private accumTime() { const time = performance.now(); @@ -43,8 +129,11 @@ export class TypingSummary { private lastTime = 0; private pressCount = 0; private totalTime = 0; + private totalValid = 0; private totalCorrect = 0; + + private keyPressEventListener = () => {}; } export type AchievementCond = diff --git a/src/utils/test/common.test.ts b/src/utils/test/common.test.ts new file mode 100644 index 0000000..c446d78 --- /dev/null +++ b/src/utils/test/common.test.ts @@ -0,0 +1,30 @@ +import { describe, test, expect } from "vitest"; +import { map, shortText } from "../common"; + +describe("积类型映射", () => { + test("Record -> Record", () => { + const testCase = { + a: 1, + b: 2, + }; + + expect(map(testCase, (a, b) => [a, b.toString()])).toMatchObject({ + a: "1", + b: "2", + }); + }); +}); + +describe("文本溢出省略 shortText", () => { + test("maxLength = 0, expect empty string", () => { + expect(shortText("abc", 0)).toBe(""); + }); + + test("input = 'abcd', maxLength = 4, expect 'abcd'", () => { + expect(shortText("abcd", 4)).toBe("abcd"); + }); + + test("input = 'abcd', maxLength = 3, expect 'a...'", () => { + expect(shortText("abcd", 3)).toBe("a..."); + }); +}); diff --git a/src/utils/test/summary.test.ts b/src/utils/test/summary.test.ts index 0d9ead9..2b156dd 100644 --- a/src/utils/test/summary.test.ts +++ b/src/utils/test/summary.test.ts @@ -1,8 +1,10 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi, it, beforeEach } from "vitest"; import { AchievementCond, achievementConds, finalAchievement, + TypeSummary, + TypingProgress, } from "../summary"; describe("成就系统", () => { @@ -17,3 +19,123 @@ describe("成就系统", () => { } }); }); + +describe("输入统计", () => { + let summary: TypeSummary; + + beforeEach(() => { + summary = new TypeSummary(); + }); + + describe("正常输入", () => { + it("初始情况", () => { + expect(summary.hanziPerMinutes).toBe(0); + expect(summary.pressPerHanzi).toBe(0); + expect(summary.accuracy).toBe(1); + }); + it("应当跳过首次闲置时间", () => { + summary.onKeyPressed(); + + expect(summary.hanziPerMinutes).toBe(0); + expect(summary.pressPerHanzi).toBe(0); + expect(summary.accuracy).toBe(1); + }); + it.each([ + [ + "输入一个正确字符", + { currentCorrectCount: 1, currentInputCount: 1 }, + { hanziPerMinutes: 60, pressPerHanzi: 2, accuracy: 1 }, + ], + [ + "输入两个正确字符", + { currentCorrectCount: 2, currentInputCount: 2 }, + { hanziPerMinutes: 120, pressPerHanzi: 1, accuracy: 1 }, + ], + [ + "输入一个错误字符", + { currentCorrectCount: 0, currentInputCount: 1 }, + { hanziPerMinutes: 0, pressPerHanzi: 0, accuracy: 0 }, + ], + [ + "输入两个错误字符", + { currentCorrectCount: 0, currentInputCount: 2 }, + { hanziPerMinutes: 0, pressPerHanzi: 0, accuracy: 0 }, + ], + ] as [string, TypingProgress, Pick][])( + "一秒内击键两次,%s", + (_, progress, expected) => { + vi.spyOn(performance, "now").mockReturnValue(1000); + summary.onKeyPressed(); + summary.update(progress); + vi.spyOn(performance, "now").mockReturnValue(2000); + summary.onKeyPressed(); + + expect(summary.hanziPerMinutes).toBe(expected.hanziPerMinutes); + expect(summary.pressPerHanzi).toBe(expected.pressPerHanzi); + expect(summary.accuracy).toBe(expected.accuracy); + } + ); + + test("输入一个错误字符,退格一个字符,再输入一个正确字符", () => { + summary = new TypeSummary(10); + vi.spyOn(performance, "now").mockReturnValue(1000); + summary.onKeyPressed(); + summary.update({ currentCorrectCount: 10, currentInputCount: 1 }); + vi.spyOn(performance, "now").mockReturnValue(2000); + summary.onKeyPressed(); + summary.update({ currentCorrectCount: 11, currentInputCount: 1 }); + + expect(summary.hanziPerMinutes).toBe(60); + expect(summary.pressPerHanzi).toBe(2); + expect(summary.accuracy).toBe(0.5); + }); + }); + + describe("暂停时间过长", () => { + it("两秒内击键三次,正常记录耗时", () => { + vi.spyOn(performance, "now").mockReturnValue(1000); + summary.onKeyPressed(); + vi.spyOn(performance, "now").mockReturnValue(2000); + summary.onKeyPressed(); + vi.spyOn(performance, "now").mockReturnValue(3000); + summary.onKeyPressed(); + summary.update({ currentCorrectCount: 1, currentInputCount: 1 }); + + expect(summary.hanziPerMinutes).toBe(30); + expect(summary.pressPerHanzi).toBe(3); + expect(summary.accuracy).toBe(1); + }); + it("两秒内击键三次,但最后一次暂停 5s 以上,则不记录最后一次间隔", () => { + vi.spyOn(performance, "now").mockReturnValue(1000); + summary.onKeyPressed(); + vi.spyOn(performance, "now").mockReturnValue(2000); + summary.onKeyPressed(); + vi.spyOn(performance, "now").mockReturnValue(7001); + summary.onKeyPressed(); + summary.update({ currentCorrectCount: 1, currentInputCount: 1 }); + + // 相当于 1s 内击键 2 次,输入一个汉字 + expect(summary.hanziPerMinutes).toBe(10); + expect(summary.pressPerHanzi).toBe(3); + expect(summary.accuracy).toBe(1); + }); + }); + + describe("重置统计", () => { + test("重置后不影响计算", () => { + vi.spyOn(performance, "now").mockReturnValue(1000); + summary.onKeyPressed(); + vi.spyOn(performance, "now").mockReturnValue(2000); + summary.onKeyPressed(); + summary.update({ currentCorrectCount: 1, currentInputCount: 1 }); + summary.reset(); + vi.spyOn(performance, "now").mockReturnValue(3000); + summary.onKeyPressed(); + summary.update({ currentCorrectCount: 1, currentInputCount: 2 }); + + expect(summary.hanziPerMinutes).toBe(60); + expect(summary.pressPerHanzi).toBe(1.5); + expect(summary.accuracy).toBe(2 / 3); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 95d05e7..c454cdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -183,6 +183,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@polka/url@^1.0.0-next.20": + version "1.0.0-next.21" + resolved "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" + integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== + "@rushstack/eslint-patch@^1.1.4": version "1.1.4" resolved "https://mirrors.cloud.tencent.com/npm/@rushstack%2feslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27" @@ -325,6 +330,15 @@ resolved "https://mirrors.cloud.tencent.com/npm/@vitejs%2fplugin-vue/-/plugin-vue-3.1.2.tgz#3cd52114e8871a0b5e7bd7d837469c032e503036" integrity sha512-3zxKNlvA3oNaKDYX0NBclgxTQ1xaFdL7PzwF6zj9tGFziKwmBa3Q/6XcJQxudlT81WxDjEhHmevvIC4Orc1LhQ== +"@vitest/ui@^0.27.0": + version "0.27.0" + resolved "https://registry.npmmirror.com/@vitest/ui/-/ui-0.27.0.tgz#3b4d3cbec5c0cf619127f5d4811efe39359722b5" + integrity sha512-eQmDfnNBB1c42eVqFSxvkNa+6OWA6O8QRQAp9oK0fdEicqToB+bts95+TrCsO2eXC0N9Q8GplFqRMqPLoJuVlA== + dependencies: + fast-glob "^3.2.12" + flatted "^3.2.7" + sirv "^2.0.2" + "@volar/language-core@1.0.6": version "1.0.6" resolved "https://mirrors.cloud.tencent.com/npm/@volar%2flanguage-core/-/language-core-1.0.6.tgz#8780d42f7d5a4190c95e83325428d20fd1c08ab2" @@ -1134,6 +1148,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://mirrors.cloud.tencent.com/npm/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.2.12: + version "3.2.12" + resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-glob@^3.2.9: version "3.2.11" resolved "https://mirrors.cloud.tencent.com/npm/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" @@ -1189,6 +1214,11 @@ flatted@^3.1.0: resolved "https://mirrors.cloud.tencent.com/npm/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== +flatted@^3.2.7: + version "3.2.7" + resolved "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + form-data@^4.0.0: version "4.0.0" resolved "https://mirrors.cloud.tencent.com/npm/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -1576,6 +1606,11 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" +mrmime@^1.0.0: + version "1.0.1" + resolved "https://registry.npmmirror.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + ms@2.1.2: version "2.1.2" resolved "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1877,6 +1912,15 @@ shebang-regex@^3.0.0: resolved "https://mirrors.cloud.tencent.com/npm/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +sirv@^2.0.2: + version "2.0.2" + resolved "https://registry.npmmirror.com/sirv/-/sirv-2.0.2.tgz#128b9a628d77568139cff85703ad5497c46a4760" + integrity sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w== + dependencies: + "@polka/url" "^1.0.0-next.20" + mrmime "^1.0.0" + totalist "^3.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://mirrors.cloud.tencent.com/npm/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1960,6 +2004,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +totalist@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd" + integrity sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw== + tough-cookie@^4.1.2: version "4.1.2" resolved "https://mirrors.cloud.tencent.com/npm/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
+ + {{ p }} + + + + + {{ char.text }} + + + + + {{ p }} +
+ + {{ p }} + + + + {{ text }} + + + {{ p }} +
- - {{ s }} - -