This commit is contained in:
2026-04-14 18:05:32 +08:00
parent 68457c9fbd
commit 6378ab4994
40 changed files with 10488 additions and 113 deletions
+160
View File
@@ -0,0 +1,160 @@
import { getRequestOrigin } from "@/lib/request-origin";
import { buildSubscriptionUrl, getDefaultClashTemplate } from "@/lib/subscription";
import {
getAdminMirrorUser,
getSubscriptionSettings,
listSubscriptionNodes,
} from "@/lib/store";
import {
deleteProxyGroupAction,
saveProxyGroupAction,
saveSubscriptionSettingsAction,
} from "./actions";
export default async function AdminSubscriptionsPage() {
const [settings, adminUser, nodes, origin] = await Promise.all([
getSubscriptionSettings(),
getAdminMirrorUser(),
listSubscriptionNodes(),
getRequestOrigin(),
]);
return (
<main className="page-stack">
<section className="hero-panel">
<div className="hero-copy">
<span className="eyebrow">SUBSCRIPTIONS</span>
<h2>订阅与模板</h2>
<p className="page-lede">
管理 Clash 基础模板、订阅文件名、代理分组以及管理员默认不限量订阅链接。
</p>
<div className="badge-row">
<span className="status-pill status-pill--ok">动态 proxies</span>
<span className="status-pill status-pill--ink">分组 {settings.proxyGroups.length}</span>
</div>
</div>
<div className="hero-aside">
<div className="stat-tile">
<span>可用节点</span>
<strong>{nodes.length}</strong>
</div>
<div className="stat-tile">
<span>默认主组</span>
<strong>{settings.profileName}</strong>
</div>
</div>
</section>
<section className="panel-card">
<div className="section-title">
<h3>管理员订阅</h3>
</div>
{adminUser?.subscription_token ? (
<div className="instruction-block no-margin">
<p className="muted mono break-all">
{buildSubscriptionUrl(adminUser.subscription_token, origin)}
</p>
</div>
) : (
<p className="muted">管理员订阅链接暂不可用。</p>
)}
</section>
<section className="panel-card">
<div className="section-title">
<h3>Proxy Groups</h3>
</div>
<div className="stack-lg">
{settings.proxyGroups.map((group) => (
<article key={group.id} className="instruction-block">
<form action={saveProxyGroupAction} className="stack-md">
<input type="hidden" name="id" value={group.id} />
<label className="field">
<span>组名</span>
<input name="name" defaultValue={group.name} required />
</label>
<div className="checkbox-grid">
{nodes.map((node) => (
<label key={`${group.id}-${node.slug}`} className="checkbox">
<input
type="checkbox"
name="member_slugs"
value={node.slug}
defaultChecked={group.memberSlugs.includes(node.slug)}
/>
<span>{node.client_name || node.name}</span>
</label>
))}
</div>
<div className="action-row">
<button className="ghost-button" type="submit">
保存分组
</button>
</div>
</form>
<form action={deleteProxyGroupAction}>
<input type="hidden" name="id" value={group.id} />
<button className="danger-button" type="submit">
删除分组
</button>
</form>
</article>
))}
<article className="instruction-block">
<form action={saveProxyGroupAction} className="stack-md">
<label className="field">
<span>新建分组名</span>
<input name="name" placeholder="ChatGPT" required />
</label>
<div className="checkbox-grid">
{nodes.map((node) => (
<label key={`new-${node.slug}`} className="checkbox">
<input type="checkbox" name="member_slugs" value={node.slug} />
<span>{node.client_name || node.name}</span>
</label>
))}
</div>
<button className="primary-button" type="submit">
新建分组
</button>
</form>
</article>
</div>
</section>
<section className="panel-card">
<div className="section-title">
<h3>Clash 模板</h3>
</div>
<form action={saveSubscriptionSettingsAction} className="stack-lg">
<label className="field">
<span>订阅文件名 / 默认主组名</span>
<input name="profile_name" defaultValue={settings.profileName} required />
</label>
<label className="field">
<span>基础模板</span>
<textarea
name="template"
rows={18}
defaultValue={settings.template || getDefaultClashTemplate()}
/>
</label>
<label className="field">
<span>附加 proxy-groups YAML</span>
<textarea
name="extra_proxy_groups"
rows={12}
defaultValue={settings.extraProxyGroups}
placeholder={` - name: ChatGPT\n type: select\n proxies: ["US 01 - hy2", "JP 01 - hy2"]`}
/>
</label>
<button className="primary-button" type="submit">
保存订阅模板
</button>
</form>
</section>
</main>
);
}