2026-04-07-english-word-daily
# English Word Daily Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a standalone VuePress page tool that generates printable A4 English spelling practice worksheets as downloadable PNG pages.
Architecture: Add a new Vue component for the tool and a small tool-specific CommonJS logic module so parsing, validation, pagination, and file naming can be tested without a browser canvas. Keep this separate from PinyinDictationTool.vue; do not extract shared helpers or change the pinyin tool.
Tech Stack: VuePress 1, Vue single-file component, browser canvas, Node assert for lightweight logic and integration checks, existing UploadData.vue tracking component.
# Task 1: Add Logic Tests
Files:
Create:
scripts/test_english_word_daily_logic.jsLater create:
docs/.vuepress/components/EnglishWordDailyWorksheetLogic.js[ ] Step 1: Write the failing logic test
Create scripts/test_english_word_daily_logic.js with:
const assert = require('assert')
const logic = require('../docs/.vuepress/components/EnglishWordDailyWorksheetLogic')
assert.deepStrictEqual(logic.parseWords('cat\n dog \n\nsun'), ['cat', 'dog', 'sun'])
assert.throws(() => logic.parseWords(' \n '), /请输入至少一个英文单词/)
assert.strictEqual(
logic.parseIntegerOption('5', { label: '听写行数', defaultValue: 5, min: 0, max: 20 }),
5
)
assert.strictEqual(
logic.parseIntegerOption('', { label: '听写行数', defaultValue: 5, min: 0, max: 20 }),
5
)
assert.throws(
() => logic.parseIntegerOption('21', { label: '听写行数', defaultValue: 5, min: 0, max: 20 }),
/听写行数必须在 0 到 20 之间/
)
assert.deepStrictEqual(
logic.paginateWords(['cat', 'dog', 'sun', 'hat', 'pen'], 2),
[['cat', 'dog'], ['sun', 'hat'], ['pen']]
)
assert.deepStrictEqual(
logic.paginateWords(['one', 'two', 'three', 'four', 'five'], 5, 3),
[['one', 'two'], ['three', 'four', 'five']]
)
assert.strictEqual(logic.estimateFinalPageWordCapacity(5, 12), 10)
assert.strictEqual(logic.estimateFinalPageWordCapacity(20, 12), 3)
assert.strictEqual(logic.buildWorksheetFileName('2026-04-07', 0), 'worksheet_2026-04-07_01.png')
assert.strictEqual(logic.buildWorksheetFileName('', 1), 'worksheet_untitled_02.png')
assert.strictEqual(logic.sanitizeFileName('a/b:c*?'), 'a_b_c__')
console.log('english word daily logic tests passed')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- [ ] Step 2: Run the test and verify it fails for the missing module
Run:
node scripts/test_english_word_daily_logic.js
Expected: FAIL with Cannot find module '../docs/.vuepress/components/EnglishWordDailyWorksheetLogic'.
# Task 2: Add Tool-Specific Logic Module
Files:
Create:
docs/.vuepress/components/EnglishWordDailyWorksheetLogic.jsTest:
scripts/test_english_word_daily_logic.js[ ] Step 1: Implement the minimal logic module
Create docs/.vuepress/components/EnglishWordDailyWorksheetLogic.js with:
const DEFAULT_WORDS = ['cat', 'dog', 'sun', 'hat', 'pen', 'map', 'fish', 'bed', 'cake', 'milk']
const LAYOUT = {
pageHeight: 3508,
margin: 150,
headerHeight: 320,
headerBottomGap: 70,
wordBlockHeight: 190,
wordBlockGap: 16,
dictationTopGap: 70,
dictationTitleHeight: 88,
dictationLineHeight: 100,
}
function sanitizeFileName(name) {
return String(name || 'untitled').trim().replace(/[\\/:*?"<>|]/g, '_') || 'untitled'
}
function parseWords(rawText) {
const words = String(rawText || '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
if (words.length === 0) {
throw new Error('请输入至少一个英文单词')
}
return words
}
function parseIntegerOption(value, options) {
const raw = String(value == null ? '' : value).trim()
const number = raw === '' ? options.defaultValue : Number(raw)
if (!Number.isInteger(number)) {
throw new Error(`${options.label}必须是整数`)
}
if (number < options.min || number > options.max) {
throw new Error(`${options.label}必须在 ${options.min} 到 ${options.max} 之间`)
}
return number
}
function estimateFinalPageWordCapacity(dictationCount, maxWordsPerPage) {
const contentTop = LAYOUT.margin + LAYOUT.headerHeight + LAYOUT.headerBottomGap
const availableHeight = LAYOUT.pageHeight - LAYOUT.margin - contentTop
const dictationHeight =
dictationCount > 0
? LAYOUT.dictationTopGap + LAYOUT.dictationTitleHeight + dictationCount * LAYOUT.dictationLineHeight
: 0
const wordStep = LAYOUT.wordBlockHeight + LAYOUT.wordBlockGap
const capacity = Math.floor((availableHeight - dictationHeight + LAYOUT.wordBlockGap) / wordStep)
return Math.max(0, Math.min(maxWordsPerPage, capacity))
}
function paginateWords(words, maxWordsPerPage, finalPageWordCapacity = maxWordsPerPage) {
const finalCapacity = Math.max(1, Math.min(maxWordsPerPage, finalPageWordCapacity))
if (words.length <= finalCapacity) {
return [words.slice()]
}
const pages = []
const regularWords = words.slice(0, words.length - finalCapacity)
for (let index = 0; index < regularWords.length; index += maxWordsPerPage) {
pages.push(regularWords.slice(index, index + maxWordsPerPage))
}
pages.push(words.slice(words.length - finalCapacity))
return pages
}
function buildWorksheetFileName(date, pageIndex) {
const safeDate = sanitizeFileName(date || 'untitled')
return `worksheet_${safeDate}_${String(pageIndex + 1).padStart(2, '0')}.png`
}
module.exports = {
DEFAULT_WORDS,
LAYOUT,
sanitizeFileName,
parseWords,
parseIntegerOption,
estimateFinalPageWordCapacity,
paginateWords,
buildWorksheetFileName,
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
- [ ] Step 2: Run the logic test and verify it passes
Run:
node scripts/test_english_word_daily_logic.js
Expected: PASS and print english word daily logic tests passed.
- [ ] Step 3: Commit the logic test and module
Run:
git add scripts/test_english_word_daily_logic.js docs/.vuepress/components/EnglishWordDailyWorksheetLogic.js
git commit -m "feat: add english worksheet logic"
2
# Task 3: Add Integration Test For New Page Wiring
Files:
Create:
scripts/test_english_word_daily_wiring.jsLater create:
docs/.vuepress/components/EnglishWordDailyTool.vueLater create:
docs/07.工具/30.英语单词日课.mdLater modify:
docs/.vuepress/config.tsLater modify:
docs/07.工具/00.概览.md[ ] Step 1: Write the failing wiring test
Create scripts/test_english_word_daily_wiring.js with:
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const root = path.resolve(__dirname, '..')
function read(relativePath) {
return fs.readFileSync(path.join(root, relativePath), 'utf8')
}
assert.ok(fs.existsSync(path.join(root, 'docs/.vuepress/components/EnglishWordDailyTool.vue')))
const page = read('docs/07.工具/30.英语单词日课.md')
assert.match(page, /permalink: \/tools\/english-word-daily\//)
assert.match(page, /<EnglishWordDailyTool \/>/)
const config = read('docs/.vuepress/config.ts')
assert.match(config, /English Word Daily/)
assert.match(config, /\/tools\/english-word-daily\//)
const overview = read('docs/07.工具/00.概览.md')
assert.match(overview, /英语单词日课/)
assert.match(overview, /\/tools\/english-word-daily\//)
console.log('english word daily wiring tests passed')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- [ ] Step 2: Run the wiring test and verify it fails
Run:
node scripts/test_english_word_daily_wiring.js
Expected: FAIL because docs/.vuepress/components/EnglishWordDailyTool.vue does not exist.
# Task 4: Add English Word Daily Vue Component
Files:
Create:
docs/.vuepress/components/EnglishWordDailyTool.vueUse:
docs/.vuepress/components/EnglishWordDailyWorksheetLogic.jsUse:
docs/.vuepress/components/UploadData.vueTest:
scripts/test_english_word_daily_logic.js[ ] Step 1: Create the component template
Create docs/.vuepress/components/EnglishWordDailyTool.vue with a template containing:
<template>
<section class="english-word-tool">
<UploadData
v-if="trackingReady"
event-type="english_word_daily_tool_view"
page-name="english-word-daily"
:title="title || DEFAULT_TITLE"
:visitor-key="visitorKey"
:visit-id="visitId"
:fingerprint-info="fingerprintInfo"
:client-kind="clientKind"
:device-type="deviceType"
:browser-family="browserFamily"
:os-family="osFamily"
source="english-word-daily-tool"
/>
<h2>英语单词日课</h2>
<p class="tool-desc">输入当天单词,生成适合打印的 Copy + Recall 英语拼写练习纸。</p>
<div class="form-grid">
<label class="field-label">标题<input v-model.trim="title" class="text-input" type="text" /></label>
<label class="field-label">副标题<input v-model.trim="subtitle" class="text-input" type="text" /></label>
<label class="field-label">日期<input v-model.trim="practiceDate" class="text-input" type="date" /></label>
<label class="field-label">学生名<input v-model.trim="student" class="text-input" type="text" placeholder="可选" /></label>
<label class="field-label">Level<input v-model.trim="level" class="text-input" type="text" placeholder="可选" /></label>
<label class="field-label">时间<input v-model.trim="durationHint" class="text-input" type="text" /></label>
<label class="field-label">听写行数<input v-model.trim="dictationCountInput" class="text-input" type="number" min="0" max="20" /></label>
<label class="field-label">每页单词数<input v-model.trim="maxWordsPerPageInput" class="text-input" type="number" min="1" max="12" /></label>
</div>
<div class="panel">
<label class="field-label" for="wordInput">单词列表(每行一个)</label>
<textarea id="wordInput" v-model="rawWords" class="data-input" spellcheck="false" />
</div>
<div class="actions">
<button class="btn primary" type="button" @click="generateSheets" :disabled="isRendering">
{{ isRendering ? '生成中...' : '生成练习纸' }}
</button>
<span class="meta" v-if="pages.length">共 {{ pages.length }} 页</span>
</div>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<section v-if="pages.length > 0" class="result-section">
<h3>练习纸预览</h3>
<div class="preview-list">
<article v-for="(page, index) in pages" :key="page.fileName" class="preview-card">
<header class="preview-head">
<strong>第 {{ index + 1 }} 页</strong>
<a class="download-link" :href="page.dataUrl" :download="page.fileName" @click="trackDownload(1)">下载本页</a>
</header>
<img :src="page.dataUrl" :alt="`英语单词日课第${index + 1}页`" class="preview-image" />
</article>
</div>
</section>
</section>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
- [ ] Step 2: Add script with canvas rendering
In the same component, add a <script> block that:
const worksheetLogic = require('./EnglishWordDailyWorksheetLogic')
import UploadData from './UploadData.vue'
const PAGE_SIZE = { width: 2480, height: 3508 }
const MARGIN = 150
const HEADER_HEIGHT = 320
const HEADER_BOTTOM_GAP = 70
const WORD_BLOCK_HEIGHT = 190
const WORD_BLOCK_GAP = 16
const DICTATION_TOP_GAP = 70
const DICTATION_LINE_HEIGHT = 100
const FONT_FAMILY = 'Arial, Helvetica, sans-serif'
const DEFAULT_TITLE = 'Spelling Practice'
const DEFAULT_SUBTITLE = 'Copy + Recall'
const DEFAULT_DURATION = '10-15 min'
function todayIsoDate() {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function createPageCanvas() {
const canvas = document.createElement('canvas')
canvas.width = PAGE_SIZE.width
canvas.height = PAGE_SIZE.height
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, PAGE_SIZE.width, PAGE_SIZE.height)
return { canvas, ctx }
}
function drawLine(ctx, x1, y1, x2, y2, width = 3) {
ctx.save()
ctx.strokeStyle = '#000'
ctx.lineWidth = width
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
ctx.restore()
}
function drawHeader(ctx, state) {
ctx.save()
ctx.fillStyle = '#000'
ctx.textBaseline = 'top'
ctx.font = `bold 76px ${FONT_FAMILY}`
ctx.fillText(state.title || DEFAULT_TITLE, MARGIN, MARGIN)
ctx.font = `42px ${FONT_FAMILY}`
ctx.fillText(state.subtitle || DEFAULT_SUBTITLE, MARGIN, MARGIN + 92)
ctx.font = `40px ${FONT_FAMILY}`
ctx.fillText(`Date: ${state.practiceDate || ''}`, MARGIN, MARGIN + 168)
ctx.fillText(`Name: ${state.student || '__________'}`, MARGIN + 820, MARGIN + 168)
if (state.level) {
ctx.fillText(`Level: ${state.level}`, MARGIN, MARGIN + 230)
}
if (state.durationHint) {
ctx.fillText(`Time: ${state.durationHint}`, MARGIN + 820, MARGIN + 230)
}
drawLine(ctx, MARGIN, MARGIN + HEADER_HEIGHT, PAGE_SIZE.width - MARGIN, MARGIN + HEADER_HEIGHT, 4)
ctx.restore()
}
function drawPracticeBlock(ctx, word, number, y) {
const x = MARGIN
const lineStart = MARGIN + 430
const lineEnd = PAGE_SIZE.width - MARGIN
ctx.save()
ctx.fillStyle = '#000'
ctx.textBaseline = 'top'
ctx.font = `bold 54px ${FONT_FAMILY}`
ctx.fillText(`${number}. ${word}`, x, y)
ctx.font = `42px ${FONT_FAMILY}`
ctx.fillText('Copy:', x + 28, y + 68)
ctx.font = `bold 42px ${FONT_FAMILY}`
ctx.fillText(word, lineStart, y + 68)
ctx.font = `42px ${FONT_FAMILY}`
ctx.fillText('Recall 1:', x + 28, y + 108)
drawLine(ctx, lineStart, y + 145, lineEnd, y + 145, 3)
ctx.fillText('Recall 2:', x + 28, y + 148)
drawLine(ctx, lineStart, y + 185, lineEnd, y + 185, 3)
ctx.restore()
}
function drawDictation(ctx, count, y) {
if (count <= 0) {
return
}
ctx.save()
ctx.fillStyle = '#000'
ctx.textBaseline = 'top'
ctx.font = `bold 54px ${FONT_FAMILY}`
ctx.fillText('Dictation', MARGIN, y)
ctx.font = `42px ${FONT_FAMILY}`
const lineStart = MARGIN + 150
const lineEnd = PAGE_SIZE.width - MARGIN
for (let index = 0; index < count; index += 1) {
const lineY = y + 88 + index * DICTATION_LINE_HEIGHT
ctx.fillText(`${index + 1}.`, MARGIN + 20, lineY - 34)
drawLine(ctx, lineStart, lineY, lineEnd, lineY, 3)
}
ctx.restore()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
- [ ] Step 3: Add Vue data and methods
In the component export default, include:
export default {
name: 'EnglishWordDailyTool',
components: { UploadData },
data() {
return {
DEFAULT_TITLE,
title: DEFAULT_TITLE,
subtitle: DEFAULT_SUBTITLE,
practiceDate: todayIsoDate(),
student: '',
level: '',
durationHint: DEFAULT_DURATION,
rawWords: worksheetLogic.DEFAULT_WORDS.join('\n'),
dictationCountInput: '5',
maxWordsPerPageInput: '10',
pages: [],
errorMessage: '',
isRendering: false,
trackingReady: false,
trackingEndpoint: 'https://wangmouren.online:9000/save_data_pv',
visitorKey: '',
visitId: '',
fingerprintInfo: '',
clientKind: '',
deviceType: '',
browserFamily: '',
osFamily: '',
}
},
mounted() {
this.initializeTracking()
this.generateSheets()
},
methods: {
initializeTracking() {
if (typeof window === 'undefined') return
this.visitorKey = this.ensureVisitorKey()
this.visitId = this.createTrackingId('visit')
this.assignClientProfile()
this.trackingReady = true
},
createTrackingId(prefix) {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return `${prefix}_${window.crypto.randomUUID()}`
}
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
},
ensureVisitorKey() {
const storageKey = 'english-word-daily-visitor-key'
const existing = window.localStorage.getItem(storageKey)
if (existing) return existing
const nextId = this.createTrackingId('visitor')
window.localStorage.setItem(storageKey, nextId)
return nextId
},
assignClientProfile() {
const nav = window.navigator || {}
const userAgent = nav.userAgent || ''
const platform = nav.platform || ''
this.clientKind = nav.webdriver ? 'bot' : 'browser'
this.deviceType = /android|iphone|ipad|ipod|mobile/i.test(userAgent) ? 'mobile' : 'desktop'
this.browserFamily = /edg\//i.test(userAgent) ? 'Edge' : /chrome\//i.test(userAgent) ? 'Chrome' : /safari\//i.test(userAgent) ? 'Safari' : /firefox\//i.test(userAgent) ? 'Firefox' : 'Unknown'
this.osFamily = /windows/i.test(userAgent) || /win/i.test(platform) ? 'Windows' : /mac os x|macintosh/i.test(userAgent) || /mac/i.test(platform) ? 'macOS' : /android/i.test(userAgent) ? 'Android' : /iphone|ipad|ipod/i.test(userAgent) ? 'iOS' : /linux/i.test(userAgent) ? 'Linux' : 'Unknown'
this.fingerprintInfo = `${this.clientKind}|${this.deviceType}|${this.browserFamily}|${this.osFamily}`
},
trackEvent(eventType, extraPayload = {}) {
if (typeof window === 'undefined') return false
const body = JSON.stringify({
type: eventType,
page: 'english-word-daily',
title: this.title || DEFAULT_TITLE,
path: this.$route && this.$route.path ? this.$route.path : window.location.pathname,
visitorKey: this.visitorKey,
visitId: this.visitId,
fingerprintInfo: this.fingerprintInfo,
clientKind: this.clientKind,
deviceType: this.deviceType,
browserFamily: this.browserFamily,
osFamily: this.osFamily,
source: 'english-word-daily-tool',
timestamp: Date.now(),
...extraPayload,
})
if (navigator.sendBeacon && navigator.sendBeacon(this.trackingEndpoint, body)) return true
if (window.fetch) {
fetch(this.trackingEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, keepalive: true }).catch(() => {})
return true
}
return false
},
generateSheets() {
this.errorMessage = ''
this.isRendering = true
this.$nextTick(() => {
try {
const words = worksheetLogic.parseWords(this.rawWords)
const dictationCount = worksheetLogic.parseIntegerOption(this.dictationCountInput, { label: '听写行数', defaultValue: 5, min: 0, max: 20 })
const maxWordsPerPage = worksheetLogic.parseIntegerOption(this.maxWordsPerPageInput, { label: '每页单词数', defaultValue: 10, min: 1, max: 12 })
const finalPageWordCapacity = worksheetLogic.estimateFinalPageWordCapacity(dictationCount, maxWordsPerPage)
const wordPages = worksheetLogic.paginateWords(words, maxWordsPerPage, finalPageWordCapacity)
this.pages = this.renderPages(wordPages, dictationCount)
this.trackEvent('english_word_daily_generate', { rowCount: words.length, pageCount: this.pages.length })
} catch (error) {
this.pages = []
this.errorMessage = error && error.message ? error.message : '生成失败,请检查输入'
this.trackEvent('english_word_daily_generate_error', { error: this.errorMessage })
} finally {
this.isRendering = false
}
})
},
renderPages(wordPages, dictationCount) {
let wordNumber = 1
return wordPages.map((pageWords, pageIndex) => {
const pageData = createPageCanvas()
drawHeader(pageData.ctx, this)
let y = MARGIN + HEADER_HEIGHT + HEADER_BOTTOM_GAP
pageWords.forEach((word) => {
drawPracticeBlock(pageData.ctx, word, wordNumber, y)
wordNumber += 1
y += WORD_BLOCK_HEIGHT + WORD_BLOCK_GAP
})
if (pageIndex === wordPages.length - 1) {
drawDictation(pageData.ctx, dictationCount, y + DICTATION_TOP_GAP)
}
return {
fileName: worksheetLogic.buildWorksheetFileName(this.practiceDate, pageIndex),
dataUrl: pageData.canvas.toDataURL('image/png'),
}
})
},
trackDownload(pageCount) {
this.trackEvent('english_word_daily_download', { pageCount })
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
- [ ] Step 4: Add component styles
In the same component, add scoped CSS modeled after the pinyin tool:
.english-word-tool {
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.tool-desc { margin-bottom: 16px; color: #4b5563; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 16px; }
.panel { margin-bottom: 16px; }
.field-label { display: grid; gap: 8px; font-weight: 600; }
.text-input, .data-input { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px; font-size: 14px; line-height: 1.6; }
.data-input { min-height: 220px; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; }
.actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.btn { border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 14px; background: #fff; cursor: pointer; }
.btn.primary { border-color: #2563eb; background: #2563eb; color: #fff; }
.btn:disabled { cursor: not-allowed; opacity: 0.6; }
.meta { color: #4b5563; font-size: 13px; }
.error { margin-bottom: 10px; color: #dc2626; font-size: 14px; }
.result-section { margin-top: 24px; }
.preview-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.preview-card { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; background: #fafafa; }
.preview-head { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
.download-link { color: #2563eb; text-decoration: none; }
.preview-image { display: block; width: 100%; height: auto; background: #fff; }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- [ ] Step 5: Run logic tests
Run:
node scripts/test_english_word_daily_logic.js
Expected: PASS.
# Task 5: Wire The New Tool Into VuePress
Files:
Create:
docs/07.工具/30.英语单词日课.mdModify:
docs/.vuepress/config.tsModify:
docs/07.工具/00.概览.mdTest:
scripts/test_english_word_daily_wiring.js[ ] Step 1: Add the page
Create docs/07.工具/30.英语单词日课.md with:
---
title: 英语单词日课
date: 2026-04-07 00:00:00
permalink: /tools/english-word-daily/
sidebar: auto
article: false
comment: false
author:
name: wangyang
---
<ClientOnly>
<EnglishWordDailyTool />
</ClientOnly>
2
3
4
5
6
7
8
9
10
11
12
13
14
- [ ] Step 2: Add the Tools navigation item
Modify the Tools nav items in docs/.vuepress/config.ts to include:
{text: 'English Word Daily', link: '/tools/english-word-daily/'},
Place it after Pinyin Dictation Sheet.
- [ ] Step 3: Add the overview link
Modify docs/07.工具/00.概览.md so the list includes:
- [英语单词日课](/tools/english-word-daily/)
Place it after the pinyin helper link.
- [ ] Step 4: Run the wiring test and verify it passes
Run:
node scripts/test_english_word_daily_wiring.js
Expected: PASS and print english word daily wiring tests passed.
- [ ] Step 5: Commit the component and route wiring
Run:
git add docs/.vuepress/components/EnglishWordDailyTool.vue docs/07.工具/30.英语单词日课.md docs/.vuepress/config.ts docs/07.工具/00.概览.md scripts/test_english_word_daily_wiring.js
git commit -m "feat: add english word daily tool"
2
# Task 6: Build Verification
Files:
Verify:
docs/.vuepress/components/EnglishWordDailyTool.vueVerify:
docs/.vuepress/components/EnglishWordDailyWorksheetLogic.jsVerify:
docs/07.工具/30.英语单词日课.md[ ] Step 1: Run all local checks
Run:
node scripts/test_english_word_daily_logic.js
node scripts/test_english_word_daily_wiring.js
npm run build
2
3
Expected:
english word daily logic tests passed
english word daily wiring tests passed
2
The VuePress build should exit with code 0.
- [ ] Step 2: Inspect git status
Run:
git status --short
Expected: only unrelated pre-existing files remain modified. The committed English Word Daily files should not appear as unstaged changes.
- 01
- 2026-04-07-paper-game-generator04-07
- 03
- 2026-04-07-english-word-daily-design04-07