feat(portal): add logical group packaging config

This commit is contained in:
phamnazage-jpg
2026-05-30 10:54:32 +08:00
parent aac18e0df6
commit ef33762db5
10 changed files with 300 additions and 41 deletions

View File

@@ -545,6 +545,36 @@
<textarea id="group-next-step-hint" placeholder="例如:先创建测试 Key再按推荐模型发起第一次请求。"></textarea>
</label>
</div>
<div class="field-grid two" style="margin-top:12px;">
<label>
visibility_scope
<select id="group-visibility-scope">
<option value="public">public</option>
<option value="login_required">login_required</option>
<option value="entitled_only">entitled_only</option>
<option value="hidden">hidden</option>
</select>
</label>
<label>
package_tier
<select id="group-package-tier">
<option value="free">free</option>
<option value="standard">standard</option>
<option value="pro">pro</option>
<option value="enterprise">enterprise</option>
</select>
</label>
</div>
<div class="field-grid two" style="margin-top:12px;">
<label>
purchase_cta_label
<input id="group-purchase-cta-label" type="text" placeholder="例如:升级到 Pro">
</label>
<label>
purchase_cta_url
<input id="group-purchase-cta-url" type="text" placeholder="例如https://sub.tksea.top/portal/upgrade/pro">
</label>
</div>
<div class="actions">
<button class="primary" id="create-group-btn" type="button">创建分组</button>
<button class="secondary" id="update-group-btn" type="button">更新分组</button>
@@ -708,6 +738,10 @@
const groupUsageScenarioInput = document.getElementById("group-usage-scenario");
const groupRecommendationInput = document.getElementById("group-recommendation");
const groupNextStepHintInput = document.getElementById("group-next-step-hint");
const groupVisibilityScopeInput = document.getElementById("group-visibility-scope");
const groupPackageTierInput = document.getElementById("group-package-tier");
const groupPurchaseCTALabelInput = document.getElementById("group-purchase-cta-label");
const groupPurchaseCTAURLInput = document.getElementById("group-purchase-cta-url");
const groupRoutePolicyInput = document.getElementById("group-route-policy");
const groupStickyModeInput = document.getElementById("group-sticky-mode");
const groupConversationTTLInput = document.getElementById("group-conversation-ttl");
@@ -888,6 +922,10 @@
usage_scenario: groupUsageScenarioInput.value.trim(),
recommendation: groupRecommendationInput.value.trim(),
next_step_hint: groupNextStepHintInput.value.trim(),
visibility_scope: groupVisibilityScopeInput.value,
package_tier: groupPackageTierInput.value,
purchase_cta_label: groupPurchaseCTALabelInput.value.trim(),
purchase_cta_url: groupPurchaseCTAURLInput.value.trim(),
route_policy: groupRoutePolicyInput.value,
sticky_mode: groupStickyModeInput.value,
conversation_ttl_seconds: Number(groupConversationTTLInput.value || "0"),
@@ -919,6 +957,10 @@
groupUsageScenarioInput.value = group?.usage_scenario || "";
groupRecommendationInput.value = group?.recommendation || "";
groupNextStepHintInput.value = group?.next_step_hint || "";
groupVisibilityScopeInput.value = group?.visibility_scope || "public";
groupPackageTierInput.value = group?.package_tier || "standard";
groupPurchaseCTALabelInput.value = group?.purchase_cta_label || "";
groupPurchaseCTAURLInput.value = group?.purchase_cta_url || "";
groupRoutePolicyInput.value = group?.route_policy || "priority";
groupStickyModeInput.value = group?.sticky_mode || "conversation_preferred";
groupConversationTTLInput.value = String(group?.conversation_ttl_seconds || 7200);

View File

