visionA/visionA-frontend/src/lib/api/model-sharing.mock.ts
jim800121chen 6a797d5eb5 feat(frontend): 設備管理三態/註冊 + 模型共享庫/公開設定(B + C)
B 設備管理:device-store 補 registeredAt + register/unregister actions;
deriveTriState 三態(已連接/未連接/已連接未註冊);三態 badge(warning token
+ icon + 文字不只色);排序/filter chips;註冊 UI(明確區分取消註冊≠移除)。

C 模型共享:/models/library cursor 無限捲動 + 搜尋/filter/排序;visibility badge
三態;公開設定 Dialog(RadioGroup + shares 管理 + public 警告 + 二次確認);
profile 頁 owner/公開雙態;radio-group 新元件;owner 用 name 不洩 email。

共用檔 types/api.ts(B error codes)+ i18n en/zh(devices.* B / models.* C)。
零新 Design Token。reviewer B(0C/0M) + C(0C/0M、設計12/12 API8/8) 通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 16:30:22 +08:00

156 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Model Sharing — Mock Fixtures平行開發用
*
* 對 API 契約 mockapi-model-sharing.md 的 response 形狀),不依賴後端跑起來。
* 形狀為契約定義的 snake_case經 model-sharing.ts 的 normalize 後才成前端型別。
*
* 用途:
* 1. store 在 `NEXT_PUBLIC_USE_MODEL_SHARING_MOCK=1` 時走 mock見 model-sharing-store
* 2. 元件測試 fixture 來源
*
* ⚠️ mock owner 資料刻意不含 email對齊契約 §4非擁有者不該看到他人 email
* shares 的 email 是 owner 授權目標,才有 email。
*/
/** 契約 §1 library item 的原始snake_case形狀。 */
export interface RawLibraryItem {
id: string;
name: string;
description?: string;
target_chip: string;
file_size: number;
source: string;
status: string;
visibility: string;
owner: { id: string; name: string; is_me: boolean };
shared_with_me: boolean;
my_access: string;
created_at: string;
updated_at: string;
}
/** 契約 §1 分頁 response 原始形狀。 */
export interface RawLibraryPage {
items: RawLibraryItem[];
next_cursor: string | null;
has_more: boolean;
}
const NOW = "2026-08-01T00:00:00Z";
/**
* 一個較大的 mock 資料集30 筆),用來驗證 cursor 無限捲動分頁。
* 混合三種可見性、owner/非 owner、shared_with_me 各態,覆蓋 UI 分支。
*/
export const MOCK_LIBRARY_ITEMS: RawLibraryItem[] = Array.from({ length: 30 }).map(
(_, i) => {
const isMine = i % 3 === 0;
const visibility = isMine ? "private" : i % 3 === 1 ? "public" : "tenant";
const sharedWithMe = !isMine && i % 4 === 0;
const chips = ["kl520", "kl720", "kl630", "kl730"];
const sources = ["converted", "uploaded", "preset"];
return {
id: `mock-model-${String(i + 1).padStart(2, "0")}`,
name: `mock-model-${i + 1} ${["yolov5s", "resnet50", "mobilenet", "ssd"][i % 4]}`,
description: i % 2 === 0 ? `Mock 模型 ${i + 1} 的描述` : undefined,
target_chip: chips[i % chips.length],
file_size: (i + 1) * 1024 * 1024,
source: isMine ? "converted" : sources[i % sources.length],
status: "ready",
visibility,
owner: isMine
? { id: "me", name: "我", is_me: true }
: { id: `owner-${i}`, name: `Owner ${(i % 5) + 1}`, is_me: false },
shared_with_me: sharedWithMe,
my_access: isMine ? "owner" : sharedWithMe ? "viewer" : "viewer",
created_at: `2026-07-${String((i % 28) + 1).padStart(2, "0")}T00:00:00Z`,
updated_at: NOW,
};
},
);
/**
* 對 MOCK_LIBRARY_ITEMS 套用 query搜尋 / filter / 排序 / cursor 分頁)產生一頁。
* cursor = 已消費筆數的 base64不透明mock 只需能往下切)。
*/
export function mockLibraryPage(query: {
cursor?: string;
limit?: number;
q?: string;
targetChip?: string;
source?: string;
visibility?: string;
owned?: boolean;
sort?: string;
order?: string;
}): RawLibraryPage {
let items = [...MOCK_LIBRARY_ITEMS];
// filter
if (query.q) {
const q = query.q.toLowerCase();
items = items.filter(
(m) =>
m.name.toLowerCase().includes(q) ||
(m.description ?? "").toLowerCase().includes(q),
);
}
if (query.targetChip) {
items = items.filter((m) => m.target_chip === query.targetChip);
}
if (query.source) {
items = items.filter((m) => m.source === query.source);
}
if (query.visibility) {
items = items.filter((m) => m.visibility === query.visibility);
}
if (query.owned === true) {
items = items.filter((m) => m.owner.is_me);
} else if (query.owned === false) {
items = items.filter((m) => !m.owner.is_me);
}
// sort
const order = query.order === "asc" ? 1 : -1;
const sortKey = query.sort ?? "created_at";
items.sort((a, b) => {
if (sortKey === "name") return a.name.localeCompare(b.name) * order;
if (sortKey === "file_size") return (a.file_size - b.file_size) * order;
return a.created_at.localeCompare(b.created_at) * order;
});
// cursor 分頁
const limit = Math.min(Math.max(query.limit ?? 20, 1), 100);
const start = query.cursor ? Number(atob(query.cursor)) : 0;
const slice = items.slice(start, start + limit);
const nextStart = start + slice.length;
const hasMore = nextStart < items.length;
return {
items: slice,
next_cursor: hasMore ? btoa(String(nextStart)) : null,
has_more: hasMore,
};
}
/** 依 id 產生一筆 profile 原始形狀(找不到回 null模擬 404。 */
export function mockProfile(id: string): Record<string, unknown> | null {
const item = MOCK_LIBRARY_ITEMS.find((m) => m.id === id);
if (!item) return null;
return {
...item,
input_shape: [1, 3, 224, 224],
classes: ["person", "car", "dog", "cat"],
framework: "onnx",
can_download: item.my_access !== "none",
uploaded_at: item.created_at,
};
}
/** mock shares 清單owner 檢視自己模型的授權對象)。 */
export const MOCK_SHARES: Record<string, Array<{ user_id: string; email: string; role: string; created_at: string }>> = {
"mock-model-01": [
{ user_id: "u-alice", email: "alice@corp.com", role: "viewer", created_at: NOW },
{ user_id: "u-bob", email: "bob@corp.com", role: "viewer", created_at: NOW },
],
};