iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
自我挑戰組

30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System系列 第 27 篇

# Day 27:CDN Build:讓 CUI 不只存在於 React

  • 分享至 

  • xImage
  •  

做到 Day 26,我們的 CUI 已經有了不少成果。

從最初的 Design Tokens、Button,到 Form、Table、Pagination、Tooltip、Dialog,再到最近兩天建立的 Storybook 與 Accessibility Testing。

目前的架構大致是:

CUI
│
├── Design Tokens
├── React Components
├── Component Contract
├── Accessibility Testing
└── Storybook

如果今天要在新的 React 專案使用 CUI,至少已經有了可以繼續整理與封裝的基礎。

但還記得 Day 1 的目標嗎?

我希望這套 UI Kit 不只能在 React 裡使用,也能讓既有的 Legacy 系統使用。

例如我工作上會接觸到的 mod_plsql 系統。

這類系統通常是由後端產生 HTML,前端不一定有 React、Vite 或其他現代化的建置流程。

可能就是:

<form>
  <label for="name">姓名</label>
  <input id="name" name="name">

  <button type="submit">
    送出
  </button>
</form>

那麼問題來了:

我們花了 26 天建立的 CUI,能不能讓這種頁面也使用?

如果答案是:

「可以,但要先把整個系統改成 React。」

那好像就失去原本的意義了。🤣

所以今天,我們要開始處理另一條輸出路線:

CDN Build。


1. 今天要完成什麼?

今天的目標是建立一套可以重複執行的 CSS Build Pipeline。

預期產出:

dist/
└── cdn/
    ├── cui.css
    └── cui.min.css

讓不使用 React 的頁面也能載入:

<link rel="stylesheet" href="/assets/cui.min.css">

並透過 CUI 的公開 Contract 套用樣式:

<button
  type="button"
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  儲存
</button>

今天先以 CSS-only 元件為主:

  • Button
  • Badge
  • Alert
  • Input
  • Table

至於 Tooltip、Dialog 等需要互動行為的元件,則留到下一階段討論。


2. 先釐清:React Component 不能直接當成 CDN 元件

假設我們現在有:

<Button variant="primary" size="md">
  儲存
</Button>

它是一個 React Component。

裡面可能包含:

React Props
TypeScript
JSX
Base UI Primitive
Tailwind Classes

瀏覽器不會因為我們提供:

<script src="cui.js"></script>

就自動理解:

<Button variant="primary">
  儲存
</Button>

因為 JSX 不是一般 HTML。

所以我們需要先區分兩件事:

React Component 的實作,以及 瀏覽器最終看到的 HTML 與 CSS。

例如 React 最後可能輸出:

<button
  class="inline-flex items-center justify-center ..."
  data-slot="button"
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  儲存
</button>

瀏覽器真正處理的是 DOM 與 CSS。

因此 Legacy 支援不一定需要 React Component 本身。

它需要的是:

一套可以用一般 HTML 表達的元件結構,以及對應的 CSS。


3. Single Source 到底要共享什麼?

這裡是今天最重要的架構問題。

我們一直說:

Single Source, Multiple Outputs。

但 Single Source 並不是指所有環境都必須執行完全相同的程式碼。

例如:

React
→ 使用 React Component

Legacy
→ 使用 HTML + CSS

兩者的執行環境不同。

真正應該共享的是:

Design Tokens
Component Contract
Styling Rules
Accessibility Requirements

架構可以理解成:

                   CUI
                    │
             Design Tokens
                    │
             Style Contract
                    │
        ┌───────────┴───────────┐
        ↓                       ↓
      React                   Legacy
        │                       │
   React Component          HTML Markup
        │                       │
        └───────────┬───────────┘
                    ↓
              Shared Styling

這樣未來修改 Primary Color 時,不需要分別修改:

React Button Color
Legacy Button Color

而是共同使用 Design Tokens。

不過還有一個更重要的問題:

我們要怎麼避免 React 和 Legacy 各自維護一份 Button CSS?


4. 先檢查目前 Button 的樣式來源

目前 CUI 的 Button 是以 shadcn/ui 為基礎。

