-
新用户注册
-
2024-01-15 08:45:00
+
+
+
+
+
审计任务
+
{stats.totalTasks}
+
-
-
-
-
数据库连接超时
-
2024-01-15 08:30:00
+
+
+
+
+
问题
+
{stats.totalIssues}
+
@@ -366,22 +346,182 @@ export default function AdminDashboard() {
+ {/* 存储管理 */}
+
+
+
+
+
+ 存储空间使用情况
+
+
+ 浏览器 IndexedDB 存储空间的使用详情
+
+
+
+ {storageDetails ? (
+ <>
+
+
+ 已使用空间
+ {storageDetails.percentage}%
+
+
+
+ {stats.storageUsed} 已使用
+ {stats.storageQuota} 总配额
+
+
+
+
+
+
已使用
+
{stats.storageUsed}
+
+
+
总配额
+
{stats.storageQuota}
+
+
+
剩余空间
+
+ {((storageDetails.quota - storageDetails.usage) / 1024 / 1024).toFixed(2)} MB
+
+
+
+
+ {storageDetails.percentage > 80 && (
+
+
+
+ 存储空间使用率已超过 80%,建议清理不需要的数据或导出备份后清空数据库。
+
+
+ )}
+ >
+ ) : (
+
+
+
+ 无法获取存储空间信息。您的浏览器可能不支持 Storage API。
+
+
+ )}
+
+
+
+
+
+ 存储优化建议
+
+
+
+
+
+
定期导出备份
+
+ 建议定期导出数据为 JSON 文件,防止数据丢失
+
+
+
+
+
+
+
清理旧数据
+
+ 删除不再需要的项目和任务可以释放存储空间
+
+
+
+
+
+
+
监控存储使用
+
+ 定期检查存储使用情况,避免超出浏览器限制
+
+
+
+
+
+
+
+ {/* 数据操作 */}
+
+
+
+
+ {/* 设置 */}
-
-
+
+
+ 数据库设置
+ 配置数据库行为和性能选项
+
+
+
+
+
+ 当前数据库模式: {dbMode === 'local' ? '本地 IndexedDB' : dbMode === 'supabase' ? 'Supabase 云端' : '演示模式'}
+
+
-
-
+
+
+
+
自动备份
+
+ 定期自动导出数据备份(开发中)
+
+
+
即将推出
+
+
+
+
+
数据压缩
+
+ 压缩存储数据以节省空间(开发中)
+
+
+
即将推出
+
+
+
+
+
数据同步
+
+ 在多个设备间同步数据(开发中)
+
+
+
即将推出
+
+
+
+
+
+
+
+ 关于本地数据库
+
+
+
+ 本地数据库使用浏览器的 IndexedDB 技术存储数据,具有以下特点:
+
+
+ - 数据完全存储在本地,不会上传到服务器
+ - 支持离线访问,无需网络连接
+ - 存储容量取决于浏览器和设备
+ - 清除浏览器数据会删除所有本地数据
+ - 不同浏览器的数据相互独立
+
+
+ 建议:定期导出数据备份,以防意外数据丢失。
+
+
+
);
-}
\ No newline at end of file
+}
diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx
index 826871c..8bd4b7a 100644
--- a/src/pages/Projects.tsx
+++ b/src/pages/Projects.tsx
@@ -147,7 +147,7 @@ export default function Projects() {
projectId: project.id,
zipFile: file,
excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
- createdBy: undefined
+ createdBy: 'local-user' // 使用默认本地用户ID
});
clearInterval(progressInterval);
diff --git a/src/shared/config/database.ts b/src/shared/config/database.ts
index 51a09e6..d10c3da 100644
--- a/src/shared/config/database.ts
+++ b/src/shared/config/database.ts
@@ -1,4 +1,5 @@
import { createClient } from "@supabase/supabase-js";
+import { localDB } from "./localDatabase";
import type {
Profile,
Project,
@@ -9,10 +10,11 @@ import type {
CreateProjectForm,
CreateAuditTaskForm,
InstantAnalysisForm
-} from "@/types/types";
+} from "../types/index";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
+const useLocalDB = import.meta.env.VITE_USE_LOCAL_DB === 'true';
const isValidUuid = (value?: string): boolean => {
if (!value) return false;
@@ -35,19 +37,21 @@ export const supabase = hasSupabaseConfig ? createClient(finalSupabaseUrl, final
}
}) : null;
-// 演示模式标识
-export const isDemoMode = !hasSupabaseConfig;
+// 数据库模式:local(本地IndexedDB)、supabase(云端)、demo(演示模式)
+export const dbMode = useLocalDB ? 'local' : (hasSupabaseConfig ? 'supabase' : 'demo');
+export const isDemoMode = dbMode === 'demo';
+export const isLocalMode = dbMode === 'local';
// 演示数据
const demoProfile: Profile = {
id: 'demo-user',
- phone: null,
+ phone: undefined,
email: 'demo@xcodereviewer.com',
full_name: 'Demo User',
- avatar_url: null,
+ avatar_url: undefined,
role: 'admin',
github_username: 'demo-user',
- gitlab_username: null,
+ gitlab_username: undefined,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
@@ -60,6 +64,10 @@ export const api = {
return demoProfile;
}
+ if (isLocalMode) {
+ return localDB.getProfileById(id);
+ }
+
if (!supabase) return null;
const { data, error } = await supabase
@@ -77,6 +85,10 @@ export const api = {
return 1;
}
+ if (isLocalMode) {
+ return localDB.getProfilesCount();
+ }
+
if (!supabase) return 0;
const { count, error } = await supabase
@@ -88,6 +100,12 @@ export const api = {
},
async createProfiles(profile: Partial
): Promise {
+ if (isLocalMode) {
+ return localDB.createProfile(profile);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('profiles')
.insert([{
@@ -108,6 +126,12 @@ export const api = {
},
async updateProfile(id: string, updates: Partial): Promise {
+ if (isLocalMode) {
+ return localDB.updateProfile(id, updates);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('profiles')
.update(updates)
@@ -120,6 +144,12 @@ export const api = {
},
async getAllProfiles(): Promise {
+ if (isLocalMode) {
+ return localDB.getAllProfiles();
+ }
+
+ if (!supabase) return [];
+
const { data, error } = await supabase
.from('profiles')
.select('*')
@@ -148,6 +178,10 @@ export const api = {
}];
}
+ if (isLocalMode) {
+ return localDB.getProjects();
+ }
+
if (!supabase) return [];
const { data, error } = await supabase
@@ -164,6 +198,12 @@ export const api = {
},
async getProjectById(id: string): Promise {
+ if (isLocalMode) {
+ return localDB.getProjectById(id);
+ }
+
+ if (!supabase) return null;
+
const { data, error } = await supabase
.from('projects')
.select(`
@@ -178,6 +218,12 @@ export const api = {
},
async createProject(project: CreateProjectForm & { owner_id?: string }): Promise {
+ if (isLocalMode) {
+ return localDB.createProject(project);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('projects')
.insert([{
@@ -201,6 +247,12 @@ export const api = {
},
async updateProject(id: string, updates: Partial): Promise {
+ if (isLocalMode) {
+ return localDB.updateProject(id, updates);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const updateData: any = { ...updates };
if (updates.programming_languages) {
updateData.programming_languages = JSON.stringify(updates.programming_languages);
@@ -221,6 +273,12 @@ export const api = {
},
async deleteProject(id: string): Promise {
+ if (isLocalMode) {
+ return localDB.deleteProject(id);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { error } = await supabase
.from('projects')
.update({ is_active: false })
@@ -231,6 +289,12 @@ export const api = {
// ProjectMember相关
async getProjectMembers(projectId: string): Promise {
+ if (isLocalMode) {
+ return localDB.getProjectMembers(projectId);
+ }
+
+ if (!supabase) return [];
+
const { data, error } = await supabase
.from('project_members')
.select(`
@@ -246,6 +310,12 @@ export const api = {
},
async addProjectMember(projectId: string, userId: string, role: string = 'member'): Promise {
+ if (isLocalMode) {
+ return localDB.addProjectMember(projectId, userId, role);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('project_members')
.insert([{
@@ -267,6 +337,12 @@ export const api = {
// AuditTask相关
async getAuditTasks(projectId?: string): Promise {
+ if (isLocalMode) {
+ return localDB.getAuditTasks(projectId);
+ }
+
+ if (!supabase) return [];
+
let query = supabase
.from('audit_tasks')
.select(`
@@ -286,6 +362,12 @@ export const api = {
},
async getAuditTaskById(id: string): Promise {
+ if (isLocalMode) {
+ return localDB.getAuditTaskById(id);
+ }
+
+ if (!supabase) return null;
+
const { data, error } = await supabase
.from('audit_tasks')
.select(`
@@ -301,6 +383,12 @@ export const api = {
},
async createAuditTask(task: CreateAuditTaskForm & { created_by: string }): Promise {
+ if (isLocalMode) {
+ return localDB.createAuditTask(task);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('audit_tasks')
.insert([{
@@ -324,6 +412,12 @@ export const api = {
},
async updateAuditTask(id: string, updates: Partial): Promise {
+ if (isLocalMode) {
+ return localDB.updateAuditTask(id, updates);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('audit_tasks')
.update(updates)
@@ -341,6 +435,12 @@ export const api = {
// AuditIssue相关
async getAuditIssues(taskId: string): Promise {
+ if (isLocalMode) {
+ return localDB.getAuditIssues(taskId);
+ }
+
+ if (!supabase) return [];
+
const { data, error } = await supabase
.from('audit_issues')
.select(`
@@ -357,6 +457,12 @@ export const api = {
},
async createAuditIssue(issue: Omit): Promise {
+ if (isLocalMode) {
+ return localDB.createAuditIssue(issue);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('audit_issues')
.insert([issue])
@@ -372,6 +478,12 @@ export const api = {
},
async updateAuditIssue(id: string, updates: Partial): Promise {
+ if (isLocalMode) {
+ return localDB.updateAuditIssue(id, updates);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('audit_issues')
.update(updates)
@@ -389,6 +501,12 @@ export const api = {
// InstantAnalysis相关
async getInstantAnalyses(userId?: string): Promise {
+ if (isLocalMode) {
+ return localDB.getInstantAnalyses(userId);
+ }
+
+ if (!supabase) return [];
+
let query = supabase
.from('instant_analyses')
.select(`
@@ -413,6 +531,12 @@ export const api = {
quality_score?: number;
analysis_time?: number;
}): Promise {
+ if (isLocalMode) {
+ return localDB.createInstantAnalysis(analysis);
+ }
+
+ if (!supabase) throw new Error('Database not available');
+
const { data, error } = await supabase
.from('instant_analyses')
.insert([{
@@ -448,6 +572,10 @@ export const api = {
};
}
+ if (isLocalMode) {
+ return localDB.getProjectStats();
+ }
+
if (!supabase) {
return {
total_projects: 0,
diff --git a/src/shared/config/env.ts b/src/shared/config/env.ts
index 51cf9b0..4c2ccba 100644
--- a/src/shared/config/env.ts
+++ b/src/shared/config/env.ts
@@ -77,6 +77,9 @@ export const env = {
MAX_ANALYZE_FILES: Number(import.meta.env.VITE_MAX_ANALYZE_FILES) || 40,
LLM_CONCURRENCY: Number(import.meta.env.VITE_LLM_CONCURRENCY) || 2,
LLM_GAP_MS: Number(import.meta.env.VITE_LLM_GAP_MS) || 500,
+
+ // ==================== 语言配置 ====================
+ OUTPUT_LANGUAGE: import.meta.env.VITE_OUTPUT_LANGUAGE || 'zh-CN', // zh-CN | en-US
// ==================== 开发环境标识 ====================
isDev: import.meta.env.DEV,
diff --git a/src/shared/config/localDatabase.ts b/src/shared/config/localDatabase.ts
new file mode 100644
index 0000000..9ce62ca
--- /dev/null
+++ b/src/shared/config/localDatabase.ts
@@ -0,0 +1,591 @@
+/**
+ * 本地数据库实现 - 使用 IndexedDB
+ * 提供与 Supabase 相同的 API 接口,但数据存储在浏览器本地
+ */
+
+import type {
+ Profile,
+ Project,
+ ProjectMember,
+ AuditTask,
+ AuditIssue,
+ InstantAnalysis,
+ CreateProjectForm,
+ CreateAuditTaskForm,
+ InstantAnalysisForm
+} from "../types/index";
+
+const DB_NAME = 'xcodereviewer_local';
+const DB_VERSION = 1;
+
+// 数据库表名
+const STORES = {
+ PROFILES: 'profiles',
+ PROJECTS: 'projects',
+ PROJECT_MEMBERS: 'project_members',
+ AUDIT_TASKS: 'audit_tasks',
+ AUDIT_ISSUES: 'audit_issues',
+ INSTANT_ANALYSES: 'instant_analyses',
+} as const;
+
+class LocalDatabase {
+ private db: IDBDatabase | null = null;
+ private initPromise: Promise | null = null;
+
+ /**
+ * 初始化数据库
+ */
+ async init(): Promise {
+ if (this.db) return;
+ if (this.initPromise) return this.initPromise;
+
+ this.initPromise = new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => {
+ this.db = request.result;
+ resolve();
+ };
+
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result;
+
+ // 创建 profiles 表
+ if (!db.objectStoreNames.contains(STORES.PROFILES)) {
+ const profileStore = db.createObjectStore(STORES.PROFILES, { keyPath: 'id' });
+ profileStore.createIndex('email', 'email', { unique: false });
+ profileStore.createIndex('role', 'role', { unique: false });
+ }
+
+ // 创建 projects 表
+ if (!db.objectStoreNames.contains(STORES.PROJECTS)) {
+ const projectStore = db.createObjectStore(STORES.PROJECTS, { keyPath: 'id' });
+ projectStore.createIndex('owner_id', 'owner_id', { unique: false });
+ projectStore.createIndex('is_active', 'is_active', { unique: false });
+ projectStore.createIndex('created_at', 'created_at', { unique: false });
+ }
+
+ // 创建 project_members 表
+ if (!db.objectStoreNames.contains(STORES.PROJECT_MEMBERS)) {
+ const memberStore = db.createObjectStore(STORES.PROJECT_MEMBERS, { keyPath: 'id' });
+ memberStore.createIndex('project_id', 'project_id', { unique: false });
+ memberStore.createIndex('user_id', 'user_id', { unique: false });
+ }
+
+ // 创建 audit_tasks 表
+ if (!db.objectStoreNames.contains(STORES.AUDIT_TASKS)) {
+ const taskStore = db.createObjectStore(STORES.AUDIT_TASKS, { keyPath: 'id' });
+ taskStore.createIndex('project_id', 'project_id', { unique: false });
+ taskStore.createIndex('created_by', 'created_by', { unique: false });
+ taskStore.createIndex('status', 'status', { unique: false });
+ taskStore.createIndex('created_at', 'created_at', { unique: false });
+ }
+
+ // 创建 audit_issues 表
+ if (!db.objectStoreNames.contains(STORES.AUDIT_ISSUES)) {
+ const issueStore = db.createObjectStore(STORES.AUDIT_ISSUES, { keyPath: 'id' });
+ issueStore.createIndex('task_id', 'task_id', { unique: false });
+ issueStore.createIndex('severity', 'severity', { unique: false });
+ issueStore.createIndex('status', 'status', { unique: false });
+ }
+
+ // 创建 instant_analyses 表
+ if (!db.objectStoreNames.contains(STORES.INSTANT_ANALYSES)) {
+ const analysisStore = db.createObjectStore(STORES.INSTANT_ANALYSES, { keyPath: 'id' });
+ analysisStore.createIndex('user_id', 'user_id', { unique: false });
+ analysisStore.createIndex('created_at', 'created_at', { unique: false });
+ }
+ };
+ });
+
+ return this.initPromise;
+ }
+
+ /**
+ * 生成 UUID
+ */
+ private generateId(): string {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ const r = Math.random() * 16 | 0;
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+
+ /**
+ * 获取对象存储
+ */
+ private getStore(storeName: string, mode: IDBTransactionMode = 'readonly'): IDBObjectStore {
+ if (!this.db) throw new Error('Database not initialized');
+ const transaction = this.db.transaction(storeName, mode);
+ return transaction.objectStore(storeName);
+ }
+
+ /**
+ * 通用查询方法
+ */
+ private async getAll(storeName: string): Promise {
+ await this.init();
+ return new Promise((resolve, reject) => {
+ const store = this.getStore(storeName);
+ const request = store.getAll();
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ /**
+ * 通过 ID 获取单条记录
+ */
+ private async getById(storeName: string, id: string): Promise {
+ if (!id) return null;
+
+ await this.init();
+ return new Promise((resolve, reject) => {
+ const store = this.getStore(storeName);
+ const request = store.get(id);
+ request.onsuccess = () => resolve(request.result || null);
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ /**
+ * 通过索引查询
+ */
+ private async getByIndex(storeName: string, indexName: string, value: any): Promise {
+ await this.init();
+ return new Promise((resolve, reject) => {
+ const store = this.getStore(storeName);
+ const index = store.index(indexName);
+ const request = index.getAll(value);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ /**
+ * 插入或更新记录
+ */
+ private async put(storeName: string, data: T): Promise {
+ await this.init();
+ return new Promise((resolve, reject) => {
+ const store = this.getStore(storeName, 'readwrite');
+ const request = store.put(data);
+ request.onsuccess = () => resolve(data);
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ /**
+ * 删除记录
+ */
+ private async deleteRecord(storeName: string, id: string): Promise {
+ await this.init();
+ return new Promise((resolve, reject) => {
+ const store = this.getStore(storeName, 'readwrite');
+ const request = store.delete(id);
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ /**
+ * 统计记录数
+ */
+ private async count(storeName: string): Promise {
+ await this.init();
+ return new Promise((resolve, reject) => {
+ const store = this.getStore(storeName);
+ const request = store.count();
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ // ==================== Profile 相关方法 ====================
+
+ async getProfileById(id: string): Promise {
+ return this.getById(STORES.PROFILES, id);
+ }
+
+ async getProfilesCount(): Promise {
+ return this.count(STORES.PROFILES);
+ }
+
+ async createProfile(profile: Partial): Promise {
+ const newProfile: Profile = {
+ id: profile.id || this.generateId(),
+ phone: profile.phone || undefined,
+ email: profile.email || undefined,
+ full_name: profile.full_name || undefined,
+ avatar_url: profile.avatar_url || undefined,
+ role: profile.role || 'member',
+ github_username: profile.github_username || undefined,
+ gitlab_username: profile.gitlab_username || undefined,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ };
+ return this.put(STORES.PROFILES, newProfile);
+ }
+
+ async updateProfile(id: string, updates: Partial): Promise {
+ const existing = await this.getProfileById(id);
+ if (!existing) throw new Error('Profile not found');
+
+ const updated: Profile = {
+ ...existing,
+ ...updates,
+ id,
+ updated_at: new Date().toISOString(),
+ };
+ return this.put(STORES.PROFILES, updated);
+ }
+
+ async getAllProfiles(): Promise {
+ return this.getAll(STORES.PROFILES);
+ }
+
+ // ==================== Project 相关方法 ====================
+
+ async getProjects(): Promise {
+ const projects = await this.getAll(STORES.PROJECTS);
+ const activeProjects = projects.filter(p => p.is_active);
+
+ // 关联 owner 信息
+ const projectsWithOwner = await Promise.all(
+ activeProjects.map(async (project) => {
+ const owner = project.owner_id ? await this.getProfileById(project.owner_id) : null;
+ return { ...project, owner: owner || undefined };
+ })
+ );
+
+ return projectsWithOwner.sort((a, b) =>
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+ );
+ }
+
+ async getProjectById(id: string): Promise {
+ if (!id) return null;
+
+ const project = await this.getById(STORES.PROJECTS, id);
+ if (!project) return null;
+
+ const owner = project.owner_id ? await this.getProfileById(project.owner_id) : null;
+ return { ...project, owner: owner || undefined };
+ }
+
+ async createProject(projectData: CreateProjectForm & { owner_id?: string }): Promise {
+ const newProject: Project = {
+ id: this.generateId(),
+ name: projectData.name,
+ description: projectData.description || undefined,
+ repository_url: projectData.repository_url || undefined,
+ repository_type: projectData.repository_type || 'other',
+ default_branch: projectData.default_branch || 'main',
+ programming_languages: JSON.stringify(projectData.programming_languages || []),
+ owner_id: projectData.owner_id || 'local-user',
+ is_active: true,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.PROJECTS, newProject);
+ return this.getProjectById(newProject.id) as Promise;
+ }
+
+ async updateProject(id: string, updates: Partial): Promise {
+ const existing = await this.getById(STORES.PROJECTS, id);
+ if (!existing) throw new Error('Project not found');
+
+ const updateData: any = { ...updates };
+ if (updates.programming_languages) {
+ updateData.programming_languages = JSON.stringify(updates.programming_languages);
+ }
+
+ const updated: Project = {
+ ...existing,
+ ...updateData,
+ id,
+ updated_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.PROJECTS, updated);
+ return this.getProjectById(id) as Promise;
+ }
+
+ async deleteProject(id: string): Promise {
+ const existing = await this.getById(STORES.PROJECTS, id);
+ if (!existing) throw new Error('Project not found');
+
+ const updated: Project = {
+ ...existing,
+ is_active: false,
+ updated_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.PROJECTS, updated);
+ }
+
+ // ==================== ProjectMember 相关方法 ====================
+
+ async getProjectMembers(projectId: string): Promise {
+ const members = await this.getByIndex(STORES.PROJECT_MEMBERS, 'project_id', projectId);
+
+ const membersWithRelations = await Promise.all(
+ members.map(async (member) => {
+ const user = member.user_id ? await this.getProfileById(member.user_id) : null;
+ const project = member.project_id ? await this.getProjectById(member.project_id) : null;
+ return {
+ ...member,
+ user: user || undefined,
+ project: project || undefined
+ };
+ })
+ );
+
+ return membersWithRelations.sort((a, b) =>
+ new Date(b.joined_at).getTime() - new Date(a.joined_at).getTime()
+ );
+ }
+
+ async addProjectMember(projectId: string, userId: string, role: string = 'member'): Promise {
+ const newMember: ProjectMember = {
+ id: this.generateId(),
+ project_id: projectId,
+ user_id: userId,
+ role: role as any,
+ permissions: '{}',
+ joined_at: new Date().toISOString(),
+ created_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.PROJECT_MEMBERS, newMember);
+
+ const user = userId ? await this.getProfileById(userId) : null;
+ const project = projectId ? await this.getProjectById(projectId) : null;
+
+ return {
+ ...newMember,
+ user: user || undefined,
+ project: project || undefined
+ };
+ }
+
+ // ==================== AuditTask 相关方法 ====================
+
+ async getAuditTasks(projectId?: string): Promise {
+ let tasks: AuditTask[];
+
+ if (projectId) {
+ tasks = await this.getByIndex(STORES.AUDIT_TASKS, 'project_id', projectId);
+ } else {
+ tasks = await this.getAll(STORES.AUDIT_TASKS);
+ }
+
+ const tasksWithRelations = await Promise.all(
+ tasks.map(async (task) => {
+ const project = task.project_id ? await this.getProjectById(task.project_id) : null;
+ const creator = task.created_by ? await this.getProfileById(task.created_by) : null;
+ return {
+ ...task,
+ project: project || undefined,
+ creator: creator || undefined
+ };
+ })
+ );
+
+ return tasksWithRelations.sort((a, b) =>
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+ );
+ }
+
+ async getAuditTaskById(id: string): Promise {
+ if (!id) return null;
+
+ const task = await this.getById(STORES.AUDIT_TASKS, id);
+ if (!task) return null;
+
+ const project = task.project_id ? await this.getProjectById(task.project_id) : null;
+ const creator = task.created_by ? await this.getProfileById(task.created_by) : null;
+
+ return {
+ ...task,
+ project: project || undefined,
+ creator: creator || undefined
+ };
+ }
+
+ async createAuditTask(taskData: CreateAuditTaskForm & { created_by: string }): Promise {
+ const newTask: AuditTask = {
+ id: this.generateId(),
+ project_id: taskData.project_id,
+ task_type: taskData.task_type,
+ status: 'pending',
+ branch_name: taskData.branch_name || undefined,
+ exclude_patterns: JSON.stringify(taskData.exclude_patterns || []),
+ scan_config: JSON.stringify(taskData.scan_config || {}),
+ total_files: 0,
+ scanned_files: 0,
+ total_lines: 0,
+ issues_count: 0,
+ quality_score: 0,
+ started_at: undefined,
+ completed_at: undefined,
+ created_by: taskData.created_by,
+ created_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.AUDIT_TASKS, newTask);
+ return this.getAuditTaskById(newTask.id) as Promise;
+ }
+
+ async updateAuditTask(id: string, updates: Partial): Promise {
+ const existing = await this.getById(STORES.AUDIT_TASKS, id);
+ if (!existing) throw new Error('Audit task not found');
+
+ const updated: AuditTask = {
+ ...existing,
+ ...updates,
+ id,
+ };
+
+ await this.put(STORES.AUDIT_TASKS, updated);
+ return this.getAuditTaskById(id) as Promise;
+ }
+
+ // ==================== AuditIssue 相关方法 ====================
+
+ async getAuditIssues(taskId: string): Promise {
+ const issues = await this.getByIndex(STORES.AUDIT_ISSUES, 'task_id', taskId);
+
+ const issuesWithRelations = await Promise.all(
+ issues.map(async (issue) => {
+ const task = issue.task_id ? await this.getAuditTaskById(issue.task_id) : null;
+ const resolver = issue.resolved_by ? await this.getProfileById(issue.resolved_by) : null;
+ return {
+ ...issue,
+ task: task || undefined,
+ resolver: resolver || undefined
+ };
+ })
+ );
+
+ // 按严重程度和创建时间排序
+ const severityOrder: Record = { critical: 0, high: 1, medium: 2, low: 3 };
+ return issuesWithRelations.sort((a, b) => {
+ const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999);
+ if (severityDiff !== 0) return severityDiff;
+ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
+ });
+ }
+
+ async createAuditIssue(issueData: Omit): Promise {
+ const newIssue: AuditIssue = {
+ ...issueData,
+ id: this.generateId(),
+ created_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.AUDIT_ISSUES, newIssue);
+
+ const task = newIssue.task_id ? await this.getAuditTaskById(newIssue.task_id) : null;
+ const resolver = newIssue.resolved_by ? await this.getProfileById(newIssue.resolved_by) : null;
+
+ return {
+ ...newIssue,
+ task: task || undefined,
+ resolver: resolver || undefined
+ };
+ }
+
+ async updateAuditIssue(id: string, updates: Partial): Promise {
+ const existing = await this.getById(STORES.AUDIT_ISSUES, id);
+ if (!existing) throw new Error('Audit issue not found');
+
+ const updated: AuditIssue = {
+ ...existing,
+ ...updates,
+ id,
+ };
+
+ await this.put(STORES.AUDIT_ISSUES, updated);
+
+ const task = updated.task_id ? await this.getAuditTaskById(updated.task_id) : null;
+ const resolver = updated.resolved_by ? await this.getProfileById(updated.resolved_by) : null;
+
+ return {
+ ...updated,
+ task: task || undefined,
+ resolver: resolver || undefined
+ };
+ }
+
+ // ==================== InstantAnalysis 相关方法 ====================
+
+ async getInstantAnalyses(userId?: string): Promise {
+ let analyses: InstantAnalysis[];
+
+ if (userId) {
+ analyses = await this.getByIndex(STORES.INSTANT_ANALYSES, 'user_id', userId);
+ } else {
+ analyses = await this.getAll(STORES.INSTANT_ANALYSES);
+ }
+
+ const analysesWithUser = await Promise.all(
+ analyses.map(async (analysis) => {
+ const user = analysis.user_id ? await this.getProfileById(analysis.user_id) : null;
+ return { ...analysis, user: user || undefined };
+ })
+ );
+
+ return analysesWithUser.sort((a, b) =>
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+ );
+ }
+
+ async createInstantAnalysis(analysisData: InstantAnalysisForm & {
+ user_id: string;
+ analysis_result?: string;
+ issues_count?: number;
+ quality_score?: number;
+ analysis_time?: number;
+ }): Promise {
+ const newAnalysis: InstantAnalysis = {
+ id: this.generateId(),
+ user_id: analysisData.user_id,
+ language: analysisData.language,
+ code_content: '', // 不持久化代码内容
+ analysis_result: analysisData.analysis_result || '{}',
+ issues_count: analysisData.issues_count || 0,
+ quality_score: analysisData.quality_score || 0,
+ analysis_time: analysisData.analysis_time || 0,
+ created_at: new Date().toISOString(),
+ };
+
+ await this.put(STORES.INSTANT_ANALYSES, newAnalysis);
+
+ const user = newAnalysis.user_id ? await this.getProfileById(newAnalysis.user_id) : null;
+ return { ...newAnalysis, user: user || undefined };
+ }
+
+ // ==================== 统计相关方法 ====================
+
+ async getProjectStats(): Promise {
+ const projects = await this.getAll(STORES.PROJECTS);
+ const tasks = await this.getAll(STORES.AUDIT_TASKS);
+ const issues = await this.getAll(STORES.AUDIT_ISSUES);
+
+ return {
+ total_projects: projects.length,
+ active_projects: projects.filter(p => p.is_active).length,
+ total_tasks: tasks.length,
+ completed_tasks: tasks.filter(t => t.status === 'completed').length,
+ total_issues: issues.length,
+ resolved_issues: issues.filter(i => i.status === 'resolved').length,
+ };
+ }
+}
+
+// 导出单例
+export const localDB = new LocalDatabase();
diff --git a/src/shared/utils/initLocalDB.ts b/src/shared/utils/initLocalDB.ts
new file mode 100644
index 0000000..a9fdb4e
--- /dev/null
+++ b/src/shared/utils/initLocalDB.ts
@@ -0,0 +1,118 @@
+/**
+ * 本地数据库初始化工具
+ * 用于在首次使用时创建默认用户和演示数据
+ */
+
+import { localDB } from '../config/localDatabase';
+import { api } from '../config/database';
+
+/**
+ * 初始化本地数据库
+ * 创建默认用户和基础数据
+ */
+export async function initLocalDatabase(): Promise {
+ try {
+ // 初始化数据库
+ await localDB.init();
+
+ // 检查是否已有用户
+ const profileCount = await localDB.getProfilesCount();
+
+ if (profileCount === 0) {
+ // 创建默认本地用户
+ await api.createProfiles({
+ id: 'local-user',
+ email: 'local@xcodereviewer.com',
+ full_name: '本地用户',
+ role: 'admin',
+ github_username: 'local-user',
+ });
+
+ console.log('✅ 本地数据库初始化成功');
+ }
+ } catch (error) {
+ console.error('❌ 本地数据库初始化失败:', error);
+ throw error;
+ }
+}
+
+/**
+ * 清空本地数据库
+ * 用于重置或清理数据
+ */
+export async function clearLocalDatabase(): Promise {
+ try {
+ const dbName = 'xcodereviewer_local';
+ const request = indexedDB.deleteDatabase(dbName);
+
+ return new Promise((resolve, reject) => {
+ request.onsuccess = () => {
+ console.log('✅ 本地数据库已清空');
+ resolve();
+ };
+ request.onerror = () => {
+ console.error('❌ 清空本地数据库失败');
+ reject(request.error);
+ };
+ });
+ } catch (error) {
+ console.error('❌ 清空本地数据库失败:', error);
+ throw error;
+ }
+}
+
+/**
+ * 导出本地数据库数据
+ * 用于备份或迁移
+ */
+export async function exportLocalDatabase(): Promise {
+ try {
+ await localDB.init();
+
+ const data = {
+ version: 1,
+ exportDate: new Date().toISOString(),
+ profiles: await localDB.getAllProfiles(),
+ projects: await localDB.getProjects(),
+ auditTasks: await localDB.getAuditTasks(),
+ };
+
+ return JSON.stringify(data, null, 2);
+ } catch (error) {
+ console.error('❌ 导出数据失败:', error);
+ throw error;
+ }
+}
+
+/**
+ * 导入数据到本地数据库
+ * 用于恢复备份或迁移数据
+ */
+export async function importLocalDatabase(jsonData: string): Promise {
+ try {
+ const data = JSON.parse(jsonData);
+
+ if (!data.version || !data.profiles) {
+ throw new Error('无效的数据格式');
+ }
+
+ await localDB.init();
+
+ // 导入用户
+ for (const profile of data.profiles) {
+ await api.createProfiles(profile);
+ }
+
+ // 导入项目
+ if (data.projects) {
+ for (const project of data.projects) {
+ await api.createProject(project);
+ }
+ }
+
+ console.log('✅ 数据导入成功');
+ } catch (error) {
+ console.error('❌ 导入数据失败:', error);
+ throw error;
+ }
+}