委託平台架構
多租戶委託平台,每個使用者(藝術家、創作者、自由接案者)都擁有一個公開頁面,並具備委託下單功能。此技能涵蓋支援內建代管範本與允許擁有自己網站的創作者進行整合的架構決策。
三層頁面系統
核心架構模式:在三個層級的技術能力與獨立性上支援創作者。
┌─────────────────────────────────────────────────┐
│ Layer 3: 自訂頁面代管 │
│ 上傳自訂 HTML/CSS/JS → 服務於 │
│ /artist/alice(來自 uploads/ 的靜態檔案) │
├─────────────────────────────────────────────────┤
│ Layer 2: 可嵌入元件 │
│ 在現有網站貼上一個 <script> 標籤 │
│ → 「委託下單」按鈕 + 訂單表單 │
├─────────────────────────────────────────────────┤
│ Layer 1: 內建範本 │
│ 選擇一個版面變體,零設定 │
│ → 在 /artist/alice 的完整頁面 │
└─────────────────────────────────────────────────┘
第一層:內建範本
適用對象:沒有自己的網站或不想維護網站的創作者。
實作方式:使用帶有 CSS 變數主題設定的 Vue 元件。
// 路由: /artist/:subdomain
routes.get('/artist/:subdomain', async (req, reply) => {
const artist = await artistService.getBySubdomain(req.params.subdomain)
if (artist.page_type === 'default') {
// 渲染內建的 Vue SPA(或 SSR)
// page_theme 欄位控制要使用哪個範本變體
return reply.sendFile('index.html')
}
// ... 否則交由第 2-3 層處理(此處省略)
})
範本 = 版面配置 × 色板(正交軸線) — 這是唯一最重要的建模決策。如果兩個「範本」主要差異僅在顏色,那麼它們不是兩個範本,而是同一個版面配置穿了兩種顏色。將顏色捆綁進範本維度會導致範本數量膨脹,並產生重複的結構代碼(使用者會指出:「這些看起來像是不同顏色的同一頁面」)。應拆分成創作者可獨立選擇的軸線:
| 軸線 | 誰選擇 | 範例 | 控制項目 |
|---|---|---|---|
| 版面配置(結構) | 創作者 | classic(兩欄工作室)/ gallery(全螢幕展覽)/ folio(單頁落地頁)/ atelier(書本風格溫暖工作室) | 開場處理、欄位邏輯、畫廊形狀、導航 |
| 色板(氛圍) | 創作者 | paper / ink / dusk / moss — 每個都包含淺色和深色變體 | 頁面背景/表面/文字顏色(--pal-* CSS 變數) |
| 強調色 + 模式 | 訪客 | 5 色強調色選擇器 + 淺色/深色偏好 | --color-primary、html.dark |
關鍵規則:
- 一個色板是一個帶有淺色和深色變體的氛圍,永遠不是「深色主題」。創作者選擇
ink;淺色模式訪客看到淺灰色,深色模式訪客看到炭黑色 — 氛圍保持一致,亮度則跟隨訪客。 - 強調色(按鈕/連結/高亮處)始終使用訪客的強調色變數(
--color-primary),永遠不使用色板變數 — 兩個系統保持正交且都能正常運作。 - 4 種版面 × 4 種色板 = 從 8 個定義產生 16 種不同的外觀。
- 版面配置必須在結構上有所不同(不同的開場、不同的欄位邏輯),否則應合併為一個版面。
可行的版面原型 — 每種都以創作者最具特色的資產(他們的藝術作品)開場,而非通用的置中姓名+簡介+按鈕英雄區:
| 版面 | 開場 | 招牌動作 |
|---|---|---|
| gallery | 全視口藝術作品,姓名作為小角落銘牌 | 編輯式大小交替畫廊、懸浮縮放、點擊預覽 |
| classic | 簽名作品橫幅,姓名疊加在藝術品上 | 桌面雙欄、固定資訊卡、始終可見的委託 CTA |
| folio | 分割畫面:左側文字,右側疊放藝術品 | 滾動追蹤導航高亮、行動版漢堡選單 + 底部固定 CTA |
| atelier | 溫暖書本風格跨頁、襯線字體(Noto Serif SC) | 紙質紋理背景、編輯式邊距、寬鬆留白、具文學氣息的藝術優先 |
共享套件架構(經實證):範本永遠不直接讀取後端欄位名稱 — 一個適配器複合函式(useArtistData(props))提供 imgUrl()、statusText()(國際化)、socialLinks、heroArtwork;可複用的 Tpl* 元件透過 props 接收變體(TplHero variant="banner|fullscreen|split"、TplGallery layout="grid|editorial|masonry"),並提供插槽作為擴充點(階層網格上的 #addons)。完整模式(包括非同步元件時序修復和舊版 ID 對應)請參閱 references/template-system.md。
創作者設定的強調色覆蓋(R49 模式):創作者可以設定一個永久的強調色(5 色白名單,儲存為 artists.accent_color TEXT),該顏色會覆蓋訪客在其公開頁面上的 ThemePicker 選擇。在 ArtistHome.vue 中實作:
const ACCENT_INDEX = { '#34dbcb': '1', '#34c2db': '2', '#3498db': '3', '#346edb': '4', '#3445db': '5' }
const accentOverride = computed(() => {
const raw = previewAccent.value || artist.value?.accentColor
return raw ? (ACCENT_INDEX[String(raw).toLowerCase()] || null) : null
})
// watch(immediate) + onUnmounted: 儲存/還原 document.documentElement.dataset.accent
關鍵決策:(1) 全域 data-accent 屬性注入 — 不需要針對每個範本修改 CSS 變數(theme.css 已有 data-accent="1"~"5" 選擇器,包含淺色和深色變體)。為所有 4 個範本增加強調色支援 = 零範本檔案變更。(2) 在創作者的頁面上,創作者的強調色優先於訪客的 ThemePicker 選擇;訪客的選擇在 onUnmounted 時還原。(3) 後端驗證 5 色白名單 + null(清除);寫入時進行 toLowerCase() 正規化。(4) 設定 UI:圓形色票按鈕(32px),啟用時有 ✓ 勾選,外加「預設」清除按鈕。不使用 el-color-picker(自由格式顏色會破壞白名單)。(5) C61 決策:強調色僅影響客戶端頁面,不影響創作者儀表板。
透過 URL 參數的預覽模式(R50 模式):設定中的「預覽主頁」按鈕會在新視窗開啟 /artist/:subdomain?_tpl=X&_pal=Y&_accent=Z。ArtistHome.vue 將 route.query._tpl/_pal/_accent 讀取為渲染層覆蓋(永遠不寫入資料層)。當存在任何預覽參數時,會顯示一個黏性警告橫幅(「預覽模式 — 修改尚未儲存」)。範本/色板/強調色的 computed 屬性會依序回退:previewParam || artist.field || default。對公開存取安全 — 預覽參數僅影響渲染,所有資料來自公開 API。
以活動時間軸取代平面筆記列表(R40 模式):在將狀態變更記錄(系統筆記)和創作者筆記合併為單一時序檢視時,使用 el-timeline 並搭配類型/空心區分:
<el-timeline-item :type="note.created_by === 'system' ? 'info' : (note.image_path ? 'success' : 'primary')"
:hollow="note.created_by === 'system'" :timestamp="formatDate(note.created_at)" placement="top">
類型指示器:🔄 狀態變更 / 📝 筆記 / 🖼 附圖片的筆記。系統筆記獲得 tl-item--system 類別(淡化色彩、較小字型)。刪除按鈕(R46)僅對非系統筆記顯示,懸浮顯示並附帶 @media (hover: none) 觸控備援。輸入框移至時間軸底部。零後端變更 — 系統筆記已存在於 order_notes 中,且 created_by='system'。操作列(推進/回退/取消)保留在時間軸卡片外部。
具有情境相關確認行為的拖曳替換(R53 vs R55):同一個應用程式中的兩個拖曳替換功能具有不同的確認需求 — 請不要將它們統一:
| R53 佇列看板焦點圖片 | R55 階層範例圖片 | |
|---|---|---|
| 覆蓋確認 | 否(舊圖片保留在訂單畫廊中) | 是 — ElMessageBox.confirm(舊圖片遺失) |
| 原因 | 替換焦點 ≠ 刪除圖片 | 覆蓋範例 = 舊圖片無法復原 |
實作:共享拖曳基礎設施(dragover.prevent/dragleave 搭配 contains(relatedTarget) 反閃爍/drop.prevent + 隱藏檔案輸入),但上傳函式僅在 R55 中檢查 if (row.example_image) { await ElMessageBox.confirm(...) }。兩者都重用現有的上傳 API。必須從 R53 的焦點圖片中移除 el-image 的 preview-src-list,以防止它吞掉點擊事件(與 R18 相同的陷阱)。 |
訂單表單複合函式萃取(R58-1 模式):當訂單表單增長超過約 400 行(資料載入 + 定價 + 驗證 + 上傳 + 提交 + 草稿復原)時,將所有業務邏輯萃取到一個共享的複合函式中(useOrderForm.js)。頁面元件變成純版面配置(約 300 行的範本 + 樣式,script 部分僅為 const of = useOrderForm(subdomain, formRef) + 解構賦值)。該複合函式回傳一個扁平物件,包含按關注點分組的 refs/reactives/方法:資料載入(artist、tiers、rulesContent、loading)、表單狀態 + 驗證規則、提交狀態(submitting、showSuccess、resultNo、submit)、參考圖片(refFileList、handleRefUpload、handleRefRemove)、定價(addonSelections、pricePreview、pricingExpanded、selectedTier、onTierChange)以及經過處理的規則 HTML。這為何對多範本至關重要:當訂單頁面支援多種版面範本(逐步式、收據風格、雜誌風格)時,每個範本都是一個薄的版面外殼,呼叫同一個複合函式 — 零邏輯重複。該複合函式是「核心」,範本是「外觀」。這是與首頁 useArtistData 相同的適配器模式,只是應用於訂單表單。關鍵實作細節:該複合函式將 formRef(來自範本的 el-form ref)作為參數,因為驗證(formRef.value.validate())必須呼叫到範本的表單元素 — 複合函式擁有邏輯但不擁有 DOM。
逐步訂單表單版面配置(R58-2 模式,經實證):預設訂單版面配置是一個三步驟引導流程,完全建構在 useOrderForm() 之上(從 R58-1 複合函式零變更 — 驗證了萃取的合理性):
- 步驟指示器:三個帶編號的圓點 + 連線。目前步驟:放大 + 主要色。已完成:✓ + 主要色填充。待處理:灰色空心。行動版(≤860px):僅顯示圓點。
- 步驟 1 — 選擇階層:響應式卡片網格(
repeat(auto-fill, minmax(240px, 1fr)))取代el-select。選中:--ease-bounce轉場 + 旋轉印章 ✓(@keyframes tier-stamp-in: 縮放 0→1.2→1,旋轉 -15°→0°)。R14 漸進式揭露保留在步驟 1 內。 - 步驟 2 — 描述 + 上傳:文字區域 + 靈感標籤(R58-4:硬編碼預設晶片,點擊附加,智慧逗號 + 2000 字元截斷)+ 參考圖片上傳(未更改的檔案與貼上)。
- 步驟 3 — 聯絡方式:QQ + 姓名 + 規則同意。
- 黏性摘要卡:寬螢幕右側欄位(280px 黏性),即時顯示階層 + 明細 + 總計 + 分期。行動版:當前步驟底部。
- 收據確認(R58-3):提交 → 驗證 → 咖啡館收據
el-dialog(teleported 到 body,全域 CSSreceipt-*命名空間在templates.css中)。紙色#fdfbf5,Courier 等寬字型,鋸齒邊緣(repeating-conic-gradient),虛線分隔線,條碼裝飾,分期晶片。固定淺色主題(紙張隱喻)。取消返回步驟 3;確認呼叫submit()。 - 複製訂單資訊(R58-5):成功對話框「複製約稿資訊」→ 多行剪貼簿(訂單號碼 + 階層 + 附加項目 + 總計)。
- 步驟導航:「下一步」在移動到下一步前驗證當前步驟;「上一步」始終允許;不可跳過。
多範本訂單頁面(R58 架構):訂單頁面支援創作者選擇的版面範本,與首頁範本無關。後端(經實證,migration v16):artists.order_template_id TEXT DEFAULT 'default' — 服務層白名單驗證(ORDER_TEMPLATES = ['default'],新增範本時擴展陣列)。GET /api/artists/:subdomain 回傳 orderTemplateId: artist.order_template_id || 'default'。PUT /api/artist/profile 接受 orderTemplateId(透過 keyMap 對應至 order_template_id)。無效值拋出 INVALID_ORDER_TEMPLATE(409)。空字串回退為 'default'。前端:創作者設定頁面顯示一個卡片式範本選擇器(與首頁範本選擇相同的 UI 模式)。客戶端路由:OrderForm.vue 讀取 artist.orderTemplateId 並透過 <component :is="..."> 或範本註冊表對應來渲染對應的版面元件。所有版面元件共享 useOrderForm() — 它們僅在視覺排列上有所不同(步驟指示器、卡片版面配置、確認對話框樣式)。新增一個訂單範本 = 一個新的 .vue 檔案(版面外殼)+ 後端白名單陣列中的一個條目,零邏輯變更。預覽:重用首頁預覽的 _tpl URL 參數機制。
QQ 聯絡方式模式(R58-6):兩種 QQ 聯絡方式:「跳轉QQ」(window.open('tencent://message/?uin=' + encodeURIComponent(qq), '_self'))+「複製QQ」(navigator.clipboard.writeText(qq) 並顯示訊息提示)。剪貼簿備援:失敗時(權限被拒、非 HTTPS),顯示 ElMessage.warning(qq) — QQ 號碼本身就是警告訊息,使用者可以手動從提示中複製。應用於兩個地方:客戶端成功對話框(創作者的 contactQq)和創作者端訂單詳情(客戶的 qq)。i18n:根據情境使用不同的鍵值(orderForm.jumpQq vs orderDetail.jumpQq),因為提示訊息不同(「QQ號碼已複製」vs 「客戶QQ號碼已複製」)。
CSS 工藝升級模式(v0.16 batch1,經實證):
- 全域彈性緩動:
--ease-bounce: cubic-bezier(.34, 1.56, .64, 1)在:root中。僅對 transform/box-shadow 轉場使用(永遠不對 opacity 使用 — 無法 overshoot 1.0)。使用端備援:var(--ease-bounce, cubic-bezier(0.22, 1, 0.36, 1))。 - 按鈕三態物理模型:
.el-button:hover:not(:disabled):not(.is-loading) { transform: translateY(-3px); box-shadow: deeper }+:active { transform: translateY(-1px) scale(0.98) }。:not()排除項目防止停用/載入中的按鈕產生動畫。 clamp()流體:
…
[因翻譯而截斷]