season ai daily card and admin controls
This commit is contained in:
1 parent
f399cfd651
commit
6217b65aa9
19 files changed
+7031
-11
No files matched your search
@@ -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"
|
||||
]
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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) {
|
||||
|
||||
@@ -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}`));
|
||||
|
||||
@@ -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}`));
|
||||
})();
|
||||
@@ -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}`));
|
||||
})();
|
||||
@@ -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}`));
|
||||
})();
|
||||
|
||||
@@ -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}`));
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user