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
+59
View File
@@ -0,0 +1,59 @@
"use server";
import { revalidatePath } from "next/cache";
import { createStableId } from "@/lib/password";
import { requireAdminAccess } from "@/lib/session";
import {
getSubscriptionSettings,
saveProxyGroups,
saveSubscriptionSettings,
} from "@/lib/store";
export async function saveSubscriptionSettingsAction(formData: FormData) {
await requireAdminAccess();
await saveSubscriptionSettings({
template: String(formData.get("template") ?? ""),
extraProxyGroups: String(formData.get("extra_proxy_groups") ?? ""),
profileName: String(formData.get("profile_name") ?? "").trim() || "FeieProxy",
});
revalidatePath("/admin/subscriptions");
}
export async function saveProxyGroupAction(formData: FormData) {
await requireAdminAccess();
const settings = await getSubscriptionSettings();
const id = String(formData.get("id") ?? "").trim() || createStableId();
const name = String(formData.get("name") ?? "").trim();
const memberSlugs = formData
.getAll("member_slugs")
.map((value) => String(value))
.filter(Boolean);
if (!name) {
return;
}
const proxyGroups = settings.proxyGroups.filter((group) => group.id !== id);
proxyGroups.push({
id,
name,
type: "select",
memberSlugs,
});
await saveProxyGroups(proxyGroups);
revalidatePath("/admin/subscriptions");
}
export async function deleteProxyGroupAction(formData: FormData) {
await requireAdminAccess();
const settings = await getSubscriptionSettings();
const id = String(formData.get("id") ?? "").trim();
await saveProxyGroups(settings.proxyGroups.filter((group) => group.id !== id));
revalidatePath("/admin/subscriptions");
}
+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>
);
}