例如:

const buttonVariants = cva(
  "inline-flex items-center justify-center ...",
  {
    variants: {
      variant: {
        primary: "...",
        secondary: "...",
        outline: "...",
      },
      size: {
        sm: "...",
        md: "...",
        lg: "...",
      },
    },
  }
)

這裡使用:

CVA
+
Tailwind Utility Classes

組合出不同樣式。

例如:

<Button variant="primary" size="md">
  儲存
</Button>

最後得到一組 Tailwind Classes。

但 Legacy 不能只寫:

<button data-cui-variant="primary">
  儲存
</button>

就期待 Tailwind 自動幫它套樣式。

因為 Tailwind 的 Utility CSS 是根據建置時偵測到的 Class 產生的。

而:

data-cui-variant="primary"

本身並不等於:

background-color: var(--cui-primary);

所以我們需要一層能讓 HTML Contract 對應到實際樣式的機制。


5. 兩種可能的做法

目前至少有兩條路線。

方案 A:讓 Legacy 使用 Tailwind Classes

例如:

<button
  class="inline-flex items-center justify-center rounded-md ..."
>
  儲存
</button>

這種方式看起來最簡單。

因為可以直接使用已經產生的 Utility CSS。

但問題是:

Legacy HTML
需要知道所有 Tailwind Classes

而且只要 Button 樣式修改,Legacy 頁面也可能需要同步修改。

這不符合我們希望提供穩定元件 API 的目標。

方案 B:使用 CUI Semantic Selectors

例如:

<button
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  儲存
</button>

CSS:

