create table if not exists public.tenant_theme_templates ( id uuid primary key default gen_random_uuid(), code text not null unique, name text not null, description text, preview_image_url text, theme jsonb not null default '{}'::jsonb, public_assets jsonb not null default '{}'::jsonb, status text not null default 'active' check (status in ('active', 'disabled')), sort_order integer not null default 100, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists public.tenant_theme_configs ( tenant_id uuid primary key references public.tenants(id) on delete cascade, active_template_code text references public.tenant_theme_templates(code) on delete set null, active_theme jsonb not null default '{}'::jsonb, active_public_assets jsonb not null default '{}'::jsonb, draft_template_code text references public.tenant_theme_templates(code) on delete set null, draft_theme jsonb not null default '{}'::jsonb, draft_public_assets jsonb not null default '{}'::jsonb, status text not null default 'published' check (status in ('draft', 'published')), published_at timestamptz, published_by uuid references public.platform_users(id) on delete set null, draft_updated_by uuid references public.platform_users(id) on delete set null, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_theme_templates_status_sort on public.tenant_theme_templates(status, sort_order, code); alter table public.tenant_theme_templates enable row level security; alter table public.tenant_theme_configs enable row level security; drop policy if exists theme_templates_public_read on public.tenant_theme_templates; create policy theme_templates_public_read on public.tenant_theme_templates for select using (status = 'active' or app.is_platform_admin()); drop policy if exists tenant_isolation on public.tenant_theme_configs; create policy tenant_isolation on public.tenant_theme_configs for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin()); drop trigger if exists set_updated_at on public.tenant_theme_templates; create trigger set_updated_at before update on public.tenant_theme_templates for each row execute function app.touch_updated_at(); drop trigger if exists set_updated_at on public.tenant_theme_configs; create trigger set_updated_at before update on public.tenant_theme_configs for each row execute function app.touch_updated_at(); insert into public.tenant_theme_templates (code, name, description, theme, public_assets, sort_order) values ( 'classic', '经典蓝', '稳重、清晰、适合默认题库和机构后台。', '{ "mode":"light", "primaryColor":"#2563eb", "accentColor":"#0f766e", "backgroundColor":"#f8fafc", "surfaceColor":"#ffffff", "textColor":"#0f172a", "mutedColor":"#64748b", "borderRadius":8, "buttonRadius":8, "fontFamily":"system", "layoutDensity":"comfortable" }'::jsonb, '{"iconSet":"classic","shareCardStyle":"clean"}'::jsonb, 10 ), ( 'focus', '专注绿', '低干扰学习主题,适合学生端刷题和背单词场景。', '{ "mode":"light", "primaryColor":"#0f766e", "accentColor":"#7c3aed", "backgroundColor":"#f5fbf8", "surfaceColor":"#ffffff", "textColor":"#10201b", "mutedColor":"#55706a", "borderRadius":8, "buttonRadius":8, "fontFamily":"system", "layoutDensity":"compact" }'::jsonb, '{"iconSet":"focus","shareCardStyle":"study"}'::jsonb, 20 ), ( 'high-contrast', '高对比', '强化标题、按钮和导航对比度,适合强运营入口和大屏后台。', '{ "mode":"light", "primaryColor":"#111827", "accentColor":"#f59e0b", "backgroundColor":"#f3f4f6", "surfaceColor":"#ffffff", "textColor":"#030712", "mutedColor":"#4b5563", "borderRadius":6, "buttonRadius":6, "fontFamily":"system", "layoutDensity":"dense" }'::jsonb, '{"iconSet":"contrast","shareCardStyle":"bold"}'::jsonb, 30 ) on conflict (code) do update set name = excluded.name, description = excluded.description, theme = excluded.theme, public_assets = excluded.public_assets, sort_order = excluded.sort_order, status = 'active', updated_at = now();