171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
/**
|
||
* 基于 localStorage 的客户端持久化存储。
|
||
* 提供会话、标签、图片资源的 CRUD 操作。
|
||
* 所有 key 按 user_id 隔离,确保多用户数据不混淆。
|
||
*/
|
||
|
||
import type { Session, Tag, ImageAsset, ChatMessage } from "./types";
|
||
|
||
// 当前登录用户的 ID,由 AppProvider 在初始化时设置
|
||
let _userId = "";
|
||
|
||
export function setStoreUserId(id: string) {
|
||
_userId = id;
|
||
}
|
||
|
||
export function getStoreUserId(): string {
|
||
return _userId;
|
||
}
|
||
|
||
function storageKey(base: string): string {
|
||
const prefix = _userId || "_anonymous";
|
||
return `epeekit-${prefix}-${base}`;
|
||
}
|
||
|
||
// --------------- 内置标签 ---------------
|
||
|
||
const BUILTIN_TAGS: Tag[] = [
|
||
{ id: "tag-ui", name: "UI", color: "#6366f1", builtin: true },
|
||
{ id: "tag-icon", name: "Icon", color: "#f59e0b", builtin: true },
|
||
{ id: "tag-illustration", name: "原画", color: "#10b981", builtin: true },
|
||
{ id: "tag-style", name: "风格探索", color: "#ec4899", builtin: true },
|
||
{ id: "tag-character", name: "立绘", color: "#8b5cf6", builtin: true },
|
||
{ id: "tag-concept", name: "概念图", color: "#06b6d4", builtin: true },
|
||
];
|
||
|
||
// --------------- 通用 helpers ---------------
|
||
|
||
function readJSON<T>(key: string, fallback: T): T {
|
||
if (typeof window === "undefined") return fallback;
|
||
try {
|
||
const raw = localStorage.getItem(key);
|
||
return raw ? (JSON.parse(raw) as T) : fallback;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
function writeJSON<T>(key: string, data: T) {
|
||
if (typeof window === "undefined") return;
|
||
localStorage.setItem(key, JSON.stringify(data));
|
||
}
|
||
|
||
// --------------- ID 生成 ---------------
|
||
|
||
let counter = 0;
|
||
export function generateId(prefix = ""): string {
|
||
counter++;
|
||
const ts = Date.now().toString(36);
|
||
const rand = Math.random().toString(36).slice(2, 6);
|
||
return `${prefix}${ts}-${rand}-${counter}`;
|
||
}
|
||
|
||
// --------------- 标签 ---------------
|
||
|
||
export function loadTags(): Tag[] {
|
||
const custom = readJSON<Tag[]>(storageKey("tags"), []);
|
||
const builtinIds = new Set(BUILTIN_TAGS.map((t) => t.id));
|
||
const merged = [...BUILTIN_TAGS, ...custom.filter((t) => !builtinIds.has(t.id))];
|
||
return merged;
|
||
}
|
||
|
||
export function saveTags(tags: Tag[]) {
|
||
const custom = tags.filter((t) => !t.builtin);
|
||
writeJSON(storageKey("tags"), custom);
|
||
}
|
||
|
||
export function addTag(name: string, color: string): Tag {
|
||
const tag: Tag = { id: generateId("tag-"), name, color, builtin: false };
|
||
const existing = loadTags();
|
||
saveTags([...existing, tag]);
|
||
return tag;
|
||
}
|
||
|
||
export function deleteTag(tagId: string) {
|
||
const tags = loadTags().filter((t) => t.id !== tagId && !t.builtin);
|
||
saveTags(tags);
|
||
}
|
||
|
||
// --------------- 会话 ---------------
|
||
|
||
export function loadSessions(): Session[] {
|
||
return readJSON<Session[]>(storageKey("sessions"), []);
|
||
}
|
||
|
||
export function saveSessions(sessions: Session[]) {
|
||
writeJSON(storageKey("sessions"), sessions);
|
||
}
|
||
|
||
export function createSession(): Session {
|
||
const now = Date.now();
|
||
const session: Session = {
|
||
id: generateId("sess-"),
|
||
title: "新对话",
|
||
tags: [],
|
||
messages: [],
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
};
|
||
const sessions = loadSessions();
|
||
saveSessions([session, ...sessions]);
|
||
return session;
|
||
}
|
||
|
||
export function updateSession(session: Session) {
|
||
const sessions = loadSessions();
|
||
const idx = sessions.findIndex((s) => s.id === session.id);
|
||
if (idx >= 0) {
|
||
sessions[idx] = { ...session, updatedAt: Date.now() };
|
||
} else {
|
||
sessions.unshift({ ...session, updatedAt: Date.now() });
|
||
}
|
||
saveSessions(sessions);
|
||
}
|
||
|
||
export function deleteSession(sessionId: string) {
|
||
const sessions = loadSessions().filter((s) => s.id !== sessionId);
|
||
saveSessions(sessions);
|
||
const assets = loadAssets().filter((a) => a.sessionId !== sessionId);
|
||
saveAssets(assets);
|
||
}
|
||
|
||
// --------------- 图片资源 ---------------
|
||
|
||
export function loadAssets(): ImageAsset[] {
|
||
return readJSON<ImageAsset[]>(storageKey("assets"), []);
|
||
}
|
||
|
||
export function saveAssets(assets: ImageAsset[]) {
|
||
writeJSON(storageKey("assets"), assets);
|
||
}
|
||
|
||
export function addAsset(asset: ImageAsset) {
|
||
const assets = loadAssets();
|
||
assets.unshift(asset);
|
||
saveAssets(assets);
|
||
}
|
||
|
||
export function updateAsset(assetId: string, patch: Partial<ImageAsset>) {
|
||
const assets = loadAssets();
|
||
const idx = assets.findIndex((a) => a.id === assetId);
|
||
if (idx >= 0) {
|
||
assets[idx] = { ...assets[idx], ...patch };
|
||
saveAssets(assets);
|
||
}
|
||
}
|
||
|
||
export function deleteAsset(assetId: string) {
|
||
saveAssets(loadAssets().filter((a) => a.id !== assetId));
|
||
}
|
||
|
||
export function toggleFavorite(assetId: string): boolean {
|
||
const assets = loadAssets();
|
||
const idx = assets.findIndex((a) => a.id === assetId);
|
||
if (idx >= 0) {
|
||
assets[idx].favorited = !assets[idx].favorited;
|
||
saveAssets(assets);
|
||
return assets[idx].favorited;
|
||
}
|
||
return false;
|
||
}
|