Bull's blog Bull's blog
Resume
  • Tools Home
  • Pinyin Dictation Sheet
  • English Word Daily
  • Paper Games
  • Work Notes
  • Categories
  • Tags
  • Archives

Bull

Resume
  • Tools Home
  • Pinyin Dictation Sheet
  • English Word Daily
  • Paper Games
  • Work Notes
  • Categories
  • Tags
  • Archives
  • superpowers
  • plans
wangyang
2026-04-07
目录

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.js

  • Later 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')
1
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
1

Expected: FAIL with Cannot find module '../docs/.vuepress/components/EnglishWordDailyWorksheetLogic'.

# Task 2: Add Tool-Specific Logic Module

Files:

  • Create: docs/.vuepress/components/EnglishWordDailyWorksheetLogic.js

  • Test: 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,
}
1
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
1

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"
1
2

# Task 3: Add Integration Test For New Page Wiring

Files:

  • Create: scripts/test_english_word_daily_wiring.js

  • Later create: docs/.vuepress/components/EnglishWordDailyTool.vue

  • Later create: docs/07.工具/30.英语单词日课.md

  • Later modify: docs/.vuepress/config.ts

  • Later 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')
1
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
1

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.vue

  • Use: docs/.vuepress/components/EnglishWordDailyWorksheetLogic.js

  • Use: docs/.vuepress/components/UploadData.vue

  • Test: 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>
1
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()
}
1
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 })
    },
  },
}
1
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; }
1
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
1

Expected: PASS.

# Task 5: Wire The New Tool Into VuePress

Files:

  • Create: docs/07.工具/30.英语单词日课.md

  • Modify: docs/.vuepress/config.ts

  • Modify: docs/07.工具/00.概览.md

  • Test: 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>
1
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/'},
1

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/)
1

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
1

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"
1
2

# Task 6: Build Verification

Files:

  • Verify: docs/.vuepress/components/EnglishWordDailyTool.vue

  • Verify: docs/.vuepress/components/EnglishWordDailyWorksheetLogic.js

  • Verify: 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
1
2
3

Expected:

english word daily logic tests passed
english word daily wiring tests passed
1
2

The VuePress build should exit with code 0.

  • [ ] Step 2: Inspect git status

Run:

git status --short
1

Expected: only unrelated pre-existing files remain modified. The committed English Word Daily files should not appear as unstaged changes.

上次更新: 2026/04/07, 14:17:08
最近更新
01
2026-04-07-paper-game-generator
04-07
02
2026-04-07-paper-game-generator-design
04-07
03
2026-04-07-english-word-daily-design
04-07
更多文章>
Theme by Vdoing | Copyright © 2018-2026 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式