[data-cui-slot="button"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

[data-cui-slot="button"][data-cui-variant="primary"] {
  background: var(--cui-primary);
  color: var(--cui-on-primary);
}

這樣 Legacy 不需要知道底層用了哪些 Tailwind Classes。

只需要知道:

slot
variant
size

也就是 Day 22 整理的 Component Contract。

對 CUI 來說,我比較傾向方案 B。

但接下來還有一個問題:

這份 Semantic CSS 要放在哪裡,才能避免 React 和 Legacy 各自維護?


6. 讓 React 和 Legacy 共用同一份樣式定義

我們不希望變成:

src/
├── components/ui/button.tsx
│   └── 一套 Button Styles
│
└── legacy/button.css
    └── 另一套 Button Styles

因為未來修改 Button 時,很可能只改到其中一份。

比較理想的方式是:

src/
├── components/
│   └── ui/
│       └── button.tsx
│
└── styles/
    ├── tokens.css
    ├── theme.css
    └── components/
        └── button.css

其中:

tokens.css
→ Design Tokens

theme.css
→ Tailwind Theme Bridge

components/button.css
→ CUI Button 的共用樣式

React Button 可以輸出:

data-cui-slot="button"
data-cui-variant="primary"
data-cui-size="md"

Legacy Button 也使用同樣的 Attributes。

兩邊就可以共用:

[data-cui-slot="button"] {
  ...
}

不過這也代表我們要逐步把原本散落在 CVA 裡的部分樣式責任,整理到共用 CSS。

這是一個架構調整,不是單純多建一個 CSS 檔案。


7. 先做一個最小可行版本

今天先不要一次重構所有元件。

我們先拿 Button 當實驗對象。

建立:

src/styles/components/button.css

例如:

[data-cui-slot="button"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;

  border: 1px solid transparent;
  border-radius: var(--cui-radius-md);

  font: inherit;
  font-weight: 500;
  line-height: 1.25;

  cursor: pointer;

  transition:
    background-color 150ms ease,
    border-color 150ms ease,
    color 150ms ease;
}

[data-cui-slot="button"][data-cui-size="md"] {
  min-height: 2.5rem;
  padding-inline: 1rem;
}

[data-cui-slot="button"][data-cui-variant="primary"] {
  background-color: var(--cui-primary);
  color: var(--cui-on-primary);
}

[data-cui-slot="button"]:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

[data-cui-slot="button"]:focus-visible {
  outline: 2px solid var(--cui-focus);
  outline-offset: 2px;
}

這裡的 Token 名稱只是示範,實際要對應目前 CUI 已經建立的 Design Tokens。

另外,這還不是完整的 Button CSS。

目前只先示範:

Base
Size
Primary Variant
Disabled
Focus Visible

其他 Variants、Sizes、Hover、Active States,後續也需要逐一對應。


8. React Button 要怎麼配合?

如果要讓 React 與 Legacy 共用剛才的 CSS,React Button 也需要輸出相同的 Contract。

例如:

<Button
  variant="primary"
  size="md"
>
  儲存
</Button>

最後應該具有:

data-cui-slot="button"
data-cui-variant="primary"
data-cui-size="md"

而原本的:

data-slot

仍然可以保留。

例如:

<button
  data-slot="button"
  data-cui-slot="button"
  data-cui-variant={variant}
  data-cui-size={size}
>
  {children}
</button>

不過這裡要特別注意:

不能只是把共用 CSS 加上去,卻完全不處理原本 CVA 裡的重複樣式。

否則可能變成:

Tailwind Utility
+
Semantic CSS

同時設定:

Background
Padding
Border
Focus

最後產生樣式覆蓋問題。

因此正式遷移時,需要決定:

哪些樣式由 Shared CSS 負責?

哪些樣式仍由 Component Class 負責?

至少對需要支援 CDN 的部分,應該建立明確的單一樣式來源。


9. 不需要今天把所有 Tailwind 都拿掉

這裡我覺得不用太激進。

因為 CUI 本來就是:

React
+
Tailwind
+
shadcn/ui

我們不是要把整套專案改回傳統 CSS。

而是讓需要跨平台使用的元件樣式,有穩定的公開 Contract。

例如:

Button
Badge
Alert
Input
Table

可以逐步建立 Shared Component Styles。

而 React 專屬的組合、Layout 或 Demo,仍然可以使用 Tailwind Utilities。

換句話說:

不是放棄 Tailwind,而是不要讓 Legacy 使用者必須理解 Tailwind 的內部實作。


10. 建立 CDN 專用的 CSS Entry

接著開始建立真正的 Build Pipeline。

新增:

src/styles/cdn.css

例如:

@import "./tokens.css";
@import "./components/button.css";

之後再加入:

@import "./components/badge.css";
@import "./components/alert.css";
@import "./components/input.css";
@import "./components/table.css";

這個檔案的用途很單純:

告訴建置工具,CDN CSS 需要包含哪些共用樣式。

它不應該直接依賴:

React Component
Storybook
App.tsx

因為 Legacy 使用者不需要這些東西。


11. Tailwind Preflight 要不要一起輸出?

這裡有個很值得注意的問題。

Tailwind CSS 通常會包含 Preflight,也就是一組基礎 CSS Reset。

例如可能影響:

Heading
Button
Input
Margin
Border

在全新的 React 專案裡,這通常沒什麼問題。

但 Legacy 系統可能已經有自己的 CSS。

如果我們直接把整份 Tailwind Preflight 放進去,可能會讓原本的頁面:

標題大小改變
表單樣式改變
按鈕邊框消失
既有 Layout 受到影響

所以對 Legacy CDN,我比較傾向:

不要直接輸出完整的 Tailwind Preflight。

而是只輸出:

CUI Tokens
+
CUI Component Styles

這樣可以減少對既有頁面的影響。

當然,實際整合時仍然要測試 CSS Cascade 與既有樣式是否衝突。


12. CSS Selector 也要控制影響範圍

例如:

button {
  background: blue;
}

這種寫法不適合直接放進 Legacy CDN。

因為它會影響整個網站的 Button。

我們希望只影響有明確使用 CUI 的元件。

所以使用:

[data-cui-slot="button"] {
  ...
}

而不是:

button {
  ...
}

同樣:

[data-cui-slot="input"] {
  ...
}

而不是:

input {
  ...
}

這樣 Legacy 系統可以逐步導入 CUI,而不需要一次改造整個網站。


13. CSS Token 也要避免命名衝突

假設 Legacy 原本就有:

:root {
  --primary: red;
}

而 CUI 又定義:

:root {
  --primary: blue;
}

兩邊就可能互相影響。

因此 CUI 的公開 Tokens 應該使用自己的 Namespace。

例如:

:root {
  --cui-primary: ...;
  --cui-on-primary: ...;
  --cui-radius-md: ...;
}

這也是我們前面整理 Token Contract 的原因。

讓:

Legacy Existing CSS

和:

CUI CSS

可以盡量降低互相干擾的機會。


14. 建立 CSS Build Script

今天先使用簡單的 CSS Build 工具完成輸出。

例如使用 PostCSS 搭配 CSS Import 與 Minification 插件。

安裝:

npm install -D postcss postcss-import cssnano

新增:

scripts/build-cdn.mjs

我們希望它能:

讀取 cdn.css
↓
解析 @import
↓
輸出 cui.css
↓
壓縮
↓
輸出 cui.min.css

示範:

import fs from "node:fs/promises"
import path from "node:path"

import postcss from "postcss"
import postcssImport from "postcss-import"
import cssnano from "cssnano"

const inputPath = "src/styles/cdn.css"
const outputDir = "dist/cdn"

const input = await fs.readFile(inputPath, "utf8")

await fs.mkdir(outputDir, {
  recursive: true,
})

const result = await postcss([
  postcssImport(),
]).process(input, {
  from: path.resolve(inputPath),
})

await fs.writeFile(
  path.join(outputDir, "cui.css"),
  result.css
)

const minified = await postcss([
  cssnano(),
]).process(result.css, {
  from: undefined,
})

await fs.writeFile(
  path.join(outputDir, "cui.min.css"),
  minified.css
)

console.log("CUI CDN CSS build completed.")

這是一個最小化的示範。

它適合處理一般 CSS 與 @import,但不是完整的 Tailwind CSS v4 編譯流程。

因此這個 CDN Entry 應該使用已經可以直接由瀏覽器理解的 CSS,而不是依賴未處理的 Tailwind 指令。


15. 加入 npm Script

接著修改:

package.json

新增:

{
  "scripts": {
    "build:cdn": "node scripts/build-cdn.mjs"
  }
}

原本的其他 Scripts 保留。

執行:

npm run build:cdn

預期產生:

dist/
└── cdn/
    ├── cui.css
    └── cui.min.css

這樣 CDN CSS 就有自己的建置流程了。


16. CDN Build 和 React Build 要分開嗎?

我覺得應該分開。

因為兩者的目標不同。

React Build:

React Application / Component Output

CDN Build:

Browser-ready CSS

所以:

npm run build

和:

npm run build:cdn

可以各自執行。

未來如果需要,也可以建立:

npm run build:all

例如:

{
  "scripts": {
    "build:all": "npm run build && npm run build:cdn && npm run build-storybook"
  }
}

這樣正式發布前就可以一次產生多種輸出。

不過目前先維持獨立流程,除錯會比較容易。


17. 先建立一個純 HTML 測試頁面

接下來我們不使用 React。

建立:

examples/
└── legacy/
    └── index.html

內容:

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, initial-scale=1">

  <title>CUI Legacy Demo</title>

  <link
    rel="stylesheet"
    href="../../dist/cdn/cui.css"
  >
</head>

<body>
  <main>
    <h1>CUI Legacy Demo</h1>

    <button
      type="button"
      data-cui-slot="button"
      data-cui-variant="primary"
      data-cui-size="md"
    >
      儲存
    </button>
  </main>
</body>
</html>

注意這裡沒有:

React
JSX
TypeScript

只有一般 HTML。

如果 CSS Build 正確,而且 Button Contract 與樣式已經完整對應,就應該能呈現 CUI 的 Button 外觀。


18. 這個測試頁面為什麼重要?

因為它可以直接驗證:

我們產出的 CSS 是否真的不依賴 React Runtime?

如果今天只是:

npm run build:cdn
→ 成功

其實還不夠。

因為成功產生 CSS 檔案,不代表 Legacy 頁面能正確使用。

還需要實際確認:

CSS 載入成功
Token 生效
Button 樣式正確
Focus 樣式正確
沒有影響其他元素

這也是 Day 28 要進一步驗證的事情。


19. 不要只測 Primary Button

假設目前 Button 支援:

primary
secondary
outline
ghost
destructive
link

以及:

sm
md
lg
icon-sm
icon-md
icon-lg

那我們至少需要確認:

每種 Variant
每種 Size
Disabled
Focus Visible
Hover
Active

都能正確呈現。

因為:

Primary Button 正常

不代表:

所有 Button 都正常

尤其是 CVA 與 Shared CSS 遷移期間,更容易出現兩邊不一致的問題。


20. React 和 Legacy 的視覺一致性怎麼確認?

我們現在已經有 Storybook。

所以可以拿:

Storybook / Button / Primary

和:

Legacy Demo / Button / Primary

進行比較。

檢查:

Background
Text Color
Border
Radius
Padding
Height
Focus Indicator
Disabled State

如果兩邊使用相同的 Shared CSS,理論上可以減少樣式差異。

但仍然要注意:

不同 HTML Structure
既有 Global CSS
Font
CSS Cascade

都可能影響最終畫面。

因此共用 CSS 是一致性的基礎,不代表不需要實際驗證。


21. CDN CSS 可以直接放到正式網站嗎?

技術上,只要是瀏覽器可讀取的 CSS,就可以透過:

<link rel="stylesheet" href="...">

載入。

但今天產生的是:

Build Artifact

還不是已經部署到公開 CDN 的檔案。

真正要讓其他網站使用,仍然需要決定:

Hosting
Versioning
Caching
Release

例如:

/assets/cui/0.1.0/cui.min.css

這樣未來更新時,舊系統不會因為 CSS 被直接覆蓋而突然改變樣式。

對 Legacy 系統而言,穩定性尤其重要。


22. 為什麼要有 Versioning?

假設今天:

cui.min.css

已經被十個系統引用。

某天我們修改:

Button Padding
Table Layout
Input Height

如果直接覆蓋同一個檔案,十個系統可能同時受到影響。

所以比較好的方式是:

cui/0.1.0/cui.min.css
cui/0.2.0/cui.min.css

讓系統可以選擇升級時間。

這樣也比較符合 UI Kit 作為共用基礎設施的定位。


23. 那 cui.js 呢?

這裡先回到 Day 23、Day 24。

當時我們做了:

Tooltip
Dialog

這些元件和 Button、Badge 不太一樣。

因為它們需要:

Open / Close
Focus
Escape
Position

所以單靠:

cui.css

通常不夠。

未來可能需要:

cui.js

但我不想今天直接把所有互動元件的 JavaScript 全部寫完。

因為那等於要另外處理:

Tooltip Behavior
Dialog Behavior
Select Behavior

而且還要確認 Accessibility。

所以今天先建立:

CSS Build Pipeline

Day 28 再討論 Legacy 的互動策略與實際整合方式。


24. 哪些元件可以先支援 CSS-only?

可以先整理成:

Component CDN CSS 額外 JS
Button 需要 一般按鈕不需要
Badge 需要 不需要
Alert 需要 靜態訊息不需要
Input 需要 原生輸入不需要
Table 需要 靜態表格不需要
Separator 需要 不需要
Tooltip 需要 通常需要
Dialog 需要 需要互動控制,或採用原生 <dialog> 搭配 JS
Select 需要 原生 <select> 不需要;自訂 Select 通常需要

這張表也讓我們更清楚:

不是所有 CUI Component 都需要 JavaScript。

對 Legacy 系統而言,能用原生 HTML 解決的事情,就不必額外增加 JavaScript 依賴。


25. Accessibility Contract 也必須共享

今天我們談了很多:

CSS
Tokens
Variants
Sizes

但 CUI 是 Accessible UI Kit。

所以 Legacy 不能只拿到外觀。

例如:

<button
  data-cui-slot="button"
  data-cui-variant="primary"
>
  儲存
</button>

這裡使用原生 <button>,就保留了 Button 的基本語意與鍵盤行為。

但如果寫成:

<div
  data-cui-slot="button"
  data-cui-variant="primary"
>
  儲存
</div>

即使外觀看起來一模一樣,也不代表它是 Accessible Button。

所以我們需要把:

Semantic HTML

視為 Contract 的一部分。

不是只要 Attribute 正確,就能隨便使用任何 HTML Tag。


26. Legacy 使用者需要文件

未來 Legacy 開發者可能不會看:

button.tsx

也不會打開:

Storybook Source

所以我們需要提供:

HTML Usage Examples

例如:

<button
  type="submit"
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  送出
</button>

以及:

<button
  type="button"
  data-cui-slot="button"
  data-cui-variant="secondary"
  data-cui-size="md"
>
  取消
</button>

再補充:

Supported Variants
Supported Sizes
Accessibility Requirements
Required HTML Structure

這樣 Legacy 才能真正使用 CUI。

而不只是拿到一個 CSS 檔案。


27. 今天的檢查清單

Build

  • [ ] npm run build:cdn 可以成功執行
  • [ ] 產生 cui.css
  • [ ] 產生 cui.min.css
  • [ ] CSS 不依賴 React Runtime
  • [ ] CSS 沒有未處理的建置指令

Contract

  • [ ] Button 使用 data-cui-slot
  • [ ] Variant 對應正確
  • [ ] Size 對應正確
  • [ ] Shared CSS 使用 CUI Design Tokens
  • [ ] React 與 Legacy 沒有各自維護衝突的樣式規則

Legacy

  • [ ] 純 HTML 頁面可以載入 CSS
  • [ ] Button 樣式正常
  • [ ] Focus Indicator 正常
  • [ ] Disabled State 正常
  • [ ] 不會意外影響未使用 CUI 的元素

28. 最後執行檢查

先建置 CDN CSS:

npm run build:cdn

接著開啟:

examples/legacy/index.html

確認純 HTML 頁面能正常使用。

再執行:

npm run build
npm run lint
npm run check:contrast

最後檢查:

git status
git diff

今天的 Branch 可以使用:

git switch -c feat/cdn-build

完成後:

git add .
git diff --staged
git commit -m "build: add CUI CDN CSS pipeline"

Day 27 完成

今天我們開始讓 CUI 從 React 專案走向 Legacy。

原本:

CUI
 │
 ├── React Components
 └── Storybook

現在增加了:

CUI
 │
 ├── React Components
 ├── Storybook
 │
 └── CDN Build
      ├── cui.css
      └── cui.min.css

但今天最重要的收穫,其實不是成功產生兩個 CSS 檔案。

而是重新確認了:

Single Source, Multiple Outputs 的真正意義。

我們希望共享的是:

Design Tokens
Component Styles
Semantic Contract
Accessibility Requirements

而不是強迫所有環境使用相同的 Framework。

React 可以繼續使用 React Component。

Legacy 可以使用一般 HTML。

只要兩邊遵守同一份 CUI Contract,就能逐步建立一致的使用方式。

當然,目前這還只是第一步。

因為我們只建立了 CSS Build 的基本流程。

真正的 Legacy 整合,還需要驗證:

Existing CSS Compatibility
Form Behavior
Responsive Layout
Accessibility
Interactive Components

所以今天最值得記住的是:

讓 UI Kit 支援 Legacy,不是把 React 拿掉,而是把可以跨環境共享的部分真正抽出來。


Next:Day 28

今天我們已經有:

cui.css
cui.min.css

明天要真的拿它們來使用。

而且這次不在 React 裡。

我們會建立一個模擬 mod_plsql 輸出 HTML 的 Legacy 頁面。

包含:

表單
Button
Alert
Table

並檢查:

CSS 是否衝突?
表單是否正常?
Keyboard 是否可操作?
Screen Reader 是否能理解?

另外也會碰到一個問題:

Tooltip、Dialog 這類互動元件,在沒有 React 的環境裡該怎麼辦?

Day 28:

Legacy 實戰:讓 mod_plsql 頁面真正用上 CUI。


上一篇
Day 26: Storybook + Accessibility:測試通過,就代表無障礙了嗎?
下一篇
Day 28: Legacy 實戰:讓 Legacy 頁面真正用上 CUI
系列文
30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言