- TypeScript 90.8%
- JavaScript 4.7%
- CSS 4.2%
- HTML 0.3%
| scripts | ||
| server | ||
| src | ||
| index.html | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
NURSING://DECODE — 護理臨床英文情境任務
2026 全國智慧健康照護創新創業競賽 Demo(國立臺南護理專科學校主辦) 主題:遊戲化學習 — 護理臨床英文判讀、醫療縮寫、ISBAR 結構化交班
架構
/ ← 前端(Vite + TypeScript + GSAP)
/server/ ← 後端(Node + Express + node:sqlite + JWT)
└─ src/question.json ← 整個遊戲的單一題庫來源
- 前端:純 TS(無框架)+ GSAP。啟動時強制 fetch
/api/questions/all取題庫,後端沒起就顯示錯誤頁 - 後端:JWT auth + bcrypt hash + Express admin API + 題庫
/api/questions/all;可選擇有同時 serve 前端dist/ - 題庫:
server/src/question.json單一檔案管所有內容(vocab / patient / isbar / rubric / mission card / mission questions),schema 見server/src/question.schema.md - 部署:單機一體(Express serve API + dist)或前後分離,見 §正式上線
正式上線(Production Deployment)
上線前請確認 Node.js ≥ 22.5(後端依賴內建
node:sqlite)。
準備:決定部署拓撲
| 方案 | 說明 | 適用 |
|---|---|---|
| A. 單機一體(推薦) | Express 同時 serve API + 前端 dist/,一個 port 搞定 |
VPS、競賽評審機、demo 教室 |
| B. 前後分離 | 前端丟 CDN/靜態主機,後端只跑 API | 高流量、前端要快取 |
以下以 方案 A 為主,B 要看 §前後分離部署。
方案 A:單機一體(十條指令)
1. Build 前端
# 在 / 目錄
npm install
VITE_API_BASE_URL="" npm run build # 空字串 = same-origin(前端 fetch /api/*)
# 產生 dist/ 目錄
⚠️ 預設
npm run build沒帶VITE_API_BASE_URL會用 same-origin("")。 若是要部署前後分離,設成 API 主機網址:VITE_API_BASE_URL="https://api.example.com" npm run build
2. Build 後端
cd server
npm install
npm run build # tsc → dist/
3. 設定環境變數
cd server
cp env.example .env # 檔名是 env.example(不帶點)
vim .env
生產必改 3 個:
| 變數 | 為什麼要設 |
|---|---|
JWT_SECRET |
沒設會用 dev secret,JWT 可被偽造。隨機 50+ 字元 |
DEFAULT_ADMIN_PASSWORD |
第一次啟動建立 admin 的初始密碼。首映後立刻登入改掉 |
CORS_ORIGIN |
單機一體留空即可(same-origin 不進 CORS) |
隨機 secret 快速產生:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
4. 檢查題庫存在
ls server/src/question.json # 必須存在
node /workspace/scripts/validate-questions.mjs # 跑一次確認結構沒壞
5. 啟動(測試)
cd server
node dist/index.js
應看到:
📦 Serving frontend from /.../dist
🩺 NURSING://DECODE server listening on http://localhost:8787
6. 開瀏覽器驗證
http://<主機>:8787 → 應看到首頁 + 可登入。
7. 設成 system service(systemd)
sudo tee /etc/systemd/system/nursing-decode.service <<'EOF'
[Unit]
Description=NURSING://DECODE nursing handoff training
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/nursing-decode/server
EnvironmentFile=/opt/nursing-decode/server/.env
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nursing-decode
sudo journalctl -u nursing-decode -f # 查看 log
8. HTTPS(Caddy 最簡)
# /etc/caddy/Caddyfile
demo.example.com {
reverse_proxy localhost:8787
}
sudo systemctl reload caddy
自動取得 Let's Encrypt 憑證。生產一定要 HTTPS(JWT token 明文傳輸不保護)。
9. 上線前檢查清單
JWT_SECRET改成隨機值DEFAULT_ADMIN_PASSWORD改成強密碼(or 首登後到「學生帳號管理」把 admin 密碼改掉)- HTTPS(Caddy/nginx/certbot 任一)
server/data/users.db會定期備份(cp users.db backup/users-$(date +%F).db進 cron)- Admin console
/#/admin/login只有老師知道 - Server 有跑 health check:
curl -sf http://localhost:8787/api/health
10. 環境變數總表(全部 server 用得到)
| 變數 | 預設 | 說明 |
|---|---|---|
NODE_ENV |
— | 設 production 會在漏設 JWT_SECRET 時印警告 |
PORT |
8787 |
Express 監聽 port |
JWT_SECRET |
dev secret ⚠️ | 生產必設隨機 |
JWT_EXPIRES_IN |
8h |
token 過期時間 |
DB_PATH |
./data/users.db |
SQLite 檔位置 |
QUESTION_FILE |
自動嘗試主目錄 | 見下 |
CORS_ORIGIN |
localhost:5173,... |
生產單機留空 |
FRONTEND_DIR |
../dist(相對 server/) |
前端 build 目錄 |
DEFAULT_ADMIN_USERNAME |
admin |
|
DEFAULT_ADMIN_PASSWORD |
admin1234 ⚠️ |
生產必改 |
QUESTION_FILE 搜尋順序(未設環境變數時):
- 本檔同目錄(dev:
server/src/question.json) dist/../src/question.json(prod 落在server/dist/時向上找server/src/)- 兩個都找不到 → server 拒絕啟動並印錯誤
部署時需要齊全的檔案
所有路徑以 repo 根目錄 / 為基準。這些檔案必須被包含在部署內容中:
/
├─ index.html
├─ package.json / package-lock.json / tsconfig.json / vite.config.ts
├─ README.md / LICENSE
├─ src/ # 完整目錄
├─ scripts/ # 完整目錄
├─ server/
│ ├─ package.json / package-lock.json / tsconfig.json
│ ├─ env.example
│ └─ src/ # 完整目錄(含 question.json / question.schema.md / types/)
上完線才需建立的檔案(server 啟動後自動產生,不屬於部署):
server/.env # 從 env.example 複製後修改(機密)
server/data/users.db # server 啟動自動建立
/dist/ # 前端 build 產生
/server/dist/ # 後端 build 產生
前後分離部署
如果要把前端丟 Netlify / Vercel / Cloudflare Pages 等靜態加速:
# 1. build 前端時帶 API 主機網址
VITE_API_BASE_URL="https://api.example.com" npm run build
# 2. dist/ 整包傳到靜態服務
# 3. server/.env 補 CORS 讓 API 接受那個 origin
CORS_ORIGIN="https://frontend.example.com"
注意:
VITE_API_BASE_URL是 build-time 嵌入,日後要改 API 位置要重 build 前端- 前端抓不到 API 時會顯示「後端未連線」錯誤頁(含目標位址)
從頭到尾的完整指令(最快 5 分鐘)
# 主機上
git clone <repo> /opt/nursing-decode && cd /opt/nursing-decode
# 前端
npm install
VITE_API_BASE_URL="" npm run build
# 後端
cd server
npm install
npm run build
# 產隨機 secret 寫 .env(server/env.example 有完整欄位可參考)
JWT_SECRET=$(node -e "console.log(require('crypto').randomBytes(48).toString('hex'))")
cat > .env <<EOF
NODE_ENV=production
PORT=8787
JWT_SECRET=${JWT_SECRET}
DEFAULT_ADMIN_PASSWORD=$(openssl rand -hex 12)
EOF
# 第一次啟動(先在手動 shell 跑,看預設 admin 密碼是否正確印出)
node dist/index.js &
sleep 1
curl -s http://localhost:8787/api/health # 應回 {"ok":true,...}
# 掛 systemd + Caddy 依上面步驟
快速啟動(本機 demo)
1. 啟動後端
cd server
npm install
npm run dev # 會在 http://localhost:8787 啟動
第一次啟動會自動建立預設管理員:
- 帳號
admin,密碼admin1234(請立即修改)
2. 啟動前端
# 在 / 目錄
npm install
npm run dev # http://localhost:5173
3. 老師批量建學生帳號
- 開
http://localhost:5173/#/admin/login - 用
admin / admin1234登入 - 「學生帳號管理」→ 把 CSV 貼上批量建立:
student001,P@ss1234,王小明,S110001
student002,P@ss1234,李小華,S110002
4. 學生玩 + 上傳成績
- 學生開
http://localhost:5173/#/login - 用自己的帳號登入
- 玩完五關 → 結算頁按「上傳成績」
- 老師回 admin「成績查詢」即可看到 + 匯出 CSV
遊戲流程
| # | 關卡 | 內容 |
|---|---|---|
| 1 | EXPLORE 探索 | 瀏覽病人資料 + 收錄詞彙 + 答 3 題快速確認 |
| 2 | DECODE 解碼 | 20 詞 4 選 1 測驗(詞彙熟練度記錄) |
| 3 | UNDERSTAND 理解 | 情境填空(詞彙熟練度記錄) |
| 4 | ORGANIZE 整理 | ISBAR 15 張卡拖曳 → 評核項目 1–15(內容面 77 分) |
| 5 | MISSION 任務 | 溝通面 5 題(評核項目 16–20, 23 分) |
結算頁顯示 ISBAR 20 項評核總分(100 分)+ 五關雷達圖。
及格標準:≥ 80 分(依競賽企劃書)
ISBAR 評核標準(20 項 = 99 分)
實際配分以
server/src/question.json的rubric[]為準。企劃書原規劃 100,目前 COMM 五項合計 22(非 23),總分 99;及格門檻 ≥ 80 不變(寫死於src/stages/rubric-const.ts)。
| 區 | 項目 | 配分 |
|---|---|---|
| I 介紹 | 1.自我介紹 2.主訴 | 3+3 = 6 |
| S 情境 | 3.診斷現況 4.生命徵象 5.本班重要事件 | 5+5+5 = 15 |
| B 背景 | 6.入院經過 7.病史 8.藥物過敏史 | 4+4+5 = 13 |
| A 評估 | 9.檢查數據 10.目前治療 11.臨床判斷 12.身體評估 | 6+5+8+8 = 27 |
| R 建議 | 13.監測計畫 14.後續檢查 15.建議請求 | 5+6+5 = 16 |
| 小計(內容面) | 1–15 | 77 |
| 溝通面 | 16.邏輯順序 17.精簡度 18.完整度 19.用詞 20.語速 | 5+5+4+4+4 = 22 |
| 總計 | 20 項 | 99 |
學習追蹤機制
| 機制 | 說明 |
|---|---|
| 詞彙熟練度 | 每個 vocab 答題會記錄,0–100 分(時間衰減 30 天),4 色顯示 |
| 評核項目熟練度 | 每個 rubric 項目獨立記錄,複習頁可視覺化弱點 |
| 書籤 | vocab / ISBAR 卡 / patient 段落都可 🔖,複習頁三 tab 瀏覽 |
| 複習模式 | /review 頁面:ISBAR 五區雷達圖 + 詞彙網格 + TOP5 弱點 + 書籤 |
| AARRR 追蹤 | 自動記錄完成關卡(activation / retention) |
隱私與資安設計
| 項目 | 做法 |
|---|---|
| 病人資料 | 全虛構(陳美麗 68F 肺炎) |
| 個人資料 | 僅存學號、姓名(無身分證/聯絡方式) |
| 密碼 | bcrypt(10) hash |
| 傳輸 | JWT 8h 過期 |
| 病歷分離 | 後端無存放任何病歷 ↔ 學號連結 |
| 用途 | 教育用途,僅記錄學習表現 |
技術規格
| 項 | 內容 |
|---|---|
| 前端 | TypeScript 5 (strict) + Vite + GSAP 3 |
| 資料(前端) | 啟動時 fetch /api/questions/all → 全程在記憶體;localStorage 只留 bookmarks / proficiency |
| 題庫 | server/src/question.json(單一 source of truth) |
| 後端 | Express + node:sqlite(Node ≥ 22.5 內建,零 native module) + JWT + bcryptjs |
| 動畫 | GSAP 3(拖曳/轉場/數字滾動) |
| 特效 | Canvas 粒子 + ECG 動畫 + CSS 玻璃擬態 |
| 音效 | Web Audio API 即時合成(零外部音檔) |
檔案結構
/
├─ index.html
├─ src/
│ ├─ main.ts # 進入點(fetch /api/questions/all → bootstrap)
│ ├─ config.ts # 集中設定(apiBaseUrl 等)
│ ├─ core/
│ │ ├─ types.ts # 前端合約(re-export server/src/types/question.ts)
│ │ ├─ db.ts # 輕量 localStorage store(只裝 bookmarks / proficiency)
│ │ ├─ game.ts # 計分 + rubric + duration tracking
│ │ ├─ router.ts # hash router + auth guard
│ │ ├─ auth.ts # JWT 管理
│ │ ├─ api.ts # fetch 封裝
│ │ ├─ proficiency.ts # 熟練度引擎
│ │ └─ bookmarks.ts # 書籤 CRUD
│ ├─ fx/ # 粒子/ECG/轉場/回饋
│ ├─ stages/
│ │ ├─ login.ts # 學生登入
│ │ ├─ home.ts # 主視覺
│ │ ├─ explore.ts # + 🔖 + 熟練度 dot + 短答題
│ │ ├─ decode.ts # + 🔖 + 熟練度 dot(mc / drag-match / clue-solve)
│ │ ├─ understand.ts # + 🔖 + fill-blank
│ │ ├─ organize.ts # ISBAR 15 卡拖曳 + rubric 計分 + 🔖
│ │ ├─ mission.ts # 溝通面 5 題(multi/single+compare/ordered)+ 計時/提示
│ │ ├─ results.ts # rubric 報告 + 上傳成績
│ │ ├─ review.ts # 複習中心(雷達圖+書籤三 tab)
│ │ ├─ rubric-const.ts # RUBRIC_PASS_SCORE = 80
│ │ └─ admin/
│ │ ├─ login.ts
│ │ ├─ dashboard.ts
│ │ ├─ users.ts # 批量建號 + 列表
│ │ └─ scores.ts # 成績篩選 + 匯出 CSV
│ ├─ components/
│ │ └─ feedback-modal.ts # 答題共用 modal(對/錯/漏選/干擾)
│ └─ styles/
├─ scripts/
│ ├─ validate-questions.mjs # 題庫 validator(結構 + crossRef + kebab id)
│ └─ build/build-question-json.mjs # 一次性 migration(已跑過,留著參考)
└─ server/
├─ src/
│ ├─ index.ts # Express 進入點(含 /api/questions/all)
│ ├─ question.json # ⭐ 題庫單一來源
│ ├─ question.schema.md # ⭐ 題庫 schema 文件
│ ├─ types/question.ts # ⭐ 前後端共用型別
│ ├─ db.ts / auth.ts
│ └─ routes/auth.ts / scores.ts / admin.ts
├─ package.json / tsconfig.json
└─ data/users.db(自動建立)
⭐ = v3 新架構核心(題庫集中後端)
改題目 / 詞彙 / 配分
所有遊戲內容集中在 server/src/question.json,改完跑 validator 重開 server 即生效:
# 1. 編輯題庫
vim server/src/question.json
# 2. 驗證(結構 + crossRef + kebab id,中文錯誤訊息)
npm run validate:questions
# 3. 重開後端
cd server && npm run dev
完整 schema 與常見操作(加 vocab、調 rubric 配分、出 mission 新題、single+compare 用法)見 server/src/question.schema.md。
老師操作手冊
建學生
- 開 admin console:
/#/admin/login admin/ 你的密碼登入- 「學生帳號管理」貼 CSV → 批量建立
看成績
- 「成績查詢與匯出」
- 上方三卡片:Activation / Retention / Pass Rate
- 篩學生、點列展開看 ISBAR 20 項明細
- 「匯出 CSV」全班成績
重設密碼
- 「學生帳號管理」→ 該列「重設密碼」