Skip to content

Commit cb27c3e

Browse files
committed
feat: levelScore
1 parent cfd7594 commit cb27c3e

File tree

9 files changed

+217
-35
lines changed

9 files changed

+217
-35
lines changed

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@cloudflare/workers-types": "^4.20241112.0",
2323
"@iconify-json/twemoji": "^1.2.0",
2424
"@types/node": "^22.9.1",
25+
"sass": "^1.83.4",
2526
"typescript": "^5.6.3",
2627
"unocss": "^0.64.1",
2728
"vite": "^5.2.10",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { LEVELS, Song } from '@clansty/maibot-types';
2+
import { component$ } from '@builder.io/qwik';
3+
import { routeLoader$ } from '@builder.io/qwik-city';
4+
import getUserProfile from '~/utils/getUserProfile';
5+
import LevelScores from '../../../components/LevelScores';
6+
7+
export const useData = routeLoader$(async ({ platform, params, error }) => {
8+
const level = decodeURIComponent(params.level) as typeof LEVELS[number];
9+
if (!LEVELS.includes(level)) {
10+
error(404, 'nya?');
11+
}
12+
13+
const profile = await getUserProfile(params.tguid, params.profile, platform.env);
14+
const requiredSongList = Song.getByCondition(it => it.sheets.some(chart => chart.level === level)) as Song[];
15+
const userMusic = await profile.getUserMusic(requiredSongList);
16+
const versionLogo = await profile.getVersionLogo();
17+
const version = await profile.getVersion();
18+
const userData = await profile.getNameplate();
19+
20+
return { requiredSongList: requiredSongList.map(it => it.dxId), userMusic, region: profile.region, level, versionLogo, version, userData };
21+
});
22+
23+
export default component$(() => {
24+
const data = useData();
25+
26+
return <LevelScores userMusic={data.value.userMusic} level={data.value.level} logo={data.value.versionLogo} version={data.value.version} user={data.value.userData} />;
27+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { LEVELS, Song } from '@clansty/maibot-types';
2+
import { routeLoader$ } from '@builder.io/qwik-city';
3+
import getAquaDxUser from '~/utils/getAquaDxUser';
4+
import { component$ } from '@builder.io/qwik';
5+
import LevelScores from '../../../components/LevelScores';
6+
7+
export const useData = routeLoader$(async ({ platform, params, error }) => {
8+
const level = decodeURIComponent(params.level) as typeof LEVELS[number];
9+
if (!LEVELS.includes(level)) {
10+
error(404, 'nya?');
11+
}
12+
13+
const profile = await getAquaDxUser(params.username);
14+
const requiredSongList = Song.getByCondition(it => it.sheets.some(chart => chart.level === level)) as Song[];
15+
const userMusic = await profile.getUserMusic(requiredSongList);
16+
const versionLogo = await profile.getVersionLogo();
17+
const version = await profile.getVersion();
18+
const userData = await profile.getNameplate();
19+
20+
return { requiredSongList: requiredSongList.map(it => it.dxId), userMusic, region: profile.region, level, versionLogo, version, userData };
21+
});
22+
23+
export default component$(() => {
24+
const data = useData();
25+
26+
return <LevelScores userMusic={data.value.userMusic} level={data.value.level} logo={data.value.versionLogo} version={data.value.version} user={data.value.userData} />;
27+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.hideOnSmallScreen {
2+
@media (max-width: 1320px) {
3+
display: none;
4+
}
5+
}
6+
7+
.hideOnSmallScreen2 {
8+
@media (max-width: 1000px) {
9+
display: none;
10+
}
11+
}
12+
13+
.header {
14+
@media (max-width: 1000px) {
15+
flex-direction: column;
16+
gap: 40px;
17+
}
18+
}
19+
20+
.title {
21+
@media (max-width: 1060px) {
22+
font-size: 50px;
23+
}
24+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { LEVELS, Song, UserMusic, MaiVersion, Nameplate as NameplateData } from '@clansty/maibot-types';
2+
import _ from 'lodash';
3+
import styles from './LevelScores.module.css';
4+
import { component$ } from '@builder.io/qwik';
5+
import B50Song from '~/routes/b50/components/B50Song';
6+
import Nameplate from '~/components/Nameplate';
7+
8+
export default component$(({ userMusic, level, logo, version, user }: { userMusic: UserMusic[], level: typeof LEVELS[number], logo: string, version: MaiVersion, user: NameplateData }) => {
9+
let rows = userMusic.map(it => {
10+
const song = Song.fromId(it.musicId, version);
11+
return {
12+
music: it,
13+
song,
14+
chart: song?.getChart(it.level)
15+
};
16+
});
17+
rows = rows.filter(it => it.chart?.level === level);
18+
rows = _.sortBy(rows, it => -it.music.achievement);
19+
rows = _.take(rows, 100);
20+
const disp = _.chunk(rows, 20);
21+
22+
return <div>
23+
<div class={`${styles.header} flex items-center p-20px gap-50px`}>
24+
<div class="text-18px">
25+
<Nameplate user={user} />
26+
</div>
27+
<div class={`${styles.title} text-60px m-t--.1em text-shadow-[1px_1px_2px_#fff]`}>
28+
LV {level} 分数表
29+
</div>
30+
<div style={{ flexGrow: 1 }} class={styles.hideOnSmallScreen2} />
31+
<img src={logo} alt="" height={120} class={styles.hideOnSmallScreen} />
32+
</div>
33+
<div class="flex flex-col gap-50px p-x-20px">
34+
{disp.map(group => <div class="grid grid-cols-5 gap-x-5px">
35+
{group.map(it => <B50Song song={it.song!} score={it.music} entry={{
36+
level: it.music.level,
37+
achievement: it.music.achievement,
38+
musicId: it.music.musicId
39+
}} />)}
40+
</div>)}
41+
</div>
42+
</div>;
43+
});

packages/botcore/src/botBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import musicSearch from './modules/musicSearch';
1212
import admin from './modules/admin';
1313
import exportUserMusic from './modules/exportUserMusic';
1414
import musicSearchChu from './modules/musicSearchChu';
15+
import levelScores from './modules/levelScores';
1516

1617
interface BuilderEnvBase<T extends BotTypes> {
1718
bot: Bot<T>,
@@ -36,7 +37,7 @@ export const buildBot = <T extends BotTypes>(env: BuilderEnvBase<T>) => {
3637
)
3738
} satisfies BuilderEnv<T>;
3839

39-
for (const attachHandlers of [callbackQuery, help, bind, scoreQuery, plateProgress, levelProgress, /* levelConstTable, */ b50, exportUserMusic, musicSearchChu, musicSearch, admin]) {
40+
for (const attachHandlers of [callbackQuery, help, bind, scoreQuery, plateProgress, levelProgress, levelScores, /* levelConstTable, */ b50, exportUserMusic, musicSearchChu, musicSearch, admin]) {
4041
attachHandlers(passEnv);
4142
}
4243
};

packages/botcore/src/modules/levelProgress.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default <T extends BotTypes>({ bot, env, getContext, musicToFile }: Build
1616
};
1717

1818
for (const level of LEVELS) {
19-
bot.registerInlineQuery(RegExp(`^ ?\\/?${level} ?(进度)?(完成表)?$`), async (event) => {
19+
bot.registerInlineQuery(RegExp(`^ ?\\/?${level.replace('+', '\\+')} ?(进度)?(完成表)?$`), async (event) => {
2020
const ctx = getContext(event);
2121
const profile = await ctx.getCurrentProfile();
2222
if (!profile) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { LEVELS, Song } from '@clansty/maibot-types';
2+
import { BotTypes, MessageButtonUrl } from '@clansty/maibot-firm';
3+
import { BuilderEnv } from '../botBuilder';
4+
import UserContext from '../UserContext';
5+
6+
export default <T extends BotTypes>({ bot, env, getContext, musicToFile }: BuilderEnv<T>) => {
7+
const sendProgressImage = async (ctx: UserContext<T>, fromId: T['ChatId'], isPrivate: boolean, level: typeof LEVELS[number], isFromStart = false) => {
8+
const profile = await ctx.getCurrentProfile();
9+
const requiredSongList = Song.getByCondition(it => it.sheets.some(chart => chart.level === level));
10+
const userMusic = await profile.getUserMusic(requiredSongList);
11+
12+
return await ctx.genCacheSendImage([level, userMusic], await ctx.getWebUrl('levelScores', encodeURIComponent(level)),
13+
2000, `LV ${level} 分数表.png`, isPrivate ? level : undefined, isFromStart, [
14+
[]
15+
]);
16+
};
17+
18+
for (const level of LEVELS) {
19+
bot.registerInlineQuery(RegExp(`^ ?\\/?${level.replace('+', '\\+')} ?(分数|成绩)[图列]?表$`), async (event) => {
20+
const ctx = getContext(event);
21+
const profile = await ctx.getCurrentProfile();
22+
if (!profile) {
23+
await event.answer()
24+
.withStartButton('请绑定用户', 'bind')
25+
.isPersonal()
26+
.withCacheTime(10)
27+
.dispatch();
28+
return;
29+
}
30+
31+
const requiredSongList = Song.getByCondition(it => it.sheets.some(chart => chart.level === level));
32+
const userMusic = await profile.getUserMusic(requiredSongList);
33+
const cachedImage = await ctx.getCacheImage([level, userMusic]);
34+
const answer = event.answer()
35+
.isPersonal()
36+
.withCacheTime(10);
37+
if (cachedImage?.type === 'image')
38+
answer.addPhotoResult('pic', cachedImage.fileId);
39+
else
40+
answer.withStartButton(`生成 LV ${level} 分数表`, level);
41+
await answer.dispatch();
42+
return true;
43+
});
44+
45+
bot.registerCommand('start', async (event) => {
46+
if (event.params[0] !== level) return false;
47+
const ctx = getContext(event);
48+
await sendProgressImage(ctx, event.fromId, event.isPrivate, level, true);
49+
return true;
50+
});
51+
52+
bot.registerKeyword(RegExp(`^\/?${level.replace('+', '\\+')} ?(分数|成绩)[图列]?表$`), async (event) => {
53+
const ctx = getContext(event);
54+
await sendProgressImage(ctx, event.fromId, event.isPrivate, level);
55+
return true;
56+
});
57+
}
58+
}

0 commit comments

Comments
 (0)