season ai daily card and admin controls

This commit is contained in:
leeexin committed 2026-03-13 22:02:21 +08:00
1 parent f399cfd651
commit 6217b65aa9
19 files changed
+7031 -11

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
[
"ed502963d93cad1b4573f4d186eafb23c2b053f914c112c54e53a47f2281daa6",
"ec44b5f6f29ac10270f275778e131c65869fb0864010e89b69698905704832f3",
"78f811e27bb1e3a2ad4882b40970c46aea2a0426bd52cc2869fd66f09959f692",
"801068c49b7b19c94a342c0c0589e3210236fe50732960a1a97f46b38af900e4",
"e0aa3d0b44303429657c8c395562056eea3b2910e1ca3ca21e5a2d7630520188",
"4c26efc3cda204cc48cf4d199fb0fa98e9625db5e7251274692e1b4162a0057a",
"abf7bd0f1f8ce57a8e7208d5c68bec29d9d6d8cc2f17912f154f568197668283",
"a1c4ef143f0b5131a1e475d3288c7a7e5bfc423be383d32b574f5e6e302f0b99",
"ad1093bc368202eff7151758bcba2f97a540b22a97d4543534003b1ff22c351e",
"26f72c5dd5a15b68fd551f7e418b6bc57c0b12eee8ecf821d0f519a50cc4ada0",
"a5bac20ec492c3dda5cce5bb9b37a45314dc827f54c9752a655f19cee377cbad",
"5ef820c37357d4aa985512034ab267ea39a5f97ccebbeb6a982b5775f5c90f9e",
"5787f8fd04702821044ddbd39134319515a7e5b17bd1e2f6686a73e72108a977",
"93ab010913bf3adba344274b3b12d775e38576d3d105386b493d3b1d5d8a651a",
"757e1bababc109505707fe48c148f19f25c0548541cb1a9b0902848d858d15bb",
"f545342ccfc3c165bf1fa0b4c5fa611fad66943d3a5838a82f0f72dd9f804f16",
"2d4f0fc8acbf6d453b242c1ae7bb41ee3dc2b35c0986be061a6c74ac163ca13f",
"90b48700fd17b050bd8b37f22bfedd3977f11022947a9f0fa2f0c2111681f220",
"6274e81aa91cd6f4aea2a9979cc9a7128b069b955c99301231247ea529721abf",
"b6b47b9a6b066fce6ad066d75a9ccee35229a5af35e14063dc46d99707a33d7d",
"f39f0d7db0d4aa4323db60033f7d15702f244f6fd5d8d627e9199b6101f5cb14",
"5c1e01f8ef5099cefeb216f30397b877b83deb1b1bb60ac602da8734d7ac4cab",
"a1e872599352cbe9ed4666a6a724c5918354d1893a4a8ef92495ef26f1a20763",
"c103c25156eb574823b979768acb6e8b63eafaff6ea85de7ed3dab3d3608b19c",
"40946f0878eedb85d9b7d12f2b1ff6a1b4ed720291251d063b3dbe573f9d27c4",
"379fe99f66104fff63d31a1b1ab8c5d302fb9b7bc7222ecd3ec13c16b7945a87",
"a9a9de4f2d20146fb96e6d526a88afd6d116a8154e2271946f49c3ddb9408b47",
"818e9ed7cdd34fd934c35b4604b8b6c93150c865e6615b98e158ea41dc501d54",
"4ae84ba497bb11d4356c4df2ed67f40bb1a1abf9f27e414c3730ea5d4dbf85b2",
"a94de0141b5fbd88034ef443210392792e337f2d9bc53b32cfaaca36dda9e17e",
"d620cbb163426505440c7a0443e9c6ad2697f0b692d612d3af791390e9990eaa",
"3307d732604695ea96392b4c1326d98aee7a571558ff7ce790e83683cda4a467",
"bcb8b9bfcf48ee7dc25b6124f3675d464881919229b250e053cfc04579623feb",
"41cc2d5fae52202782110f57c2a993c5c092d3eaf433a6e7004ed52cead39e99",
"695ff3017e7656b401c8d90b93ad7595f3633d959153602a0d41fe664e4454dc",
"fe2cd5167e8a9e60457448af8bc5359e6606d5fac059d3d815b82e1232b2a968",
"c9bcf4422eee790282c21cc41fbaa2b9af56515740bbd732879b3db340704bf3",
"d24285671c0b3f9f8e2ba681a80aa16f673b83985933abb0a523d20856290427",
"cba2c5f40fcb62ed6056408b3a77919a16e69512831da0080a3849e8b8697986",
"90577d61220eb2b0678d415d44c154da9f4ee40f7d587f3837e72afa8756511a",
"484a75be2e9619ceecd055c905186a6a3e851b066218dc7b4417090116167341",
"66a8ee003caa020900d66f75506a0d5b10e86fd8965d58cf94da80078c86e1cd",
"874127247a8277a4523697b4c3e35039f720d557956227abf30d998f62f2e46c",
"29d05e8433569e3cb1883cee9d07df0224374076104e134f4782cb68f62b18f1",
"dae8e648021259f7a66b98e8e33a979671d00056eba34f0720b8e620480b1cd8",
"a70f2a9c228354122c1595b9e2b083719f93c2af079683d1410462c3ff78a55e",
"b74ee9207ac599d32c0206840f4608de126b327f4aa66635e03182047f74c35a",
"8e3a6cc1d231173d9d2c2f144fa675e2bf8c1fdf405b8e26f6740c98e51d9e28",
"79387785a701d2ce6942a52db8967fc0a81d854772fe26a2f7f12df425be1f2a",
"26f6b3d1d8f0d7a80161f4c9714dc3ca863e724d5c1024000a27ea5e7a2a6553",
"5386430aafb755036c8705f51b53e3b9f2ff69a1245117bccc6115f9d3a370a1",
"d6d118e6cb936d00c915c837b44a9f241ccdbc4aa1cedb207462f876290c3f3c",
"37b7953fd37b3e287df75bb6931078af7fd53d7ca6f8b47d25edc876a9baee82",
"95a856bf164bb34acda9028095dae3afe27206d4a0bbf643acff8ad26f354a1d",
"42f5cbf67162ffed7e3dce4989ad25df6a1f965c5a9a792e12e60fae3f57b6ac",
"d00c585b417d4b8e86eeb2da87852d9f3682ee8ecb5ec3b986bc6fc10c8c4afe",
"fe9e3361c8a38d36fe7d05af1b16d2c26d779d74e5847b2831a72009fc440070",
"02402507634ef9fe3b6bb059ffe25c524bc44e1e6aae0429210f5ba2cd5f47f6",
"9796cadf780443d7fce8e22997df6f498e737c3d6c7b9d3232e5a29a537ce131",
"4219fcd91c476955538a80ce393c13d291d635ad05f6158c2ca5cf004385bbba",
"4e85967bcbaa9458b3c8b325ef12ffb4f2f4a7e770cc0aca98b975975b2305b2",
"666d3c3416bb2307b1c83c19b72fac414c1813de3829499e8c0fc59566bf3102",
"59959a36730470d1e4b8f917bf586796a65d598094548fa7f4622f053ff497a6",
"96e353630eae2488c92157b716d1bbdf52752e531396e67640e96dd441f8e124",
"d9ddaecbf4574fd9ec3f61771d8f1c5b4821c873fb87a8e5714878b454268441",
"a5c6f3b89c71f9f669b1024291b771ba09b88c791d9863cebe49ba721aebea72",
"bea512cd140484509d04d4a723290ba4b8a7fca64a2b8effe7b066542b0fcc39",
"1d5f064747afa7008a9b0c711e0c3639f51bc2eb359ddc84e5c0a660ec13a838",
"ce5fa4443a66aeb93a5d51661f0fe9889cfd37cafc453408747ab2bfa5e1c0f6",
"02f05da567f5250eabaac823d4b827d46e6ddf0ce2a2ef5c347e6b4ad8db4a8e",
"5d15e0dd312241447ccde28758276f800cae00d102dca9f68fd319b35d276e3f",
"c026005d0279e745b959ff60517d89646d1d7baacc7f98a1cac0bda099147efd",
"5b1db2930bdccbd2f1254d7e22481ae59d810ead617a280979a288f2f1667a06",
"1c1adc29c26c8323feddb766c74b9221989d4b13d8e17a8fe2927636ac56686d",
"fb8be6618a9ddd2387b41c7574da90f288155b35232b64cc3ad16fd262584cf2",
"6ee266dbc54478780c32a120f889cc32291650217b6482b359be6a2165a3c1ae",
"7af6aafee82e825997acb72f66780de78d658f699f26cebd2e19acde01b8fb87",
"668a6f277ca69d08d173debea1ac08e129105484e49aa68cf4d4f4c6e26bdeb1",
"6b2b83cb68f65ca63bd418d0ee94f2bb961da9bb24fe5aa02cc0942d76040fda",
"0a6670cf4614fa27d9be7e7f713bc333853dd6b7b24f604f1706497bb2a03b87",
"3a0fecc674ebdede88d85f80008235c2e03d0046dab68aa6832234af45814c1d",
"fd696ae60c11589a08cff36825cd9f4d91d831cede8c62dc6dfd58e271016e79",
"3bfdff076bfaa2e84866eabae359c18dd57463c83c122f6e692a73f2ef19fa72",
"d710d55c1d7e2d4e7961bf3b189d59c96865425ba6bef2a651c66b7c38a95500",
"0eb8a9b97098ec424504999bfef6a26b6d6f68039b4af831f8b1082ed5e716e3",
"acbd71c3f19a4a4aa1ec3e55685593508d832f181040745d5356ef34b3302c8e",
"b9f37815e87a36df1ecb585ba09ab3910fb75b89741b1d667a826ef8fdcb1a10",
"f64b138395c5dfd21779e635d095a4e767732748e1b5edfc8be6279f69143a26",
"b5ebafef1d744a43307149158b57018b947d45c0d3e1eb42ae012b00d21dfd1a",
"c22894b2a5b544e9831bd4a788fb289059979148bb57c1d885e54a5669f47ce9",
"c37efc039e8a8b1239148669d69833a4921feb690edd88470f6924486a18dd58",
"9339c12c482b3531947885014e40bd88c3423e6021e582efbd75c666ee8fb86e",
"a8d86de6a5aae1ad06fbee3c51d24457f9a67f387e564055ad893f772b40e1b2",
"cfdf598aeeebf38ea8a2929f917d855fab9aebd87f846e6b73327fe43ad248e2",
"89b947e925cd0c51505bec7de62dfe36b67781a61a0345e2acc46909ab910567",
"77f7207a6c99dfb1c296fadb613b200a0a6ce6802b546baab270234a42e63050",
"0587b591ba70dadac453e51989b193e08b7d7c9da4d6f93415c4413c05af15b7",
"145e750c41f608a8f36004c922d4d86039355ebd988d9850aac630faaa8c693b",
"6d6f2fffb49432daebcdc37bcc375269c4b2052424259a329a7fdcce1f631ff1",
"47f2fe35bda1650d038da34e7e4d3eaef06f9aee863397f6b42076d103a17e56",
"2e8a6ae29d0945d65767de2d0cf9e659e0b1c8a40947ae0fc7d2bbedca741e30",
"46bfa506b85419de3dac5e95aeca0c146608e276513873475ba849f7cf14901a",
"2c2277b25f1a08f85e54b568bd96c71868f5c788ec2a490e533b30ce2fbcedc4",
"d72b51edb8fe6d3305810892b72477031f94dfd9deaeac1666dd020ca0eb9aca",
"5cced5d8dae81af217da4faf84f226c8828ebfdf5c7632299697404f8d96e989",
"26f15bc2e0c41ffc5c9e9c6206f8b6a1b408e1a4c9b6d2c5bfc7ec6655f521da",
"323bf70203d3ce2122df4699a48a57995a14becdddff632c2796a435cb552354",
"9b6d1e2f23de48e083673aff7711abe0730f3002178393828ddb21b3a2d8d4ea",
"0497bc6d412ed31eb1f49cece6079963c2f55a999d50d410530da17df54f9013"
]
+1229 -1
View File
File diff suppressed because it is too large. Load diff
+11
View File
@@ -35,6 +35,17 @@ DEFAULT_SETTINGS: dict[str, Any] = {
"redeem_ip_limit": 12,
"redeem_user_limit": 10,
},
"season_system": {
"enabled": True,
"auto_create_enabled": True,
"auto_generate_theme": True,
"season_duration_days": 30,
"rest_days": 3,
"daily_card_count": 3,
"refresh_hours": [0, 8, 18],
"manual_refresh_cost": 60,
"premium_pass_price": 680,
},
"app_host": "0.0.0.0",
"app_port": 8095,
"debug": True,
+3698 -10
View File
File diff suppressed because it is too large. Load diff
+137
View File
@@ -387,6 +387,12 @@ button:disabled {
backdrop-filter: blur(6px);
}
.chronicle-scroll {
max-height: min(70vh, 860px);
overflow: auto;
padding-right: 0.25rem;
}
.status-banner:not(:empty) {
display: block;
}
@@ -1740,6 +1746,117 @@ body.room-page .meta-panel li {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
body.season-page {
background:
radial-gradient(circle at 10% -8%, rgba(61, 110, 255, 0.2) 0, transparent 34%),
radial-gradient(circle at 92% 6%, rgba(255, 96, 125, 0.18) 0, transparent 32%),
linear-gradient(180deg, #ebf1ff 0%, #e8f0ff 52%, #edf4ff 100%);
}
body.season-admin-page {
background:
radial-gradient(circle at 8% -10%, rgba(0, 188, 157, 0.2) 0, transparent 34%),
radial-gradient(circle at 92% 5%, rgba(77, 125, 255, 0.2) 0, transparent 32%),
linear-gradient(180deg, #e9f3ff 0%, #eff6ff 56%, #e8f1ff 100%);
}
.season-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
.season-grid > .card:first-child {
grid-column: 1 / -1;
}
.season-hero {
padding: 0;
overflow: hidden;
display: grid;
grid-template-columns: minmax(220px, 0.52fr) minmax(0, 1fr);
border-radius: 22px;
}
.season-hero-cover {
min-height: 220px;
background:
radial-gradient(circle at 18% 12%, rgba(255, 90, 95, 0.22), transparent 36%),
radial-gradient(circle at 85% 8%, rgba(77, 125, 255, 0.22), transparent 34%),
linear-gradient(160deg, rgba(12, 25, 55, 0.58), rgba(12, 25, 55, 0.32)),
var(--season-cover);
background-size: cover;
background-position: center;
border-right: 1px solid rgba(160, 183, 235, 0.54);
}
.season-hero-body {
padding: 1rem;
}
.season-hero-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.9rem;
flex-wrap: wrap;
margin-bottom: 0.6rem;
}
.season-hero-cta {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.season-hero-cta button {
width: auto;
}
.season-daily-card {
border: 1px dashed rgba(127, 160, 228, 0.55);
border-radius: 14px;
background: rgba(246, 250, 255, 0.82);
padding: 0.7rem 0.75rem;
display: grid;
gap: 0.45rem;
}
.season-daily-head {
font-weight: 900;
letter-spacing: 0.2px;
color: #20335c;
}
.season-admin-grid {
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
align-items: start;
}
.season-admin-grid > .card:last-child {
grid-column: 1 / -1;
}
.season-grid .compact-list li,
.season-admin-grid .compact-list li {
padding: 0.56rem 0.62rem;
}
#season-system-config {
margin-bottom: 0.36rem;
}
#season-admin-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
}
#summary-season-progress {
margin-top: 0.2rem;
}
.achievement-rows {
display: grid;
gap: 0.55rem;
@@ -2331,6 +2448,26 @@ body[data-theme="sunset"].room-page .meta-panel li {
.invite-grid {
grid-template-columns: 1fr;
}
.season-grid,
.season-admin-grid {
grid-template-columns: 1fr;
}
.season-grid > .card:first-child,
.season-admin-grid > .card:last-child {
grid-column: auto;
}
.season-hero {
grid-template-columns: 1fr;
}
.season-hero-cover {
min-height: 180px;
border-right: none;
border-bottom: 1px solid rgba(160, 183, 235, 0.54);
}
}
@media (max-width: 760px) {
+47
View File
@@ -5,6 +5,7 @@
const overviewEl = document.getElementById('summary-overview');
const mvpEl = document.getElementById('summary-mvp');
const playersEl = document.getElementById('summary-players');
const seasonProgressEl = document.getElementById('summary-season-progress');
const statusEl = document.getElementById('status');
const setStatus = (text) => {
@@ -122,6 +123,51 @@
});
};
const renderSeasonProgress = (summary) => {
if (!seasonProgressEl) return;
seasonProgressEl.textContent = '';
const progress = summary && typeof summary === 'object' ? summary.season_progress : null;
if (!progress || typeof progress !== 'object') {
const hint = document.createElement('div');
hint.className = 'helper-text';
hint.textContent = '登录后在对局结算时会自动推进赛季任务与活动卡片进度。';
seasonProgressEl.appendChild(hint);
return;
}
const events = Array.isArray(progress.events) ? progress.events : [];
const eventText = events.length
? events.map((item) => `${item.event_key || '-'} +${Number(item.amount || 0)}`).join('、')
: '无';
const taskSet = new Set();
const taskUpdates = Array.isArray(progress.task_updates) ? progress.task_updates : [];
taskUpdates.forEach((entry) => {
const completed = (entry && Array.isArray(entry.completed_ids)) ? entry.completed_ids : [];
completed.forEach((taskId) => taskSet.add(Number(taskId)));
});
const cardSet = new Set();
const cardUpdates = Array.isArray(progress.card_updates) ? progress.card_updates : [];
cardUpdates.forEach((entry) => {
const completed = (entry && Array.isArray(entry.completed_ids)) ? entry.completed_ids : [];
completed.forEach((cardId) => cardSet.add(Number(cardId)));
});
const rows = [
`赛季ID:${progress.season_id || '-'}`,
`结算写入:${progress.applied ? '已写入' : '已处理过或当前无可用赛季'}`,
`触发事件:${eventText}`,
`任务推进:${taskUpdates.length} 项,新增完成 ${taskSet.size} 个`,
`卡片推进:${cardUpdates.length} 项,新增完成 ${cardSet.size} 个`,
];
rows.forEach((text) => {
const div = document.createElement('div');
div.textContent = text;
seasonProgressEl.appendChild(div);
});
};
const load = async () => {
if (!roomId) {
setStatus('房间不存在');
@@ -139,6 +185,7 @@
renderOverview(summary);
renderMvp(summary);
renderPlayers(summary);
renderSeasonProgress(summary);
};
load().catch((error) => setStatus(`网络错误: ${error}`));
+647
View File
@@ -0,0 +1,647 @@
(function () {
const body = document.body;
const subtitleEl = document.getElementById('season-subtitle');
const bannerEl = document.getElementById('season-banner');
const profileEl = document.getElementById('season-profile');
const announcementsEl = document.getElementById('season-announcements');
const cardsEl = document.getElementById('season-cards');
const tasksEl = document.getElementById('season-tasks');
const rewardsEl = document.getElementById('season-rewards');
const decorationsEl = document.getElementById('season-decorations');
const heroCoverEl = document.getElementById('season-hero-cover');
const heroTitleEl = document.getElementById('season-hero-title');
const heroSubEl = document.getElementById('season-hero-sub');
const dailyCardEl = document.getElementById('season-daily-card');
const dailyStoryEl = document.getElementById('season-daily-story');
const dailyStoryHintEl = document.getElementById('season-daily-story-hint');
const intelEl = document.getElementById('season-intel');
const intelUnlockBtn = document.getElementById('season-intel-unlock-btn');
const statusEl = document.getElementById('status');
const cardRefreshHintEl = document.getElementById('season-card-refresh-hint');
const passUnlockBtn = document.getElementById('season-pass-unlock-btn');
const claimAllTasksBtn = document.getElementById('season-claim-all-tasks-btn');
const chroniclePinEl = document.getElementById('season-chronicle-pin');
const seasonSwitchEl = document.getElementById('season-switch');
const seasonSwitchCardsEl = document.getElementById('season-switch-cards');
const seasonSwitchChronicleEl = document.getElementById('season-switch-chronicle');
const tabAllBtn = document.getElementById('season-task-tab-all');
const tabDailyBtn = document.getElementById('season-task-tab-daily');
const tabWeeklyBtn = document.getElementById('season-task-tab-weekly');
const tabSeasonBtn = document.getElementById('season-task-tab-season');
let currentTaskFilter = '';
let cachedPayload = null;
const setStatus = (text) => {
if (statusEl) statusEl.textContent = text || '';
};
const getJson = async (url) => {
const res = await fetch(url);
return res.json();
};
const postJson = async (url, payload = {}) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return res.json();
};
const getCardDetail = async (cardId) => {
const safeId = String(cardId || '').trim();
if (!safeId) return null;
const res = await fetch(`/api/cards/${encodeURIComponent(safeId)}`);
const data = await res.json();
if (!data.ok) return null;
return data.card || null;
};
const levelBadge = (rarity) => {
const key = String(rarity || '').toLowerCase();
if (key === 'legendary') return '传说';
if (key === 'epic') return '史诗';
if (key === 'rare') return '稀有';
return '普通';
};
const formatCountdown = (nextRefreshAt) => {
const text = String(nextRefreshAt || '').trim();
if (!text) return '下次刷新时间:-';
const nextTs = Date.parse(text);
if (!Number.isFinite(nextTs)) return `下次刷新时间:${text}`;
const diff = Math.max(0, nextTs - Date.now());
const hour = Math.floor(diff / (1000 * 60 * 60));
const minute = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
return `距下次刷新约 ${hour} 小时 ${minute} 分钟(${text})`;
};
const renderBanner = (payload) => {
if (!bannerEl) return;
const season = payload.season || {};
const profile = payload.profile || {};
bannerEl.textContent = '';
if (heroCoverEl) {
const cover = String(season.cover_url || '').trim();
if (cover) heroCoverEl.style.setProperty('--season-cover', `url('${cover.replace(/'/g, "\\'")}')`);
}
const rows = [
`赛季:${season.name || '-'}`,
`主题:${season.theme || '-'}`,
`状态:${season.time_status || '-'}`,
`时间:${season.start_at || '-'} ~ ${season.end_at || '-'}`,
season.rest_end_at ? `休整期截止:${season.rest_end_at}` : '',
`通行证价格:${Number(season.pass_price || 0)} 积分`,
`赛季等级:Lv.${Number(profile.level || 1)}(总经验 ${Number(profile.xp || 0)})`,
`赛季币:${Number(profile.season_points || 0)}`,
].filter(Boolean);
rows.forEach((text) => {
const div = document.createElement('div');
div.textContent = text;
bannerEl.appendChild(div);
});
const titleText = `${season.name || '赛季'} · ${season.theme || '未命名主题'}`;
if (subtitleEl) subtitleEl.textContent = titleText;
if (heroTitleEl) heroTitleEl.textContent = season.name || '赛季中心';
if (heroSubEl) heroSubEl.textContent = titleText;
if (cardRefreshHintEl) {
cardRefreshHintEl.textContent = formatCountdown(payload.next_refresh_at || '');
}
};
const renderSeasonCardCenter = (payload) => {
const season = payload.season || {};
const seasonId = String(season.season_id || '').trim();
if (!seasonId) return;
const parent = dailyCardEl ? dailyCardEl.parentElement : null;
if (!parent) return;
let box = document.getElementById('season-card-center');
if (!box) {
box = document.createElement('div');
box.id = 'season-card-center';
box.className = 'admin-inline-actions';
box.style.marginTop = '0.4rem';
parent.insertBefore(box, dailyCardEl);
}
box.textContent = '';
const link = document.createElement('a');
link.className = 'link-btn';
link.href = `/season/${encodeURIComponent(seasonId)}/cards`;
link.textContent = '进入本赛季游戏卡列表';
box.appendChild(link);
};
const renderProfile = (payload) => {
if (!profileEl) return;
const profile = payload.profile || {};
profileEl.textContent = '';
const rows = [
`通行证:${profile.pass_type === 'premium' ? '高级档' : '免费档'}`,
`当前等级:${Number(profile.level || 1)}`,
`本级经验:${Number(profile.xp_in_current_level || 0)} / ${Number(profile.xp_per_level || 100)}`,
`距离下一级:${Number(profile.xp_to_next_level || 0)}`,
`已完成任务:${Number(profile.completed_tasks || 0)}`,
`已领取卡片:${Number(profile.total_claimed_cards || 0)}`,
];
rows.forEach((text) => {
const div = document.createElement('div');
div.textContent = text;
profileEl.appendChild(div);
});
if (passUnlockBtn) {
passUnlockBtn.disabled = profile.pass_type === 'premium';
passUnlockBtn.textContent = profile.pass_type === 'premium' ? '已解锁高级通行证' : '解锁高级通行证';
}
};
const renderAnnouncements = (rows) => {
if (!announcementsEl) return;
announcementsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂无赛季公告';
announcementsEl.appendChild(li);
return;
}
list.forEach((row) => {
const li = document.createElement('li');
li.textContent = `${row.title || '公告'} · ${row.created_at || '-'} · ${row.content || ''}`;
announcementsEl.appendChild(li);
});
};
const buildActionBtn = (text, onClick, className = 'secondary-btn') => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = className;
btn.textContent = text;
btn.addEventListener('click', onClick);
return btn;
};
const renderCards = (rows, payload) => {
if (!cardsEl) return;
cardsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '当前刷新槽位暂无卡片,稍后重试。';
cardsEl.appendChild(li);
return;
}
list.forEach((row) => {
const li = document.createElement('li');
const head = document.createElement('div');
head.textContent = `【${levelBadge(row.rarity)}】${row.title || '未命名卡片'} · ${row.status || 'active'}`;
li.appendChild(head);
const story = document.createElement('p');
story.className = 'helper-text';
story.textContent = row.story_text || '';
li.appendChild(story);
const meta = document.createElement('p');
meta.className = 'helper-text';
meta.textContent = `目标:${row.objective || '-'} | 进度 ${Number(row.progress_value || 0)}/${Number(row.target_value || 1)} | 奖励 ${Number(row.reward_points || 0)} 积分 + ${Number(row.reward_xp || 0)} 经验`;
li.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'admin-inline-actions';
if (String(row.status || '') === 'completed') {
actions.appendChild(buildActionBtn('领取奖励', async () => {
setStatus(`正在领取卡片奖励 #${row.id}...`);
const data = await postJson(`/api/seasons/active/cards/${row.id}/claim`, {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus('卡片奖励领取成功。');
await reload();
}));
}
if (String(row.status || '') === 'active') {
actions.appendChild(buildActionBtn('手动刷新', async () => {
setStatus(`正在刷新卡片 #${row.id}...`);
const data = await postJson(`/api/seasons/active/cards/${row.id}/refresh`, {});
if (!data.ok) {
setStatus(data.error || '刷新失败');
return;
}
setStatus('卡片已刷新。');
await reload();
}, 'ghost-btn'));
}
if (actions.children.length) li.appendChild(actions);
cardsEl.appendChild(li);
});
if (cardRefreshHintEl) {
cardRefreshHintEl.textContent = formatCountdown(payload.next_refresh_at || '');
}
};
const renderTasks = (rows) => {
if (!tasksEl) return;
tasksEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
const filtered = currentTaskFilter
? list.filter((row) => String(row.task_type || '') === currentTaskFilter)
: list;
if (!filtered.length) {
const li = document.createElement('li');
li.textContent = '暂无任务';
tasksEl.appendChild(li);
return;
}
filtered.forEach((row) => {
const li = document.createElement('li');
const title = document.createElement('div');
title.textContent = `[${row.task_type || '-'}] ${row.title || '未命名任务'} · ${row.status || 'active'}`;
li.appendChild(title);
const desc = document.createElement('p');
desc.className = 'helper-text';
desc.textContent = `${row.description || ''} | 进度 ${Number(row.progress_value || 0)}/${Number(row.target_value || 1)} | 奖励 ${Number(row.reward_points || 0)} 积分 + ${Number(row.reward_xp || 0)} 经验`;
li.appendChild(desc);
if (String(row.status || '') === 'completed') {
li.appendChild(buildActionBtn('领取任务奖励', async () => {
setStatus(`正在领取任务 #${row.id}...`);
const data = await postJson(`/api/seasons/active/tasks/${row.id}/claim`, {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus('任务奖励领取成功。');
await reload();
}));
}
tasksEl.appendChild(li);
});
};
const renderRewards = (rows, claimedIds, profile) => {
if (!rewardsEl) return;
rewardsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
const claimedSet = new Set(Array.isArray(claimedIds) ? claimedIds : []);
const level = Number(profile?.level || 1);
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂无通行证奖励配置';
rewardsEl.appendChild(li);
return;
}
list.slice(0, 120).forEach((row) => {
const rewardId = String(row.reward_id || '');
const li = document.createElement('li');
const locked = level < Number(row.reward_level || 1);
const claimed = claimedSet.has(rewardId);
const title = `${row.title || rewardId} · Lv.${Number(row.reward_level || 1)} · ${row.reward_track || 'free'}`;
li.textContent = `${title} | 奖励 ${Number(row.reward_points || 0)} 积分`;
if (!locked && !claimed) {
li.appendChild(buildActionBtn('领取', async () => {
setStatus(`正在领取赛季奖励 ${rewardId}...`);
const data = await postJson(`/api/seasons/active/rewards/${encodeURIComponent(rewardId)}/claim`, {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus('赛季奖励领取成功。');
await reload();
}));
} else {
const mark = document.createElement('span');
mark.className = 'helper-text';
mark.style.marginLeft = '0.5rem';
mark.textContent = claimed ? '已领取' : '未解锁';
li.appendChild(mark);
}
rewardsEl.appendChild(li);
});
};
const renderDecorations = (rows, equipped) => {
if (!decorationsEl) return;
decorationsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂未获得赛季装扮';
decorationsEl.appendChild(li);
return;
}
const equippedTitleId = String((equipped?.title || {}).decoration_id || '');
const equippedFrameId = String((equipped?.avatar_frame || {}).decoration_id || '');
list.forEach((row) => {
const type = String(row.decoration_type || '');
const decorationId = String(row.decoration_id || '');
const isEquipped = (type === 'title' && decorationId === equippedTitleId)
|| (type === 'avatar_frame' && decorationId === equippedFrameId)
|| Boolean(row.is_equipped);
const li = document.createElement('li');
li.textContent = `${type === 'title' ? '称号' : '头像框'}:${decorationId}${isEquipped ? '(已佩戴)' : ''}`;
if (!isEquipped) {
li.appendChild(buildActionBtn('佩戴', async () => {
setStatus(`正在佩戴 ${decorationId}...`);
const data = await postJson('/api/seasons/decorations/equip', {
decoration_type: type,
decoration_id: decorationId,
});
if (!data.ok) {
setStatus(data.error || '佩戴失败');
return;
}
setStatus('佩戴成功。');
await reload();
}, 'ghost-btn'));
}
decorationsEl.appendChild(li);
});
};
const renderDailyCard = (payload) => {
if (!dailyCardEl) return;
dailyCardEl.textContent = '';
const daily = payload.daily_game_card || {};
const title = String(daily.title || '').trim() || '今日赛季日卡未生成';
const summary = String(daily.summary || '').trim();
const cardId = String(daily.card_id || '').trim();
const head = document.createElement('div');
head.className = 'season-daily-head';
head.textContent = title;
dailyCardEl.appendChild(head);
if (summary) {
const p = document.createElement('p');
p.className = 'helper-text';
p.textContent = summary;
dailyCardEl.appendChild(p);
}
if (cardId) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'secondary-btn';
btn.textContent = '用今日游戏卡开一局';
btn.addEventListener('click', async () => {
setStatus('正在创建房间...');
const data = await postJson('/api/rooms/create', { card_id: cardId });
if (!data.ok) {
setStatus(data.error || '创建失败');
return;
}
if (data.player_id) localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
if (data.player_name) localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
});
dailyCardEl.appendChild(btn);
}
const hint = document.createElement('p');
hint.className = 'helper-text';
hint.textContent = '提示:通关这张日卡后,才能解锁当日资料。';
dailyCardEl.appendChild(hint);
};
const renderDailyStory = async (payload) => {
if (!dailyStoryEl) return;
dailyStoryEl.textContent = '';
const daily = payload.daily_game_card || {};
const cardId = String(daily.card_id || '').trim();
if (!cardId) {
dailyStoryEl.textContent = '今日故事未生成';
return;
}
const card = await getCardDetail(cardId);
if (!card) {
dailyStoryEl.textContent = '今日故事加载失败';
return;
}
const title = document.createElement('div');
title.className = 'season-daily-head';
title.textContent = `“${card.title || '未命名故事'}”`;
dailyStoryEl.appendChild(title);
const summary = document.createElement('p');
summary.className = 'helper-text';
summary.textContent = card.summary || '';
dailyStoryEl.appendChild(summary);
const goals = Array.isArray(card.starter_goals) ? card.starter_goals : [];
const objective = document.createElement('p');
objective.className = 'helper-text';
objective.textContent = goals[0] || '探索今日线索并推进剧情';
dailyStoryEl.appendChild(objective);
const reward = document.createElement('div');
reward.className = 'helper-text';
reward.textContent = '奖励:赛季经验/积分结算';
dailyStoryEl.appendChild(reward);
const actions = document.createElement('div');
actions.className = 'admin-inline-actions';
const acceptBtn = document.createElement('button');
acceptBtn.type = 'button';
acceptBtn.className = 'secondary-btn';
acceptBtn.textContent = '接受挑战';
acceptBtn.addEventListener('click', async () => {
setStatus('正在创建房间...');
const data = await postJson('/api/rooms/create', { card_id: cardId });
if (!data.ok) {
setStatus(data.error || '创建失败');
return;
}
if (data.player_id) localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
if (data.player_name) localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
});
const refreshBtn = document.createElement('button');
refreshBtn.type = 'button';
refreshBtn.className = 'ghost-btn';
refreshBtn.textContent = '刷新';
refreshBtn.addEventListener('click', async () => {
setStatus('正在刷新今日故事...');
await reload();
setStatus('今日故事已刷新。');
});
actions.appendChild(acceptBtn);
actions.appendChild(refreshBtn);
dailyStoryEl.appendChild(actions);
if (dailyStoryHintEl) {
const locked = Array.isArray(daily.locked_intel) ? daily.locked_intel.length : 0;
const unlocked = Array.isArray(payload.intel)
? payload.intel.filter((it) => String(it.day_key || '') === String(daily.day_key || '')).length
: 0;
const pending = Math.max(0, locked - unlocked);
dailyStoryHintEl.textContent = pending ? `还有 ${pending} 条线索待查看 →` : '今日线索已全部查看';
}
};
const renderIntel = (payload) => {
if (!intelEl) return;
intelEl.textContent = '';
const rows = Array.isArray(payload.intel) ? payload.intel : [];
if (!rows.length) {
const li = document.createElement('li');
li.textContent = '暂无资料,先解锁一条。';
intelEl.appendChild(li);
return;
}
rows.slice(0, 80).forEach((row) => {
const li = document.createElement('li');
li.textContent = `${row.intel_title || '资料'} · ${row.day_key || '-'} · ${row.intel_text || ''}`;
intelEl.appendChild(li);
});
};
const renderTaskTabs = () => {
const update = () => {
[tabAllBtn, tabDailyBtn, tabWeeklyBtn, tabSeasonBtn].forEach((btn) => {
if (!btn) return;
btn.classList.remove('secondary-btn');
});
if (!currentTaskFilter && tabAllBtn) tabAllBtn.classList.add('secondary-btn');
if (currentTaskFilter === 'daily' && tabDailyBtn) tabDailyBtn.classList.add('secondary-btn');
if (currentTaskFilter === 'weekly' && tabWeeklyBtn) tabWeeklyBtn.classList.add('secondary-btn');
if (currentTaskFilter === 'season' && tabSeasonBtn) tabSeasonBtn.classList.add('secondary-btn');
if (cachedPayload) renderTasks(cachedPayload.tasks || []);
};
if (tabAllBtn) tabAllBtn.addEventListener('click', () => { currentTaskFilter = ''; update(); });
if (tabDailyBtn) tabDailyBtn.addEventListener('click', () => { currentTaskFilter = 'daily'; update(); });
if (tabWeeklyBtn) tabWeeklyBtn.addEventListener('click', () => { currentTaskFilter = 'weekly'; update(); });
if (tabSeasonBtn) tabSeasonBtn.addEventListener('click', () => { currentTaskFilter = 'season'; update(); });
update();
};
const bindTopActions = () => {
if (passUnlockBtn) {
passUnlockBtn.addEventListener('click', async () => {
setStatus('正在解锁高级通行证...');
const data = await postJson('/api/seasons/active/pass/unlock', {});
if (!data.ok) {
setStatus(data.error || '解锁失败');
return;
}
setStatus('高级通行证已解锁。');
await reload();
});
}
if (claimAllTasksBtn) {
claimAllTasksBtn.addEventListener('click', async () => {
setStatus('正在一键领取任务奖励...');
const data = await postJson('/api/seasons/active/tasks/claim-all', {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus(`已领取 ${Number((data.result || {}).claimed_count || 0)} 个任务奖励。`);
await reload();
});
}
};
const bindIntelActions = () => {
if (!intelUnlockBtn) return;
intelUnlockBtn.addEventListener('click', async () => {
const payload = cachedPayload || {};
const daily = payload.daily_game_card || {};
const dayKey = String(daily.day_key || '').trim();
if (!dayKey) {
setStatus('今日赛季日卡尚未生成,稍后再试。');
return;
}
setStatus('正在解锁资料...');
const data = await postJson('/api/seasons/active/intel/unlock', { day_key: dayKey });
if (!data.ok) {
if (String(data.error || '').includes('通关')) {
setStatus(data.error);
} else {
setStatus(data.error || '解锁失败');
}
return;
}
setStatus(`已解锁资料:${(data.unlocked || {}).intel_title || ''}`);
await reload();
});
};
const renderAll = (payload) => {
cachedPayload = payload || {};
renderBanner(payload || {});
renderProfile(payload || {});
renderAnnouncements(payload.announcements || []);
renderSeasonCardCenter(payload || {});
bindSeasonSwitch(payload || {});
renderDailyStory(payload || {}).catch((error) => setStatus(`网络错误: ${error}`));
renderDailyCard(payload || {});
renderIntel(payload || {});
renderCards(payload.cards || [], payload || {});
renderTasks(payload.tasks || []);
renderRewards(payload.rewards || [], payload.claimed_reward_ids || [], payload.profile || {});
renderDecorations(payload.decorations || [], payload.equipped_decorations || {});
const timeStatus = String((payload.season || {}).time_status || '');
if (chroniclePinEl) {
chroniclePinEl.style.display = timeStatus === 'rest' ? '' : 'none';
}
};
const bindSeasonSwitch = (payload) => {
if (!seasonSwitchEl || !seasonSwitchCardsEl || !seasonSwitchChronicleEl) return;
if (seasonSwitchEl.dataset.bound === '1') return;
seasonSwitchEl.dataset.bound = '1';
const applyLinks = () => {
const seasonId = String(seasonSwitchEl.value || '').trim();
seasonSwitchCardsEl.href = seasonId ? `/season/${encodeURIComponent(seasonId)}/cards` : '#';
seasonSwitchChronicleEl.href = seasonId ? `/season/${encodeURIComponent(seasonId)}/chronicle` : '#';
seasonSwitchCardsEl.classList.toggle('disabled', !seasonId);
seasonSwitchChronicleEl.classList.toggle('disabled', !seasonId);
};
seasonSwitchEl.addEventListener('change', applyLinks);
applyLinks();
(async () => {
const res = await getJson('/api/seasons/history');
if (!res.ok) {
setStatus(res.error || '加载赛季列表失败');
return;
}
const seasons = Array.isArray(res.seasons) ? res.seasons : [];
const currentId = String((payload.season || {}).season_id || '').trim();
seasonSwitchEl.textContent = '';
seasons.slice(0, 60).forEach((s) => {
const sid = String((s || {}).season_id || '').trim();
if (!sid) return;
const opt = document.createElement('option');
const name = String((s || {}).name || '').trim() || '未命名赛季';
const theme = String((s || {}).theme || '').trim();
const status = String((s || {}).time_status || '').trim();
opt.value = sid;
opt.textContent = `${name}${theme ? ` · ${theme}` : ''}${status ? ` · ${status}` : ''}`;
if (sid === currentId) opt.selected = true;
seasonSwitchEl.appendChild(opt);
});
applyLinks();
})().catch((error) => setStatus(`网络错误: ${error}`));
};
const reload = async () => {
const data = await getJson('/api/seasons/active');
if (!data.ok) {
setStatus(data.error || '加载赛季信息失败');
return;
}
renderAll(data);
};
bindTopActions();
bindIntelActions();
renderTaskTabs();
reload().catch((error) => setStatus(`网络错误: ${error}`));
})();
+573
View File
@@ -0,0 +1,573 @@
(function () {
const statusEl = document.getElementById('status');
const seasonSystemConfigEl = document.getElementById('season-system-config');
const seasonSystemSaveBtn = document.getElementById('season-system-save-btn');
const seasonAutoThemeInput = document.getElementById('season-auto-theme');
const seasonAutoGenerateBtn = document.getElementById('season-auto-generate-btn');
const seasonCreateBtn = document.getElementById('season-create-btn');
const seasonCreateName = document.getElementById('season-create-name');
const seasonCreateTheme = document.getElementById('season-create-theme');
const seasonCreateStart = document.getElementById('season-create-start');
const seasonCreateEnd = document.getElementById('season-create-end');
const seasonCreatePassPrice = document.getElementById('season-create-pass-price');
const seasonCreateDescription = document.getElementById('season-create-description');
const seasonListEl = document.getElementById('season-admin-list');
const seasonCurrentHintEl = document.getElementById('season-admin-current-hint');
const seasonRefreshBtn = document.getElementById('season-admin-refresh-btn');
const announceTitleInput = document.getElementById('season-announce-title');
const announceContentInput = document.getElementById('season-announce-content');
const announceBtn = document.getElementById('season-announce-btn');
const taskTitleInput = document.getElementById('season-task-title');
const taskTypeInput = document.getElementById('season-task-type');
const taskEventInput = document.getElementById('season-task-event');
const taskTargetInput = document.getElementById('season-task-target');
const taskRewardPointsInput = document.getElementById('season-task-reward-points');
const taskRewardXpInput = document.getElementById('season-task-reward-xp');
const taskDescInput = document.getElementById('season-task-description');
const taskSaveBtn = document.getElementById('season-task-save-btn');
const taskListEl = document.getElementById('season-task-list');
const poolTitleInput = document.getElementById('season-pool-title');
const poolTypeInput = document.getElementById('season-pool-type');
const poolEventInput = document.getElementById('season-pool-event');
const poolTargetInput = document.getElementById('season-pool-target');
const poolRarityInput = document.getElementById('season-pool-rarity');
const poolWeightInput = document.getElementById('season-pool-weight');
const poolRewardPointsInput = document.getElementById('season-pool-reward-points');
const poolRewardXpInput = document.getElementById('season-pool-reward-xp');
const poolHookInput = document.getElementById('season-pool-hook');
const poolObjectiveInput = document.getElementById('season-pool-objective');
const poolHintInput = document.getElementById('season-pool-hint');
const poolSaveBtn = document.getElementById('season-pool-save-btn');
const poolListEl = document.getElementById('season-pool-list');
const rewardLevelInput = document.getElementById('season-reward-level');
const rewardTrackInput = document.getElementById('season-reward-track');
const rewardTitleInput = document.getElementById('season-reward-title');
const rewardPointsInput = document.getElementById('season-reward-points');
const rewardXpInput = document.getElementById('season-reward-xp');
const rewardCoinInput = document.getElementById('season-reward-coin');
const rewardPayloadInput = document.getElementById('season-reward-payload');
const rewardSaveBtn = document.getElementById('season-reward-save-btn');
const rewardListEl = document.getElementById('season-reward-list');
const seasonInfoName = document.getElementById('season-info-name');
const seasonInfoTheme = document.getElementById('season-info-theme');
const seasonInfoStart = document.getElementById('season-info-start');
const seasonInfoEnd = document.getElementById('season-info-end');
const seasonInfoRestEnd = document.getElementById('season-info-rest-end');
const seasonInfoRestDays = document.getElementById('season-info-rest-days');
const seasonInfoPassPrice = document.getElementById('season-info-pass-price');
const seasonInfoCover = document.getElementById('season-info-cover');
const seasonInfoAccent = document.getElementById('season-info-accent');
const seasonInfoDescription = document.getElementById('season-info-description');
const seasonInfoSeed = document.getElementById('season-info-seed');
const seasonInfoOutline = document.getElementById('season-info-outline');
const seasonInfoSaveBtn = document.getElementById('season-info-save-btn');
const seasonInfoRestBtn = document.getElementById('season-info-rest-btn');
const seasonInfoResumeBtn = document.getElementById('season-info-resume-btn');
const seasonInfoEndBtn = document.getElementById('season-info-end-btn');
const aiDailyEnabledInput = document.getElementById('season-ai-daily-enabled');
const aiDailyToneInput = document.getElementById('season-ai-daily-tone');
const aiDailyDifficultyInput = document.getElementById('season-ai-daily-difficulty');
const aiDailyExtraInput = document.getElementById('season-ai-daily-extra');
const aiDailySaveBtn = document.getElementById('season-ai-daily-save-btn');
let selectedSeasonId = '';
let seasonSystemConfig = {};
let selectedSeasonConfig = {};
let selectedSeasonMeta = {};
const setStatus = (text) => {
if (statusEl) statusEl.textContent = text || '';
};
const getJson = async (url) => {
const res = await fetch(url);
return res.json();
};
const postJson = async (url, payload = {}) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return res.json();
};
const patchJson = async (url, payload = {}) => {
const res = await fetch(url, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return res.json();
};
const renderSystemConfig = () => {
if (!seasonSystemConfigEl) return;
seasonSystemConfigEl.textContent = '';
const rows = [
`enabled=${Boolean(seasonSystemConfig.enabled)}`,
`auto_create_enabled=${Boolean(seasonSystemConfig.auto_create_enabled)}`,
`auto_generate_theme=${Boolean(seasonSystemConfig.auto_generate_theme)}`,
`season_duration_days=${Number(seasonSystemConfig.season_duration_days || 30)}`,
`daily_card_count=${Number(seasonSystemConfig.daily_card_count || 3)}`,
`refresh_hours=${Array.isArray(seasonSystemConfig.refresh_hours) ? seasonSystemConfig.refresh_hours.join(',') : '0,8,18'}`,
`manual_refresh_cost=${Number(seasonSystemConfig.manual_refresh_cost || 60)}`,
`premium_pass_price=${Number(seasonSystemConfig.premium_pass_price || 680)}`,
];
rows.forEach((text) => {
const div = document.createElement('div');
div.textContent = text;
seasonSystemConfigEl.appendChild(div);
});
};
const loadSystemConfig = async () => {
const data = await getJson('/api/admin/settings/season-system');
if (!data.ok) {
setStatus(data.error || '加载赛季系统配置失败');
return;
}
seasonSystemConfig = data.config || {};
renderSystemConfig();
};
const saveSystemConfig = async () => {
const duration = prompt('赛季默认时长(天)', String(seasonSystemConfig.season_duration_days || 30));
if (duration === null) return;
const dailyCards = prompt('每日卡片数量', String(seasonSystemConfig.daily_card_count || 3));
if (dailyCards === null) return;
const refreshHours = prompt('刷新时段(逗号分隔)', (seasonSystemConfig.refresh_hours || [0, 8, 18]).join(','));
if (refreshHours === null) return;
const manualCost = prompt('手动刷新消耗积分', String(seasonSystemConfig.manual_refresh_cost || 60));
if (manualCost === null) return;
const passPrice = prompt('高级通行证价格', String(seasonSystemConfig.premium_pass_price || 680));
if (passPrice === null) return;
const payload = {
season_duration_days: Number(duration) || 30,
daily_card_count: Number(dailyCards) || 3,
refresh_hours: String(refreshHours).split(',').map((v) => Number(v.trim())).filter((v) => Number.isFinite(v)),
manual_refresh_cost: Number(manualCost) || 60,
premium_pass_price: Number(passPrice) || 680,
enabled: Boolean(seasonSystemConfig.enabled),
auto_create_enabled: Boolean(seasonSystemConfig.auto_create_enabled),
auto_generate_theme: Boolean(seasonSystemConfig.auto_generate_theme),
};
const data = await patchJson('/api/admin/settings/season-system', payload);
if (!data.ok) {
setStatus(data.error || '保存赛季系统配置失败');
return;
}
seasonSystemConfig = data.config || {};
renderSystemConfig();
setStatus('赛季系统配置已更新。');
};
const renderSeasonList = (rows, active) => {
if (!seasonListEl) return;
seasonListEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂无赛季';
seasonListEl.appendChild(li);
return;
}
list.forEach((row) => {
const seasonId = String(row.season_id || '');
const li = document.createElement('li');
const activeMark = active && String(active.season_id || '') === seasonId ? '(进行中)' : '';
li.textContent = `${row.name || seasonId} ${activeMark} · ${row.time_status || row.status || '-'} · ${row.start_at || '-'} ~ ${row.end_at || '-'}`;
const selectBtn = document.createElement('button');
selectBtn.type = 'button';
selectBtn.className = 'ghost-btn';
selectBtn.textContent = selectedSeasonId === seasonId ? '已选中' : '选中';
selectBtn.addEventListener('click', async () => {
selectedSeasonId = seasonId;
await loadSelectedSeason();
await loadSeasons();
});
li.appendChild(selectBtn);
seasonListEl.appendChild(li);
});
};
const renderSimpleList = (el, rows, formatFn) => {
if (!el) return;
el.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂无数据';
el.appendChild(li);
return;
}
list.forEach((row) => {
const li = document.createElement('li');
li.textContent = formatFn(row);
el.appendChild(li);
});
};
const loadSeasons = async () => {
const data = await getJson('/api/admin/seasons');
if (!data.ok) {
setStatus(data.error || '加载赛季列表失败');
return;
}
if (!selectedSeasonId && data.active && data.active.season_id) {
selectedSeasonId = String(data.active.season_id);
}
renderSeasonList(data.seasons || [], data.active || null);
};
const loadSelectedSeason = async () => {
if (!selectedSeasonId) {
if (seasonCurrentHintEl) seasonCurrentHintEl.textContent = '请选择一个赛季后再配置。';
renderSimpleList(taskListEl, [], () => '');
renderSimpleList(poolListEl, [], () => '');
renderSimpleList(rewardListEl, [], () => '');
return;
}
if (seasonCurrentHintEl) seasonCurrentHintEl.textContent = `当前选中赛季:${selectedSeasonId}`;
const [dashboardRes, tasksRes, poolsRes, rewardsRes] = await Promise.all([
getJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/dashboard`),
getJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/tasks`),
getJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/card-pools`),
getJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/pass-rewards`),
]);
if (!dashboardRes.ok) {
setStatus(dashboardRes.error || '加载赛季看板失败');
return;
}
selectedSeasonMeta = dashboardRes.season || {};
selectedSeasonConfig = (dashboardRes.season || {}).config || {};
if (aiDailyEnabledInput) aiDailyEnabledInput.checked = Boolean(selectedSeasonConfig.ai_daily_card_enabled);
if (aiDailyToneInput) aiDailyToneInput.value = selectedSeasonConfig.ai_daily_card_tone || '';
if (aiDailyDifficultyInput) aiDailyDifficultyInput.value = selectedSeasonConfig.ai_daily_card_difficulty || '';
if (aiDailyExtraInput) aiDailyExtraInput.value = selectedSeasonConfig.ai_daily_card_prompt_extra || '';
const dashboard = dashboardRes.dashboard || {};
setStatus(`参与 ${Number(dashboard.participants || 0)},高级档 ${Number(dashboard.premium_users || 0)},转化 ${Number(dashboard.premium_conversion || 0)}%`);
if (seasonInfoName) seasonInfoName.value = selectedSeasonMeta.name || '';
if (seasonInfoTheme) seasonInfoTheme.value = selectedSeasonMeta.theme || '';
if (seasonInfoStart) seasonInfoStart.value = selectedSeasonMeta.start_at || '';
if (seasonInfoEnd) seasonInfoEnd.value = selectedSeasonMeta.end_at || '';
if (seasonInfoRestEnd) seasonInfoRestEnd.value = selectedSeasonMeta.rest_end_at || '';
if (seasonInfoRestDays) seasonInfoRestDays.value = Number(selectedSeasonConfig.rest_days || 0);
if (seasonInfoPassPrice) seasonInfoPassPrice.value = Number(selectedSeasonMeta.pass_price || 0);
if (seasonInfoCover) seasonInfoCover.value = selectedSeasonMeta.cover_url || '';
if (seasonInfoAccent) seasonInfoAccent.value = selectedSeasonMeta.accent_color || '';
if (seasonInfoDescription) seasonInfoDescription.value = selectedSeasonMeta.description || '';
if (seasonInfoSeed) seasonInfoSeed.value = selectedSeasonMeta.story_seed || '';
if (seasonInfoOutline) seasonInfoOutline.value = selectedSeasonMeta.story_outline || '';
renderSimpleList(
taskListEl,
tasksRes.ok ? tasksRes.task_templates || [] : [],
(row) => `[${row.task_type || '-'}] ${row.title || '-'} · 事件 ${row.event_key || '-'} · 目标 ${Number(row.target_value || 1)} · 奖励 ${Number(row.reward_points || 0)} 积分`
);
renderSimpleList(
poolListEl,
poolsRes.ok ? poolsRes.card_pools || [] : [],
(row) => `[${row.card_type || '-'}] ${row.title_hint || '-'} · 事件 ${row.event_key || '-'} · 稀有度 ${row.rarity || 'normal'}`
);
renderSimpleList(
rewardListEl,
rewardsRes.ok ? rewardsRes.pass_rewards || [] : [],
(row) => `[${row.reward_track || '-'}] Lv.${Number(row.reward_level || 1)} ${row.title || '-'} · ${Number(row.reward_points || 0)} 积分`
);
};
const saveAiDailyConfig = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
const payload = {
config: {
ai_daily_card_enabled: Boolean(aiDailyEnabledInput && aiDailyEnabledInput.checked),
ai_daily_card_tone: String(aiDailyToneInput?.value || '').trim(),
ai_daily_card_difficulty: String(aiDailyDifficultyInput?.value || '').trim(),
ai_daily_card_prompt_extra: String(aiDailyExtraInput?.value || '').trim(),
},
};
const data = await patchJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}`, payload);
if (!data.ok) {
setStatus(data.error || '保存AI日卡配置失败');
return;
}
setStatus('AI日卡配置已保存。');
await loadSelectedSeason();
};
const saveSeasonInfo = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
const payload = {
name: String(seasonInfoName?.value || '').trim(),
theme: String(seasonInfoTheme?.value || '').trim(),
start_at: String(seasonInfoStart?.value || '').trim(),
end_at: String(seasonInfoEnd?.value || '').trim(),
rest_end_at: String(seasonInfoRestEnd?.value || '').trim(),
pass_price: Number(seasonInfoPassPrice?.value || 0),
cover_url: String(seasonInfoCover?.value || '').trim(),
accent_color: String(seasonInfoAccent?.value || '').trim(),
description: String(seasonInfoDescription?.value || '').trim(),
story_seed: String(seasonInfoSeed?.value || '').trim(),
story_outline: String(seasonInfoOutline?.value || '').trim(),
config: {
rest_days: Number(seasonInfoRestDays?.value || 0),
},
};
const data = await patchJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}`, payload);
if (!data.ok) {
setStatus(data.error || '保存赛季信息失败');
return;
}
setStatus('赛季信息已保存。');
await loadSelectedSeason();
};
const endSeason = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
if (!confirm('确认手动结束该赛季?')) return;
const nowIso = new Date().toISOString();
const payload = { status: 'ended', end_at: nowIso };
const data = await patchJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}`, payload);
if (!data.ok) {
setStatus(data.error || '结束赛季失败');
return;
}
setStatus('赛季已结束。');
await loadSeasons();
await loadSelectedSeason();
};
const enterRestSeason = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
if (!confirm('确认将赛季进入休整期?')) return;
const payload = { status: 'rest' };
const data = await patchJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}`, payload);
if (!data.ok) {
setStatus(data.error || '进入休整期失败');
return;
}
setStatus('赛季已进入休整期。');
await loadSeasons();
await loadSelectedSeason();
};
const resumeSeason = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
if (!confirm('确认重新开启该赛季?')) return;
const payload = { status: 'active' };
const data = await patchJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}`, payload);
if (!data.ok) {
setStatus(data.error || '重新开启赛季失败');
return;
}
setStatus('赛季已重新开启。');
await loadSeasons();
await loadSelectedSeason();
};
const createSeason = async () => {
const payload = {
name: String(seasonCreateName?.value || '').trim(),
theme: String(seasonCreateTheme?.value || '').trim(),
start_at: String(seasonCreateStart?.value || '').trim(),
end_at: String(seasonCreateEnd?.value || '').trim(),
pass_price: Number(seasonCreatePassPrice?.value || 680),
description: String(seasonCreateDescription?.value || '').trim(),
status: 'active',
source: 'manual',
config: {
daily_card_count: 3,
daily_task_count: 3,
weekly_task_count: 3,
refresh_hours: [0, 8, 18],
manual_refresh_cost: 60,
ai_daily_card_enabled: false,
ai_daily_card_tone: '悬疑冒险',
ai_daily_card_difficulty: '中等',
},
};
if (!payload.name) {
setStatus('请填写赛季名称。');
return;
}
const data = await postJson('/api/admin/seasons', payload);
if (!data.ok) {
setStatus(data.error || '创建赛季失败');
return;
}
selectedSeasonId = String((data.season || {}).season_id || '');
setStatus(`赛季已创建:${(data.season || {}).name || selectedSeasonId}`);
await loadSeasons();
await loadSelectedSeason();
};
const autoGenerateSeason = async () => {
const themeHint = String(seasonAutoThemeInput?.value || '').trim() || '迷雾电波';
const data = await postJson('/api/admin/seasons/auto-generate', { theme_hint: themeHint });
if (!data.ok) {
setStatus(data.error || '自动生成赛季失败');
return;
}
selectedSeasonId = String((data.season || {}).season_id || '');
setStatus(`已自动生成赛季:${(data.season || {}).name || selectedSeasonId}`);
await loadSeasons();
await loadSelectedSeason();
};
const publishAnnouncement = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
const payload = {
title: String(announceTitleInput?.value || '').trim(),
content: String(announceContentInput?.value || '').trim(),
};
if (!payload.title) {
setStatus('请输入公告标题。');
return;
}
const data = await postJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/announce`, payload);
if (!data.ok) {
setStatus(data.error || '发布公告失败');
return;
}
setStatus('赛季公告已发布。');
if (announceTitleInput) announceTitleInput.value = '';
if (announceContentInput) announceContentInput.value = '';
};
const saveTaskTemplate = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
const payload = {
title: String(taskTitleInput?.value || '').trim(),
task_type: String(taskTypeInput?.value || '').trim() || 'daily',
event_key: String(taskEventInput?.value || '').trim() || 'complete_game',
target_value: Number(taskTargetInput?.value || 1),
reward_points: Number(taskRewardPointsInput?.value || 0),
reward_xp: Number(taskRewardXpInput?.value || 0),
description: String(taskDescInput?.value || '').trim(),
is_active: true,
};
if (!payload.title) {
setStatus('请输入任务标题。');
return;
}
const data = await postJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/tasks`, payload);
if (!data.ok) {
setStatus(data.error || '保存任务模板失败');
return;
}
setStatus('任务模板已保存。');
await loadSelectedSeason();
};
const saveCardPool = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
const payload = {
title_hint: String(poolTitleInput?.value || '').trim(),
card_type: String(poolTypeInput?.value || '').trim() || 'rotating',
event_key: String(poolEventInput?.value || '').trim() || 'complete_game',
target_value: Number(poolTargetInput?.value || 1),
rarity: String(poolRarityInput?.value || '').trim() || 'normal',
weight: Number(poolWeightInput?.value || 100),
reward_points: Number(poolRewardPointsInput?.value || 0),
reward_xp: Number(poolRewardXpInput?.value || 0),
story_hook: String(poolHookInput?.value || '').trim(),
objective_template: String(poolObjectiveInput?.value || '').trim(),
hint_template: String(poolHintInput?.value || '').trim(),
is_active: true,
};
if (!payload.title_hint) {
setStatus('请输入卡池标题提示。');
return;
}
const data = await postJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/card-pools`, payload);
if (!data.ok) {
setStatus(data.error || '保存卡池失败');
return;
}
setStatus('活动卡池模板已保存。');
await loadSelectedSeason();
};
const savePassReward = async () => {
if (!selectedSeasonId) {
setStatus('请先选择赛季。');
return;
}
let rewardPayload = {};
const payloadText = String(rewardPayloadInput?.value || '').trim();
if (payloadText) {
try {
rewardPayload = JSON.parse(payloadText);
} catch {
setStatus('奖励 JSON 格式无效。');
return;
}
}
const payload = {
reward_level: Number(rewardLevelInput?.value || 1),
reward_track: String(rewardTrackInput?.value || '').trim() || 'free',
title: String(rewardTitleInput?.value || '').trim(),
reward_points: Number(rewardPointsInput?.value || 0),
reward_xp: Number(rewardXpInput?.value || 0),
reward_season_coin: Number(rewardCoinInput?.value || 0),
reward_payload: rewardPayload,
};
const data = await postJson(`/api/admin/seasons/${encodeURIComponent(selectedSeasonId)}/pass-rewards`, payload);
if (!data.ok) {
setStatus(data.error || '保存通行证奖励失败');
return;
}
setStatus('通行证奖励已保存。');
await loadSelectedSeason();
};
if (seasonSystemSaveBtn) seasonSystemSaveBtn.addEventListener('click', () => saveSystemConfig().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonAutoGenerateBtn) seasonAutoGenerateBtn.addEventListener('click', () => autoGenerateSeason().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonCreateBtn) seasonCreateBtn.addEventListener('click', () => createSeason().catch((e) => setStatus(`网络错误: ${e}`)));
if (announceBtn) announceBtn.addEventListener('click', () => publishAnnouncement().catch((e) => setStatus(`网络错误: ${e}`)));
if (taskSaveBtn) taskSaveBtn.addEventListener('click', () => saveTaskTemplate().catch((e) => setStatus(`网络错误: ${e}`)));
if (poolSaveBtn) poolSaveBtn.addEventListener('click', () => saveCardPool().catch((e) => setStatus(`网络错误: ${e}`)));
if (rewardSaveBtn) rewardSaveBtn.addEventListener('click', () => savePassReward().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonRefreshBtn) seasonRefreshBtn.addEventListener('click', () => loadSelectedSeason().catch((e) => setStatus(`网络错误: ${e}`)));
if (aiDailySaveBtn) aiDailySaveBtn.addEventListener('click', () => saveAiDailyConfig().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonInfoSaveBtn) seasonInfoSaveBtn.addEventListener('click', () => saveSeasonInfo().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonInfoRestBtn) seasonInfoRestBtn.addEventListener('click', () => enterRestSeason().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonInfoResumeBtn) seasonInfoResumeBtn.addEventListener('click', () => resumeSeason().catch((e) => setStatus(`网络错误: ${e}`)));
if (seasonInfoEndBtn) seasonInfoEndBtn.addEventListener('click', () => endSeason().catch((e) => setStatus(`网络错误: ${e}`)));
Promise.all([loadSystemConfig(), loadSeasons()])
.then(() => loadSelectedSeason())
.catch((error) => setStatus(`网络错误: ${error}`));
})();
+127
View File
@@ -0,0 +1,127 @@
(function () {
const body = document.body;
const seasonId = String(body.dataset.seasonId || '').trim();
const titleEl = document.getElementById('season-cards-title');
const subEl = document.getElementById('season-cards-sub');
const gridEl = document.getElementById('season-cards-grid');
const statusEl = document.getElementById('status');
const setStatus = (text) => {
if (statusEl) statusEl.textContent = text || '';
};
const getJson = async (url) => {
const res = await fetch(url);
return res.json();
};
const postJson = async (url, payload = {}) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return res.json();
};
const safeCoverUrl = (url) => {
const value = String(url || '').trim();
if (!value) return '';
return value.replace(/["\\]/g, '\\$&');
};
const renderEmpty = (text) => {
if (!gridEl) return;
gridEl.textContent = '';
const div = document.createElement('div');
div.className = 'helper-text';
div.textContent = text;
gridEl.appendChild(div);
};
const createRoomFromCard = async (cardId) => {
const safeId = String(cardId || '').trim();
if (!safeId) return;
setStatus('正在创建房间...');
const data = await postJson('/api/rooms/create', { card_id: safeId });
if (!data.ok) {
setStatus(data.error || '创建失败');
return;
}
if (data.player_id) localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
if (data.player_name) localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
};
const renderCards = (cards) => {
if (!gridEl) return;
gridEl.textContent = '';
const list = Array.isArray(cards) ? cards : [];
if (!list.length) {
renderEmpty('本赛季暂无游戏卡(先生成赛季日卡或等待解锁)。');
return;
}
list.forEach((card) => {
const article = document.createElement('article');
article.className = 'game-card';
const cover = document.createElement('div');
cover.className = 'game-card-cover';
cover.style.backgroundImage = `url("${safeCoverUrl(card.cover_url)}")`;
const panel = document.createElement('div');
panel.className = 'game-card-body';
const h3 = document.createElement('h3');
h3.textContent = card.title || '未命名赛季游戏卡';
panel.appendChild(h3);
const sub = document.createElement('p');
sub.className = 'game-card-sub';
sub.textContent = card.subtitle || '赛季专属';
panel.appendChild(sub);
const summary = document.createElement('p');
summary.textContent = card.summary || '';
panel.appendChild(summary);
const actions = document.createElement('div');
actions.className = 'card-actions-row';
const startBtn = document.createElement('button');
startBtn.type = 'button';
startBtn.textContent = '用此卡开一局';
startBtn.addEventListener('click', () => {
createRoomFromCard(card.card_id).catch((error) => setStatus(`网络错误: ${error}`));
});
actions.appendChild(startBtn);
panel.appendChild(actions);
article.appendChild(cover);
article.appendChild(panel);
gridEl.appendChild(article);
});
};
const load = async () => {
if (!seasonId) {
renderEmpty('赛季参数缺失。');
return;
}
const data = await getJson(`/api/seasons/${encodeURIComponent(seasonId)}/cards`);
if (!data.ok) {
setStatus(data.error || '加载失败');
renderEmpty('加载失败');
return;
}
const season = data.season || {};
const head = `${season.name || '赛季'} · ${season.theme || '未命名主题'}`;
if (titleEl) titleEl.textContent = '赛季游戏卡';
if (subEl) subEl.textContent = head;
renderCards(data.cards || []);
};
load().catch((error) => setStatus(`网络错误: ${error}`));
})();
+97
View File
@@ -0,0 +1,97 @@
(function () {
const body = document.body;
const subtitleEl = document.getElementById('chronicle-subtitle');
const scrollEl = document.getElementById('chronicle-scroll');
const statusEl = document.getElementById('status');
const seasonId = String(body.dataset.seasonId || '').trim();
const setStatus = (text) => {
if (statusEl) statusEl.textContent = text || '';
};
const getJson = async (url) => {
const res = await fetch(url);
return res.json();
};
const renderEmpty = (text) => {
if (!scrollEl) return;
scrollEl.textContent = '';
const div = document.createElement('div');
div.className = 'helper-text';
div.textContent = text;
scrollEl.appendChild(div);
};
const renderChronicle = (payload) => {
if (!scrollEl) return;
scrollEl.textContent = '';
const season = payload.season || {};
const items = Array.isArray(payload.items) ? payload.items : [];
const title = `${season.name || '赛季'} · ${season.theme || '未命名主题'}`;
if (subtitleEl) subtitleEl.textContent = title;
if (!items.length) {
renderEmpty('暂无编年史内容,先在赛季中心解锁资料。');
return;
}
items.forEach((row) => {
const block = document.createElement('section');
block.className = 'card';
block.style.margin = '0.8rem 0';
const head = document.createElement('div');
head.textContent = `${row.day_key || '-'} · ${row.title || '赛季日卡'}${row.is_today ? '(今日)' : ''}`;
block.appendChild(head);
const guide = String(row.guide_text || '').trim();
if (guide) {
const p = document.createElement('p');
p.className = 'helper-text';
p.textContent = guide;
block.appendChild(p);
}
const intro = String(row.intro_text || '').trim();
if (intro) {
const p = document.createElement('p');
p.className = 'helper-text';
p.textContent = intro;
block.appendChild(p);
}
const intel = Array.isArray(row.intel) ? row.intel : [];
const ul = document.createElement('ul');
ul.className = 'compact-list';
if (!intel.length) {
const li = document.createElement('li');
li.textContent = '(当日尚未解锁资料)';
ul.appendChild(li);
} else {
intel.slice(0, 80).forEach((it) => {
const li = document.createElement('li');
li.textContent = `${it.intel_title || '资料'}:${it.intel_text || ''}`;
ul.appendChild(li);
});
}
block.appendChild(ul);
scrollEl.appendChild(block);
});
};
const load = async () => {
const url = seasonId
? `/api/seasons/${encodeURIComponent(seasonId)}/chronicle`
: '/api/seasons/active/chronicle';
const data = await getJson(url);
if (!data.ok) {
setStatus(data.error || '加载失败');
renderEmpty('加载失败');
return;
}
renderChronicle(data);
};
load().catch((error) => setStatus(`网络错误: ${error}`));
})();
+2
View File
@@ -21,6 +21,7 @@
<span class="user-badge {% if user.avatar_url %}has-avatar{% endif %}" style="--avatar-color: {{ user.avatar_color }}; --avatar-url: url('{{ user.avatar_url }}')">
<i></i>{{ user.username }}(管理员)
</span>
<a class="link-btn" href="{{ url_for('season_admin_page') }}">赛季后台</a>
<a class="link-btn" href="{{ url_for('index') }}">返回大厅</a>
</div>
</header>
@@ -37,6 +38,7 @@
<a href="{{ url_for('admin_section_page', section='shop_orders') }}" class="{% if admin_section == 'shop_orders' %}active{% endif %}">订单管理</a>
<a href="{{ url_for('admin_section_page', section='points') }}" class="{% if admin_section == 'points' %}active{% endif %}">积分管理</a>
<a href="{{ url_for('admin_section_page', section='finance') }}" class="{% if admin_section == 'finance' %}active{% endif %}">财务统计</a>
<a href="{{ url_for('season_admin_page') }}">赛季后台</a>
<a href="{{ url_for('admin_section_page', section='oauth') }}" class="{% if admin_section == 'oauth' %}active{% endif %}">OAuth 配置</a>
</aside>
+1
View File
@@ -24,6 +24,7 @@
<a class="link-btn" href="{{ url_for('user_achievements_page', username=user.username) }}">我的成就</a>
<a class="link-btn" href="{{ url_for('wallet_page') }}">积分中心</a>
<a class="link-btn" href="{{ url_for('store_page') }}">积分商城</a>
<a class="link-btn" href="{{ url_for('season_page') }}">赛季中心</a>
<a class="link-btn" href="{{ url_for('rooms_page') }}">联机大厅</a>
<a class="link-btn" href="{{ url_for('leaderboard_page') }}">玩家排行</a>
<a class="link-btn" href="{{ url_for('plaza_page') }}">游戏卡广场</a>
+1
View File
@@ -21,6 +21,7 @@
<a class="link-btn" href="{{ url_for('user_achievements_page', username=user.username) }}">我的成就</a>
<a class="link-btn" href="{{ url_for('wallet_page') }}">积分中心</a>
<a class="link-btn" href="{{ url_for('store_page') }}">积分商城</a>
<a class="link-btn" href="{{ url_for('season_page') }}">赛季中心</a>
{% endif %}
<a class="link-btn" href="{{ url_for('leaderboard_page') }}">玩家排行</a>
<a class="link-btn" href="{{ url_for('rooms_page') }}">联机大厅</a>
+3
View File
@@ -21,6 +21,7 @@
<a class="link-btn" href="{{ url_for('user_achievements_page', username=user.username) }}">我的成就</a>
<a class="link-btn" href="{{ url_for('wallet_page') }}">积分中心</a>
<a class="link-btn" href="{{ url_for('store_page') }}">积分商城</a>
<a class="link-btn" href="{{ url_for('season_page') }}">赛季中心</a>
{% endif %}
<a class="link-btn" id="summary-back-room" href="{{ url_for('room_page', room_id=room_id) }}">返回房间</a>
<a class="link-btn" href="{{ url_for('rooms_page') }}">联机大厅</a>
@@ -34,6 +35,8 @@
<div id="summary-overview" class="status-grid"></div>
<h2 style="margin-top: 1rem;">MVP</h2>
<div id="summary-mvp" class="profile-hero"></div>
<h2 style="margin-top: 1rem;">赛季进度</h2>
<div id="summary-season-progress" class="status-grid"></div>
</section>
<section class="card">
+123
View File
@@ -0,0 +1,123 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>赛季中心 - Echo Tavern</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
</head>
<body class="season-page"
data-user='{{ user | tojson | safe if user else "null" }}'
data-theme="{{ user.theme if user else 'dopamine' }}">
<header class="lobby-topbar">
<div class="brand-block">
<h1>赛季中心</h1>
<p id="season-subtitle">加载赛季信息...</p>
</div>
<div class="top-actions">
<a class="link-btn" href="{{ url_for('index') }}">返回大厅</a>
<a class="link-btn" href="{{ url_for('rooms_page') }}">联机大厅</a>
<a class="link-btn" href="{{ url_for('wallet_page') }}">积分中心</a>
<a class="link-btn" href="{{ url_for('store_page') }}">积分商城</a>
</div>
</header>
<main class="plaza-grid season-grid">
<section class="card season-hero">
<div class="season-hero-cover" id="season-hero-cover"></div>
<div class="season-hero-body">
<div class="season-hero-head">
<div>
<h2 id="season-hero-title">赛季加载中...</h2>
<p class="helper-text" id="season-hero-sub">加载赛季信息...</p>
</div>
<div class="season-hero-cta">
<button id="season-pass-unlock-btn" type="button" class="secondary-btn">解锁高级通行证</button>
<button id="season-claim-all-tasks-btn" type="button">一键领取任务</button>
</div>
</div>
<div id="season-banner" class="status-grid"></div>
<div id="season-profile" class="status-grid" style="margin-top: 0.6rem;"></div>
</div>
</section>
<section class="card">
<h2>赛季切换</h2>
<p class="helper-text">选择任意赛季后,可进入该赛季的专属游戏卡列表与编年史(与玩家/全局卡不互通)。</p>
<div class="admin-inline-actions">
<select id="season-switch" style="max-width: 100%;"></select>
<a class="link-btn" id="season-switch-cards" href="#">打开赛季游戏卡</a>
<a class="link-btn" id="season-switch-chronicle" href="#">打开赛季编年史</a>
</div>
</section>
<section class="card">
<h2>📖 今日故事</h2>
<div id="season-daily-story" class="season-daily-card"></div>
<div class="admin-inline-actions" style="justify-content: space-between;">
<div id="season-daily-story-hint" class="helper-text"></div>
<a class="link-btn" href="{{ url_for('season_chronicle_page') }}">进入编年史 →</a>
</div>
</section>
<section class="card">
<h2>赛季公告</h2>
<ul id="season-announcements" class="compact-list"></ul>
</section>
<section class="card">
<h2>每日赛季日卡</h2>
<p class="helper-text" id="season-daily-card-hint">每天生成 1 张赛季专属的完整游戏卡。通关后可在“我的资料”解锁当日资料,并结算赛季积分/经验。</p>
<div class="admin-inline-actions" style="margin-bottom: 0.4rem;">
<a class="link-btn" href="{{ url_for('season_chronicle_page') }}">查看赛季编年史</a>
</div>
<div id="season-daily-card" class="season-daily-card"></div>
<div class="admin-inline-actions">
<button id="season-intel-unlock-btn" type="button" class="ghost-btn">解锁一条资料</button>
</div>
<h2 style="margin-top: 0.9rem;">我的资料</h2>
<ul id="season-intel" class="compact-list"></ul>
</section>
<section class="card" id="season-chronicle-pin" style="display:none; grid-column: 1 / -1;">
<h2>休整期置顶:赛季编年史</h2>
<p class="helper-text">休整期将赛季引导语 + 每日资料汇总为可滚动时间线,便于复盘世界观。</p>
<div class="admin-inline-actions" style="margin-top: 0.6rem;">
<a class="link-btn" href="{{ url_for('season_chronicle_page') }}">进入赛季编年史</a>
</div>
</section>
<section class="card">
<h2>活动任务卡片</h2>
<p class="helper-text" id="season-card-refresh-hint"></p>
<ul id="season-cards" class="compact-list"></ul>
</section>
<section class="card">
<h2>任务总览</h2>
<div class="admin-inline-actions">
<button class="ghost-btn" id="season-task-tab-all" type="button">全部</button>
<button class="ghost-btn" id="season-task-tab-daily" type="button">每日</button>
<button class="ghost-btn" id="season-task-tab-weekly" type="button">每周</button>
<button class="ghost-btn" id="season-task-tab-season" type="button">赛季</button>
</div>
<ul id="season-tasks" class="compact-list"></ul>
</section>
<section class="card">
<h2>通行证奖励</h2>
<ul id="season-rewards" class="compact-list"></ul>
</section>
<section class="card">
<h2>装扮展柜</h2>
<ul id="season-decorations" class="compact-list"></ul>
</section>
</main>
<section id="status" class="status-banner"></section>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/invite_notify.js') }}"></script>
<script src="{{ url_for('static', filename='js/season.js') }}"></script>
</body>
</html>
+143
View File
@@ -0,0 +1,143 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>赛季后台 - Echo Tavern</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
</head>
<body class="season-admin-page"
data-user='{{ user | tojson | safe if user else "null" }}'
data-theme="{{ user.theme if user else 'dopamine' }}">
<header class="lobby-topbar">
<div class="brand-block">
<h1>赛季后台</h1>
<p>创建赛季、发布公告、配置任务池/卡池/奖励</p>
</div>
<div class="top-actions">
<a class="link-btn" href="{{ url_for('admin_page') }}">返回管理后台</a>
<a class="link-btn" href="{{ url_for('season_page') }}">进入赛季中心</a>
<a class="link-btn" href="{{ url_for('index') }}">返回大厅</a>
</div>
</header>
<main class="plaza-grid season-admin-grid">
<section class="card">
<h2>赛季系统配置</h2>
<div id="season-system-config" class="status-grid"></div>
<div class="admin-inline-actions">
<button id="season-system-save-btn" type="button">保存赛季系统配置</button>
</div>
<h2 style="margin-top: 1rem;">AI 自动生成赛季</h2>
<div class="admin-inline-actions">
<input id="season-auto-theme" maxlength="40" placeholder="主题关键词,例如:赛博迷城" />
<button id="season-auto-generate-btn" type="button" class="secondary-btn">自动生成并落库</button>
</div>
<h2 style="margin-top: 1rem;">创建赛季</h2>
<div id="season-create-form" class="editor-form-grid">
<input id="season-create-name" maxlength="80" placeholder="赛季名称" />
<input id="season-create-theme" maxlength="80" placeholder="赛季主题" />
<input id="season-create-start" placeholder="开始时间 ISO,如 2026-03-10T00:00:00+00:00" />
<input id="season-create-end" placeholder="结束时间 ISO,如 2026-04-09T00:00:00+00:00" />
<input id="season-create-pass-price" type="number" min="0" max="999999" value="680" />
<textarea id="season-create-description" maxlength="360" placeholder="赛季描述"></textarea>
<button id="season-create-btn" type="button">创建赛季</button>
</div>
</section>
<section class="card">
<h2>赛季列表</h2>
<ul id="season-admin-list" class="compact-list"></ul>
</section>
<section class="card">
<h2>当前赛季配置</h2>
<p class="helper-text" id="season-admin-current-hint">请选择一个赛季后再配置。</p>
<div class="admin-inline-actions">
<button id="season-admin-refresh-btn" type="button" class="ghost-btn">刷新当前赛季数据</button>
</div>
<h3 style="margin-top: 0.6rem;">赛季信息</h3>
<div id="season-info-form" class="editor-form-grid">
<input id="season-info-name" maxlength="80" placeholder="赛季名称" />
<input id="season-info-theme" maxlength="80" placeholder="赛季主题" />
<input id="season-info-start" placeholder="开始时间 ISO" />
<input id="season-info-end" placeholder="结束时间 ISO" />
<input id="season-info-rest-end" placeholder="休整期截止 ISO(可选)" />
<input id="season-info-rest-days" type="number" min="0" max="30" placeholder="休整期天数(自动推算)" />
<input id="season-info-pass-price" type="number" min="0" max="999999" />
<input id="season-info-cover" maxlength="720" placeholder="封面 URL" />
<input id="season-info-accent" maxlength="16" placeholder="主题色 #RRGGBB" />
<textarea id="season-info-description" maxlength="360" placeholder="赛季描述"></textarea>
<textarea id="season-info-seed" maxlength="240" placeholder="赛季故事种子"></textarea>
<textarea id="season-info-outline" maxlength="1200" placeholder="赛季故事大纲"></textarea>
<div class="admin-inline-actions">
<button id="season-info-save-btn" type="button">保存赛季信息</button>
<button id="season-info-rest-btn" type="button" class="ghost-btn">进入休整期</button>
<button id="season-info-resume-btn" type="button" class="ghost-btn">重新开启赛季</button>
<button id="season-info-end-btn" type="button" class="ghost-btn">手动结束赛季</button>
</div>
</div>
<h3 style="margin-top: 0.6rem;">公告</h3>
<div class="editor-form-grid">
<input id="season-announce-title" maxlength="120" placeholder="公告标题" />
<textarea id="season-announce-content" maxlength="2400" placeholder="公告正文"></textarea>
<button id="season-announce-btn" type="button">发布公告</button>
</div>
<h3 style="margin-top: 0.8rem;">任务模板</h3>
<div class="editor-form-grid">
<input id="season-task-title" maxlength="84" placeholder="任务标题" />
<input id="season-task-type" maxlength="20" placeholder="任务类型 daily/weekly/season/achievement" />
<input id="season-task-event" maxlength="48" placeholder="事件键 event_key,如 complete_game" />
<input id="season-task-target" type="number" min="1" value="1" />
<input id="season-task-reward-points" type="number" min="0" value="0" />
<input id="season-task-reward-xp" type="number" min="0" value="0" />
<textarea id="season-task-description" maxlength="240" placeholder="任务描述"></textarea>
<button id="season-task-save-btn" type="button">新增任务模板</button>
</div>
<ul id="season-task-list" class="compact-list"></ul>
<h3 style="margin-top: 0.8rem;">活动卡池</h3>
<div class="editor-form-grid">
<input id="season-pool-title" maxlength="84" placeholder="卡池标题提示" />
<input id="season-pool-type" maxlength="20" placeholder="卡片类型 daily/rotating/surprise" />
<input id="season-pool-event" maxlength="48" placeholder="事件键 event_key" />
<input id="season-pool-target" type="number" min="1" value="1" />
<input id="season-pool-rarity" maxlength="20" placeholder="稀有度 normal/rare/epic/legendary" />
<input id="season-pool-weight" type="number" min="1" value="100" />
<input id="season-pool-reward-points" type="number" min="0" value="0" />
<input id="season-pool-reward-xp" type="number" min="0" value="0" />
<textarea id="season-pool-hook" maxlength="360" placeholder="故事钩子 story_hook"></textarea>
<textarea id="season-pool-objective" maxlength="240" placeholder="目标模板 objective_template"></textarea>
<textarea id="season-pool-hint" maxlength="240" placeholder="提示模板 hint_template"></textarea>
<button id="season-pool-save-btn" type="button">新增卡池模板</button>
</div>
<ul id="season-pool-list" class="compact-list"></ul>
<h3 style="margin-top: 0.8rem;">通行证奖励</h3>
<div class="editor-form-grid">
<input id="season-reward-level" type="number" min="1" value="1" />
<input id="season-reward-track" maxlength="20" placeholder="赛道 free/premium" />
<input id="season-reward-title" maxlength="84" placeholder="奖励标题" />
<input id="season-reward-points" type="number" min="0" value="0" />
<input id="season-reward-xp" type="number" min="0" value="0" />
<input id="season-reward-coin" type="number" min="0" value="0" />
<textarea id="season-reward-payload" placeholder='奖励 JSON,如 {"titles":["x"],"avatar_frames":["y"]}'></textarea>
<button id="season-reward-save-btn" type="button">新增通行证奖励</button>
</div>
<ul id="season-reward-list" class="compact-list"></ul>
<h3 style="margin-top: 0.8rem;">AI 赛季日卡配置</h3>
<div class="editor-form-grid">
<label><input id="season-ai-daily-enabled" type="checkbox" /> 启用AI生成日卡</label>
<input id="season-ai-daily-tone" maxlength="24" placeholder="风格,如:悬疑冒险" />
<input id="season-ai-daily-difficulty" maxlength="12" placeholder="难度,如:中等" />
<textarea id="season-ai-daily-extra" maxlength="400" placeholder="额外提示(可选)"></textarea>
<button id="season-ai-daily-save-btn" type="button">保存AI日卡配置</button>
</div>
</section>
</main>
<section id="status" class="status-banner"></section>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/invite_notify.js') }}"></script>
<script src="{{ url_for('static', filename='js/season_admin.js') }}"></script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>赛季游戏卡 - Echo Tavern</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
</head>
<body class="plaza-page"
data-user='{{ user | tojson | safe if user else "null" }}'
data-theme="{{ user.theme if user else 'dopamine' }}"
data-season-id="{{ season_id }}">
<header class="lobby-topbar">
<div class="brand-block">
<h1 id="season-cards-title">赛季游戏卡</h1>
<p id="season-cards-sub">加载赛季信息...</p>
</div>
<div class="top-actions">
<a class="link-btn" href="{{ url_for('season_page') }}">返回赛季中心</a>
<a class="link-btn" href="{{ url_for('plaza_page') }}">返回游戏卡广场</a>
<a class="link-btn" href="{{ url_for('index') }}">返回大厅</a>
</div>
</header>
<main class="plaza-grid">
<section class="card plaza-cards">
<h2>本赛季专属游戏卡</h2>
<p class="helper-text">仅显示该赛季生成/归档的游戏卡,与玩家/全局游戏卡不互通。</p>
<div id="season-cards-grid" class="cards-grid"></div>
</section>
<section class="card plaza-create">
<h2>提示</h2>
<p class="helper-text">赛季日卡是完整的游戏卡;每天解锁的资料是该游戏卡的“结局/沉淀”。</p>
<p class="helper-text">在赛季中心推进并解锁资料后,编年史会自动汇总。</p>
<a class="link-btn" href="{{ url_for('season_chronicle_page') }}">查看赛季编年史</a>
</section>
</main>
<section id="status" class="status-banner"></section>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/season_cards.js') }}"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>赛季编年史 - Echo Tavern</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
</head>
<body class="season-page"
data-user='{{ user | tojson | safe if user else "null" }}'
data-theme="{{ user.theme if user else 'dopamine' }}"
data-season-id="{{ season_id if season_id else '' }}">
<header class="lobby-topbar">
<div class="brand-block">
<h1>赛季编年史</h1>
<p id="chronicle-subtitle">加载赛季编年史...</p>
</div>
<div class="top-actions">
<a class="link-btn" href="{{ url_for('season_page') }}">返回赛季中心</a>
<a class="link-btn" href="{{ url_for('index') }}">返回大厅</a>
</div>
</header>
<main class="plaza-grid season-grid">
<section class="card" style="grid-column: 1 / -1;">
<h2>可滚动赛季编年史</h2>
<p class="helper-text">引导语 + 每日卡资料将按时间线沉淀。休整期会在赛季中心置顶展示。</p>
<div id="chronicle-scroll" class="chronicle-scroll"></div>
</section>
</main>
<section id="status" class="status-banner"></section>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/season_chronicle.js') }}"></script>
</body>
</html>
+1
View File
@@ -21,6 +21,7 @@
<a class="link-btn" href="{{ url_for('user_achievements_page', username=user.username) }}">我的成就</a>
<a class="link-btn" href="{{ url_for('wallet_page') }}">积分中心</a>
<a class="link-btn" href="{{ url_for('store_page') }}">积分商城</a>
<a class="link-btn" href="{{ url_for('season_page') }}">赛季中心</a>
{% endif %}
<a class="link-btn" href="{{ url_for('rooms_page') }}">联机大厅</a>
<a class="link-btn" href="{{ url_for('leaderboard_page') }}">返回排行</a>