diff --git a/app/schema.sql b/app/schema.sql index a602976..e5a5486 100644 --- a/app/schema.sql +++ b/app/schema.sql @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS footprint_type ( color TEXT NOT NULL DEFAULT '#64748b', sort INTEGER NOT NULL DEFAULT 100, form_template TEXT NOT NULL DEFAULT '[]', + profile TEXT, is_builtin INTEGER NOT NULL DEFAULT 0, closed INTEGER NOT NULL DEFAULT 0, deleted_at TEXT, diff --git a/app/server/db.mjs b/app/server/db.mjs index ab198dd..5d9c863 100644 --- a/app/server/db.mjs +++ b/app/server/db.mjs @@ -125,6 +125,7 @@ function preMigrateColumns(d) { } }; for (const t of SYNC_TABLES) addCols(t, SYNC_COLS); + addCols('footprint_type', [['profile', 'TEXT']]); addCols('attachment', [['sha256', 'TEXT'], ['thumb_rel_path', 'TEXT'], ['remote_url', 'TEXT'], ['remote_thumb', 'TEXT'], ['remote_key', 'TEXT'], ['remote_thumb_key', 'TEXT']]); } @@ -564,7 +565,7 @@ function listFootprints(d, query) { const items = d.prepare(` SELECT t.*, (SELECT COUNT(*) FROM footprint f WHERE f.type_id = t.id AND f.deleted_at IS NULL) AS usage_count FROM footprint_type t WHERE t.deleted_at IS NULL ORDER BY t.sort, t.id`).all() - .map(t => ({ ...t, form_template: JSON.parse(t.form_template || '[]'), is_builtin: !!t.is_builtin, closed: !!t.closed })); + .map(t => ({ ...t, form_template: JSON.parse(t.form_template || '[]'), profile: t.profile ?? null, is_builtin: !!t.is_builtin, closed: !!t.closed })); return { items }; } @@ -575,8 +576,8 @@ function listFootprints(d, query) { if (id) { withTx(d, () => { const h = hlcNext(d); - d.prepare('UPDATE footprint_type SET name=?, icon=?, color=?, sort=?, form_template=?, closed=?, hlc=? WHERE id=?') - .run(p.name, p.icon, p.color, p.sort, tpl, p.closed ? 1 : 0, h, id); + d.prepare('UPDATE footprint_type SET name=?, icon=?, color=?, sort=?, form_template=?, profile=?, closed=?, hlc=? WHERE id=?') + .run(p.name, p.icon, p.color, p.sort, tpl, p.profile ?? null, p.closed ? 1 : 0, h, id); touchOplog(d, 'footprint_type', getUuid(d, 'footprint_type', id), 'upsert', h); }); } else { @@ -584,8 +585,8 @@ function listFootprints(d, query) { const h = hlcNext(d); const uuid = uuidv7(); id = Number(d.prepare( - 'INSERT INTO footprint_type (name, icon, color, sort, form_template, closed, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?,?)') - .run(p.name, p.icon, p.color, p.sort, tpl, p.closed ? 1 : 0, uuid, h, getDeviceId(d)).lastInsertRowid); + 'INSERT INTO footprint_type (name, icon, color, sort, form_template, profile, closed, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?,?,?,?)') + .run(p.name, p.icon, p.color, p.sort, tpl, p.profile ?? null, p.closed ? 1 : 0, uuid, h, getDeviceId(d)).lastInsertRowid); touchOplog(d, 'footprint_type', uuid, 'upsert', h); }); } diff --git a/app/src/components/records/RecordEditorDialog.tsx b/app/src/components/records/RecordEditorDialog.tsx index 40ec1b6..c1df499 100644 --- a/app/src/components/records/RecordEditorDialog.tsx +++ b/app/src/components/records/RecordEditorDialog.tsx @@ -84,7 +84,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: { () => typesQ.data?.items.find(t => t.id === form.type_id), [typesQ.data, form.type_id] ); - const profile = getTypeProfile(selectedType?.name); + const profile = getTypeProfile(selectedType?.name, selectedType?.profile); const parentQ = useQuery({ queryKey: ['parent-candidates', form.type_id], diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts index 81024ec..caef54a 100644 --- a/app/src/lib/api.ts +++ b/app/src/lib/api.ts @@ -86,7 +86,7 @@ export const syncStatus = () => call('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 }) => +export const upsertType = (p: { id?: number; name: string; icon: string; color: string; sort: number; form_template: FormField[]; profile?: string; closed?: boolean }) => call<{ item: FootprintType }>('type_upsert', p); export const deleteType = (id: number) => call<{ ok: true }>('type_delete', { id }); diff --git a/app/src/lib/typeProfiles.ts b/app/src/lib/typeProfiles.ts index 384f5b4..0698044 100644 --- a/app/src/lib/typeProfiles.ts +++ b/app/src/lib/typeProfiles.ts @@ -19,7 +19,14 @@ const PROFILES: Record = { const DEFAULT_PROFILE: TypeProfile = { core: ['level', 'role', 'award_grade', 'organizer'] }; -export function getTypeProfile(typeName?: string | null): TypeProfile { +export const PROFILE_OPTIONS = [ + { value: '', label: '通用布局(级别/角色/奖等/组织方)' }, + ...Object.keys(PROFILES).map(k => ({ value: k, label: k + '式布局' })), +]; + +/** 自定义类型可绑定一种布局(profileKey),否则按类型名匹配,最后回退通用 */ +export function getTypeProfile(typeName?: string | null, profileKey?: string | null): TypeProfile { + if (profileKey && PROFILES[profileKey]) return PROFILES[profileKey]; if (!typeName) return DEFAULT_PROFILE; return PROFILES[typeName] ?? DEFAULT_PROFILE; } diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index 736aaac..04c92d7 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -20,6 +20,7 @@ export interface FootprintType { color: string; sort: number; form_template: FormField[]; + profile?: string | null; is_builtin: boolean; closed: boolean; usage_count?: number; diff --git a/app/src/pages/types/TypesPage.tsx b/app/src/pages/types/TypesPage.tsx index b942901..2932726 100644 --- a/app/src/pages/types/TypesPage.tsx +++ b/app/src/pages/types/TypesPage.tsx @@ -7,6 +7,7 @@ import { Badge, Button, Card, Confirm, Dialog, Field, Input, Select, useToast } from '@/components/ui'; import { TypeIcon, ICON_CHOICES } from '@/components/common/typeIcons'; +import { PROFILE_OPTIONS } from '@/lib/typeProfiles'; const PALETTE = ['#3056d3', '#7c3aed', '#0891b2', '#0f766e', '#16a34a', '#65a30d', '#d97706', '#dc2626', '#db2777', '#64748b']; @@ -117,6 +118,7 @@ function TypeEditor({ source, onClose, onSaved }: { source: FootprintType | null const [sort, setSort] = useState(String(source?.sort ?? 100)); const [closed, setClosed] = useState(source?.closed ?? false); const [fields, setFields] = useState(source?.form_template ?? []); + const [profile, setProfile] = useState(source?.profile ?? ''); const [error, setError] = useState(''); const saveMut = useMutation({ @@ -137,6 +139,7 @@ function TypeEditor({ source, onClose, onSaved }: { source: FootprintType | null icon, color, sort: Number(sort) || 100, form_template: fields, + profile: profile || undefined, closed }); }, @@ -166,6 +169,7 @@ function TypeEditor({ source, onClose, onSaved }: { source: FootprintType | null >
+ setName(e.target.value)} placeholder="如:学科竞赛" /> @@ -225,7 +229,10 @@ function TypeEditor({ source, onClose, onSaved }: { source: FootprintType | null
-
+ + without a
');let b=n.getAttribute("formaction")||v.getAttribute("action");if(c=b?ma(b,r):null,s=n.getAttribute("formmethod")||v.getAttribute("method")||Ks,f=If(n.getAttribute("formenctype"))||If(v.getAttribute("enctype"))||Zs,h=new FormData(v,n),!gS()){let{name:p,type:x,value:g}=n;if(x==="image"){let N=p?`${p}.`:"";h.append(`${N}x`,"0"),h.append(`${N}y`,"0")}else p&&h.append(p,g)}}else{if(lc(n))throw new Error('Cannot submit element that is not ,