這一頁的元件都是用 kitDesign 直接畫出來的:每一段先是 R
程式碼,下面就是它的樣子。 規格(什麼時候用、不要怎麼用)在設計語言;在自己的電腦上看所有元件:
kitDesign::kit_gallery()。
按鈕、收合這些在 Shiny 裡才會動的地方,這裡只看樣子。有狀態的元件(模組)放在最後,用截圖。
頁首(Header)
一個 app
只有一個,放在最上面:標誌、工具名稱、副標、版本行。kit_page()
已經放好了。
kit_header(
"KIT 資料串接工具", "臺灣幼兒發展調查資料庫", tagline = "跨波次資料處理",
version = "0.5.0", date = "2026-10-31",
logo = "inline" # 在 Shiny 裡用預設的 TRUE;這裡是靜態網頁,所以把標誌寫進頁面
)KIT 資料串接工具
臺灣幼兒發展調查資料庫 · 跨波次資料處理
步驟分頁(NavTabs)
分組、不編號的分頁列(kitJoin 的樣子)。目前的分頁是萊姆色;虛線框是「工具」組。
kit_navgroups(list(
kit_navgroup("準備", c(upload = "檔案上傳", missing = "遺漏值處理")),
kit_navgroup("做出資料", c(join = "串接", kits = "轉成 kits 作答檔")),
kit_navgroup("其他工具", c(manual = "使用手冊"), tools = TRUE)
), tabset = "tabs", selected = "join")範圍選擇列(ScopeBar)
全 app
共用的一個選擇(例如用哪一份題庫),放在分頁列正下方:單選圓鈕、一行
hint、右邊的小按鈕;
要確認的選項打開下面的確認區(panel)。
kit_scope_bar(
radioButtons("bank", "題庫", c("TDCYP 題庫(建議)" = "tdcyp", "上傳自己的題庫" = "own"),
inline = TRUE),
line = tags$span("目前:", tags$strong("TDCYP 題庫"), " · 37 題 · 4 個波次"),
actions = list(kit_button("bank_dl", "下載題庫"), kit_button("bank_new", "新增題庫"))
)頁面說明:這一頁做什麼(PageGuide)
每一頁最上面一句話,旁邊「看步驟 ▸」;步驟與會得到什麼預設收起。
kit_tab_guide(
goal = "把兩個波次的檔案依 ID 接成一張寬資料。",
steps = c("選 ID 欄", "選要保留哪些人", "按「執行串接」", "看狀態框與結果"),
output = "一張寬資料:一人一列。"
)這一頁做什麼: 把兩個波次的檔案依 ID 接成一張寬資料。
- 選 ID 欄
- 選要保留哪些人
- 按「執行串接」
- 看狀態框與結果
會得到: 一張寬資料:一人一列。
按鈕(Button)
一頁只有一個主要按鈕(萊姆色)。小按鈕給下載、全選;危險按鈕是鮭魚色。停用的按鈕是灰的, 上面一定寫「還差:…」。
tags$div(
style = "display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1rem;",
kit_button("run", "開始校準", "primary"),
kit_button("csv", "下載 CSV"),
kit_button("skip", "這一回合不做", "danger"),
kit_button("convert", "執行轉換", "primary", disabled = TRUE)
)下載按鈕(DownloadButton)
和小按鈕一樣的樣子;中文檔名在每個瀏覽器都存得對(網址最後一段、檔名標頭、download
屬性都是同一個檔名)。放在 server 的 renderUI()
裡,檔案在按下載時才寫。這裡用測試用的 session
畫出來,所以按了沒有作用。
session <- shiny::MockShinySession$new()
tags$div(
class = "kit-downloads",
kit_download_button(session, "dl_csv", "下載 CSV", "第一波 資料.csv",
function(file) utils::write.csv(mtcars, file),
class = kit_btn_class("secondary")),
kit_download_button(session, "dl_gz", "下載作答檔(.csv.gz)", "kits_long.csv.gz",
function(file) writeLines("x", file), class = "btn-primary")
)狀態框(StatusBox)
按了主要按鈕以後第一眼看的框,只有四種:【完成】【完成,但請注意】【停下來】【要重新執行】。 第一行是一句結論,下面最多 3 點,其他收起來。
kit_done_status("串接完成:結果共 3,389 列 × 2,058 欄。",
points = c("688 人每個檔案都有資料。", "第 1 波 3,389 人,第 2 波 3,102 人。"))- 688 人每個檔案都有資料。
- 第 1 波 3,389 人,第 2 波 3,102 人。
kit_done_status(
"串接完成:結果共 3,389 列 × 2,058 欄。",
warnings = c("有 12 人的 ID 重複,只留第一筆。", "第 3 波沒有 sex 欄。")
)- 有 12 人的 ID 重複,只留第一筆。
- 第 3 波沒有 sex 欄。
kit_error_status(simpleError("object 'subj_id' not found"), "串接")- 怎麼辦:確認檔案內容與設定後再試一次;一直發生的話,請把下面的「原始訊息」回報給開發者。
原始訊息
object 'subj_id' not found
kit_stale_status("執行串接")提示框(Callout)
資訊是天藍色,要注意是黃色。提示框不是結果,所以沒有陰影。
kit_callout("外部連結不適用:這一回合只有一個世代,這不是錯誤。")
kit_callout("資料裡有疑似遺漏值代碼,建議先到「遺漏值處理」。", warn = TRUE)雲端版(kit_is_cloud()):頁首下面一行黃色的橫幅,頁面最下面是灰色小字的使用須知。
文字是 app 的:每一段的段名與內文、參考連結。
kit_cloud_banner("此為雲端版:上傳的資料會傳送至位於美國的 Posit 伺服器。",
tags$a(href = "#kit-cloud-terms", "〈雲端版使用須知〉"), "。")
kit_cloud_footer(
sections = list(
"一、資料傳輸與儲存" = c("本雲端版部署於 Posit Connect Cloud,主機位於美國。",
"本工具不另行保存、備份或分享您的資料。"),
"二、免責聲明" = "本工具以現況提供,僅供資料處理輔助之用。"
),
links = c("Posit Trust Center" = "https://trust.posit.co",
"Posit 隱私權政策" = "https://posit.co/about/privacy-policy/")
)資料表(DataTable)
表格寬度跟著內容;太寬時在自己的框裡左右捲。上面一行小字寫共幾列幾欄。
waves <- data.frame(
id = 101:105, cohort = c("M03", "M36", "M03", "M36", "M03"), wave_m = c(3, 6, 12, 3, 6),
income = c(32000, 45500, 28750, 51200, 39900)
)
tagList(
tags$p(class = "kit-hint", kit_rows_note(120, ncol(waves), shown_rows = 5)),
kit_table_scroll(kit_static_table(waves))
)共 120 列 × 4 欄,以下前 5 列。
| id | cohort | wave_m | income |
|---|---|---|---|
| 101 | M03 | 3 | 32,000 |
| 102 | M36 | 6 | 45,500 |
| 103 | M03 | 12 | 28,750 |
| 104 | M36 | 3 | 51,200 |
| 105 | M03 | 6 | 39,900 |
要搜尋、換頁、點選的表(題目清單、搜尋結果)用
kit_dt(),放在 DT::renderDT() 裡(app 要
Imports: DT)。
樣子一樣;選到的列是淡萊姆,換頁按鈕是小按鈕,文字是中文。樣子見
kit_gallery() 的「互動表與範圍」。
圖(Chart)
plotly 圖最後交給
kit_plotly_style():字型、軸線、圖例、工具列和 app 一致。
前後對照與用法見「圖」。類別色用色盲友善的
Okabe–Ito:
swatch <- function(col, name) {
tags$div(
tags$div(style = paste0("width: 4rem; height: 2rem; border: 2px solid var(--nb-ink); ",
"border-radius: 8px; background: ", col)),
tags$small(class = "kit-hint", name)
)
}
cols <- kit_chart_colors()
tags$div(style = "display: flex; flex-wrap: wrap; gap: 0.6rem;",
Map(swatch, cols, names(cols)), swatch(kit_chart_neutral(), "neutral"))細節列(Details)
結果下面一排收起來的細節:紀錄、檢查表、用 R 怎麼做。要看再打開。
kit_details_row(
kit_detail("這一回合的紀錄", tags$p("2026-10-31 14:02 串接 2 個檔案。")),
kit_detail("用 R 怎麼做", kit_code("joined <- dplyr::left_join(t1, t2, by = \"subj_id\")"))
)這一回合的紀錄
2026-10-31 14:02 串接 2 個檔案。
用 R 怎麼做
joined <- dplyr::left_join(t1, t2, by = "subj_id")
進階設定(預設收起)與設定摘要:
tagList(
kit_advanced(checkboxInput("trim", "比對前去掉 ID 前後的空白", TRUE)),
kit_settings_line(c("2 個檔案", "ID:subj_id", "向後追蹤樣本"))
)進階設定
本次:2 個檔案 · ID:subj_id · 向後追蹤樣本
步驟卡(StepCard)
一次做一步:現在這一步有框和陰影,做完的收成一行摘要(「改」可以回去),還不能做的是灰的。
tagList(
kit_step_card(kit_step_head(1, "每個檔案是哪個世代、哪個波次", "done",
summary = "2 個檔案 · 世代 TDCYP", edit_id = "edit1")),
kit_step_card(
kit_step_head(2, "哪些欄是題目", "current", title_id = "st2"),
kit_step_body(tags$p("挑出哪些欄是題目(至少一題)。"), actionButton("ok2", "對,下一步")),
title_id = "st2"
),
kit_step_card(kit_step_head(3, "計分檢查", "locked"))
)挑出哪些欄是題目(至少一題)。
檢查清單(Checklist)
轉換前要看的事:! 要決定(旁邊有按鈕)、✓ 沒問題、i 提醒。
kit_checklist(
kit_check("todo", "有 3 題的分數從 1 開始", "kits 要的分數是 0、1、2…。可以全部減 1,或照原樣。",
actions = list(kit_button("shift", "全部減 1(建議)"), kit_button("keep", "照原樣"))),
kit_check("ok", "45 題的分數都是整數"),
kit_check("note", "有 2 題沒有人作答,不會放進作答檔")
)-
要決定:
有 3 題的分數從 1 開始 kits 要的分數是 0、1、2…。可以全部減 1,或照原樣。
-
沒問題:
45 題的分數都是整數
-
提醒:
有 2 題沒有人作答,不會放進作答檔
還差什麼(ReadyLine)
主要按鈕還不能按時,上面一行寫還差什麼,旁邊一個連結帶使用者去補。
tagList(
kit_ready_line("至少要 2 個波次的檔案,目前只有 1 個",
actionLink("goto_upload", "到「檔案上傳」加入檔案 →")),
kit_button("join", "執行串接", "primary", disabled = TRUE)
)還差:至少要 2 個波次的檔案,目前只有 1 個。 到「檔案上傳」加入檔案 →
下一步(NextStep)
做完一頁,最下面告訴使用者接著去哪裡。
kit_next_step("串接好了。接著把資料轉成 kits 作答檔。", "next", "下一步:轉成作答檔")圖解(Diagram)
處理前的小表 → 處理後的小表。新加的欄是淡萊姆色,欄名後面寫「(新)」。
kit_diagram(
list(kit_dg_table(data.frame(id = 1:2, score = c(3, 5)), "第 1 波(t1)"),
kit_dg_table(data.frame(id = c(1, 3), score = c(4, 6)), "第 2 波(t2)")),
kit_dg_table(data.frame(id = 1:2, score_t1 = c(3, 5), score_t2 = c(4, NA), wave = c(2, 1)),
"結果", added = "wave"),
"依 id 接起來:第 2 波沒有出席的人,t2 的格子是空的。"
)| id | score |
|---|---|
| 1 | 3 |
| 2 | 5 |
| id | score |
|---|---|
| 1 | 4 |
| 3 | 6 |
| id | score_t1 | score_t2 | wave (新) |
|---|---|---|---|
| 1 | 3 | 4 | 2 |
| 2 | 5 | (空) | 1 |
執行分頁(RunTab)
一頁一個主要按鈕的分頁(kitJoin
的串接、疊加、流失樣本、遺漏值、長寬轉換、格式轉換)都是同一個骨架:
kit_run_tab_ui()
照頁面骨架排好固定的部分,分頁自己的設定放在中間;kit_run_tab_server()
在分頁的 moduleServer()
裡呼叫,做好還差、設定摘要、執行、狀態框、通知、捲到狀態框、
【要重新執行】、結果的細節與「用 R 怎麼做」。下面是還沒有 server
時的樣子(設定摘要、狀態框、 結果都是空的,主要按鈕停用)。
kit_run_tab_ui(
"stack", "執行疊加",
guide = kit_tab_guide("把各波資料上下疊起來:每個人在每一波各佔一列。", c("按「執行疊加」", "看狀態框與預覽"), "一張長格式的資料。"),
selectInput("stack-wave_col", "波次欄的名稱", c("wave", "t"))
)這一頁做什麼: 把各波資料上下疊起來:每個人在每一波各佔一列。
- 按「執行疊加」
- 看狀態框與預覽
會得到: 一張長格式的資料。
用 R 怎麼做
server(在分頁的 moduleServer() 裡):
tab <- kit_run_tab_server(
input, output, session, "執行疊加",
ready = reactive(kit_need_files(length(files()), 2L)), # 還差:…(停用主要按鈕)
settings = function() paste0(length(files()), " 個檔案"), # 本次:…
stale_on = function() list(files(), input$wave_col), # 改了 →【要重新執行】
progress = "正在疊加資料…",
run = function() dplyr::bind_rows(files(), .id = input$wave_col),
done = function(res) kit_done_status(paste0("疊加完成:結果共 ", kit_dims(nrow(res), ncol(res)), "。")),
details_ui = function(res) kit_detail("結果預覽", kit_preview_table(res)),
code = function() "stacked <- dplyr::bind_rows(t1, t2, .id = \"wave\")"
)
list(state = tab$state) # 步驟分頁打 ✓ 用模組(有狀態的元件)
下面四個元件要在 Shiny 裡才會動(哪一步打開、按鈕能不能按、分頁的
✓), 所以用截圖。每一個都是一頁一行:*_ui(id)
放在頁面,*_server(id) 放在 server。
步驟卡:kit_stepcards_ui()/kit_stepcards_server()

主要按鈕+還差:kit_run_gate_ui()/kit_run_gate_server()

步驟分頁:kit_step_tabs_ui()/kit_step_tabs_server()(流程分頁:做完
✓、還不能做是灰的)

用 R
怎麼做:kit_code_panel_ui()/kit_code_panel_server()(打開時才算程式碼)
