软件功能基本开发完成,内测BUG等待修复
This commit is contained in:
@@ -1,224 +1,282 @@
|
||||
import React, { useEffect, useState } from "../../../recovery-preview/node_modules/react/index.js";
|
||||
import {
|
||||
compactText,
|
||||
requestJson,
|
||||
} from "../../runtime/client.js";
|
||||
import { requestJson } from "../../runtime/client.js";
|
||||
|
||||
// Wot: Wrapper structure for the config page
|
||||
function Wot({ children }) {
|
||||
return <div className="recovery-live-page" style={{ padding: '2rem' }}>{children}</div>;
|
||||
return <div className="flex h-full w-full flex-col">{children}</div>;
|
||||
}
|
||||
|
||||
// Hot: Header component for sections or the page
|
||||
function Hot({ title, description }) {
|
||||
function Hot({ children }) {
|
||||
return (
|
||||
<header className="recovery-live-hero" style={{ marginBottom: '2.5rem', borderBottom: '1px solid var(--border-soft)', pb: '1.5rem' }}>
|
||||
<div style={{ maxWidth: '800px' }}>
|
||||
<p className="eyebrow" style={{ color: 'var(--brand-color)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
Theme Customization
|
||||
</p>
|
||||
<h2 style={{ fontSize: '2.25rem', marginBottom: '0.75rem' }}>{title}</h2>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '1.1rem', lineHeight: '1.6' }}>{description}</p>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// zot: Setting Item (Zone) component for form fields
|
||||
function zot({ label, children, span = 1 }) {
|
||||
return (
|
||||
<div className={`recovery-table-card ${span === 2 ? 'span-2' : ''}`} style={{ padding: '1.25rem', background: 'var(--card-bg)', borderRadius: '12px', border: '1px solid var(--card-border)' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.75rem', fontWeight: '500', color: 'var(--text-main)' }}>{label}</label>
|
||||
<div className="form-control-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex h-[var(--header-height)] flex-none items-center justify-between gap-4 bg-background p-4 md:px-8">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// QKt: The main Nebula configuration component
|
||||
function zot({ children }) {
|
||||
return <div className="flex-1 overflow-hidden px-4 py-6 md:px-8">{children}</div>;
|
||||
}
|
||||
|
||||
function Card({ title, description, children }) {
|
||||
return (
|
||||
<div className="rounded-xl border bg-card text-card-foreground shadow">
|
||||
<div className="flex flex-col space-y-1.5 p-6">
|
||||
<h3 className="font-semibold leading-none tracking-tight">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
<div className="p-6 pt-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, description, children, span = 1 }) {
|
||||
return (
|
||||
<div className={span === 2 ? "space-y-2 md:col-span-2" : "space-y-2"}>
|
||||
<label className="text-sm font-medium leading-none">{label}</label>
|
||||
{children}
|
||||
<p className="text-[0.8rem] text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
nebula_theme_color: "aurora",
|
||||
nebula_hero_slogan: "",
|
||||
nebula_welcome_target: "",
|
||||
nebula_register_title: "",
|
||||
nebula_background_url: "",
|
||||
nebula_metrics_base_url: "",
|
||||
nebula_default_theme_mode: "system",
|
||||
nebula_light_logo_url: "",
|
||||
nebula_dark_logo_url: "",
|
||||
nebula_custom_html: "",
|
||||
nebula_static_cdn_url: "",
|
||||
};
|
||||
|
||||
const themeColorOptions = [
|
||||
{ value: "aurora", label: "Aurora" },
|
||||
{ value: "sunset", label: "Sunset" },
|
||||
{ value: "ember", label: "Ember" },
|
||||
{ value: "violet", label: "Violet" },
|
||||
];
|
||||
|
||||
const themeModeOptions = [
|
||||
{ value: "system", label: "Follow system" },
|
||||
{ value: "dark", label: "Prefer dark" },
|
||||
{ value: "light", label: "Prefer light" },
|
||||
];
|
||||
|
||||
function QKt() {
|
||||
const [form, setForm] = useState(defaults);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
nebula_theme_color: 'aurora',
|
||||
nebula_hero_slogan: '',
|
||||
nebula_welcome_target: '',
|
||||
nebula_register_title: '',
|
||||
nebula_background_url: '',
|
||||
nebula_metrics_base_url: '',
|
||||
nebula_default_theme_mode: 'system',
|
||||
nebula_light_logo_url: '',
|
||||
nebula_dark_logo_url: '',
|
||||
nebula_custom_html: '',
|
||||
nebula_static_cdn_url: '',
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
const [messageType, setMessageType] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const payload = await requestJson("/config/fetch?key=nebula");
|
||||
if (payload?.nebula) {
|
||||
setForm(prev => ({ ...prev, ...payload.nebula }));
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load configuration: " + err.message);
|
||||
const nebula = payload?.data?.nebula || payload?.nebula || {};
|
||||
setForm((prev) => ({ ...prev, ...nebula }));
|
||||
} catch (error) {
|
||||
setMessage(error?.message || "Failed to load Nebula settings");
|
||||
setMessageType("error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
const updateField = (key, value) => {
|
||||
setForm((prev) => ({ ...prev, [key]: value ?? "" }));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
setSuccess("");
|
||||
setError("");
|
||||
setMessage("");
|
||||
setMessageType("");
|
||||
try {
|
||||
await requestJson("/config/save", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
setSuccess("Nebula configuration updated successfully.");
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} catch (err) {
|
||||
setError("Failed to save configuration: " + err.message);
|
||||
await requestJson("/config/save", { method: "POST", body: form });
|
||||
setMessage("Nebula settings saved");
|
||||
setMessageType("success");
|
||||
} catch (error) {
|
||||
setMessage(error?.message || "Failed to save Nebula settings");
|
||||
setMessageType("error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <Wot><div className="empty-cell">Initializing Nebula settings...</div></Wot>;
|
||||
|
||||
return (
|
||||
<Wot>
|
||||
<Hot
|
||||
title="Nebula Theme settings"
|
||||
description="Optimize your user dashboard with specific Nebula theme settings and branding options."
|
||||
/>
|
||||
|
||||
{error && <div className="toast toast-error" style={{ marginBottom: '1.5rem' }}>{error}</div>}
|
||||
{success && <div className="toast toast-success" style={{ marginBottom: '1.5rem' }}>{success}</div>}
|
||||
|
||||
<form onSubmit={handleSave} className="modal-grid" style={{ maxWidth: '1200px' }}>
|
||||
<zot label="Primary Accent Color" span={2}>
|
||||
<select
|
||||
value={form.nebula_theme_color}
|
||||
onChange={e => setForm({...form, nebula_theme_color: e.target.value})}
|
||||
className="recovery-input"
|
||||
style={{ width: '100%', height: '42px' }}
|
||||
<Hot>
|
||||
<div />
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={save}
|
||||
disabled={loading || saving}
|
||||
>
|
||||
<option value="aurora">极光蓝 (Aurora Blue)</option>
|
||||
<option value="sunset">日落橙 (Sunset Orange)</option>
|
||||
<option value="ember">余烬红 (Ember Red)</option>
|
||||
<option value="violet">星云紫 (Violet Purple)</option>
|
||||
</select>
|
||||
</zot>
|
||||
|
||||
<zot label="Hero Slogan">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_hero_slogan}
|
||||
onChange={e => setForm({...form, nebula_hero_slogan: e.target.value})}
|
||||
placeholder="Main visual headline"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Welcome Target">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_welcome_target}
|
||||
onChange={e => setForm({...form, nebula_welcome_target: e.target.value})}
|
||||
placeholder="Name displayed after WELCOME TO"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Register Title">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_register_title}
|
||||
onChange={e => setForm({...form, nebula_register_title: e.target.value})}
|
||||
placeholder="Title on registration panel"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Default Appearance">
|
||||
<select
|
||||
value={form.nebula_default_theme_mode}
|
||||
onChange={e => setForm({...form, nebula_default_theme_mode: e.target.value})}
|
||||
className="recovery-input"
|
||||
style={{ width: '100%', height: '42px' }}
|
||||
>
|
||||
<option value="system">Adaptive (System)</option>
|
||||
<option value="dark">Dark Theme</option>
|
||||
<option value="light">Light Theme</option>
|
||||
</select>
|
||||
</zot>
|
||||
|
||||
<zot label="Background Image URL">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_background_url}
|
||||
onChange={e => setForm({...form, nebula_background_url: e.target.value})}
|
||||
placeholder="Direct link to background image"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Metrics API Domain">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_metrics_base_url}
|
||||
onChange={e => setForm({...form, nebula_metrics_base_url: e.target.value})}
|
||||
placeholder="https://stats.example.com"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Light Mode Logo">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_light_logo_url}
|
||||
onChange={e => setForm({...form, nebula_light_logo_url: e.target.value})}
|
||||
placeholder="Logo for light mode"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Dark Mode Logo">
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_dark_logo_url}
|
||||
onChange={e => setForm({...form, nebula_dark_logo_url: e.target.value})}
|
||||
placeholder="Logo for dark mode"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Static CDN Assets" span={2}>
|
||||
<input
|
||||
className="recovery-input"
|
||||
value={form.nebula_static_cdn_url}
|
||||
onChange={e => setForm({...form, nebula_static_cdn_url: e.target.value})}
|
||||
placeholder="e.g. https://cdn.example.com/nebula (no trailing slash)"
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<zot label="Custom Scripts / CSS" span={2}>
|
||||
<textarea
|
||||
className="recovery-input"
|
||||
rows={6}
|
||||
value={form.nebula_custom_html}
|
||||
onChange={e => setForm({...form, nebula_custom_html: e.target.value})}
|
||||
placeholder="Add analytics codes or custom styles here"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
</zot>
|
||||
|
||||
<div className="span-2" style={{ marginTop: '2rem', display: 'flex', justifyContent: 'flex-end', borderTop: '1px solid var(--border-soft)', paddingTop: '1.5rem' }}>
|
||||
<button type="submit" className="primary-btn" disabled={saving}>
|
||||
{saving ? "Persisting..." : "Save Configuration"}
|
||||
{saving ? "Saving..." : "Save settings"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Hot>
|
||||
<zot>
|
||||
<div className="space-y-6">
|
||||
<header className="space-y-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Nebula Theme</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Configure Nebula theme colors, copywriting, branding assets, and custom injections.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{message ? (
|
||||
<div
|
||||
className={
|
||||
messageType === "success"
|
||||
? "rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-600"
|
||||
: "rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
||||
}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Card
|
||||
title="Display"
|
||||
description="Set the primary color, theme mode, and hero copy shown by Nebula."
|
||||
>
|
||||
{loading ? (
|
||||
<div className="rounded-lg border border-dashed bg-muted/30 px-4 py-8 text-sm text-muted-foreground">
|
||||
Loading Nebula settings...
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label="Theme palette" description="Controls the default Nebula color palette.">
|
||||
<select
|
||||
className="flex h-9 w-full appearance-none rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_theme_color}
|
||||
onChange={(event) => updateField("nebula_theme_color", event.target.value)}
|
||||
>
|
||||
{themeColorOptions.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Theme mode"
|
||||
description="Applied when a user opens the Nebula frontend for the first time."
|
||||
>
|
||||
<select
|
||||
className="flex h-9 w-full appearance-none rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_default_theme_mode}
|
||||
onChange={(event) =>
|
||||
updateField("nebula_default_theme_mode", event.target.value)
|
||||
}
|
||||
>
|
||||
{themeModeOptions.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label="Hero slogan" description="Shown as the main title in the hero area.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_hero_slogan}
|
||||
onChange={(event) => updateField("nebula_hero_slogan", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Welcome target" description="Appended after the Welcome to heading.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_welcome_target}
|
||||
onChange={(event) => updateField("nebula_welcome_target", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Register title" description="Displayed at the top of the register panel.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_register_title}
|
||||
onChange={(event) => updateField("nebula_register_title", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Metrics API URL"
|
||||
description="Base URL used when Nebula shows public metrics before login."
|
||||
>
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_metrics_base_url}
|
||||
onChange={(event) => updateField("nebula_metrics_base_url", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Branding"
|
||||
description="Set background assets, logo URLs, CDN roots, and custom HTML."
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Field label="Background URL" description="Used by the Nebula login or landing background.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_background_url}
|
||||
onChange={(event) => updateField("nebula_background_url", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Static CDN URL" description="Root CDN path for Nebula static assets.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_static_cdn_url}
|
||||
onChange={(event) => updateField("nebula_static_cdn_url", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Light logo URL" description="Displayed while the light theme is active.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_light_logo_url}
|
||||
onChange={(event) => updateField("nebula_light_logo_url", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Dark logo URL" description="Displayed while the dark theme is active.">
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_dark_logo_url}
|
||||
onChange={(event) => updateField("nebula_dark_logo_url", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Custom HTML / scripts"
|
||||
description="Inject custom HTML, scripts, or styles into Nebula pages."
|
||||
span={2}
|
||||
>
|
||||
<textarea
|
||||
className="min-h-[220px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-xs shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={form.nebula_custom_html}
|
||||
onChange={(event) => updateField("nebula_custom_html", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</zot>
|
||||
</Wot>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user