feat: 足迹 Footprints 个人留痕系统
桌面应用(Tauri+React+SQLite):时间线/记录/学年学科成绩梯度/统计/类型与字段模板/附件内容寻址/详情视图/PWA 同步服务端(sync-server):多用户+邀请码/LWW推拉/seq游标/附件去重/Docker部署 多端同步地基:uuid v7/HLC混合逻辑时钟/软删除墓碑/sync_oplog,Node与Rust双端一致并通过自动化测试
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
AppInfo, Attachment, Dimension, FootprintDetail, FootprintInput, FootprintQuery,
|
||||
FootprintRow, FootprintType, FormField, GradeBand, GradesOverview, MetricValue,
|
||||
StatsOverview, SyncStatus, TaxonomyItem, TimelineGroup
|
||||
} from './types';
|
||||
|
||||
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
|
||||
|
||||
async function call<T>(cmd: string, payload?: unknown): Promise<T> {
|
||||
if (isTauri) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
return invoke<T>(cmd, { payload: payload ?? {} });
|
||||
}
|
||||
const res = await fetch('/api/' + cmd, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload ?? {})
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok || !json.ok) throw new Error(json.error || ('HTTP ' + res.status));
|
||||
return json.data as T;
|
||||
}
|
||||
|
||||
// ---- app ----
|
||||
export const appInfo = () => call<AppInfo>('app_info');
|
||||
export const syncStatus = () => call<SyncStatus>('sync_status');
|
||||
|
||||
// ---- footprint type ----
|
||||
export const listTypes = () => call<{ items: FootprintType[] }>('type_list');
|
||||
export const upsertType = (p: { id?: number; name: string; icon: string; color: string; sort: number; form_template: FormField[]; closed?: boolean }) =>
|
||||
call<{ item: FootprintType }>('type_upsert', p);
|
||||
export const deleteType = (id: number) => call<{ ok: true }>('type_delete', { id });
|
||||
|
||||
// ---- taxonomy ----
|
||||
export const listTaxonomy = (kind?: string) => call<{ items: TaxonomyItem[] }>('taxonomy_list', { kind });
|
||||
export const upsertTaxonomy = (p: { id?: number; kind: string; value: string; color?: string | null; sort?: number }) =>
|
||||
call<{ item: TaxonomyItem }>('taxonomy_upsert', p);
|
||||
export const deleteTaxonomy = (id: number) => call<{ ok: true }>('taxonomy_delete', { id });
|
||||
|
||||
// ---- dimension ----
|
||||
export const listDimensions = () => call<{ items: Dimension[] }>('dimension_list');
|
||||
export const upsertDimension = (p: { id?: number; name: string; color?: string | null; sort?: number }) =>
|
||||
call<{ item: Dimension }>('dimension_upsert', p);
|
||||
export const deleteDimension = (id: number) => call<{ ok: true }>('dimension_delete', { id });
|
||||
|
||||
// ---- footprint ----
|
||||
export const footprintList = (query: FootprintQuery = {}) =>
|
||||
call<{ items: FootprintRow[]; total: number; page: number; page_size: number }>('footprint_list', { query });
|
||||
export const footprintGet = (id: number) => call<{ item: FootprintDetail }>('footprint_get', { id });
|
||||
export const footprintUpsert = (item: FootprintInput) => call<{ id: number }>('footprint_upsert', { item });
|
||||
export const footprintDelete = (id: number) => call<{ ok: true }>('footprint_delete', { id });
|
||||
export const timeline = (query: FootprintQuery = {}) => call<{ groups: TimelineGroup[]; total: number }>('timeline', { query });
|
||||
|
||||
// ---- stats ----
|
||||
export const statsOverview = (year?: number) => call<StatsOverview>('stats_overview', { year });
|
||||
export const metricList = () => call<{ items: MetricValue[] }>('metric_list');
|
||||
export const metricUpsert = (p: { key: string; label: string; unit?: string; agg?: string; section?: string }) =>
|
||||
call<{ item: MetricValue }>('metric_upsert', p);
|
||||
export const metricDelete = (key: string) => call<{ ok: true }>('metric_delete', { key });
|
||||
|
||||
// ---- grades ----
|
||||
export const gradesOverview = () => call<GradesOverview>('grades_overview');
|
||||
export const semesterUpsert = (p: { id?: number; term: string; gpa?: number | null; rank?: number | null; total_students?: number | null }) =>
|
||||
call<{ item: { id: number } }>('semester_upsert', p);
|
||||
export const semesterDelete = (id: number) => call<{ ok: true }>('semester_delete', { id });
|
||||
export const courseUpsert = (p: { id?: number; semester_id: number; course_name: string; credit: number; score?: number | null; grade_point?: number | null }) =>
|
||||
call<{ item: { id: number } }>('course_upsert', p);
|
||||
export const courseDelete = (id: number) => call<{ ok: true }>('course_delete', { id });
|
||||
export const bandUpsert = (p: GradeBand) => call<{ item: { id: number } }>('band_upsert', p);
|
||||
export const bandDelete = (id: number) => call<{ ok: true }>('band_delete', { id });
|
||||
|
||||
// ---- attachment ----
|
||||
export const attachmentAdd = (p: { owner_id: number; file_name: string; mime?: string; size: number; data_b64: string }) =>
|
||||
call<{ item: Attachment }>('attachment_add', p);
|
||||
export const attachmentList = (owner_id: number) => call<{ items: Attachment[] }>('attachment_list', { owner_id });
|
||||
export const attachmentDelete = (id: number) => call<{ ok: true }>('attachment_delete', { id });
|
||||
export const attachmentRead = (id: number) =>
|
||||
call<{ data_b64: string; mime?: string; file_name: string }>('attachment_read', { id });
|
||||
|
||||
// ---- saved view ----
|
||||
export interface SavedView { id: number; name: string; filter: Record<string, unknown>; sort?: Record<string, unknown>; is_default: boolean; }
|
||||
export const viewList = () => call<{ items: SavedView[] }>('view_list');
|
||||
export const viewUpsert = (p: { id?: number; name: string; filter: Record<string, unknown>; sort?: Record<string, unknown>; is_default?: boolean }) =>
|
||||
call<{ item: SavedView }>('view_upsert', p);
|
||||
export const viewDelete = (id: number) => call<{ ok: true }>('view_delete', { id });
|
||||
Reference in New Issue
Block a user