wip
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
## 目标
|
||||
- 在 `onTyping` 方法(291-382行)中判断当前/下一个单词是否为人名,并在练习时自动忽略(不要求输入、不提示错误、不停顿等待空格)。
|
||||
|
||||
## 数据来源
|
||||
- 使用 `props.article.nameList: string[]`(来自编辑页保存),作为要忽略的人名列表。
|
||||
|
||||
## 匹配策略
|
||||
- 构建一个人名集合 `nameSet`:
|
||||
- `trim()` 后的字符串;
|
||||
- 若开启 `ignoreCase` 则统一转小写匹配。
|
||||
- 判定函数 `isNameWord(word: ArticleWord)`:
|
||||
- 仅当 `word.type === PracticeArticleWordType.Word` 时参与匹配;
|
||||
- 对 `word.word` 进行同样的规范化后 `nameSet.has(...)`。
|
||||
|
||||
## 处理时机与行为
|
||||
- 在 `onTyping` 开始处、拿到 `currentWord` 后:
|
||||
- 若是“人名”,则直接跳过本词;若该词 `nextSpace` 为真,连带空格也跳过(避免进入 `isSpace` 状态)。
|
||||
- 跳过后继续处理当前按键:复用已有模式(如 `isSpace` 分支里)调用 `next()` 和递归 `onTyping(e)`。
|
||||
- 在 `next()` 内也追加“人名跳过”逻辑(与已有忽略符号/数字类似),保证连续多个需要忽略的词可以被连续跳过:
|
||||
- 当 `currentWord` 是人名:
|
||||
- 若 `currentWord.nextSpace` 为真:`isSpace = false`;
|
||||
- 递归调用 `next()` 继续到下一个词;
|
||||
- 否则正常 `emit('nextWord', currentWord)`。
|
||||
|
||||
## 代码改动点
|
||||
- 在组件顶部或方法内构建 `nameSet`(建议用 `$computed`):
|
||||
```ts
|
||||
const nameSet = $computed(() => {
|
||||
const list = props.article?.nameList ?? []
|
||||
return new Set(list.map(s => (settingStore.ignoreCase ? s.toLowerCase() : s).trim()).filter(Boolean))
|
||||
})
|
||||
function isNameWord(w: ArticleWord) {
|
||||
if (w.type !== PracticeArticleWordType.Word) return false
|
||||
const token = (settingStore.ignoreCase ? w.word.toLowerCase() : w.word).trim()
|
||||
return nameSet.has(token)
|
||||
}
|
||||
```
|
||||
- 在 `onTyping` 里,`let currentWord = currentSentence.words[wordIndex]` 之后:
|
||||
```ts
|
||||
if (isNameWord(currentWord)) {
|
||||
// 跳过当前人名,连带空格
|
||||
isSpace = false
|
||||
const savedTypingFlag = isTyping
|
||||
next()
|
||||
isTyping = false
|
||||
return onTyping(e)
|
||||
}
|
||||
```
|
||||
- 在 `next()` 内,设置 `currentWord` 后、`emit('nextWord', currentWord)` 之前:
|
||||
```ts
|
||||
if (isNameWord(currentWord)) {
|
||||
// 人名与后续空格都跳过
|
||||
isSpace = false
|
||||
return next()
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
- 保持与现有忽略规则一致(符号/数字已通过 `ignoreSymbol` 处理),人名跳过逻辑与其同层级。
|
||||
- 忽略人名时不触发 `wrong` 或提示音,不进入 `isSpace` 等待。
|
||||
- 若连续出现多个需要忽略的词(如人名+标点+人名),递归 `next()` 将逐个跳过。
|
||||
|
||||
## 验证
|
||||
- 在包含人名的文本中练习:
|
||||
- 人名处不需要输入,光标自动跳到下一非忽略词;
|
||||
- 不会卡在空格等待;
|
||||
- 大小写忽略效果符合 `settingStore.ignoreCase` 设置。
|
||||
|
||||
## 交付
|
||||
- 按上述方案在 `TypingArticle.vue` 中实现辅助函数与两处调用点的改动;
|
||||
- 仅修改该文件,不影响其他页面。
|
||||
69
.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md
Normal file
69
.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md
Normal file
@@ -0,0 +1,69 @@
|
||||
## 目标
|
||||
- 在“人物名称管理”弹框中使用临时变量编辑名称列表,只在点击“确定”时写回 `editArticle.nameList: string[]`
|
||||
|
||||
## 数据结构
|
||||
- `editArticle.nameList: string[]`
|
||||
- 临时变量:`let nameListRef = $ref<string[]>([])`
|
||||
|
||||
## 生命周期
|
||||
- 弹框打开时初始化:`nameListRef = cloneDeep(editArticle.nameList || [])`
|
||||
- 弹框关闭时不写回:丢弃修改
|
||||
|
||||
## 交互设计
|
||||
- 弹框 `v-model="showNameDialog"`、`:footer="true"`、`@close="showNameDialog = false"`、`@ok="saveNameList"`
|
||||
- 按钮:
|
||||
- “添加名称” → `nameListRef.push('')`
|
||||
- 每行名称使用 `BaseInput v-model="nameListRef[i]"`
|
||||
- “删除”名称 → `nameListRef.splice(i,1)`
|
||||
|
||||
## 保存逻辑
|
||||
- `saveNameList()`:
|
||||
- 清理:`trim()` + `filter(Boolean)`
|
||||
- 写回:`editArticle.nameList = cleaned`
|
||||
- 关闭弹框:`showNameDialog = false`
|
||||
|
||||
## 实现细节(EditArticle.vue 增加)
|
||||
- 脚本:
|
||||
```ts
|
||||
let showNameDialog = $ref(false)
|
||||
let nameListRef = $ref<string[]>([])
|
||||
|
||||
watch(() => showNameDialog, (v) => {
|
||||
if (v) nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : [])
|
||||
})
|
||||
|
||||
function addName() { nameListRef.push('') }
|
||||
function removeName(i: number) { nameListRef.splice(i,1) }
|
||||
function saveNameList() {
|
||||
const cleaned = nameListRef.map(s => (s ?? '').trim()).filter(Boolean)
|
||||
editArticle.nameList = cleaned
|
||||
}
|
||||
```
|
||||
- 模板(620-628 区域):
|
||||
```vue
|
||||
<Dialog title="人物名称管理"
|
||||
v-model="showNameDialog"
|
||||
:footer="true"
|
||||
@close="showNameDialog = false"
|
||||
@ok="saveNameList">
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-base">配置需要忽略的人名,练习时自动忽略这些名称</div>
|
||||
<BaseButton size="small" type="info" @click="addName">添加名称</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2" v-for="(name,i) in nameListRef" :key="i">
|
||||
<BaseInput v-model="nameListRef[i]" placeholder="输入名称" size="large" />
|
||||
<BaseButton size="small" type="info" @click="removeName(i)">删除</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## 验证
|
||||
- 打开弹框 → 编辑临时列表 → 点击“确定”后检查 `editArticle.nameList` 是否更新;点击关闭则不更新
|
||||
|
||||
## 注意
|
||||
- 若类型仍为旧版 `string[][]`,请同步调整为 `string[]` 以与当前实现一致
|
||||
109
.trae/documents/静态首页移动端适配.md
Normal file
109
.trae/documents/静态首页移动端适配.md
Normal file
@@ -0,0 +1,109 @@
|
||||
## 目标与范围
|
||||
|
||||
* 适配 `public/static-home.html` 在 768px 以下与 480px 以下的移动端展示与触控体验
|
||||
|
||||
* 不改变页面文案与结构,只进行样式与布局响应式改造
|
||||
|
||||
## 结构与布局
|
||||
|
||||
* 将固定宽度 `.w { width: 60vw; }` 改为容器 `.container { width: min(1200px, 92%); }` 并在模板中替换为 `.container`
|
||||
|
||||
* `.card-wrap` 改为自适应栅格:`grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))`
|
||||
|
||||
* 移动端(<=768px)卡片单列展示;间距适当增大,避免拥挤
|
||||
|
||||
## 样式改造
|
||||
|
||||
* 标题缩放:
|
||||
|
||||
* `@media (max-width: 768px) h1 { font-size: 3rem; }`
|
||||
|
||||
* `@media (max-width: 480px) h1 { font-size: 2.4rem; }`
|
||||
|
||||
* 主按钮组:
|
||||
|
||||
* 移动端按钮全宽:`.base-button { width: 100%; margin: .5rem 0; height: 2.8rem; font-size: 1rem; }`
|
||||
|
||||
* 悬浮降低透明度改为轻微位移:`transform: translateY(-1px);`
|
||||
|
||||
* 内容区与间距:
|
||||
|
||||
* `@media (max-width: 768px) .content { margin-top: 4rem; gap: 1.4rem; }`
|
||||
|
||||
* `@media (max-width: 480px) .content { margin-top: 3.2rem; gap: 1.2rem; }`
|
||||
|
||||
* 赞助区 `.sky`:
|
||||
|
||||
* 图片强制全宽:`.sky a { width: 100% !important; } .sky-img { width: 100%; }`
|
||||
|
||||
* 移动端上下内边距缩小,减少跳动
|
||||
|
||||
* 卡片 `.card`:
|
||||
|
||||
* 移动端去掉固定 `width: 25%`,改为自适应栅格控制
|
||||
|
||||
* 增加触控阴影:`box-shadow: 0 6px 20px rgba(0,0,0,.08);`
|
||||
|
||||
* 底部链接 `.bottom`:
|
||||
|
||||
* 在 <=768px 改为纵向堆叠:`flex-direction: column; align-items: flex-start; gap: .6rem;`
|
||||
|
||||
## 示例变更片段(将添加到 <style> 内)
|
||||
|
||||
```css
|
||||
/* 容器与栅格 */
|
||||
.container { width: min(1200px, 92%); }
|
||||
.card-wrap { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.2rem; }
|
||||
.card { width: auto; box-shadow: 0 6px 20px rgba(0,0,0,.08); }
|
||||
|
||||
/* 标题与内容间距 */
|
||||
@media (max-width: 768px) {
|
||||
h1 { font-size: 3rem !important; }
|
||||
.content { margin-top: 4rem; gap: 1.4rem; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
h1 { font-size: 2.4rem !important; }
|
||||
.content { margin-top: 3.2rem; gap: 1.2rem; }
|
||||
}
|
||||
|
||||
/* 按钮移动端全宽 */
|
||||
@media (max-width: 768px) {
|
||||
.base-button { width: 100%; margin: .5rem 0; height: 2.8rem; font-size: 1rem; }
|
||||
}
|
||||
|
||||
/* 赞助区图片全宽 */
|
||||
.sky a { width: 100% !important; }
|
||||
.sky-img { width: 100%; }
|
||||
|
||||
/* 底部链接在移动端纵向排布 */
|
||||
@media (max-width: 768px) {
|
||||
.bottom { flex-direction: column; align-items: flex-start; gap: .6rem; }
|
||||
}
|
||||
```
|
||||
|
||||
## 交互与触控优化
|
||||
|
||||
* 增加触控可点击区域:`.icon { padding: .2rem; }`
|
||||
|
||||
* 禁用点击高亮:`html, body { -webkit-tap-highlight-color: transparent; }`
|
||||
|
||||
## 可访问性与表现
|
||||
|
||||
* 保持当前 `meta viewport` 设置,确保缩放正确
|
||||
|
||||
* 文本对比度在暗色模式下保持可读
|
||||
|
||||
## 验证方式
|
||||
|
||||
* 本地预览在 Chrome DevTools 切换 iPhone 14 / Pixel 7 视窗
|
||||
|
||||
* 检查首屏按钮是否折行、卡片是否单列、赞助图是否铺满
|
||||
|
||||
* 交互:滚动、点击弹窗与社交按钮触控区域确认
|
||||
|
||||
## 执行与交付
|
||||
|
||||
* 我将按上述片段更新 `<style>` 并把模板里的 `.w` 替换为 `.container`
|
||||
|
||||
* 修改完成后提供预览链接与截图确认
|
||||
|
||||
Reference in New Issue
Block a user