@@ -528,6 +528,23 @@
font-size: 12px;
line-height: 1.6;
}
.cta-link {
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(15, 118, 110, 0.24);
background: rgba(15, 118, 110, 0.08);
color: var(--teal);
font-size: 13px;
font-weight: 700;
text-decoration: none;
}
.cta-link:hover {
background: rgba(15, 118, 110, 0.14);
}
.toast {
position: fixed;
right: 20px;
@@ -1106,6 +1123,38 @@
: [];
}
function logicalGroupVisibilityScope(group) {
return String(group && group.visibility_scope ? group.visibility_scope : "public").trim() || "public";
}
function logicalGroupPackageTier(group) {
return String(group && group.package_tier ? group.package_tier : "standard").trim() || "standard";
}
function logicalGroupPurchaseCTA(group) {
return {
label: String(group && group.purchase_cta_label ? group.purchase_cta_label : "").trim(),
url: String(group && group.purchase_cta_url ? group.purchase_cta_url : "").trim()
};
}
function logicalGroupVisibleForViewer(row) {
const scope = logicalGroupVisibilityScope(row.logicalGroup);
if (scope === "hidden") {
return false;
}
if (scope === "public") {
return true;
}
if (scope === "login_required") {
return !!state.user;
}
if (scope === "entitled_only") {
return !!state.user && (row.stateKey === "active" || row.stateKey === "granted" || row.stateKey === "ambiguous");
}
return true;
}
function legacyCompatibilityCandidates(group) {
const portalModels = new Set(portalLogicalGroupModels(group));
if (!portalModels.size) {
@@ -1125,7 +1174,8 @@
enabledCandidates
};
});
rows.sort((left, right) => {
const filtered = rows.filter((row) => logicalGroupVisibleForViewer(row));
filtered.sort((left, right) => {
const leftActive = Number(left.logicalGroup.active_route_count || 0);
const rightActive = Number(right.logicalGroup.active_route_count || 0);
if (leftActive !== rightActive) {
@@ -1136,7 +1186,7 @@
"zh-CN"
);
});
return rows;
return filtered;
}
function activeSubscriptionItems() {
@@ -1221,11 +1271,15 @@
: "pending";
const compatibilityNames = (row.candidates || []).map((candidate) => candidate.title).join(" / ") || "无兼容线路";
const models = portalLogicalGroupModels(group).join(", ") || "--";
const packageTier = logicalGroupPackageTier(group);
const visibilityScope = logicalGroupVisibilityScope(group);
return (
'<article class="group-card ' + (row.stateKey === "active" ? "active" : (row.stateKey === "catalog_only" ? "neutral" : "pending")) + '">' +
'<h4>' + escapeHTML(group.display_name || group.logicalGroupID || "未命名逻辑分组") + '</h4>' +
'<div class="group-meta">' +
'<span class="badge strong ' + stateBadgeClass + '">' + escapeHTML(row.stateText) + '</span>' +
'<span class="badge">' + escapeHTML(packageTier) + '</span>' +
'<span class="badge">' + escapeHTML(visibilityScope) + '</span>' +
'<span class="badge">兼容线路 ' + String(row.candidates.length) + '</span>' +
'<span class="badge">活跃订阅 ' + String(row.matchingActiveSubscriptions.length) + '</span>' +
'<span class="badge">已有 Key ' + String(row.matchingKeys.length) + '</span>' +
@@ -1277,10 +1331,13 @@
models,
stateText: row.stateText,
stateKey: row.stateKey,
packageTier: logicalGroupPackageTier(group),
visibilityScope: logicalGroupVisibilityScope(group),
scenario: configuredScenario || guidance.scenario,
recommendation: configuredRecommendation || guidance.recommendation,
nextStep: configuredNextStep || defaultNextStep,
compatibility,
cta: logicalGroupPurchaseCTA(group),
stickyMode: group.sticky_mode || "conversation_preferred",
routePolicy: group.route_policy || "priority"
};
@@ -1304,6 +1361,8 @@
'<article class="guide-card">' +
'<div class="group-meta">' +
'<span class="badge strong ' + badgeClass + '">' + escapeHTML(guide.stateText) + '</span>' +
'<span class="badge">' + escapeHTML(guide.packageTier) + '</span>' +
'<span class="badge">' + escapeHTML(guide.visibilityScope) + '</span>' +
'<span class="badge mono">' + escapeHTML(guide.logicalGroupID) + '</span>' +
'</div>' +
'<h4>' + escapeHTML(guide.title) + '</h4>' +
@@ -1315,6 +1374,9 @@
'<div class="guide-line"><strong>兼容线路:</strong>' + escapeHTML(guide.compatibility) + '</div>' +
'<div class="guide-line"><strong>路由策略:</strong><span class="mono">' + escapeHTML(guide.routePolicy) + '</span> / <span class="mono">' + escapeHTML(guide.stickyMode) + '</span></div>' +
'</div>' +
((guide.cta.label && guide.cta.url && guide.stateKey !== "active")
? ('<a class="cta-link" href="' + escapeHTML(guide.cta.url) + '" target="_blank" rel="noreferrer">' + escapeHTML(guide.cta.label) + '</a>')
: '') +
'</article>'
);
}).join("");
@@ -1392,6 +1454,8 @@
const group = row.logicalGroup;
const presentation = getPresentationStatus(row);
const models = portalLogicalGroupModels(group);
const packageTier = logicalGroupPackageTier(group);
const visibilityScope = logicalGroupVisibilityScope(group);
const modelsHTML = models.length
? "<ul class=\"group-models\">" + models.map((model) => "<li><span class=\"mono\">" + escapeHTML(model) + "</span></li>").join("") + "</ul>"
: "<div class=\"group-note\">当前尚未登记公开模型。</div>";
@@ -1409,6 +1473,8 @@
'<div class="group-meta">' +
'<span class="badge">logical group</span>' +
'<span class="badge mono">' + escapeHTML(group.logical_group_id || "--") + '</span>' +
'<span class="badge">' + escapeHTML(packageTier) + '</span>' +
'<span class="badge">' + escapeHTML(visibilityScope) + '</span>' +
'<span class="badge">' + escapeHTML(group.route_policy || "priority") + '</span>' +
'<span class="badge">' + escapeHTML(group.sticky_mode || "conversation_preferred") + '</span>' +
'<span class="badge strong ' + presentation.cls + '">' + escapeHTML(presentation.text) + '</span>' +
@@ -1456,11 +1522,14 @@
? "你的账号暂未开通兼容宿主线路,当前只能浏览目录,不能直接申请测试 Key。"
: "当前逻辑分组尚未建立自动申请测试 Key 的兼容线路。";
$("create-key-btn").disabled = !canCreate;
const cta = logicalGroupPurchaseCTA(row.logicalGroup);
$("selection-summary").innerHTML = [
'<div><strong>' + escapeHTML(row.logicalGroup.display_name || row.logicalGroup.logical_group_id || "未命名逻辑分组") + '</strong> / <span class="mono">' + escapeHTML(logicalGroupID) + '</span></div>',
'<div class="mono">公开模型: ' + escapeHTML(models.join(", ") || "--") + '</div>',
'<div class="mono">package_tier = ' + escapeHTML(logicalGroupPackageTier(row.logicalGroup)) + ' / visibility_scope = ' + escapeHTML(logicalGroupVisibilityScope(row.logicalGroup)) + '</div>',
'<div class="mono">route_policy = ' + escapeHTML(row.logicalGroup.route_policy || "priority") + ' / sticky_mode = ' + escapeHTML(row.logicalGroup.sticky_mode || "conversation_preferred") + '</div>',
'<div>' + escapeHTML(compatibility) + '</div>'
'<div>' + escapeHTML(compatibility) + '</div>',
(cta.label && cta.url ? ('<div><a class="cta-link" href="' + escapeHTML(cta.url) + '" target="_blank" rel="noreferrer">' + escapeHTML(cta.label) + '</a></div>') : '')
].join("");
}