Group game operation sections
This commit is contained in:
@@ -173,6 +173,8 @@ describe("ServerDetailPage config write approval", () => {
|
|||||||
it("routes plugin-declared pages into server detail tabs", () => {
|
it("routes plugin-declared pages into server detail tabs", () => {
|
||||||
expect(serverDetailPageSource).toContain("serverDetailSectionEntries(readyPlugin)");
|
expect(serverDetailPageSource).toContain("serverDetailSectionEntries(readyPlugin)");
|
||||||
expect(serverDetailPageSource).toContain("plugin:${page.key}");
|
expect(serverDetailPageSource).toContain("plugin:${page.key}");
|
||||||
|
expect(serverDetailPageSource).toContain("ServerDetailSectionNav");
|
||||||
|
expect(serverDetailPageSource).toContain('aria-label="游戏运营功能"');
|
||||||
expect(serverDetailPageSource).toContain('section === "files"');
|
expect(serverDetailPageSource).toContain('section === "files"');
|
||||||
expect(serverDetailPageSource).toContain("PluginPageSection");
|
expect(serverDetailPageSource).toContain("PluginPageSection");
|
||||||
expect(serverDetailPageSource).toContain("PluginPageHostPage");
|
expect(serverDetailPageSource).toContain("PluginPageHostPage");
|
||||||
|
|||||||
@@ -166,19 +166,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{instance.status !== "ready" && (
|
{instance.status !== "ready" && (
|
||||||
<nav className="section-tabs" aria-label="server sections">
|
<ServerDetailSectionNav entries={detailSections} section={section} onChange={setSection} />
|
||||||
{detailSections.map((entry) => (
|
|
||||||
<button
|
|
||||||
key={entry.id}
|
|
||||||
type="button"
|
|
||||||
className={cx("section-tab", section === entry.id && "section-tab-active")}
|
|
||||||
aria-current={section === entry.id ? "page" : undefined}
|
|
||||||
onClick={() => setSection(entry.id)}
|
|
||||||
>
|
|
||||||
{entry.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{instance.status === "ready" && (
|
{instance.status === "ready" && (
|
||||||
@@ -230,19 +218,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav className="section-tabs" aria-label="server sections">
|
<ServerDetailSectionNav entries={detailSections} section={section} onChange={setSection} />
|
||||||
{detailSections.map((entry) => (
|
|
||||||
<button
|
|
||||||
key={entry.id}
|
|
||||||
type="button"
|
|
||||||
className={cx("section-tab", section === entry.id && "section-tab-active")}
|
|
||||||
aria-current={section === entry.id ? "page" : undefined}
|
|
||||||
onClick={() => setSection(entry.id)}
|
|
||||||
>
|
|
||||||
{entry.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{pluginPageKeyFromSection(section) && readyPlugin && <PluginPageSection pageProps={props} serverId={serverId} plugin={readyPlugin} routeKey={pluginPageKeyFromSection(section) ?? ""} />}
|
{pluginPageKeyFromSection(section) && readyPlugin && <PluginPageSection pageProps={props} serverId={serverId} plugin={readyPlugin} routeKey={pluginPageKeyFromSection(section) ?? ""} />}
|
||||||
{section === "manage" && <ServerDeploymentSection instance={instance.data} deployment={deployment} />}
|
{section === "manage" && <ServerDeploymentSection instance={instance.data} deployment={deployment} />}
|
||||||
@@ -289,6 +265,30 @@ function serverDetailSectionEntries(plugin?: GamePluginResponse): Array<{ id: Se
|
|||||||
return [...pluginPages, ...serverDetailSections];
|
return [...pluginPages, ...serverDetailSections];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ServerDetailSectionNav({ entries, section, onChange }: { entries: Array<{ id: ServerDetailSection; label: string }>; section: ServerDetailSection; onChange: (section: ServerDetailSection) => void }) {
|
||||||
|
const pluginEntries = entries.filter((entry) => entry.id.startsWith("plugin:"));
|
||||||
|
const platformEntries = entries.filter((entry) => !entry.id.startsWith("plugin:"));
|
||||||
|
const activePluginSection = section.startsWith("plugin:") ? section : "";
|
||||||
|
return (
|
||||||
|
<nav className="section-tabs" aria-label="server sections">
|
||||||
|
{pluginEntries.length > 0 && (
|
||||||
|
<label className="server-plugin-section-picker">
|
||||||
|
<span>游戏运营</span>
|
||||||
|
<select aria-label="游戏运营功能" value={activePluginSection} onChange={(event) => event.target.value && onChange(event.target.value as ServerDetailSection)}>
|
||||||
|
<option value="" disabled>选择功能</option>
|
||||||
|
{pluginEntries.map((entry) => <option key={entry.id} value={entry.id}>{entry.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{platformEntries.map((entry) => (
|
||||||
|
<button key={entry.id} type="button" className={cx("section-tab", section === entry.id && "section-tab-active")} aria-current={section === entry.id ? "page" : undefined} onClick={() => onChange(entry.id)}>
|
||||||
|
{entry.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function pluginPageKeyFromSection(section: ServerDetailSection): string | null {
|
function pluginPageKeyFromSection(section: ServerDetailSection): string | null {
|
||||||
return section.startsWith("plugin:") ? section.slice("plugin:".length) : null;
|
return section.startsWith("plugin:") ? section.slice("plugin:".length) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
|||||||
.server-detail-title-row>div:first-child{min-width:0}
|
.server-detail-title-row>div:first-child{min-width:0}
|
||||||
.server-detail-title-row h1{margin:0;font-size:24px;color:var(--ink);overflow-wrap:anywhere}
|
.server-detail-title-row h1{margin:0;font-size:24px;color:var(--ink);overflow-wrap:anywhere}
|
||||||
.section-tabs{display:flex;gap:6px;flex-wrap:wrap;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),color-mix(in srgb,var(--surface) 72%,transparent);box-shadow:inset 0 1px 0 var(--crystal-rim)}
|
.section-tabs{display:flex;gap:6px;flex-wrap:wrap;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),color-mix(in srgb,var(--surface) 72%,transparent);box-shadow:inset 0 1px 0 var(--crystal-rim)}
|
||||||
|
.server-plugin-section-picker{display:flex;align-items:center;gap:8px;min-height:38px;padding:0 10px;border:1px solid var(--line-strong);border-radius:8px;background:var(--control-surface);color:var(--ink-soft);font-size:12px;font-weight:700;white-space:nowrap}.server-plugin-section-picker select{min-width:132px;height:30px;border:0;border-left:1px solid var(--line);border-radius:0;padding:0 24px 0 9px;background:transparent;color:var(--ink);font:inherit;outline:0}.server-plugin-section-picker:focus-within{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
|
||||||
.section-tab{display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:0 14px;border:1px solid var(--line-strong);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);cursor:pointer;white-space:nowrap}
|
.section-tab{display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:0 14px;border:1px solid var(--line-strong);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);cursor:pointer;white-space:nowrap}
|
||||||
.section-tab:focus-visible,.section-tab:hover{border-color:var(--accent);outline:0}
|
.section-tab:focus-visible,.section-tab:hover{border-color:var(--accent);outline:0}
|
||||||
.section-tab-active{background:var(--primary-command-surface);border-color:var(--accent-deep);color:#fff;font-weight:700;box-shadow:0 10px 24px var(--candy-glow),inset 0 1px 0 var(--crystal-rim),0 0 18px var(--moonbeam)}
|
.section-tab-active{background:var(--primary-command-surface);border-color:var(--accent-deep);color:#fff;font-weight:700;box-shadow:0 10px 24px var(--candy-glow),inset 0 1px 0 var(--crystal-rim),0 0 18px var(--moonbeam)}
|
||||||
|
|||||||
Reference in New Issue
Block a user