├── .env.production ├── .eslintrc.cjs ├── .gitignore ├── .stylelintrc.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── assets │ ├── chunithm │ │ ├── character │ │ │ ├── copper.webp │ │ │ ├── gold.webp │ │ │ ├── holographic.webp │ │ │ ├── normal.webp │ │ │ ├── platina.webp │ │ │ ├── rainbow.webp │ │ │ └── silver.webp │ │ ├── class_emblem │ │ │ ├── base │ │ │ │ ├── 1.webp │ │ │ │ ├── 2.webp │ │ │ │ ├── 3.webp │ │ │ │ ├── 4.webp │ │ │ │ ├── 5.webp │ │ │ │ └── 6.webp │ │ │ └── medal │ │ │ │ ├── 1.webp │ │ │ │ ├── 2.webp │ │ │ │ ├── 3.webp │ │ │ │ ├── 4.webp │ │ │ │ ├── 5.webp │ │ │ │ └── 6.webp │ │ ├── music_icon │ │ │ ├── absolute.webp │ │ │ ├── absolutep.webp │ │ │ ├── absolutepp.webp │ │ │ ├── alljustice.webp │ │ │ ├── alljustice_s.webp │ │ │ ├── alljustice_xs.webp │ │ │ ├── alljusticecritical.webp │ │ │ ├── alljusticecritical_s.webp │ │ │ ├── alljusticecritical_xs.webp │ │ │ ├── blank_xs.webp │ │ │ ├── catastrophy.webp │ │ │ ├── clear.webp │ │ │ ├── clear_s.webp │ │ │ ├── failed.webp │ │ │ ├── fullchain.webp │ │ │ ├── fullchain2.webp │ │ │ ├── fullchain2_xs.webp │ │ │ ├── fullchain_blank.webp │ │ │ ├── fullchain_xs.webp │ │ │ ├── fullcombo.webp │ │ │ ├── fullcombo_blank.webp │ │ │ ├── fullcombo_s.webp │ │ │ ├── fullcombo_xs.webp │ │ │ └── hard.webp │ │ ├── music_rank │ │ │ ├── a.webp │ │ │ ├── a_s.webp │ │ │ ├── aa.webp │ │ │ ├── aa_s.webp │ │ │ ├── aaa.webp │ │ │ ├── aaa_s.webp │ │ │ ├── b.webp │ │ │ ├── b_s.webp │ │ │ ├── bb.webp │ │ │ ├── bb_s.webp │ │ │ ├── bbb.webp │ │ │ ├── bbb_s.webp │ │ │ ├── c.webp │ │ │ ├── c_s.webp │ │ │ ├── d.webp │ │ │ ├── d_s.webp │ │ │ ├── s.webp │ │ │ ├── s_s.webp │ │ │ ├── sp.webp │ │ │ ├── sp_s.webp │ │ │ ├── ss.webp │ │ │ ├── ss_s.webp │ │ │ ├── ssp.webp │ │ │ ├── ssp_s.png │ │ │ ├── ssp_s.webp │ │ │ ├── sss.webp │ │ │ ├── sss_s.webp │ │ │ ├── sssp.webp │ │ │ └── sssp_s.webp │ │ ├── reborn_star.webp │ │ └── version │ │ │ ├── 20000.webp │ │ │ ├── 20500.webp │ │ │ └── 22000.webp │ └── maimai │ │ ├── class_rank │ │ ├── 0.webp │ │ ├── 1.webp │ │ ├── 10.webp │ │ ├── 11.webp │ │ ├── 12.webp │ │ ├── 13.webp │ │ ├── 14.webp │ │ ├── 15.webp │ │ ├── 16.webp │ │ ├── 17.webp │ │ ├── 18.webp │ │ ├── 19.webp │ │ ├── 2.webp │ │ ├── 20.webp │ │ ├── 21.webp │ │ ├── 22.webp │ │ ├── 23.webp │ │ ├── 24.webp │ │ ├── 25.webp │ │ ├── 3.webp │ │ ├── 4.webp │ │ ├── 5.webp │ │ ├── 6.webp │ │ ├── 7.webp │ │ ├── 8.webp │ │ └── 9.webp │ │ ├── course_rank │ │ ├── 0.webp │ │ ├── 1.webp │ │ ├── 10.webp │ │ ├── 11.webp │ │ ├── 12.webp │ │ ├── 13.webp │ │ ├── 14.webp │ │ ├── 15.webp │ │ ├── 16.webp │ │ ├── 17.webp │ │ ├── 18.webp │ │ ├── 19.webp │ │ ├── 2.webp │ │ ├── 20.webp │ │ ├── 21.webp │ │ ├── 22.webp │ │ ├── 23.webp │ │ ├── 3.webp │ │ ├── 4.webp │ │ ├── 5.webp │ │ ├── 6.webp │ │ ├── 7.webp │ │ ├── 8.webp │ │ └── 9.webp │ │ ├── dx_score │ │ ├── 1.webp │ │ ├── 2.webp │ │ └── 3.webp │ │ ├── icon_star.webp │ │ ├── music_icon │ │ ├── ap.webp │ │ ├── app.webp │ │ ├── blank.webp │ │ ├── fc.webp │ │ ├── fcp.webp │ │ ├── fs.webp │ │ ├── fsd.webp │ │ ├── fsdp.webp │ │ ├── fsp.webp │ │ └── sync.webp │ │ ├── music_rank │ │ ├── a.webp │ │ ├── aa.webp │ │ ├── aaa.webp │ │ ├── b.webp │ │ ├── bb.webp │ │ ├── bbb.webp │ │ ├── c.webp │ │ ├── d.webp │ │ ├── s.webp │ │ ├── sp.webp │ │ ├── ss.webp │ │ ├── ssp.webp │ │ ├── sss.webp │ │ └── sssp.webp │ │ └── version │ │ ├── 20000.webp │ │ ├── 21000.webp │ │ ├── 22000.webp │ │ ├── 23000.webp │ │ └── 24000.webp ├── docs │ ├── about.md │ ├── api │ │ ├── chunithm.md │ │ └── maimai.md │ ├── changelog.md │ ├── faq.md │ ├── index.md │ ├── lxbot.md │ ├── privacy-policy.md │ ├── settings.md │ ├── sync.md │ └── terms-of-use.md ├── empty.webp ├── error.webp ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon.webp ├── logo.webp ├── product │ ├── akihabot.webp │ ├── findmaimaidx.webp │ ├── lxbot.webp │ ├── maiproberplus.webp │ └── youmubot.webp └── robots.txt ├── src ├── App.module.css ├── App.tsx ├── components │ ├── Alias │ │ ├── Alias.module.css │ │ ├── Alias.tsx │ │ ├── AliasButton.tsx │ │ ├── AliasList.tsx │ │ ├── AliasModal.tsx │ │ └── CreateAliasModal.tsx │ ├── AudioPlayer.tsx │ ├── Home │ │ ├── Product.module.css │ │ ├── Product.tsx │ │ └── ProductCarousel.tsx │ ├── LoginAlert.tsx │ ├── Marquee.tsx │ ├── Page │ │ ├── Page.tsx │ │ ├── PageHeader.module.css │ │ ├── PageHeader.tsx │ │ ├── PageRawContent.module.css │ │ ├── PageRawContent.tsx │ │ ├── PageTabs.module.css │ │ └── PageTabs.tsx │ ├── Plates │ │ ├── PlateCombobox.tsx │ │ └── RequiredSong.tsx │ ├── Profile │ │ ├── PlayerPanel │ │ │ ├── NotFoundAlert.tsx │ │ │ ├── PlayerModal.module.css │ │ │ ├── PlayerModal.tsx │ │ │ ├── PlayerPanel.module.css │ │ │ ├── PlayerPanel.tsx │ │ │ ├── Skeleton.tsx │ │ │ ├── chunithm │ │ │ │ ├── PlayerContent.tsx │ │ │ │ ├── PlayerModal.tsx │ │ │ │ └── RatingTrend.tsx │ │ │ └── maimai │ │ │ │ ├── PlayerContent.tsx │ │ │ │ ├── PlayerModal.tsx │ │ │ │ └── RatingTrend.tsx │ │ ├── PlayerSection.tsx │ │ ├── Profile.module.css │ │ ├── UserBindSection.tsx │ │ ├── UserSection.tsx │ │ └── UserTokenSection.tsx │ ├── RadioCardGroup.module.css │ ├── RadioCardGroup.tsx │ ├── RouterTransition.tsx │ ├── Scores │ │ ├── AdvancedFilter.tsx │ │ ├── ChartComment.module.css │ │ ├── ChartComment.tsx │ │ ├── RatingHistoryModal.tsx │ │ ├── RatingSegments.module.css │ │ ├── RatingSegments.tsx │ │ ├── ScoreHistory.tsx │ │ ├── ScoreList.tsx │ │ ├── ScoreModal.module.css │ │ ├── ScoreModal.tsx │ │ ├── ScoreModalMenu.module.css │ │ ├── ScoreModalMenu.tsx │ │ ├── ScoreRanking.tsx │ │ ├── Scores.module.css │ │ ├── chunithm │ │ │ ├── Chart.tsx │ │ │ ├── CreateScoreModal.tsx │ │ │ ├── Score.tsx │ │ │ ├── ScoreHistory.tsx │ │ │ ├── ScoreModal.tsx │ │ │ └── StatisticsSection.tsx │ │ └── maimai │ │ │ ├── Chart.tsx │ │ │ ├── CreateScoreModal.tsx │ │ │ ├── DeluxeRatingCalculator.module.css │ │ │ ├── DeluxeRatingCalculator.tsx │ │ │ ├── DeluxeScoreDetail.tsx │ │ │ ├── Score.tsx │ │ │ ├── ScoreHistory.tsx │ │ │ ├── ScoreModal.tsx │ │ │ ├── StatisticsSection.module.css │ │ │ └── StatisticsSection.tsx │ ├── Settings │ │ ├── SettingList.tsx │ │ ├── Settings.module.css │ │ └── SettingsModal.tsx │ ├── Shell │ │ ├── Footer │ │ │ ├── Footer.module.css │ │ │ └── Footer.tsx │ │ ├── Header │ │ │ ├── ColorSchemeToggle.tsx │ │ │ ├── GameTabs.module.css │ │ │ ├── GameTabs.tsx │ │ │ ├── Header.module.css │ │ │ ├── Header.tsx │ │ │ └── Logo.tsx │ │ ├── Navbar │ │ │ ├── Navbar.module.css │ │ │ ├── Navbar.tsx │ │ │ └── NavbarButton.tsx │ │ ├── Shell.module.css │ │ └── Shell.tsx │ ├── SongCombobox.tsx │ ├── SongDisabledIndicator.tsx │ ├── Songs │ │ ├── SongCard.module.css │ │ ├── SongCard.tsx │ │ ├── SongDifficulty.module.css │ │ ├── SongDifficultyList.tsx │ │ ├── chunithm │ │ │ └── SongDifficulty.tsx │ │ └── maimai │ │ │ └── SongDifficulty.tsx │ ├── Sync │ │ ├── CopyButtonWithIcon.tsx │ │ ├── CrawlTokenAlert.tsx │ │ ├── ScoresChangesModal.tsx │ │ └── WechatOAuthLink.tsx │ ├── Users │ │ ├── EditUserModal.tsx │ │ └── SendBatchEmailModal.tsx │ └── YearInReview │ │ ├── SongRankingSection.module.css │ │ ├── SongRankingSection.tsx │ │ ├── SongTimelineSection.module.css │ │ ├── SongTimelineSection.tsx │ │ ├── TagRadarSection.tsx │ │ ├── UploadRhythmSection.tsx │ │ ├── YearSummarySection.module.css │ │ └── YearSummarySection.tsx ├── data │ ├── products.json │ └── tags.json ├── hooks │ ├── swr │ │ ├── fetcher.ts │ │ ├── useAliasVotes.ts │ │ ├── useAliases.ts │ │ ├── useBests.ts │ │ ├── usePlayer.ts │ │ ├── useScores.ts │ │ ├── useSiteConfig.ts │ │ ├── useUser.ts │ │ ├── useUserConfig.ts │ │ ├── useUserToken.ts │ │ └── useYearInReview.ts │ ├── useAliasListStore.ts │ ├── useFixedGame.ts │ ├── useGame.ts │ ├── useShellViewportSize.ts │ ├── useSongListStore.ts │ └── useVersionChecker.tsx ├── index.css ├── main.tsx ├── pages │ ├── Form.module.css │ ├── Page.module.css │ ├── admin │ │ └── Panel │ │ │ ├── Panel.tsx │ │ │ ├── developers │ │ │ ├── AdminDevelopersSection.module.css │ │ │ └── AdminDevelopersSection.tsx │ │ │ ├── index.ts │ │ │ └── users │ │ │ └── AdminUsersSection.tsx │ ├── alias │ │ └── Vote.tsx │ ├── developer │ │ ├── Apply.tsx │ │ └── Info.tsx │ ├── public │ │ ├── Docs.module.css │ │ ├── Docs.tsx │ │ ├── ErrorPage.module.css │ │ ├── Fallback.tsx │ │ ├── ForgotPassword.tsx │ │ ├── Home.module.css │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ ├── NotFound.tsx │ │ ├── Register.tsx │ │ ├── ResetPassword.tsx │ │ ├── YearInReview.module.css │ │ └── YearInReview.tsx │ └── user │ │ ├── Plates.tsx │ │ ├── Profile.tsx │ │ ├── Scores │ │ ├── Scores.tsx │ │ ├── backup │ │ │ ├── ScoreBackupSection.module.css │ │ │ └── ScoreBackupSection.tsx │ │ ├── bests │ │ │ └── ScoreBestsSection.tsx │ │ ├── index.ts │ │ └── list │ │ │ └── ScoreListSection.tsx │ │ ├── Settings.tsx │ │ ├── Songs.tsx │ │ ├── Sync.module.css │ │ └── Sync.tsx ├── router.tsx ├── types │ ├── alias.d.ts │ ├── game.d.ts │ ├── player.d.ts │ ├── score.d.ts │ └── user.d.ts ├── utils │ ├── api │ │ ├── alias.ts │ │ ├── api.ts │ │ ├── comment.ts │ │ ├── developer.ts │ │ ├── misc.ts │ │ ├── player.ts │ │ ├── song │ │ │ ├── chunithm.ts │ │ │ ├── maimai.ts │ │ │ └── song.tsx │ │ └── user.ts │ ├── checkProxy.ts │ ├── color.ts │ ├── context.ts │ ├── modal.tsx │ ├── reCaptcha.ts │ ├── session.ts │ └── validator.ts ├── vite-env.d.ts └── wdyr.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_URL=https://maimai.lxns.net/api/v0 2 | VITE_ASSET_URL=https://assets.lxns.net 3 | VITE_RECAPTCHA_SITE_KEY=6LefxhIjAAAAADI0_XvRZmguDUharyWf3kGFhxqX -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/ 127 | .pnp.* 128 | 129 | # JetBrains IDEs 130 | .idea -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard-scss"], 3 | "rules": { 4 | "custom-property-pattern": null, 5 | "selector-class-pattern": null, 6 | "scss/no-duplicate-mixins": null, 7 | "declaration-empty-line-before": null, 8 | "declaration-block-no-redundant-longhand-properties": null, 9 | "alpha-value-notation": null, 10 | "custom-property-empty-line-before": null, 11 | "property-no-vendor-prefix": null, 12 | "color-function-notation": null, 13 | "length-zero-no-unit": null, 14 | "selector-not-notation": null, 15 | "no-descending-specificity": null, 16 | "comment-empty-line-before": null, 17 | "scss/at-mixin-pattern": null, 18 | "scss/at-rule-no-unknown": null, 19 | "value-keyword-case": null, 20 | "media-feature-range-notation": null, 21 | "selector-pseudo-class-no-unknown": [ 22 | true, 23 | { 24 | "ignorePseudoClasses": ["global"] 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 | maimai DX 查分器 4 |

5 | 6 | ![](https://img.shields.io/uptimerobot/ratio/m796711561-69ba3b113942c693fdde2db8) 7 | ![](https://img.shields.io/github/last-commit/Lxns-Network/maimai-prober-frontend?color=blue) 8 | ![](https://img.shields.io/codacy/grade/81bf94766124465aa512c883b2d6a9b5) 9 | ![](https://img.shields.io/discord/815106295614144512) 10 | 11 | 一个简单的舞萌 DX & 中二节奏国服查分器,玩家可以查看并管理自己的成绩,同时也有公共的 API 接口供开发者获取玩家的成绩数据。 12 | 13 | 该项目为查分器的前端部分,使用 React + TypeScript + Vite 编写。 14 | 15 | 了解查分器的使用方法,请参见:[官方文档](https://maimai.lxns.net/docs) 16 | 17 | ## 特性 18 | 19 | - 📱 支持「舞萌 DX」与「中二节奏」的游戏数据同步 20 | - 💻 不受设备限制,支持多平台同步、管理游戏数据 21 | - 🚀 快速的游戏数据同步体验 22 | - 🤩 易于使用的成绩、曲目与姓名框查询功能 23 | - 🌐 开放的 API 接口,支持第三方开发者对接 24 | - 🌈 支持暗色与自动主题 25 | 26 | ## 调试 27 | 28 | ```bash 29 | npm run dev 30 | ``` 31 | 32 | ## 构建 33 | 34 | ```bash 35 | npm run build 36 | ``` -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | maimai DX 查分器 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-mantine': { 4 | autoRem: true, 5 | }, 6 | 'postcss-simple-vars': { 7 | variables: { 8 | 'mantine-breakpoint-xs': '36em', 9 | 'mantine-breakpoint-sm': '48em', 10 | 'mantine-breakpoint-md': '62em', 11 | 'mantine-breakpoint-lg': '75em', 12 | 'mantine-breakpoint-xl': '88em', 13 | }, 14 | }, 15 | }, 16 | }; -------------------------------------------------------------------------------- /public/assets/chunithm/character/copper.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/copper.webp -------------------------------------------------------------------------------- /public/assets/chunithm/character/gold.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/gold.webp -------------------------------------------------------------------------------- /public/assets/chunithm/character/holographic.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/holographic.webp -------------------------------------------------------------------------------- /public/assets/chunithm/character/normal.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/normal.webp -------------------------------------------------------------------------------- /public/assets/chunithm/character/platina.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/platina.webp -------------------------------------------------------------------------------- /public/assets/chunithm/character/rainbow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/rainbow.webp -------------------------------------------------------------------------------- /public/assets/chunithm/character/silver.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/character/silver.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/base/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/1.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/base/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/2.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/base/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/3.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/base/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/4.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/base/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/5.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/base/6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/base/6.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/medal/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/1.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/medal/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/2.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/medal/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/3.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/medal/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/4.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/medal/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/5.webp -------------------------------------------------------------------------------- /public/assets/chunithm/class_emblem/medal/6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/class_emblem/medal/6.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/absolute.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/absolute.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/absolutep.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/absolutep.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/absolutepp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/absolutepp.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/alljustice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljustice.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/alljustice_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljustice_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/alljustice_xs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljustice_xs.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/alljusticecritical.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljusticecritical.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/alljusticecritical_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljusticecritical_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/alljusticecritical_xs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/alljusticecritical_xs.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/blank_xs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/blank_xs.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/catastrophy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/catastrophy.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/clear.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/clear.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/clear_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/clear_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/failed.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/failed.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullchain.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullchain2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain2.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullchain2_xs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain2_xs.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullchain_blank.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain_blank.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullchain_xs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullchain_xs.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullcombo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullcombo_blank.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo_blank.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullcombo_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/fullcombo_xs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/fullcombo_xs.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_icon/hard.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_icon/hard.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/a.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/a.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/a_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/a_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/aa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aa.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/aa_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aa_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/aaa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aaa.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/aaa_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/aaa_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/b.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/b.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/b_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/b_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/bb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bb.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/bb_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bb_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/bbb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bbb.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/bbb_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/bbb_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/c.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/c.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/c_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/c_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/d.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/d.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/d_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/d_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/s_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/s_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/sp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sp.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/sp_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sp_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/ss.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ss.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/ss_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ss_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/ssp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ssp.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/ssp_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ssp_s.png -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/ssp_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/ssp_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/sss.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sss.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/sss_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sss_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/sssp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sssp.webp -------------------------------------------------------------------------------- /public/assets/chunithm/music_rank/sssp_s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/music_rank/sssp_s.webp -------------------------------------------------------------------------------- /public/assets/chunithm/reborn_star.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/reborn_star.webp -------------------------------------------------------------------------------- /public/assets/chunithm/version/20000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/version/20000.webp -------------------------------------------------------------------------------- /public/assets/chunithm/version/20500.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/version/20500.webp -------------------------------------------------------------------------------- /public/assets/chunithm/version/22000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/chunithm/version/22000.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/0.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/0.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/1.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/10.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/11.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/12.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/13.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/14.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/14.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/15.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/15.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/16.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/16.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/17.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/17.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/18.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/18.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/19.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/19.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/2.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/20.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/20.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/21.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/21.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/22.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/22.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/23.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/23.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/24.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/24.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/25.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/25.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/3.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/4.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/5.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/6.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/7.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/8.webp -------------------------------------------------------------------------------- /public/assets/maimai/class_rank/9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/class_rank/9.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/0.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/0.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/1.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/10.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/11.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/12.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/13.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/14.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/14.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/15.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/15.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/16.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/16.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/17.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/17.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/18.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/18.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/19.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/19.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/2.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/20.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/20.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/21.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/21.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/22.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/22.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/23.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/23.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/3.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/4.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/5.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/6.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/7.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/8.webp -------------------------------------------------------------------------------- /public/assets/maimai/course_rank/9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/course_rank/9.webp -------------------------------------------------------------------------------- /public/assets/maimai/dx_score/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/dx_score/1.webp -------------------------------------------------------------------------------- /public/assets/maimai/dx_score/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/dx_score/2.webp -------------------------------------------------------------------------------- /public/assets/maimai/dx_score/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/dx_score/3.webp -------------------------------------------------------------------------------- /public/assets/maimai/icon_star.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/icon_star.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/ap.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/ap.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/app.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/app.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/blank.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/blank.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/fc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fc.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/fcp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fcp.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/fs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fs.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/fsd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fsd.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/fsdp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fsdp.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/fsp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/fsp.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_icon/sync.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_icon/sync.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/a.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/a.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/aa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/aa.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/aaa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/aaa.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/b.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/b.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/bb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/bb.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/bbb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/bbb.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/c.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/c.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/d.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/d.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/s.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/s.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/sp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/sp.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/ss.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/ss.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/ssp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/ssp.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/sss.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/sss.webp -------------------------------------------------------------------------------- /public/assets/maimai/music_rank/sssp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/music_rank/sssp.webp -------------------------------------------------------------------------------- /public/assets/maimai/version/20000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/20000.webp -------------------------------------------------------------------------------- /public/assets/maimai/version/21000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/21000.webp -------------------------------------------------------------------------------- /public/assets/maimai/version/22000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/22000.webp -------------------------------------------------------------------------------- /public/assets/maimai/version/23000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/23000.webp -------------------------------------------------------------------------------- /public/assets/maimai/version/24000.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/assets/maimai/version/24000.webp -------------------------------------------------------------------------------- /public/docs/about.md: -------------------------------------------------------------------------------- 1 | # 关于 2 | 3 | --- 4 | 5 | [落雪咖啡屋 maimai DX 查分器](/)是一个简单的舞萌 DX & 中二节奏国服查分器,玩家可以查看并管理自己的成绩,同时也有公共的 API 接口供开发者获取玩家的成绩数据。 6 | 7 | ## 联系我们 8 | 9 | QQ 群: 10 | - 用户交流群:**597413470** 11 | - 开发者交流群:**991669419** 12 | 13 | GitHub:[@Lxns-Network](https://github.com/Lxns-Network) 14 | 15 | 哔哩哔哩:[@软糖酱_Official](https://space.bilibili.com/1432317833) 16 | 17 | ## 开放源代码 18 | 19 | 查分器前端的源代码是开放的,您可以在 [GitHub](https://github.com/Lxns-Network/maimai-prober-frontend) 上查看并下载源代码。 20 | 21 | ## 赞助 22 | 23 | 查分器的维护及运营需要费用,您可以通过[爱发电](https://ifdian.net/a/lxnssama)支持查分器的可持续运行。 -------------------------------------------------------------------------------- /public/docs/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | --- 4 | 5 | 仍然没有找到答案?请[联系我们](/docs/about#联系我们)寻求帮助。 6 | 7 | ## 查分器相关问题 8 | 9 | ### 如果我忘记邮箱与密码,我该如何找回? 10 | 11 | 请[联系我们](/docs/about#联系我们)提供有关你账号的其它信息,我们会协助你找回账号。 12 | 13 | ### 为什么我微信里提示“上传成功”,但是查询成绩的时候发现并没有上传? 14 | 15 | ![](https://image.lxns.net/i/2024/02/21/183103.png) 16 | 17 | 因为微信中的“上传成功”指的是你的**玩家信息**(如玩家名、好友码、DX Rating 等)已经提交到查分器了,但是成绩爬取是在你提交后进行的。成绩爬取所需要的时间由当时 NET 的访问速度(通常晚上耗时较长),以及你的爬取设置所决定。 18 | 19 | 查询成绩爬取的进度与结果,你需要前往查分器,并按照查分器的步骤进行。针对如何使用查分器进行传分,请参考[同步游戏数据](/docs/sync)中的**在线同步**。 20 | 21 | ### 我没有注册查分器账号,能否使用第三方开发者提供的服务同步游戏数据? 22 | 23 | 可以的,但得注意第三方开发者是否支持使用好友码上传游戏数据。 24 | 25 | ### 为什么部分成绩不会显示在游玩历史记录中? 26 | 27 | 游玩历史记录只会显示包含游玩时间(`play_time`)的成绩,如果你的成绩没有游玩时间,那么该成绩将不会显示在游玩历史记录中。 28 | 29 | #### 舞萌 DX 30 | 31 | 使用查分器爬取舞萌 DX 的成绩时,你需要在[账号设置](/user/settings)中将**爬取谱面成绩的方式**设置为“自动检测”或“增量爬取”,以确保成绩能够爬取到游玩时间。 32 | 33 | #### 中二节奏 34 | 35 | 中二节奏默认会爬取成绩的游玩时间,但是 WORLD'S END 难度的成绩不包含游玩时间:[为什么 WORLD'S END 难度的成绩不会出现在 Recent 50 中?](#为什么-worlds-end-难度的成绩不会出现在-recent-50-中) 36 | 37 | ### 如果我的舞萌 DX 账号被警告了,我还能同步游戏数据吗? 38 | 39 | ![](https://image.lxns.net/i/2024/12/08/005325.png) 40 | 41 | 可以,我们会自动同意条款并跳过警告页面,以便你能够继续同步游戏数据。 42 | 43 | ::: warning 注意 44 | 不包含被封禁的情况,如果你的账号被封禁,查分器将无法同步你的游戏数据。 45 | ::: 46 | 47 | ### 为什么爬取中二节奏的成绩时,提示“无法获取到好友码,请稍后再试”? 48 | 49 | 请确保你在最新版本的中二节奏有游玩记录,并且能正常进入 CHUNITHM-NET,而不是提示错误。 50 | 51 | ### 为什么 WORLD'S END 难度的成绩不会出现在 Recent 50 中? 52 | 53 | 由于 NET 返回 WORLD'S END 难度的成绩时,没有包含谱面的难度信息,查分器无法准确判断该成绩是哪个难度的谱面。故在爬取时无法获取到游玩时间,所以无法将该成绩加入 Recent 50。 54 | 55 | ## LxBot 相关问题 56 | 57 | ### 为什么我的 Best 50 没有姓名框和背景? 58 | 59 | 请前往查分器的[账号设置](/user/settings),将“允许爬取姓名框”与“允许爬取背景”打开,并重新进行爬取操作。 60 | 61 | ### 为什么我查询 Best 50 会提示“成绩有误或同步不完全”? 62 | 63 | ![](https://image.lxns.net/i/2024/02/19/102647.png) 64 | 65 | 请前往查分器的[账号设置](/user/settings),将**爬取谱面成绩的方式**设置为“自动检测”,该选项会自动检测并使用最适合的爬取方式。 66 | 67 | 如果执行完上述操作后仍然无法解决问题,请将“允许覆盖最佳成绩”打开,并重新进行完整爬取操作。 68 | 69 | ::: warning 注意 70 | 手动创建错误成绩,或是通过其他方式导入了错误成绩,均会导致查分器计算出的 Best 50 与玩家 DX Rating 不一致。 71 | ::: 72 | -------------------------------------------------------------------------------- /public/docs/index.md: -------------------------------------------------------------------------------- 1 | # 帮助文档 2 | 3 | --- 4 | 5 | 欢迎来到[落雪咖啡屋 maimai DX 查分器](/)的帮助文档! 6 | 7 | 在这里,你可以作为用户查阅基本的查分器使用方法,也可以作为开发者了解如何对接查分器。 8 | 9 | ## 特性 10 | 11 | - 📱 支持「舞萌 DX」与「中二节奏」的游戏数据同步 12 | - 💻 不受设备限制,支持多平台同步、管理游戏数据 13 | - 🚀 快速的游戏数据同步体验 14 | - 🤩 易于使用的成绩、曲目与姓名框查询功能 15 | - 🌐 开放的 API 接口,支持第三方开发者对接 16 | - 🌈 支持暗色与自动主题 17 | 18 | ## 用户使用手册 19 | 20 | - [同步游戏数据](/docs/sync) 21 | - [账号设置](/docs/settings) 22 | - [使用 LxBot 查询成绩](/docs/lxbot) 23 | - [常见问题](/docs/faq) 24 | 25 | ## 开发者文档 26 | 27 | - [舞萌 DX API 文档](/docs/api/maimai) 28 | - [中二节奏 API 文档](/docs/api/chunithm) 29 | 30 | ## 其它 31 | 32 | - [关于](/docs/about) 33 | - [服务条款](/docs/terms-of-use) 34 | - [隐私政策](/docs/privacy-policy) 35 | - [更新日志](/docs/changelog) -------------------------------------------------------------------------------- /public/docs/lxbot.md: -------------------------------------------------------------------------------- 1 | # 使用 LxBot 查询成绩 2 | 3 | --- 4 | 5 | ## 通过 QQ 官方机器人查询(推荐) 6 | 7 | 使用 QQ 扫描下方二维码,将 LxBot 添加到你所管理的 QQ 群。 8 | 9 | ![](https://image.lxns.net/i/2024/02/19/102513.png) 10 | 11 | 由于官方机器人无法直接获取到你的 QQ 号,你需要使用指令绑定你的游戏数据: 12 | - 舞萌 DX:`/mai bind <好友码>` 13 | - 中二节奏:`/chu bind <好友码>` 14 | 15 | 绑定完成后使用指令查询你的游戏成绩: 16 | - 舞萌 DX Best 50:`/mai b50` 17 | - 中二节奏 Best 30:`/chu b30` -------------------------------------------------------------------------------- /public/docs/privacy-policy.md: -------------------------------------------------------------------------------- 1 | # 隐私政策 2 | 3 | > 最后更新日期:2024/1/30 4 | 5 | --- 6 | 7 | 若要使用[落雪咖啡屋 maimai DX 查分器](/)(以下简称本网站)的服务,请您务必仔细阅读并透彻理解本声明。 8 | 9 | 请注意,访问和使用本网站,您的使用行为将被视为对本声明全部内容的认可。如果您不同意本声明的任何内容,请您立即停止使用本网站。 10 | 11 | ## 定义 12 | 13 | 为了使本条款更加清晰,我们使用缩短的术语来表达某些概念。 14 | 15 | - **本网站**指[落雪咖啡屋 maimai DX 查分器](/)。 16 | - **我们**指本网站的运营者 [Lxns Network](https://lxns.net)。 17 | - **您**指使用本网站的用户。 18 | - **舞萌DX**、**中二节奏**是由 SEGA 发行的音乐游戏。 19 | - **游戏数据**指您在上述游戏中产生的数据,包括但不限于您的好友码、玩家信息、玩家收藏品、游玩成绩等。 20 | - **开发者**指申请、对接本网站查分器 API 的第三方开发者。 21 | 22 | ## 信息收集 23 | 24 | - 您使用本网站时,我们会收集您的 IP 地址、浏览器信息、操作系统信息、访问时间等信息。 25 | - 您使用本网站提供的舞萌DX、中二节奏 NET 游戏数据爬取服务时,我们会收集您的游戏数据。 26 | 27 | ## 信息使用 28 | 29 | - 本网站收集的信息仅用于提供本网站的服务,不会用于其他用途。 30 | - 本网站会将您的信息用于生成排行榜、统计数据,但不会包含您的任何个人信息。 31 | - 本网站会向开发者提供您的游戏数据,用于提供本网站的服务,但不会包含您在本网站注册的用户信息。 32 | - 本网站不会将您的信息用于任何商业用途。 33 | 34 | ## 条款修改 35 | 36 | - 本条款是您在本网站签署的使用协议的组成部分之一,请您仔细阅读。 37 | - 当条款发生变更时,我们会在本页面上发布通知。如果您在条款修改后继续使用本网站提供的服务,即表示您同意并接受修改后的条款。 38 | - Lxns Network 保留对本条款作出不定时修改的权利。 39 | - Lxns Network 对本页面内容拥有最终解释权。 40 | -------------------------------------------------------------------------------- /public/docs/settings.md: -------------------------------------------------------------------------------- 1 | # 账号设置 2 | 3 | --- 4 | 5 | ## 爬取数据 6 | 7 | ### 舞萌 DX 8 | 9 | #### 爬取谱面成绩的方式 10 | 11 | 设置每次爬取时使用的爬取方式,增量爬取依赖最近游玩记录,适合已经完整爬取后频繁爬取,更加稳定。 12 | 13 | > 当爬取方式为自动检测时,会先尝试使用增量爬取。爬取后若检测到玩家 DX Rating 与成绩不一致时,会使用完整爬取重新爬取一次。 14 | 15 | | 特性 | 自动检测 | 完整爬取 | 增量爬取 | 16 | |-|-|-|-| 17 | | 成绩爬取数量 | - | 全部 | 最多 50 条 | 18 | | 爬取游玩时间 | ✔️ | ❌ | ✔️ | 19 | | 爬取非最佳成绩 | ✔️ | ❌ | ✔️ | 20 | | 爬取速度 | - | 慢 | 相对较快 | 21 | | 成绩来源 | - | 最佳成绩 | 最近游玩记录 | 22 | 23 | ::: warning 注意 24 | 完整爬取爬取的是**历史最佳** Full Combo、Full Sync、DX 分数(下称参数),并非增量爬取爬取的当前成绩的真实参数。在部分情况下,自动检测可能会出现来自不同爬取方式的同一个成绩(但参数不一致)。 25 | ::: 26 | 27 | ### 通用选项 28 | 29 | #### 允许覆盖最佳成绩 30 | 31 | 允许后,每次“完整爬取”或通过第三方开发者写入时会检查成绩是否低于最佳成绩,低于则覆盖最佳成绩。 32 | 33 | ##### 适用场景 34 | 35 | - 你导入了其他玩家的成绩(如通过第三方开发者) 36 | - 你手动创建了错误成绩 37 | 38 | ::: warning 注意 39 | 目前不支持使用空成绩(即在 NET 中没有显示的成绩)覆盖最佳成绩。达成率 0% 的成绩可以覆盖最佳成绩。 40 | ::: -------------------------------------------------------------------------------- /public/docs/sync.md: -------------------------------------------------------------------------------- 1 | # 同步游戏数据 2 | 3 | --- 4 | 5 | ## 同步方法 6 | 7 | 针对不同需求,你可以选择不同的同步方法。 8 | 9 | | 特性 | 在线同步 | 离线同步 | 10 | |-|-|-| 11 | | 绑定查分器账号 | ✔️ | ❌ | 12 | | 查询同步结果 | ✔️ | ❌ | 13 | | 同步超时时间 | 10 分钟 | 10 分钟 | 14 | | OAuth 链接固定不变 | ❌ | ✔️ | 15 | | OAuth 链接有效期 | 15 分钟 | 长期有效 | 16 | 17 | ### 在线同步(查分器网页) 18 | 19 | 在线同步需要注册并登录查分器账号,在[同步游戏数据页](/user/sync)跟随页面内指引,正确配置 HTTP 代理并获取 OAuth 链接(链接中通常带 `token` 参数)。 20 | 21 | 适用于用户初次同步,将玩家数据绑定到查分器账号。 22 | 23 | ### 离线同步 24 | 25 | 离线同步不需要登录查分器账号,且 OAuth 链接通常固定不变。 26 | 27 | 如果先前已经将玩家数据绑定到查分器账号,则查分器账号中的设置也会在离线同步中生效。 28 | 29 | ## 同步步骤 30 | 31 | ::: info 提示 32 | 可观看同步教程视频:[BV1mz421z7pg](https://www.bilibili.com/video/BV1mz421z7pg) 33 | ::: 34 | 35 | ### 一、配置 HTTP 代理 36 | 37 | #### Windows 11 38 | 39 | 打开系统设置中的代理(或在浏览器地址栏输入 `ms-settings:network-proxy` 快捷打开): 40 | 41 | ![](https://image.lxns.net/i/2024/02/21/170708.png) 42 | 43 | 输入以下代理服务器地址,启用代理并保存: 44 | 45 | ``` 46 | proxy.maimai.lxns.net:8080 47 | ``` 48 | 49 | ![](https://image.lxns.net/i/2024/02/21/170828.png) 50 | 51 | #### Android 52 | 53 | ##### 通过接入点名称(APN)配置 54 | 55 | 在设置中搜索“接入点名称”,并新建接入点: 56 | 57 | ![](https://image.lxns.net/i/2024/10/25/224328.png) 58 | 59 | 编辑以下参数(未提及的参数无需修改): 60 | 61 | - 名称:随意 62 | - APN(根据 SIM 卡的运营商填写): 63 | - 中国移动:`cmnet` 64 | - 中国电信:`ctnet` 65 | - 中国联通:`3gnet` 66 | - 中国广电:`cbnet` 67 | > 仅列举常见运营商,其它运营商请自行搜索 APN 的值,或参考手机已有 APN 修改。 68 | - 代理服务器(代理):`proxy.maimai.lxns.net` 69 | - 端口:`8080` 70 | 71 | 保存并选中新建的 APN,同步完成后记得选回原有 APN,否则无法正常联网。 72 | 73 | ::: info 仍然无法同步? 74 | 选中新建的 APN 后尝试重启手机。 75 | ::: 76 | 77 | #### iOS 或 iPadOS 78 | 79 | ##### 通过无线局域网配置 80 | 81 | 在无线局域网设置中查看当前连接 WLAN 的详情: 82 | 83 | ![](https://image.lxns.net/i/2024/02/21/130347.png) 84 | 85 | 在二级页面找到 HTTP 代理: 86 | 87 | ![](https://image.lxns.net/i/2024/02/21/130749.png) 88 | 89 | 编辑代理配置为手动,输入以下代理服务器地址,并存储: 90 | 91 | ``` 92 | proxy.maimai.lxns.net:8080 93 | ``` 94 | 95 | ![](https://image.lxns.net/i/2024/02/21/131148.png) 96 | 97 | #### Clash 或 Shadowrocket 98 | 99 | 通过 URL 导入订阅: 100 | 101 | ``` 102 | https://maimai.lxns.net/api/v0/proxy-config/clash 103 | ``` 104 | 105 | 代理选中“maimai DX 查分器代理”,并启用系统代理。 106 | 107 | ### 二、使用微信打开 OAuth 链接 108 | 109 | #### 在线同步获取 110 | 111 | 正确配置代理后,在[同步游戏数据页](/user/sync)选择爬取的游戏并直接复制 OAuth 链接(带 `token`)。 112 | 113 | #### 离线同步获取 114 | 115 | 根据需要选择对应游戏的 OAuth 链接: 116 | 117 | - 舞萌 DX: 118 | ``` 119 | https://maimai.lxns.net/api/v0/maimai/wechat/auth 120 | ``` 121 | 122 | - 中二节奏: 123 | ``` 124 | https://maimai.lxns.net/api/v0/chunithm/wechat/auth 125 | ``` 126 | 127 | 在聊天中输入 OAuth 链接并发送至安全的聊天(如文件传输助手),直接点击链接打开网页。 128 | 129 | ::: warning 注意 130 | 不要将 OAuth 链接粘贴到搜索框打开,否则可能会导致 OAuth 链接失效。 131 | ::: 132 | 133 | #### 确认是否开始同步 134 | 135 | 若页面显示为如下内容,则代表你的玩家数据已经被上传至服务器进行处理: 136 | 137 | ![](https://image.lxns.net/i/2024/02/19/102330.png) 138 | 139 | 若页面提示网络出错,请检查代理配置是否正确: 140 | 141 | ![](https://image.lxns.net/i/2024/02/21/130131.png) 142 | 143 | ### 三、等待数据同步完成(仅在线同步) 144 | 145 | ::: info 提示 146 | 离线同步不支持查询数据同步状态。 147 | ::: 148 | 149 | 同步完成后,返回同步游戏数据页查询成绩爬取状态。届时,你的玩家数据与成绩将会被同步到查分器,并与你的查分器账号绑定。 150 | -------------------------------------------------------------------------------- /public/docs/terms-of-use.md: -------------------------------------------------------------------------------- 1 | # 服务条款 2 | 3 | > 最后更新日期:2024/1/30 4 | 5 | --- 6 | 7 | 若要使用[落雪咖啡屋 maimai DX 查分器](/)(以下简称本网站)的服务,请您务必仔细阅读并透彻理解本声明。 8 | 9 | 请注意,访问和使用本网站,您的使用行为将被视为对本声明全部内容的认可。如果您不同意本声明的任何内容,请您立即停止使用本网站。 10 | 11 | ## 定义 12 | 13 | 为了使本条款更加清晰,我们使用缩短的术语来表达某些概念。 14 | 15 | - **本网站**指[落雪咖啡屋 maimai DX 查分器](/)。 16 | - **我们**指本网站的运营者 [Lxns Network](https://lxns.net)。 17 | - **您**指使用本网站的用户。 18 | - **舞萌DX**、**中二节奏**是由 SEGA 发行的音乐游戏。 19 | - **游戏数据**指您在上述游戏中产生的数据,包括但不限于您的好友码、玩家信息、玩家收藏品、游玩成绩等。 20 | - **开发者**指申请、对接本网站查分器 API 的第三方开发者。 21 | 22 | ## 服务内容 23 | 24 | - 本网站提供的服务内容包括但不限于为用户提供舞萌DX、中二节奏 NET 游戏数据爬取、查询、管理等服务。 25 | - 本网站提供的服务仅供个人学习、研究或欣赏使用,您不得将本网站提供的服务用于商业用途。 26 | - 本网站提供的服务内容可能会随时变更,本网站不承诺对用户提供任何形式的通知。 27 | 28 | ## 用户行为 29 | 30 | - 您在使用本网站提供的服务时,应遵守中华人民共和国相关法律法规,不得利用本网站提供的服务从事任何违法违规行为。 31 | - 您不得利用本网站提供的服务干扰本网站的正常运行,包括但不限于利用本网站提供的服务对本网站的服务器进行攻击、利用本网站提供的服务对本网站的数据进行篡改等。 32 | - 开发者不得利用本网站提供的服务从事任何违法违规行为,包括但不限于利用本网站提供的服务通过任何方式获取用户的个人信息。 33 | - 开发者不得将本网站提供的服务,以及从本网站提供的服务获取的游戏数据用于任何商业用途。 34 | - 对于任何违反上述协议的行为,本网站有权采取措施,包括但不限于限制、禁止您使用本网站提供的服务。 35 | 36 | ## 免责声明 37 | 38 | 您已知晓并同意,您使用本网站提供的服务存在违反[《舞萌&中二 游戏条款》](http://wc.wahlap.net/sega/music/terms/index.html)的风险,本网站不对您使用本网站提供的服务所产生的后果承担任何责任。 39 | 40 | ## 条款修改 41 | 42 | - 本条款是您在本网站签署的使用协议的组成部分之一,请您仔细阅读。 43 | - 当条款发生变更时,我们会在本页面上发布通知。如果您在条款修改后继续使用本网站提供的服务,即表示您同意并接受修改后的条款。 44 | - Lxns Network 保留对本条款作出不定时修改的权利。 45 | - Lxns Network 对本页面内容拥有最终解释权。 46 | -------------------------------------------------------------------------------- /public/empty.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/empty.webp -------------------------------------------------------------------------------- /public/error.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/error.webp -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/favicon.webp -------------------------------------------------------------------------------- /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/logo.webp -------------------------------------------------------------------------------- /public/product/akihabot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/akihabot.webp -------------------------------------------------------------------------------- /public/product/findmaimaidx.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/findmaimaidx.webp -------------------------------------------------------------------------------- /public/product/lxbot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/lxbot.webp -------------------------------------------------------------------------------- /public/product/maiproberplus.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/maiproberplus.webp -------------------------------------------------------------------------------- /public/product/youmubot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/maimai-prober-frontend/587784f83c0a456f720f9533222832bb14e1a485/public/product/youmubot.webp -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /user/ 3 | Allow: / -------------------------------------------------------------------------------- /src/App.module.css: -------------------------------------------------------------------------------- 1 | .active { 2 | transition: transform 50ms ease-in-out; 3 | 4 | &:active { 5 | transform: perspective(100px) translate3d(0, 0, -5px); 6 | } 7 | } 8 | 9 | .photoViewerIcon { 10 | color: white; 11 | opacity: 0.75; 12 | transition: opacity 0.2s ease-in-out; 13 | } 14 | 15 | .photoViewerIcon:hover { 16 | opacity: 1; 17 | } -------------------------------------------------------------------------------- /src/components/Alias/Alias.module.css: -------------------------------------------------------------------------------- 1 | .section { 2 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 3 | } 4 | 5 | .alias { 6 | display: block; 7 | width: 100%; 8 | padding: var(--mantine-spacing-md); 9 | color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); 10 | 11 | &:hover { 12 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)); 13 | } 14 | } 15 | 16 | .voteButton { 17 | border: 0; 18 | } -------------------------------------------------------------------------------- /src/components/Alias/AliasButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Badge, Center, Flex, Group, Space, Text, ThemeIcon, Tooltip, UnstyledButton, UnstyledButtonProps 3 | } from "@mantine/core"; 4 | import { IconCheck, IconChevronRight, IconNorthStar } from "@tabler/icons-react"; 5 | import classes from "./Alias.module.css"; 6 | import { useEffect, useState } from "react"; 7 | import { MaimaiSongProps } from "@/utils/api/song/maimai.ts"; 8 | import { ChunithmSongProps } from "@/utils/api/song/chunithm.ts"; 9 | import useFixedGame from "@/hooks/useFixedGame.ts"; 10 | import useSongListStore from "@/hooks/useSongListStore.ts"; 11 | import { useShallow } from "zustand/react/shallow"; 12 | import { AliasProps } from "@/types/alias"; 13 | 14 | export function AliasButton({ alias, onClick, ...others }: { alias: AliasProps, onClick?: () => void } & UnstyledButtonProps) { 15 | const [song, setSong] = useState(); 16 | const [game] = useFixedGame(); 17 | const { songList } = useSongListStore( 18 | useShallow((state) => ({ songList: state[game] })), 19 | ) 20 | 21 | useEffect(() => { 22 | setSong(songList.find(alias.song.id)); 23 | }, [alias.song.id]); 24 | 25 | return ( 26 | 27 | 28 | 29 | {alias.song.name || "未知"} 30 | 31 | {game === "maimai" && alias.song.id >= 100000 && ( 32 | 33 | 宴 34 | 35 | )} 36 | {game === "chunithm" && alias.song.id >= 8000 && ( 37 | 38 | {song && (song as ChunithmSongProps).difficulties[0].kanji} 39 | 40 | )} 41 | {new Date(alias.upload_time).getTime() > new Date().getTime() - 86400000 && ( 42 |
43 | 44 | 45 | 46 | 47 | 48 | {alias.approved && ( 49 | 50 | )} 51 |
52 | )} 53 | {alias.approved && ( 54 |
55 | 56 | 57 | 58 | 59 | 60 |
61 | )} 62 |
63 | 64 | {alias.alias} 65 | 66 | 67 |
68 | ); 69 | } -------------------------------------------------------------------------------- /src/components/Alias/AliasList.tsx: -------------------------------------------------------------------------------- 1 | import { Box, SimpleGrid } from "@mantine/core"; 2 | import { Alias } from "./Alias.tsx"; 3 | import { useSetState } from "@mantine/hooks"; 4 | import {AliasModal, calculateNewAliasWeight} from "./AliasModal.tsx"; 5 | import { useEffect, useState } from "react"; 6 | import { useAutoAnimate } from "@formkit/auto-animate/react"; 7 | import { AliasProps } from "@/types/alias"; 8 | 9 | interface AliasListProps { 10 | aliases: AliasProps[]; 11 | onVote: () => void; 12 | onDelete: () => void; 13 | } 14 | 15 | export const AliasList = ({ aliases, onVote, onDelete }: AliasListProps) => { 16 | const [parent] = useAutoAnimate(); 17 | const [opened, setOpened] = useState(false); 18 | const [alias, setAlias] = useSetState({} as AliasProps); 19 | const [displayAliases, setDisplayAliases] = useState([]); 20 | 21 | useEffect(() => { 22 | setDisplayAliases(aliases); 23 | }, [aliases]); 24 | 25 | useEffect(() => { 26 | if (alias.alias_id) { 27 | const newDisplayAliases = displayAliases; 28 | const index = displayAliases.findIndex((a) => a.alias_id === alias.alias_id); 29 | if (index !== -1) { 30 | newDisplayAliases[index] = alias; 31 | } 32 | setDisplayAliases(newDisplayAliases); 33 | } 34 | }, [alias]); 35 | 36 | return ( 37 | 38 | setOpened(false)} /> 39 | 45 | {displayAliases.map((alias) => ( 46 | { 50 | if (!alias.uploader) setAlias({ 51 | uploader: undefined, 52 | }); 53 | setAlias(alias); 54 | setOpened(true); 55 | }} 56 | onVote={(vote) => { 57 | setAlias(calculateNewAliasWeight(alias, vote)); 58 | onVote(); 59 | }} 60 | onDelete={onDelete} 61 | /> 62 | ))} 63 | 64 | 65 | ); 66 | } -------------------------------------------------------------------------------- /src/components/Home/Product.module.css: -------------------------------------------------------------------------------- 1 | .product { 2 | height: 100%; 3 | padding: var(--mantine-spacing-xl); 4 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 5 | 6 | flex-direction: row; 7 | align-items: center; 8 | 9 | row-gap: var(--mantine-spacing-md); 10 | column-gap: var(--mantine-spacing-md); 11 | 12 | @mixin smaller-than $mantine-breakpoint-xs { 13 | flex-direction: column-reverse; 14 | align-items: flex-start; 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/Home/Product.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Badge, Button, Card, Group, Stack, Text, Title } from "@mantine/core"; 2 | import classes from "./Product.module.css"; 3 | 4 | interface ProductProps { 5 | title: string; 6 | tags: string[]; 7 | description: string; 8 | image: string; 9 | button: string; 10 | url: string; 11 | } 12 | 13 | export const Product = ({ title, tags, description, image, button, url }: ProductProps) => { 14 | return ( 15 | 16 | 17 |
18 | {title} 19 | 20 | {tags.map((tag) => ( 21 | {tag} 22 | ))} 23 | 24 | 25 | {description} 26 | 27 |
28 | 33 |
34 | 35 |
36 | ) 37 | } -------------------------------------------------------------------------------- /src/components/Home/ProductCarousel.tsx: -------------------------------------------------------------------------------- 1 | import { Carousel, Embla } from "@mantine/carousel"; 2 | import { useInViewport } from "@mantine/hooks"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import useShellViewportSize from "@/hooks/useShellViewportSize.ts"; 5 | import Autoplay from "embla-carousel-autoplay"; 6 | 7 | import { Product } from "@/components/Home/Product.tsx"; 8 | import products from '@/data/products.json'; 9 | 10 | const isTouchScreen = () => { 11 | return window.matchMedia('(pointer: coarse)').matches; 12 | }; 13 | 14 | export const ProductCarousel = () => { 15 | const autoplay = useRef(Autoplay({ 16 | delay: 2000, 17 | playOnInit: false, 18 | })); 19 | const { ref, inViewport } = useInViewport(); 20 | const { width } = useShellViewportSize(); 21 | const [containerWidth, setContainerWidth] = useState(width); 22 | const [embla, setEmbla] = useState(null); 23 | 24 | useEffect(() => { 25 | if (width < 576) { 26 | setContainerWidth(width - 32); 27 | } else { 28 | setContainerWidth(width - 64); 29 | } 30 | }, [width]); 31 | 32 | useEffect(() => { 33 | if (embla) { 34 | embla.reInit(); 35 | } 36 | }, [embla, containerWidth]); 37 | 38 | useEffect(() => { 39 | if (inViewport) { 40 | autoplay.current.play(); 41 | } else { 42 | autoplay.current.stop(); 43 | } 44 | }, [inViewport]); 45 | 46 | return ( 47 | 60 | {products.map((product, index) => ( 61 | 62 | 63 | 64 | ))} 65 | 66 | ); 67 | } -------------------------------------------------------------------------------- /src/components/LoginAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Button, Group, Text, Transition } from "@mantine/core"; 2 | import { IconAlertCircle } from "@tabler/icons-react"; 3 | import React, { useEffect, useState } from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | interface LoginAlertProps extends React.ComponentPropsWithoutRef { 7 | content: string; 8 | props?: never; 9 | } 10 | 11 | export const LoginAlert = ({ content, ...props }: LoginAlertProps) => { 12 | const [opened, setOpened] = useState(false); 13 | const isLoggedOut = !localStorage.getItem("token"); 14 | const navigate = useNavigate(); 15 | 16 | useEffect(() => { 17 | if (isLoggedOut) { 18 | setOpened(true); 19 | } 20 | }, [isLoggedOut]); 21 | 22 | return ( 23 | 24 | {(styles) => ( 25 | } 28 | title="登录提示" 29 | withCloseButton 30 | onClose={() => setOpened(false)} 31 | style={styles} 32 | {...props} 33 | > 34 | 35 | {content} 36 | 37 | 38 | 41 | 44 | 45 | 46 | )} 47 | 48 | ) 49 | } -------------------------------------------------------------------------------- /src/components/Marquee.tsx: -------------------------------------------------------------------------------- 1 | import { Children, ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; 2 | import { Group } from "@mantine/core"; 3 | import { useHoverDirty } from "react-use"; 4 | import { useAnimationFrame } from "motion/react" 5 | 6 | interface MarqueeProps { 7 | speed?: number; 8 | startDelay?: number; // 开始滚动的延迟 9 | intervalDelay?: number; // 滚动间隔 10 | pauseOnHover?: boolean; // 鼠标悬停暂停滚动 11 | children: ReactNode; 12 | props?: any; 13 | } 14 | 15 | export const Marquee = ({ speed = 0.5, startDelay = 1000, intervalDelay = 1000, pauseOnHover = true, children, ...props }: MarqueeProps) => { 16 | const [isScrolling, setIsScrolling] = useState(false); // 是否需要滚动 17 | const [isPaused, setIsPaused] = useState(false); // 是否暂停滚动 18 | const [translateX, setTranslateX] = useState(0); 19 | 20 | const directionRef = useRef(1); 21 | const delayUntilRef = useRef(0); 22 | 23 | const ref = useRef(null); 24 | const isHovering = useHoverDirty(ref); // 是否鼠标悬停 25 | 26 | useAnimationFrame((time) => { 27 | if (!isScrolling || isPaused) return; 28 | 29 | // 若处于延迟期,不滚动 30 | if (delayUntilRef.current && time < delayUntilRef.current) return; 31 | 32 | if (!ref.current) return; 33 | 34 | setTranslateX((prev) => { 35 | const newTranslateX = prev - directionRef.current * speed; 36 | const maxTranslateX = ref.current!.scrollWidth - ref.current!.clientWidth; 37 | 38 | if (newTranslateX <= -maxTranslateX - speed || newTranslateX >= speed) { 39 | directionRef.current = -directionRef.current; // 反转方向 40 | delayUntilRef.current = time + intervalDelay; // 设置延迟截止时间 41 | return prev; // 暂停这一帧,等下次再移动 42 | } 43 | 44 | return newTranslateX; 45 | }); 46 | }); 47 | 48 | useEffect(() => { 49 | if (!isScrolling) { 50 | setTranslateX(0); 51 | } 52 | }, [isScrolling]); 53 | 54 | useLayoutEffect(() => { 55 | if (pauseOnHover) { 56 | setIsPaused(isHovering); 57 | } 58 | 59 | if (ref.current && ref.current.scrollWidth > ref.current.clientWidth) { 60 | setTimeout(() => { 61 | setIsScrolling(true); 62 | }, startDelay); 63 | } else { 64 | setIsScrolling(false); 65 | } 66 | }); 67 | 68 | return 69 | {Children.map(children, (child) => ( 70 |
{child}
75 | ))} 76 |
77 | } -------------------------------------------------------------------------------- /src/components/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PageTabs } from "./PageTabs.tsx"; 3 | import { PageHeader } from "./PageHeader.tsx"; 4 | import { PageRawContent } from "./PageRawContent.tsx"; 5 | import { NAVBAR_BREAKPOINT } from "@/App.tsx"; 6 | import { Helmet } from "react-helmet"; 7 | 8 | export interface PageProps { 9 | meta: { 10 | title: string; 11 | description: string; 12 | }; 13 | tabs?: { 14 | id: string; 15 | name: string; 16 | children: React.ReactNode; 17 | }[]; 18 | children?: React.ReactNode; 19 | } 20 | 21 | export const Page = (props: PageProps) => { 22 | return ( 23 |
26 | 30 | {props.meta.title && {props.meta.title}} 31 | {props.meta.description && } 32 | 33 | 34 | 35 | 36 | {props.tabs && ( 37 | 38 | )} 39 | 40 | {props.children && ( 41 | 42 | )} 43 |
44 | ); 45 | } -------------------------------------------------------------------------------- /src/components/Page/PageHeader.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8)); 4 | 5 | @mixin smaller-than $mantine-breakpoint-md { 6 | text-align: center; 7 | } 8 | } 9 | 10 | .header { 11 | padding: var(--mantine-spacing-xl) 16px var(--mantine-spacing-xl); 12 | max-width: var(--page-max-width); 13 | margin-left: auto; 14 | margin-right: auto; 15 | 16 | @mixin smaller-than $mantine-breakpoint-xs { 17 | padding: var(--mantine-spacing-lg) 16px var(--mantine-spacing-lg); 18 | } 19 | } 20 | 21 | 22 | .title { 23 | font-size: 32px; 24 | margin-bottom: 5px; 25 | font-weight: 900; 26 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); 27 | 28 | @mixin smaller-than $mantine-breakpoint-xs { 29 | font-size: 28px; 30 | } 31 | } 32 | 33 | .description { 34 | font-size: var(--mantine-font-size-md); 35 | color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-1)); 36 | 37 | @mixin smaller-than $mantine-breakpoint-xs { 38 | font-size: var(--mantine-font-size-sm); 39 | } 40 | } -------------------------------------------------------------------------------- /src/components/Page/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import { PageProps } from "./Page.tsx"; 2 | import { Box, Text, Title } from "@mantine/core"; 3 | import classes from "./PageHeader.module.css"; 4 | 5 | export const PageHeader = ({ meta }: PageProps) => { 6 | return ( 7 |
8 | 9 | {meta.title} 10 | {meta.description} 11 | 12 |
13 | ) 14 | } -------------------------------------------------------------------------------- /src/components/Page/PageRawContent.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | max-width: var(--page-max-width); 3 | margin-left: auto; 4 | margin-right: auto; 5 | padding: 16px; 6 | } -------------------------------------------------------------------------------- /src/components/Page/PageRawContent.tsx: -------------------------------------------------------------------------------- 1 | import { PageProps } from "./Page.tsx"; 2 | import classes from "./PageRawContent.module.css"; 3 | 4 | export const PageRawContent = (props: PageProps) => { 5 | return ( 6 |
7 | {props.children} 8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /src/components/Page/PageTabs.module.css: -------------------------------------------------------------------------------- 1 | .tabsWrapper { 2 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8)); 3 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); 4 | } 5 | 6 | .tabsList { 7 | max-width: var(--page-max-width); 8 | margin-left: auto; 9 | margin-right: auto; 10 | margin-bottom: -1px; 11 | padding: 0 16px; 12 | 13 | @mixin smaller-than $mantine-breakpoint-xs { 14 | max-width: 100%; 15 | padding-right: 0; 16 | } 17 | 18 | &::before { 19 | border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6)); 20 | } 21 | } 22 | 23 | .tab { 24 | font-size: var(--mantine-font-size-md); 25 | font-weight: 500; 26 | height: 46px; 27 | padding-left: var(--mantine-spacing-lg); 28 | padding-right: var(--mantine-spacing-lg); 29 | background-color: transparent; 30 | 31 | @mixin smaller-than $mantine-breakpoint-xs { 32 | font-size: var(--mantine-font-size-sm); 33 | padding-left: var(--mantine-spacing-md); 34 | padding-right: var(--mantine-spacing-md); 35 | } 36 | 37 | &[data-active] { 38 | background-color: var(--mantine-color-body); 39 | 40 | @mixin light { 41 | color: var(--mantine-color-black); 42 | border-color: var(--mantine-color-gray-2); 43 | border-bottom-color: transparent; 44 | 45 | &::before, 46 | &::after { 47 | background-color: var(--mantine-color-gray-2); 48 | } 49 | } 50 | 51 | @mixin dark { 52 | color: var(--mantine-color-white); 53 | border-color: var(--mantine-color-dark-6); 54 | border-bottom-color: transparent; 55 | 56 | &::before, 57 | &::after { 58 | background-color: var(--mantine-color-dark-6); 59 | } 60 | } 61 | } 62 | } 63 | 64 | .tabContent { 65 | max-width: var(--page-max-width); 66 | margin-left: auto; 67 | margin-right: auto; 68 | padding: 16px; 69 | } -------------------------------------------------------------------------------- /src/components/Page/PageTabs.tsx: -------------------------------------------------------------------------------- 1 | import { PageProps } from "./Page.tsx"; 2 | import { Tabs } from "@mantine/core"; 3 | import { useState } from "react"; 4 | import classes from "./PageTabs.module.css"; 5 | import { useSearchParams } from "react-router-dom"; 6 | 7 | export const PageTabs = (props: PageProps) => { 8 | const [searchParams, setSearchParams] = useSearchParams(); 9 | const [activeTab, setActiveTab] = useState(searchParams.get('tab') || props.tabs?.[0].id); 10 | 11 | return ( 12 | { 19 | setSearchParams({ tab: tab! }); 20 | setActiveTab(tab!) 21 | }} 22 | > 23 |
24 | 25 | {props.tabs?.map((tab) => ( 26 | 27 | {tab.name} 28 | 29 | ))} 30 | 31 |
32 | 33 | {props.tabs?.map((tab) => ( 34 | 35 |
36 | {tab.children} 37 |
38 |
39 | ))} 40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/NotFoundAlert.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { Alert, Button, Group, Text} from "@mantine/core"; 3 | import Icon from "@mdi/react"; 4 | import { mdiAlertCircleOutline } from "@mdi/js"; 5 | 6 | export const NotFoundAlert = () => { 7 | const navigate = useNavigate(); 8 | 9 | return ( 10 | } title="没有获取到游戏数据" color="red"> 11 | 12 | 请检查你的查分器账号是否已经绑定游戏账号。 13 | 14 | 15 | 18 | 19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/PlayerModal.module.css: -------------------------------------------------------------------------------- 1 | @import "../../Scores/ScoreModalMenu.module.css"; 2 | 3 | .subParameters { 4 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 5 | padding: 6px 12px; 6 | border-radius: var(--mantine-radius-md); 7 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/PlayerPanel.module.css: -------------------------------------------------------------------------------- 1 | .section { 2 | padding: var(--mantine-spacing-md); 3 | } 4 | 5 | .playerButton { 6 | display: block; 7 | width: 100%; 8 | padding: var(--mantine-spacing-md); 9 | color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); 10 | 11 | &:hover { 12 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 13 | } 14 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/PlayerPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Group, ScrollArea, Text, UnstyledButton, UnstyledButtonProps } from "@mantine/core"; 2 | import { useState } from "react"; 3 | import { useViewportSize } from "@mantine/hooks"; 4 | import classes from "./PlayerPanel.module.css" 5 | import { ChunithmPlayerProps, MaimaiPlayerProps } from "@/types/player"; 6 | import { PlayerModal } from "./PlayerModal.tsx"; 7 | import { MaimaiPlayerContent } from "./maimai/PlayerContent.tsx"; 8 | import { ChunithmPlayerContent } from "./chunithm/PlayerContent.tsx"; 9 | import { isChunithmPlayerProps, isMaimaiPlayerProps } from "@/utils/api/player.ts"; 10 | import useGame from "@/hooks/useGame.ts"; 11 | 12 | const examplePlayer = { 13 | maimai: { 14 | name: "maimai", 15 | rating: 0, 16 | friend_code: 888888888888888, 17 | trophy: { 18 | "name": "欢迎来到“舞萌DX”!", 19 | "color": "Normal" 20 | }, 21 | course_rank: 0, 22 | class_rank: 0, 23 | star: 0, 24 | icon: { 25 | id: 1, 26 | name: "" 27 | }, 28 | upload_time: "2024-01-01T08:00:00Z" 29 | }, 30 | chunithm: { 31 | name: "CHUNITHM", 32 | level: 1, 33 | rating: 0, 34 | friend_code: 888888888888888, 35 | class_emblem: { 36 | base: 0, 37 | medal: 0 38 | }, 39 | reborn_count: 0, 40 | over_power: 0, 41 | over_power_progress: 0, 42 | currency: 0, 43 | total_currency: 0, 44 | trophy: { 45 | name: "NEW COMER", 46 | color: "normal" 47 | }, 48 | character: { 49 | id: 0, 50 | name: "" 51 | }, 52 | upload_time: "2024-01-01T08:00:00Z" 53 | }, 54 | } 55 | 56 | interface PlayerButtonProps { 57 | player: MaimaiPlayerProps | ChunithmPlayerProps; 58 | onClick?: () => void; 59 | } 60 | 61 | const PlayerButton = ({ player, onClick, ...others }: PlayerButtonProps & UnstyledButtonProps) => { 62 | return ( 63 | 64 | {isMaimaiPlayerProps(player) && } 65 | {isChunithmPlayerProps(player) && } 66 | 67 | ); 68 | } 69 | 70 | export const PlayerPanel = ({ player }: { player?: MaimaiPlayerProps | ChunithmPlayerProps }) => { 71 | const { width } = useViewportSize(); 72 | const [game] = useGame(); 73 | const [opened, setOpened] = useState(false); 74 | 75 | if (!player) player = examplePlayer[game]; 76 | 77 | return ( 78 | <> 79 | setOpened(false)} /> 80 | 81 | setOpened(true)} /> 82 | 83 | 84 |
85 | 86 | 好友码 87 | {player.friend_code} 88 | 89 | 90 | 上次同步时间 91 | {(new Date(Date.parse(player.upload_time))).toLocaleString()} 92 | 93 |
94 | 95 | ) 96 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, rem, Skeleton } from "@mantine/core"; 2 | 3 | export const PlayerPanelSkeleton = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/chunithm/PlayerModal.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Paper, Text } from "@mantine/core"; 2 | import { ChunithmPlayerProps } from "@/types/player"; 3 | import classes from "../PlayerModal.module.css"; 4 | import { Marquee } from "@/components/Marquee.tsx"; 5 | 6 | export const ChunithmPlayerModalContent = ({ player }: { player: ChunithmPlayerProps }) => { 7 | return ( 8 | 9 | 10 | 11 | {player.name_plate && ( 12 | 13 | 名牌版 14 | 15 | {player.name_plate.name} 16 | 17 | 18 | )} 19 | 20 | 21 | {player.map_icon && ( 22 | 23 | 地图头像 24 | 25 | {player.map_icon.name} 26 | 27 | 28 | )} 29 | 30 | 31 | 32 | 上次同步时间 33 | 34 | {(new Date(Date.parse(player.upload_time))).toLocaleString()} 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/chunithm/RatingTrend.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Flex, Text } from "@mantine/core"; 2 | import { IconDatabaseOff } from "@tabler/icons-react"; 3 | import { Area, CartesianGrid, ComposedChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; 4 | 5 | export interface ChunithmRatingTrendProps { 6 | rating: number; 7 | bests_rating: number; 8 | selections_rating: number; 9 | recents_rating: number; 10 | date: string | number; 11 | } 12 | 13 | const RatingTrendChart = ({ trend }: { trend: ChunithmRatingTrendProps[] }) => { 14 | trend = trend.map((item) => { 15 | return { 16 | ...item, 17 | date: new Date(item.date).getTime(), 18 | } 19 | }) 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | new Date(value).toLocaleDateString('zh-CN', { 31 | month: "numeric", 32 | day: "numeric", 33 | })} fontSize={14} /> 34 | { 35 | dataMin -= 0.01; 36 | dataMax += 0.01; 37 | return [Math.round(dataMin*100)/100, Math.round(dataMax*100)/100]; 38 | }} fontSize={14} /> 39 | 40 | { 41 | if (!props.active || !props.payload || props.payload.length < 1) return null; 42 | const payload = props.payload[0].payload; 43 | return ( 44 | 45 | {new Date(payload.date).toLocaleDateString()} 46 | Rating: {Math.round(payload.rating*100)/100} 47 | Best 30: {Math.round(payload.bests_rating*100)/100} 48 | Selection 10: {Math.round(payload.selections_rating*100)/100} 49 | Recent 10 (MAX): {Math.round(payload.recents_rating*100)/100} 50 | 51 | ) 52 | }} /> 53 | 54 | 55 | 56 | ) 57 | } 58 | 59 | export const ChunithmRatingTrend = ({ trend }: { trend: ChunithmRatingTrendProps[] | null }) => { 60 | if (!trend || trend.length < 2) { 61 | return ( 62 | 63 | 64 | 历史记录不足,无法生成图表 65 | 66 | ) 67 | } 68 | 69 | return <> 70 | 71 | ※ Recent 10 均为 Best #1 曲目,最终结果为理论不推分最高 Rating。 72 | ※ 该数据由历史同步成绩推出,而非玩家的历史 Rating,结果仅供参考。 73 | 74 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/maimai/PlayerContent.tsx: -------------------------------------------------------------------------------- 1 | import { MaimaiPlayerProps } from "@/types/player"; 2 | import { Avatar, Badge, Divider, Flex, Group, Image, rem, Text, useComputedColorScheme } from "@mantine/core"; 3 | import { IconPhotoOff } from "@tabler/icons-react"; 4 | import { getDeluxeRatingGradient, getTrophyColor } from "@/utils/color.ts"; 5 | import { ASSET_URL } from "@/main.tsx"; 6 | import { Marquee } from "@/components/Marquee.tsx"; 7 | 8 | export const MaimaiPlayerContent = ({ player }: { player: MaimaiPlayerProps }) => { 9 | const computedColorScheme = useComputedColorScheme('light'); 10 | 11 | return ( 12 | 13 | ({ 14 | root: { 15 | backgroundColor: computedColorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[1], 16 | } 17 | })}> 18 | 19 | 20 |
21 | 22 | {player.trophy && ( 23 | 25 | 28 | {player.trophy.name} 29 | 30 | 31 | } /> 32 | )} 33 | DX Rating: {player.rating} 36 | 37 | 38 | 39 | {player.name} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ×{player.star} 49 | 50 | 51 | 52 |
53 |
54 | ) 55 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/maimai/PlayerModal.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Paper, Text } from "@mantine/core"; 2 | import { MaimaiPlayerProps } from "@/types/player"; 3 | import classes from "../PlayerModal.module.css"; 4 | import { Marquee } from "@/components/Marquee.tsx"; 5 | 6 | export const MaimaiPlayerModalContent = ({ player }: { player: MaimaiPlayerProps }) => { 7 | return ( 8 | 9 | 10 | 11 | {player.name_plate && ( 12 | 13 | 姓名框 14 | 15 | {player.name_plate.name} 16 | 17 | 18 | )} 19 | 20 | 21 | {player.frame && ( 22 | 23 | 背景板 24 | 25 | {player.frame.name} 26 | 27 | 28 | )} 29 | 30 | 31 | 32 | 上次同步时间 33 | 34 | {(new Date(Date.parse(player.upload_time))).toLocaleString()} 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Profile/PlayerPanel/maimai/RatingTrend.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Flex, Group, Text } from "@mantine/core"; 2 | import { IconDatabaseOff } from "@tabler/icons-react"; 3 | import { Area, CartesianGrid, ComposedChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; 4 | 5 | export interface MaimaiRatingTrendProps { 6 | total: number; 7 | standard_total: number; 8 | dx_total: number; 9 | date: string | number; 10 | } 11 | 12 | const RatingTrendChart = ({ trend }: { trend: MaimaiRatingTrendProps[] }) => { 13 | trend = trend.map((item) => { 14 | return { 15 | ...item, 16 | date: new Date(item.date).getTime(), 17 | } 18 | }) 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | new Date(value).toLocaleDateString('zh-CN', { 30 | month: "numeric", 31 | day: "numeric", 32 | })} fontSize={14} /> 33 | { 34 | return [Math.floor(dataMin), Math.floor(dataMax)]; 35 | }} fontSize={14} /> 36 | 37 | { 38 | if (!props.active || !props.payload || props.payload.length < 1) return null; 39 | const payload = props.payload[0].payload; 40 | return ( 41 | 42 | {new Date(payload.date).toLocaleDateString()} 43 | DX Rating: {payload.total} 44 | 45 | B35: {payload.standard_total} 46 | B15: {payload.dx_total} 47 | 48 | 49 | ) 50 | }} /> 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | export const MaimaiRatingTrend = ({ trend }: { trend: MaimaiRatingTrendProps[] | null }) => { 58 | if (!trend || trend.length < 2) { 59 | return ( 60 | 61 | 62 | 历史记录不足,无法生成图表 63 | 64 | ) 65 | } 66 | return <> 67 | 68 | ※ 该数据由历史同步成绩推出,而非玩家的历史 DX Rating,结果仅供参考。 69 | 70 | } -------------------------------------------------------------------------------- /src/components/Profile/PlayerSection.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Overlay, Stack, Text, useComputedColorScheme } from "@mantine/core"; 2 | import { PlayerPanelSkeleton } from "./PlayerPanel/Skeleton.tsx"; 3 | import { useNavigate } from "react-router-dom"; 4 | import classes from "./Profile.module.css"; 5 | import { usePlayer } from "@/hooks/swr/usePlayer.ts"; 6 | import { PlayerPanel } from "./PlayerPanel/PlayerPanel.tsx"; 7 | import useGame from "@/hooks/useGame.ts"; 8 | 9 | export const PlayerSection = () => { 10 | const [game] = useGame(); 11 | const { player, isLoading } = usePlayer(game); 12 | const computedColorScheme = useComputedColorScheme('light'); 13 | const navigate = useNavigate(); 14 | 15 | return ( 16 | 17 | {isLoading ? ( 18 | 19 | ) : ( 20 | 21 | {!player && ( 22 | 25 | 26 | 尚未同步游戏数据 27 | 30 | 31 | 32 | )} 33 | 34 | 35 | )} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Profile/Profile.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 3 | } 4 | 5 | .section { 6 | padding: var(--mantine-spacing-md); 7 | } 8 | 9 | .tab { 10 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 11 | color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0)); 12 | border: 0; 13 | border-top: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-6)); 14 | padding: 8px var(--mantine-spacing-md); 15 | cursor: pointer; 16 | font-size: var(--mantine-font-size-sm); 17 | flex: 1; 18 | 19 | @mixin hover { 20 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); 21 | } 22 | 23 | &[data-active] { 24 | background-color: var(--mantine-primary-color-light); 25 | color: var(--mantine-primary-color-light-color); 26 | 27 | @mixin hover { 28 | background-color: var(--mantine-primary-color-light-hover); 29 | } 30 | } 31 | } 32 | 33 | .list { 34 | display: flex; 35 | } -------------------------------------------------------------------------------- /src/components/Profile/UserBindSection.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Group, Switch, Text, TextInput } from "@mantine/core"; 2 | import { useForm } from "@mantine/form"; 3 | import { updateUserBind } from "@/utils/api/user.ts"; 4 | import Icon from "@mdi/react"; 5 | import { mdiEye, mdiEyeOff } from "@mdi/js"; 6 | import { useDisclosure } from "@mantine/hooks"; 7 | import classes from "./Profile.module.css"; 8 | import { openAlertModal, openRetryModal } from "../../utils/modal.tsx"; 9 | import { useUser } from "@/hooks/swr/useUser.ts"; 10 | 11 | export const UserBindSection = () => { 12 | const { user, mutate } = useUser(); 13 | const [visible, visibleHandler] = useDisclosure(false) 14 | 15 | const form = useForm({ 16 | initialValues: { 17 | qq: '', 18 | }, 19 | 20 | validate: { 21 | qq: (value: string) => /^\d{5,11}$/.test(value) ? null : "QQ 号格式不正确", 22 | }, 23 | 24 | transformValues: (values) => ({ 25 | qq: parseInt(values.qq), 26 | }) 27 | }); 28 | 29 | const updateUserBindHandler = async () => { 30 | try { 31 | const res = await updateUserBind(form.getTransformedValues()) 32 | const data = await res.json() 33 | if (!data.success) { 34 | throw new Error(data.message) 35 | } 36 | openAlertModal("绑定成功", "第三方开发者将可以通过绑定信息获取你的游戏数据。"); 37 | mutate({ ...user, bind: { ...(user?.bind || {}), qq: parseInt(form.values.qq)} } as any, false); 38 | } catch (error) { 39 | openRetryModal("绑定失败", `${error}`, updateUserBindHandler); 40 | } finally { 41 | form.reset(); 42 | } 43 | } 44 | 45 | return ( 46 | 47 | 48 |
49 | 50 | 第三方账号绑定 51 | 52 | 53 | 绑定第三方账号,第三方开发者将可以通过绑定信息获取你的游戏数据 54 | 55 |
56 | } 61 | offLabel={} 62 | /> 63 |
64 |
updateUserBindHandler())}> 65 | 73 | 74 | 75 | 76 | 77 |
78 | ) 79 | } -------------------------------------------------------------------------------- /src/components/RadioCardGroup.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | padding: var(--mantine-spacing-md); 4 | transition: border-color 150ms ease; 5 | 6 | &[data-checked] { 7 | border-color: var(--mantine-primary-color-filled); 8 | } 9 | 10 | @mixin hover { 11 | @mixin light { 12 | background-color: var(--mantine-color-gray-0); 13 | } 14 | 15 | @mixin dark { 16 | background-color: var(--mantine-color-dark-6); 17 | } 18 | } 19 | } 20 | 21 | .label { 22 | font-weight: bold; 23 | font-size: var(--mantine-font-size-md); 24 | line-height: 1.3; 25 | color: var(--mantine-color-bright); 26 | } 27 | 28 | .description { 29 | margin-top: 8px; 30 | color: var(--mantine-color-dimmed); 31 | font-size: var(--mantine-font-size-xs); 32 | } -------------------------------------------------------------------------------- /src/components/RadioCardGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Radio, Group, Stack, Text } from '@mantine/core'; 2 | import classes from './RadioCardGroup.module.css'; 3 | 4 | interface RadioCardGroupProps { 5 | data: { name: string; description: string; value: string }[]; 6 | value?: string; 7 | onChange?: (value: string) => void; 8 | } 9 | 10 | export const RadioCardGroup = ({ data, value, onChange }: RadioCardGroupProps) => { 11 | const cards = data.map((item) => ( 12 | 13 | 14 | 15 |
16 | {item.name} 17 | {item.description} 18 |
19 |
20 |
21 | )); 22 | 23 | return 27 | 28 | {cards} 29 | 30 | 31 | } -------------------------------------------------------------------------------- /src/components/RouterTransition.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from "react-router" 3 | import { NavigationProgress, nprogress } from '@mantine/nprogress'; 4 | 5 | export default function RouterTransition() { 6 | const location = useLocation(); 7 | 8 | useEffect(() => { 9 | return () => { 10 | nprogress.start(); 11 | nprogress.complete(); 12 | }; 13 | }, [location.pathname]) 14 | 15 | return ; 16 | } -------------------------------------------------------------------------------- /src/components/Scores/ChartComment.module.css: -------------------------------------------------------------------------------- 1 | .actionIcon { 2 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); 3 | 4 | &:hover { 5 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); 6 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); 7 | } 8 | } 9 | 10 | .textarea { 11 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 12 | } -------------------------------------------------------------------------------- /src/components/Scores/RatingSegments.module.css: -------------------------------------------------------------------------------- 1 | .subParameters { 2 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 3 | padding: 6px 12px; 4 | border-radius: var(--mantine-radius-md); 5 | } 6 | 7 | .statCount { 8 | line-height: 1.3; 9 | } 10 | 11 | .diff { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | .icon { 17 | color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); 18 | } 19 | 20 | .ratingNumberTotal { 21 | font-size: var(--mantine-h2-font-size); 22 | font-family: 'Roboto Mono', 'Courier New', monospace; 23 | font-weight: 700; 24 | line-height: var(--mantine-h2-line-height); 25 | } 26 | 27 | .ratingNumberSubtotal { 28 | color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); 29 | font-size: var(--mantine-h4-font-size); 30 | font-family: 'Roboto Mono', 'Courier New', monospace; 31 | font-weight: 700; 32 | line-height: var(--mantine-h4-line-height); 33 | } -------------------------------------------------------------------------------- /src/components/Scores/ScoreModal.module.css: -------------------------------------------------------------------------------- 1 | @import "./ScoreModalMenu.module.css"; 2 | 3 | .subParameters { 4 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 5 | padding: 6px 12px; 6 | border-radius: var(--mantine-radius-md); 7 | } 8 | 9 | .subParametersButton { 10 | cursor: pointer; 11 | } 12 | 13 | .subParametersButton:hover { 14 | background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); 15 | } 16 | 17 | .worldsEndText { 18 | background: conic-gradient( 19 | from 240deg, 20 | rgb(2, 224, 1), 21 | rgb(213, 226, 17), 22 | rgb(226, 121, 87), 23 | rgb(212, 1, 150), 24 | rgb(141, 12, 202), 25 | rgb(77,120,107), 26 | rgb(2, 224, 1) 27 | ); 28 | -webkit-text-fill-color: transparent; 29 | -webkit-background-clip: text; 30 | background-clip: text; 31 | } -------------------------------------------------------------------------------- /src/components/Scores/ScoreModalMenu.module.css: -------------------------------------------------------------------------------- 1 | .actionIcon { 2 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); 3 | 4 | &:hover { 5 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); 6 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/Scores/Scores.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 3 | } 4 | 5 | .scoreCard { 6 | cursor: pointer; 7 | transition: transform 200ms ease; 8 | 9 | &:hover { 10 | transform: scale(1.03); 11 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 12 | box-shadow: var(--mantine-shadow-md); 13 | border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 14 | border-radius: var(--mantine-radius-md); 15 | z-index: 1; 16 | } 17 | } 18 | 19 | .scoreWorldsEnd { 20 | --angle: 0deg; 21 | padding: 2px !important; 22 | } 23 | 24 | .scoreWorldsEnd::before { 25 | content: ''; 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | border-radius: var(--mantine-radius-md); 32 | padding: 2px; 33 | background: conic-gradient(from var(--angle), red, yellow, lime, aqua, blue, magenta, red); 34 | -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 35 | -webkit-mask-composite: xor; 36 | mask-composite: exclude; 37 | animation: 10s rotate linear infinite; 38 | } 39 | 40 | @keyframes rotate { 41 | to { 42 | --angle: 360deg; 43 | } 44 | } 45 | 46 | @property --angle { 47 | syntax: ''; 48 | initial-value: 0deg; 49 | inherits: false; 50 | } -------------------------------------------------------------------------------- /src/components/Scores/chunithm/Chart.tsx: -------------------------------------------------------------------------------- 1 | import { ChunithmDifficultyProps, ChunithmNotesProps } from "@/utils/api/song/chunithm.ts"; 2 | import { Box, Group, keys, Space, Table, Text } from "@mantine/core"; 3 | import useSongListStore from "@/hooks/useSongListStore.ts"; 4 | import { useShallow } from "zustand/react/shallow"; 5 | 6 | const ChartNotes = ({ notes }: { notes: ChunithmNotesProps }) => { 7 | if (!notes) return; 8 | 9 | return 10 | 11 | 12 | {keys(notes).map((key) => { 13 | return 14 | {key.toLocaleUpperCase()} 15 | {notes[key]} 16 | ; 17 | })} 18 | 19 |
20 |
; 21 | } 22 | 23 | export const ChunithmChart = ({ difficulty }: { difficulty: ChunithmDifficultyProps }) => { 24 | const { versions } = useSongListStore( 25 | useShallow((state) => ({ versions: state.chunithm.versions })), 26 | ) 27 | 28 | if (!difficulty) return; 29 | 30 | return <> 31 | 32 | {difficulty.note_designer && ( 33 | 34 | 谱师 35 | {difficulty.note_designer} 36 | 37 | )} 38 | 39 | 版本 40 | 41 | {versions.slice().reverse().find((version) => difficulty.version >= version.version)?.title || "未知"} 42 | 43 | 44 | 45 | 46 | 物量 47 | 48 | ; 49 | } -------------------------------------------------------------------------------- /src/components/Scores/chunithm/Score.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Card, Flex, Group, NumberFormatter, Rating, rem, Text } from "@mantine/core"; 2 | import { getScoreCardBackgroundColor, getScoreSecondaryColor, getTransparentColor } from "@/utils/color.ts"; 3 | import { getDifficulty, ChunithmSongProps, ChunithmDifficultyProps } from "@/utils/api/song/chunithm.ts"; 4 | import { useEffect, useState } from "react"; 5 | import { ChunithmScoreProps } from "@/types/score"; 6 | 7 | interface ScoreContentProps { 8 | score: ChunithmScoreProps; 9 | song?: ChunithmSongProps; 10 | } 11 | 12 | export const ChunithmScoreContent = ({ score, song }: ScoreContentProps) => { 13 | const [difficulty, setDifficulty] = useState(null); 14 | const [level, setLevel] = useState(score.level); 15 | 16 | const rating = score.id < 8000 ? `${Math.floor(score.rating * 100) / 100}` : "-"; 17 | const levelIndex = score.id < 8000 ? score.level_index : 5; 18 | 19 | useEffect(() => { 20 | if (!song) return; 21 | const difficulty = getDifficulty(song, score.level_index); 22 | if (!difficulty) return; 23 | setDifficulty(difficulty); 24 | if (score.id >= 8000) { 25 | setLevel(difficulty.kanji); 26 | } else { 27 | setLevel(difficulty.level_value.toFixed(1)); 28 | } 29 | }, [song]); 30 | 31 | return <> 32 | 35 | {score.song_name} 36 | {score.id >= 8000 && difficulty && 37 | 38 | } 39 | 40 | 43 | 44 | {score.score != -1 ? ( 45 |
46 | 47 | 48 | 49 | 50 | Rating: {rating} 51 | 52 |
53 | ) : ( 54 |
55 | 56 | 未游玩 57 | 58 | 59 | 或未上传至查分器 60 | 61 |
62 | )} 63 | 64 | 67 | {level} 68 | 69 | 70 |
71 |
72 | 73 | } -------------------------------------------------------------------------------- /src/components/Scores/maimai/DeluxeRatingCalculator.module.css: -------------------------------------------------------------------------------- 1 | .changeLabel { 2 | position: relative; 3 | } 4 | 5 | .changeLabel::after { 6 | content: attr(data-label); 7 | position: absolute; 8 | left: 0.5rem; 9 | bottom: -0.5rem; 10 | padding: 0.1rem 0.25rem; 11 | border-radius: 0.1rem; 12 | font-size: 0.75rem; 13 | line-height: 1; 14 | font-weight: 500; 15 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); 16 | } -------------------------------------------------------------------------------- /src/components/Scores/maimai/Score.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Box, Card, Flex, Group, rem, Text } from "@mantine/core"; 2 | import { 3 | getScoreCardBackgroundColor, getScoreSecondaryColor, getTransparentColor, 4 | } from "@/utils/color.ts"; 5 | import { getDifficulty, MaimaiSongProps } from "@/utils/api/song/maimai.ts"; 6 | import { useEffect, useState } from "react"; 7 | import { MaimaiScoreProps } from "@/types/score"; 8 | 9 | interface ScoreContentProps { 10 | score: MaimaiScoreProps; 11 | song?: MaimaiSongProps; 12 | } 13 | 14 | export const MaimaiScoreContent = ({ score, song }: ScoreContentProps) => { 15 | const [isBuddy, setIsBuddy] = useState(false); 16 | const [level, setLevel] = useState(score.level); 17 | 18 | const deluxeRating = score.type !== "utage" ? `${parseInt(String(score.dx_rating))}` : "-"; 19 | const levelIndex = score.type !== "utage" ? score.level_index : 5; 20 | 21 | useEffect(() => { 22 | if (!song) return; 23 | const difficulty = getDifficulty(song, score.type, score.level_index); 24 | if (!difficulty) return; 25 | setIsBuddy(difficulty.is_buddy); 26 | if (score.type === "utage") return; 27 | setLevel(difficulty.level_value.toFixed(1)); 28 | }, [song]); 29 | 30 | return <> 31 | 34 | {score.song_name} 35 | {score.type === "standard" && 标准} 36 | {score.type === "dx" && DX} 37 | {isBuddy && BUDDY} 38 | 39 | 42 | 43 | {score.achievements != -1 ? ( 44 |
45 | 46 | {parseInt(String(score.achievements))} 47 | .{ 48 | (String(score.achievements).split(".")[1] || "0").padEnd(4, "0") 49 | }% 50 | 51 | 52 | DX Rating: {deluxeRating} 53 | 54 |
55 | ) : ( 56 |
57 | 58 | 未游玩 59 | 60 | 61 | 或未上传至查分器 62 | 63 |
64 | )} 65 | 66 | 69 | {level} 70 | 71 | 72 |
73 |
74 | 75 | } -------------------------------------------------------------------------------- /src/components/Scores/maimai/StatisticsSection.module.css: -------------------------------------------------------------------------------- 1 | .fullComboSyncSection { 2 | flex-direction: column; 3 | 4 | @media (max-width: 28rem) { 5 | flex-direction: row; 6 | } 7 | 8 | @media (max-width: 26rem) { 9 | flex-direction: column; 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/Settings/Settings.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | &:not(:last-child) { 3 | padding-bottom: var(--mantine-spacing-sm); 4 | } 5 | &:not(:first-child) { 6 | padding-top: var(--mantine-spacing-sm); 7 | } 8 | & + & { 9 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 10 | } 11 | } 12 | 13 | .switch { 14 | & * { 15 | cursor: pointer; 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/Settings/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from "@mantine/core"; 2 | import { SettingProps, SettingList } from "./SettingList.tsx"; 3 | 4 | interface SettingsModalProps { 5 | title: string; 6 | data: SettingProps[]; 7 | value?: any; 8 | opened: boolean; 9 | onClose: (value?: any) => void; 10 | onChange?: (key: string, value: any) => void; 11 | } 12 | 13 | export const SettingsModal = ({ title, data, value, opened, onClose, onChange }: SettingsModalProps) => { 14 | return ( 15 | 16 | 17 | 18 | 19 | {title} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } -------------------------------------------------------------------------------- /src/components/Shell/Footer/Footer.module.css: -------------------------------------------------------------------------------- 1 | .link { 2 | color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); 3 | font-size: var(--mantine-font-size-sm); 4 | 5 | &:hover { 6 | text-decoration: underline; 7 | } 8 | } 9 | 10 | .footer { 11 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 12 | } 13 | 14 | .footerInner { 15 | max-width: calc(896px + var(--mantine-spacing-xl) * 2); 16 | margin: 0 auto; 17 | padding: var(--mantine-spacing-xl); 18 | 19 | @mixin smaller-than $mantine-breakpoint-xs { 20 | flex-direction: column; 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/Shell/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Flex, Group, Text } from "@mantine/core"; 2 | import classes from './Footer.module.css'; 3 | 4 | export const Footer = () => { 5 | return ( 6 |
7 | 8 | 9 | maimai DX 查分器 10 | 11 | 12 | © {new Date().getFullYear() + ' '} 13 | 14 | component="a" 15 | className={classes.link} 16 | href="https://lxns.net/" 17 | target="_blank" 18 | > 19 | Lxns Network 20 | 21 | 22 | 23 | 24 | component="a" 25 | className={classes.link} 26 | size="sm" 27 | href="https://beian.miit.gov.cn/" 28 | target="_blank" 29 | > 30 | 粤ICP备18035696号 31 | 32 | 33 | 34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /src/components/Shell/Header/ColorSchemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Group, rem, Tooltip, useMantineColorScheme } from "@mantine/core"; 2 | import { IconMoonStars, IconSun, IconSunMoon } from "@tabler/icons-react"; 3 | 4 | 5 | const colorSchemes = { 6 | auto: { 7 | icon: , 8 | label: '跟随系统', 9 | color: 'blue' 10 | }, 11 | dark: { 12 | icon: , 13 | label: '深色模式', 14 | color: 'blue' 15 | }, 16 | light: { 17 | icon: , 18 | label: '浅色模式', 19 | color: 'yellow' 20 | } 21 | } 22 | 23 | export const ColorSchemeToggle = () => { 24 | const { colorScheme, setColorScheme } = useMantineColorScheme(); 25 | 26 | return ( 27 | 28 | 29 | setColorScheme( 30 | colorScheme === 'auto' ? 'dark' : colorScheme === 'dark' ? 'light' : 'auto' 31 | )} color={colorSchemes[colorScheme].color}> 32 | {colorSchemes[colorScheme].icon} 33 | 34 | 35 | 36 | ); 37 | } -------------------------------------------------------------------------------- /src/components/Shell/Header/GameTabs.module.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | position: relative; 3 | 4 | @mixin larger-than $mantine-breakpoint-xs { 5 | display: none !important; 6 | } 7 | } 8 | 9 | .tab { 10 | height: 32px; 11 | text-wrap: nowrap; 12 | border-radius: 4px 4px 0 0; 13 | 14 | &:hover { 15 | background: var(--mantine-color-default-hover); 16 | } 17 | 18 | &[data-active='true'] { 19 | color: var(--mantine-primary-color-light-color); 20 | } 21 | } 22 | 23 | .indicator { 24 | height: 33px !important; 25 | pointer-events: none; 26 | border-bottom: 1px solid var(--mantine-primary-color-light-color); 27 | } -------------------------------------------------------------------------------- /src/components/Shell/Header/GameTabs.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingIndicator, SimpleGrid, UnstyledButton } from "@mantine/core"; 2 | import classes from "./GameTabs.module.css"; 3 | import React, { useState } from "react"; 4 | 5 | interface GameTabsProps { 6 | tabs: { id: string; name: string }[]; 7 | activeTab: string; 8 | onTabChange(tab: string): void; 9 | style?: React.CSSProperties; 10 | } 11 | 12 | export const GameTabs = ({ tabs, activeTab, onTabChange, style }: GameTabsProps) => { 13 | const [rootRef, setRootRef] = useState(null); 14 | const [controlsRefs, setControlsRefs] = useState>({}); 15 | 16 | const setControlRef = (index: string) => (node: HTMLButtonElement) => { 17 | controlsRefs[index] = node; 18 | setControlsRefs(controlsRefs); 19 | }; 20 | 21 | return ( 22 | 23 | {tabs.map((item) => ( 24 | onTabChange(item.id)} 31 | mod={{ active: activeTab === item.id }} 32 | > 33 | {item.name} 34 | 35 | ))} 36 | 37 | 42 | 43 | ); 44 | } -------------------------------------------------------------------------------- /src/components/Shell/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | z-index: var(--mantine-z-index-modal); 4 | top: 0; 5 | width: 100%; 6 | padding: 0 var(--mantine-spacing-md); 7 | background-color: var(--mantine-color-body); 8 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 9 | } 10 | 11 | .navbarToggle { 12 | @mixin larger-than $mantine-breakpoint-md { 13 | display: none; 14 | } 15 | } 16 | 17 | .logo { 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | .game { 23 | background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-4)) !important; 24 | font-weight: bold; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | line-height: 1; 29 | padding: 2px 8px 2px 12px !important; 30 | border-radius: var(--mantine-radius-xl); 31 | 32 | @mixin smaller-than $mantine-breakpoint-xs { 33 | display: none; 34 | } 35 | 36 | &:hover { 37 | background: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)) !important; 38 | } 39 | } 40 | 41 | .gameChevron { 42 | display: block; 43 | width: 14px; 44 | height: 14px; 45 | margin-left: 5px; 46 | } -------------------------------------------------------------------------------- /src/components/Shell/Header/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Image, Text } from '@mantine/core' 2 | import { Link } from "react-router-dom"; 3 | 4 | export default function Logo() { 5 | return ( 6 | 26 | ); 27 | } -------------------------------------------------------------------------------- /src/components/Shell/Navbar/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | position: fixed; 3 | z-index: var(--mantine-z-index-modal); 4 | height: calc(100vh - var(--header-height)); 5 | width: var(--navbar-width); 6 | background-color: var(--mantine-color-body); 7 | border-right: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 8 | 9 | display: flex; 10 | flex-direction: column; 11 | 12 | @mixin smaller-than $mantine-breakpoint-md { 13 | width: var(--navbar-width); 14 | } 15 | 16 | @supports (max-height: 100dvh) { 17 | height: calc(100dvh - var(--header-height)); 18 | } 19 | } 20 | 21 | .navbarHeader { 22 | padding-bottom: var(--mantine-spacing-md); 23 | margin-bottom: calc(var(--mantine-spacing-md) * 1.5); 24 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 25 | } 26 | 27 | .navbarMain { 28 | flex: 1 1 auto; 29 | overflow-y: auto; 30 | } 31 | 32 | .navbarFooter { 33 | padding-top: var(--mantine-spacing-md); 34 | padding-bottom: var(--mantine-spacing-md); 35 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 36 | } 37 | 38 | .divider { 39 | color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 40 | } 41 | 42 | .navbarLink { 43 | display: flex; 44 | align-items: center; 45 | text-decoration: none; 46 | font-size: var(--mantine-font-size-sm); 47 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); 48 | padding: 8px var(--mantine-spacing-sm); 49 | border-radius: var(--mantine-radius-md); 50 | font-weight: 500; 51 | cursor: pointer; 52 | -webkit-tap-highlight-color: transparent; 53 | transition: background-color 50ms ease-in-out, color 50ms ease-in-out; 54 | 55 | @mixin hover { 56 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 57 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); 58 | 59 | .navbarLinkIcon { 60 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); 61 | } 62 | } 63 | 64 | &[data-active] { 65 | &, 66 | &:hover { 67 | background-color: var(--mantine-primary-color-light); 68 | color: var(--mantine-primary-color-light-color); 69 | 70 | .navbarLinkIcon { 71 | color: var(--mantine-primary-color-light-color); 72 | } 73 | } 74 | 75 | @mixin hover { 76 | background-color: var(--mantine-primary-color-light-hover); 77 | } 78 | } 79 | } 80 | 81 | .navbarLinkIcon { 82 | color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); 83 | display: flex; 84 | padding: 2px 0; 85 | transition: color 50ms ease-in-out; 86 | } -------------------------------------------------------------------------------- /src/components/Shell/Navbar/NavbarButton.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { Badge, Group, Text } from "@mantine/core"; 3 | import React from "react"; 4 | import classes from './Navbar.module.css'; 5 | 6 | interface NavbarButtonProps { 7 | label: string; 8 | icon: React.ReactNode; 9 | is_new?: boolean; 10 | to?: string; 11 | active?: string; 12 | onClose(): void; 13 | onClick?(): void; 14 | } 15 | 16 | export const NavbarButton = ({ label, icon, is_new, to, active, onClose, onClick }: NavbarButtonProps) => { 17 | const navigate = useNavigate(); 18 | 19 | return ( 20 | { 24 | event.preventDefault(); 25 | onClick && onClick(); 26 | if (to) navigate(to); 27 | onClose(); 28 | }} 29 | > 30 | 31 |
{icon}
32 | {label} 33 | {is_new && New} 36 |
37 |
38 | ) 39 | } -------------------------------------------------------------------------------- /src/components/Shell/Shell.module.css: -------------------------------------------------------------------------------- 1 | .routesWrapper { 2 | position: relative; 3 | height: calc(100vh - var(--header-height)); 4 | box-sizing: border-box; 5 | margin-top: var(--header-height); 6 | 7 | @supports (max-height: 100dvh) { 8 | height: calc(100dvh - var(--header-height)); 9 | } 10 | } -------------------------------------------------------------------------------- /src/components/SongDisabledIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Flex, Indicator } from "@mantine/core"; 3 | import { IconTrash } from "@tabler/icons-react"; 4 | 5 | export const SongDisabledIndicator = ({ disabled, children }: { disabled?: boolean, children: React.ReactElement }) => { 6 | if (!disabled) { 7 | return children; 8 | } 9 | return 被移除} 13 | size={18} 14 | offset={12} 15 | zIndex={2} 16 | disabled={!disabled} 17 | > 18 | 25 | {children} 26 | 27 | } -------------------------------------------------------------------------------- /src/components/Songs/SongCard.module.css: -------------------------------------------------------------------------------- 1 | @import '../../pages/Page.module.css'; 2 | 3 | .jacket { 4 | position: relative; 5 | 6 | &::before { 7 | content: ""; 8 | inset: calc(var(--scale) * -1.25rem * var(--mantine-scale)); 9 | z-index: -1; 10 | position: absolute; 11 | background-image: linear-gradient(-45deg, var(--primary-color) 50%, var(--secondary-color) 0); 12 | filter: blur(72px); 13 | } 14 | } 15 | 16 | .audioPlayer { 17 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 18 | } -------------------------------------------------------------------------------- /src/components/Songs/SongDifficulty.module.css: -------------------------------------------------------------------------------- 1 | .scoreCard { 2 | cursor: pointer; 3 | transition: transform 200ms ease; 4 | 5 | &:hover { 6 | transform: scale(1.03); 7 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 8 | box-shadow: var(--mantine-shadow-md); 9 | border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 10 | border-radius: var(--mantine-radius-md); 11 | z-index: 1; 12 | } 13 | } 14 | 15 | .scoreWorldsEnd { 16 | --angle: 0deg; 17 | } 18 | 19 | .scoreWorldsEnd::before { 20 | content: ''; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | right: 0; 25 | bottom: 0; 26 | border-radius: var(--mantine-radius-md); 27 | padding: 2px; 28 | background: conic-gradient(from var(--angle), red, yellow, lime, aqua, blue, magenta, red); 29 | -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 30 | -webkit-mask-composite: xor; 31 | mask-composite: exclude; 32 | animation: 10s rotate linear infinite; 33 | } 34 | 35 | @keyframes rotate { 36 | to { 37 | --angle: 360deg; 38 | } 39 | } 40 | 41 | @property --angle { 42 | syntax: ''; 43 | initial-value: 0deg; 44 | inherits: false; 45 | } -------------------------------------------------------------------------------- /src/components/Sync/CopyButtonWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, CopyButton, TextInput, Tooltip } from "@mantine/core"; 2 | import { IconCheck, IconCopy } from "@tabler/icons-react"; 3 | 4 | export const CopyButtonWithIcon = ({ label, content, ...others }: any) => { 5 | return ( 6 | e.target.select()} 10 | rightSection={ 11 | 12 | {({ copied, copy }) => ( 13 | 14 | 15 | {copied ? : } 16 | 17 | 18 | )} 19 | 20 | } 21 | readOnly 22 | {...others} 23 | /> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Sync/CrawlTokenAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Button, Text } from "@mantine/core"; 2 | import { IconAlertCircle, IconRefresh } from "@tabler/icons-react"; 3 | 4 | export const CrawlTokenAlert = ({ token, resetHandler }: any) => { 5 | const getExpireTime = (crawlToken: string) => { 6 | return Math.floor(((JSON.parse(atob(crawlToken.split('.')[1])).exp - new Date().getTime() / 1000)) / 60) 7 | } 8 | 9 | const isTokenExpired = token && getExpireTime(token) < 0; 10 | const alertColor = isTokenExpired ? 'yellow' : 'blue'; 11 | 12 | return ( 13 | } title="链接有效期提示" color={alertColor}> 14 | 15 | {token ? `该链接${ 16 | isTokenExpired ? "已失效," : `将在 ${getExpireTime(token) + 1} 分钟内失效,逾时` 17 | }请点击下方按钮刷新 OAuth 链接。` : "链接未生成,请点击下方按钮生成 OAuth 链接。"} 18 | 19 | 22 | 23 | ); 24 | }; -------------------------------------------------------------------------------- /src/components/Sync/WechatOAuthLink.tsx: -------------------------------------------------------------------------------- 1 | import { API_URL } from "../../main.tsx"; 2 | import { Button, Group } from "@mantine/core"; 3 | import { IconExternalLink } from "@tabler/icons-react"; 4 | import { CopyButtonWithIcon } from "./CopyButtonWithIcon.tsx"; 5 | 6 | export const WechatOAuthLink = ({ game = 'maimai', crawlToken }: { game: string, crawlToken: string | null }) => { 7 | const authLink = `${API_URL}/${game}/wechat/auth${crawlToken ? `?token=${window.btoa(crawlToken)}` : ''}`; 8 | const isMicroMessenger = /MicroMessenger/i.test(window.navigator.userAgent); 9 | 10 | return ( 11 | 12 | 17 | {isMicroMessenger && ( 18 | 24 | )} 25 | 26 | ); 27 | }; -------------------------------------------------------------------------------- /src/components/YearInReview/SongRankingSection.module.css: -------------------------------------------------------------------------------- 1 | .jacket { 2 | position: relative; 3 | 4 | &::before { 5 | content: ""; 6 | inset: calc(1.25rem * var(--mantine-scale)); 7 | position: absolute; 8 | background-image: linear-gradient(-45deg, var(--primary-color) 50%, var(--secondary-color) 0); 9 | filter: blur(72px); 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/YearInReview/SongTimelineSection.module.css: -------------------------------------------------------------------------------- 1 | .jacket { 2 | position: relative; 3 | 4 | &::before { 5 | content: ""; 6 | inset: calc(1.25rem * var(--mantine-scale)); 7 | position: absolute; 8 | background-image: linear-gradient(-45deg, var(--primary-color) 50%, var(--secondary-color) 0); 9 | filter: blur(72px); 10 | } 11 | } 12 | 13 | .timeline { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .timelineMonth { 19 | display: flex; 20 | flex-direction: column; 21 | width: calc(50% + 3px); 22 | align-items: center; 23 | 24 | @mixin smaller-than $mantine-breakpoint-xs { 25 | width: 100%; 26 | } 27 | } 28 | 29 | .timelineMonthTitle { 30 | font-size: 18px; 31 | font-weight: 500; 32 | text-transform: uppercase; 33 | margin-bottom: 10px; 34 | border-bottom: 2px solid var(--mantine-primary-color-filled); 35 | } 36 | 37 | .timelineMonthContent { 38 | display: flex; 39 | flex-direction: row; 40 | flex-wrap: wrap; 41 | justify-content: center; 42 | gap: 8px; 43 | width: 100%; 44 | font-size: 2em; 45 | } 46 | 47 | .timelineMonth:nth-child(odd) { 48 | align-self: flex-end; 49 | align-items: flex-start; 50 | border-left: 6px dotted var(--mantine-primary-color-filled); 51 | padding-left: 20px; 52 | box-sizing: border-box; 53 | margin-bottom: 4px; 54 | 55 | .timelineMonthTitle { 56 | margin-left: -22px; 57 | padding-left: 22px; 58 | } 59 | 60 | .timelineMonthContent { 61 | justify-content: flex-start; 62 | } 63 | 64 | @mixin smaller-than $mantine-breakpoint-xs { 65 | align-self: flex-start; 66 | } 67 | } 68 | 69 | .timelineMonth:nth-child(even) { 70 | align-self: flex-start; 71 | align-items: flex-end; 72 | border-right: 6px dotted var(--mantine-primary-color-filled); 73 | padding-right: 20px; 74 | box-sizing: border-box; 75 | margin-bottom: 4px; 76 | 77 | .timelineMonthTitle { 78 | margin-right: -22px; 79 | padding-right: 22px; 80 | } 81 | 82 | .timelineMonthContent { 83 | justify-content: flex-end; 84 | } 85 | 86 | @mixin smaller-than $mantine-breakpoint-xs { 87 | align-items: flex-start; 88 | border-left: 6px dotted var(--mantine-primary-color-filled); 89 | border-right: none; 90 | padding-left: 20px; 91 | padding-right: 0; 92 | box-sizing: border-box; 93 | 94 | .timelineMonthTitle { 95 | margin-left: -22px; 96 | margin-right: 0; 97 | padding-left: 22px; 98 | padding-right: 0; 99 | } 100 | 101 | .timelineMonthContent { 102 | justify-content: flex-start; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/components/YearInReview/SongTimelineSection.tsx: -------------------------------------------------------------------------------- 1 | import { YearInReviewProps } from "@/pages/public/YearInReview.tsx"; 2 | import classes from "./SongTimelineSection.module.css"; 3 | import { ASSET_URL } from "@/main.tsx"; 4 | import { IconPhotoOff } from "@tabler/icons-react"; 5 | import { Avatar, Box } from "@mantine/core"; 6 | import {useEffect, useState} from "react"; 7 | import { ColorExtractor } from "react-color-extractor"; 8 | import { Game } from "@/types/game"; 9 | import LazyLoad, { forceCheck } from "react-lazyload"; 10 | import useSongListStore from "@/hooks/useSongListStore.ts"; 11 | import {useShallow} from "zustand/react/shallow"; 12 | 13 | const SongImage = ({ game, id }: { game: Game, id: number }) => { 14 | const [colors, setColors] = useState([]); 15 | 16 | return ( 17 | <> 18 | setColors(colors)} 21 | /> 22 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export const SongTimelineSection = ({ data }: { data: YearInReviewProps }) => { 35 | const { songList } = useSongListStore( 36 | useShallow((state) => ({ songList: state[data.game] })), 37 | ); 38 | 39 | useEffect(() => { 40 | const scrollArea = document.querySelector( 41 | "#shell-root>.mantine-ScrollArea-root>.mantine-ScrollArea-viewport" 42 | ); 43 | 44 | if (!scrollArea) return; 45 | 46 | forceCheck(); 47 | 48 | scrollArea.addEventListener("scroll", forceCheck); 49 | 50 | return () => { 51 | scrollArea.removeEventListener("scroll", forceCheck); 52 | }; 53 | }, []); 54 | 55 | return ( 56 |
57 | {Object.entries(data.player_song_timeline).map(([month, songIds]) => ( 58 |
59 |
{month} 月
60 |
61 | {songIds.map((id) => ( 62 |
63 | 64 | 65 | 66 |
67 | ))} 68 |
69 |
70 | ))} 71 |
72 | ) 73 | } -------------------------------------------------------------------------------- /src/components/YearInReview/UploadRhythmSection.tsx: -------------------------------------------------------------------------------- 1 | import { YearInReviewProps } from "@/pages/public/YearInReview.tsx"; 2 | import { BarChart, BubbleChart } from "@mantine/charts"; 3 | import { Space } from "@mantine/core"; 4 | 5 | export const UploadRhythmSection = ({ data }: { data: YearInReviewProps }) => { 6 | return ( 7 | <> 8 | ({ key: `${key} 月`, value }))} 11 | dataKey="key" 12 | series={[ 13 | { name: 'value', label: '次数', color: 'blue.6' }, 14 | ]} 15 | unit=" 次" 16 | barProps={{ barSize: 50 }} 17 | /> 18 | 19 | ({ 22 | hour: key, 23 | index: 1, 24 | value 25 | }))} 26 | range={[16, 225]} 27 | label="上传量/时" 28 | color="lime.6" 29 | dataKey={{ x: 'hour', y: 'index', z: 'value' }} 30 | /> 31 | 32 | ) 33 | } -------------------------------------------------------------------------------- /src/components/YearInReview/YearSummarySection.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | height: 440px; 3 | padding: var(--mantine-spacing-xl); 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-between; 7 | align-items: flex-start; 8 | } 9 | 10 | .cardImage { 11 | background-size: cover; 12 | background-position: center; 13 | 14 | & .title { 15 | color: var(--mantine-color-white); 16 | } 17 | 18 | & .description { 19 | color: var(--mantine-color-white); 20 | } 21 | } 22 | 23 | .title { 24 | font-family: 25 | Greycliff CF, 26 | sans-serif; 27 | font-weight: 900; 28 | font-size: 32px; 29 | margin-top: var(--mantine-spacing-xs); 30 | } 31 | 32 | .description { 33 | opacity: 0.7; 34 | font-weight: 700; 35 | text-transform: uppercase; 36 | } -------------------------------------------------------------------------------- /src/data/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "软糖酱 @LxBot", 4 | "tags": ["QQ 机器人", "舞萌 DX", "中二节奏"], 5 | "description": "你可以通过落雪咖啡屋提供的 LxBot QQ 机器人,查询你在 maimai DX 查分器中的游戏数据,使用我们精心设计的图片查询样式。", 6 | "image": "lxbot", 7 | "button": "添加", 8 | "url": "https://qun.qq.com/qunpro/robot/qunshare?robot_appid=102072150&robot_uin=2854207029" 9 | }, 10 | { 11 | "title": "秋葉 @AkihaBot", 12 | "tags": ["Telegram 机器人", "舞萌 DX", "中二节奏"], 13 | "description": "秋葉是由 ☆ 开发的 Telegram 机器人,支持查询你在 maimai DX 查分器中的游戏数据并绘制最佳成绩图,也具有包括 maimai、CHUNITHM、Arcaea 在内的各种功能。", 14 | "image": "akihabot", 15 | "button": "添加", 16 | "url": "https://t.me/AkihaBot" 17 | }, 18 | { 19 | "title": "FindMaimaiDX", 20 | "tags": ["手机应用", "舞萌 DX"], 21 | "description": "FindMaimaiDX 是由 Spasol 开发的一款以寻找附近机厅为主要功能的软件,支持 Best 50 歌曲信息具体查看,店铺“附近商店”及“导航”和“可视化地图”等多项功能。", 22 | "image": "findmaimaidx", 23 | "button": "了解更多", 24 | "url": "https://github.com/Spaso1/FindMaimaiDX_Phone" 25 | }, 26 | { 27 | "title": "MaiProberPlus", 28 | "tags": ["手机应用", "舞萌 DX", "中二节奏"], 29 | "description": "基于 Android VpnService 的分数上传器,旨在帮助用户在各种环境下更方便的上传分数至目标查分器。查分器还支持本地分数个性化管理,Best 50 生成。", 30 | "image": "maiproberplus", 31 | "button": "了解更多", 32 | "url": "https://github.com/SkyDynamic/MaiproberPlus" 33 | }, 34 | { 35 | "title": "妖梦 @youmu-bot", 36 | "tags": ["QQ 机器人", "舞萌 DX"], 37 | "description": "基于 NapCat + OverFlow 的机器人,提供了舞萌玩家最常用的一些功能。", 38 | "image": "youmubot", 39 | "button": "了解更多", 40 | "url": "https://youmu.kagg886.top" 41 | } 42 | ] -------------------------------------------------------------------------------- /src/hooks/swr/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "@/utils/api/api.ts"; 2 | 3 | export const fetcher = async (url: string) => { 4 | const res = await fetchAPI(url, { method: "GET" }); 5 | const data = await res.json(); 6 | if (!data.success) { 7 | throw new Error(data.message); 8 | } 9 | return data.data; 10 | } -------------------------------------------------------------------------------- /src/hooks/swr/useAliasVotes.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { VoteProps } from "@/types/alias"; 4 | import { Game } from "@/types/game"; 5 | 6 | export const useAliasVotes = (game: Game) => { 7 | const { 8 | data, 9 | error, 10 | isLoading, 11 | mutate 12 | } = useSWR(`user/${game}/alias/votes`, fetcher); 13 | 14 | return { 15 | votes: data || [], 16 | isLoading: isLoading, 17 | error: error, 18 | mutate: mutate, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/swr/useAliases.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { AliasListProps } from "@/types/alias"; 4 | import { Game } from "@/types/game"; 5 | 6 | export const useAliases = (game: Game, page: number, approved: boolean = false, sort: string = "alias_id", order: string = "desc", songId: number = 0) => { 7 | const params = new URLSearchParams({ 8 | page: String(page), 9 | sort: `${sort} ${order}`, 10 | approved: String(approved) 11 | }); 12 | 13 | if (songId !== 0) { 14 | params.append("song_id", String(songId)); 15 | } 16 | 17 | const { 18 | data, 19 | error, 20 | isLoading, 21 | mutate 22 | } = useSWR(`user/${game}/alias/list?${params.toString()}`, fetcher); 23 | 24 | return { 25 | aliases: data ? data.aliases : [], 26 | pageCount: data ? data.page_count : 0, 27 | pageSize: data ? data.page_size : 0, 28 | isLoading: isLoading, 29 | error: error, 30 | mutate: mutate, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/swr/useBests.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { ChunithmBestsProps, MaimaiBestsProps } from "@/types/score"; 4 | import { Game } from "@/types/game"; 5 | 6 | export const useBests = (game: Game) => { 7 | const { 8 | data, 9 | error, 10 | isLoading, 11 | mutate 12 | } = useSWR(`user/${game}/player/bests`, fetcher); 13 | 14 | return { 15 | bests: data, 16 | isLoading: isLoading, 17 | error: error, 18 | mutate: mutate, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/swr/usePlayer.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { MaimaiPlayerProps, ChunithmPlayerProps } from "@/types/player"; 4 | import { Game } from "@/types/game"; 5 | 6 | export const usePlayer = (game: Game) => { 7 | const { 8 | data, 9 | error, 10 | isLoading, 11 | mutate 12 | } = useSWR(`user/${game}/player`, fetcher); 13 | 14 | return { 15 | player: data, 16 | isLoading: isLoading, 17 | error: error, 18 | mutate: mutate, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/swr/useScores.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { ChunithmScoreProps, MaimaiScoreProps } from "@/types/score"; 4 | import { Game } from "@/types/game"; 5 | 6 | export const useScores = (game: Game) => { 7 | const { 8 | data, 9 | error, 10 | isLoading, 11 | mutate 12 | } = useSWR<(MaimaiScoreProps | ChunithmScoreProps)[]>(`user/${game}/player/scores`, fetcher); 13 | 14 | return { 15 | scores: data || [], 16 | isLoading: isLoading, 17 | error: error, 18 | mutate: mutate, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/swr/useSiteConfig.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | 4 | interface ConfigProps { 5 | resource_hashes: { 6 | [key: string]: { 7 | [key: string]: string; 8 | } 9 | } 10 | } 11 | 12 | export const useSiteConfig = () => { 13 | const { 14 | data, 15 | error, 16 | isLoading, 17 | mutate 18 | } = useSWR(`site/config`, fetcher, { 19 | revalidateIfStale: false, 20 | revalidateOnFocus: false, 21 | revalidateOnReconnect: false 22 | }); 23 | 24 | return { 25 | config: data, 26 | isLoading: isLoading, 27 | error: error, 28 | mutate: mutate, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/hooks/swr/useUser.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { UserProps } from "@/types/user"; 4 | 5 | export const useUser = () => { 6 | const { 7 | data, 8 | error, 9 | isLoading, 10 | mutate 11 | } = useSWR(`user/profile`, fetcher); 12 | 13 | return { 14 | user: data, 15 | isLoading: isLoading, 16 | error: error, 17 | mutate: mutate, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/hooks/swr/useUserConfig.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { ConfigProps } from "@/types/user"; 4 | import { Game } from "@/types/game"; 5 | 6 | export const useUserConfig = (game: Game) => { 7 | const { 8 | data, 9 | error, 10 | isLoading, 11 | mutate 12 | } = useSWR(`user/${game}/config`, fetcher); 13 | 14 | return { 15 | config: data, 16 | isLoading: isLoading, 17 | error: error, 18 | mutate: mutate, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hooks/swr/useUserToken.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { isTokenExpired, isTokenUndefined } from "@/utils/session.ts"; 4 | 5 | export const useUserToken = () => { 6 | const { 7 | data, 8 | error, 9 | isLoading, 10 | mutate 11 | } = useSWR(!isTokenUndefined() && `user/refresh`, fetcher); 12 | 13 | if (data) { 14 | localStorage.setItem("token", data.token); 15 | } 16 | 17 | if (!isTokenExpired()) { 18 | return { 19 | token: localStorage.getItem("token") || "", 20 | isLoading: false, 21 | error: null, 22 | mutate: () => {}, 23 | }; 24 | } 25 | 26 | return { 27 | token: data ? data.token : "", 28 | isLoading: isLoading, 29 | error: error, 30 | mutate: mutate, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/swr/useYearInReview.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { fetcher } from "@/hooks/swr/fetcher.ts"; 3 | import { Game } from "@/types/game"; 4 | import { YearInReviewProps } from "@/pages/public/YearInReview.tsx"; 5 | 6 | export const useYearInReview = (game: Game, year: number, shareToken?: string, agree?: boolean) => { 7 | let url; 8 | if (shareToken) { 9 | url = `${game}/year-in-review/${year}/share/${shareToken}`; 10 | } else { 11 | url = `user/${game}/player/year-in-review/${year}` 12 | } 13 | if (agree) { 14 | url += "?agree=true"; 15 | } 16 | const { 17 | data, 18 | error, 19 | isLoading, 20 | mutate 21 | } = useSWR(url, fetcher, { 22 | revalidateIfStale: false, 23 | revalidateOnFocus: false, 24 | revalidateOnReconnect: false, 25 | onErrorRetry: (error) => { 26 | if (error.status === 400) return; 27 | } 28 | }); 29 | 30 | return { 31 | data: data, 32 | isLoading: isLoading, 33 | error: error, 34 | mutate: mutate, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/hooks/useAliasListStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { AliasList } from "../utils/api/alias.ts"; 3 | import { Game } from "@/types/game"; 4 | 5 | interface AliasListState { 6 | maimai: AliasList, 7 | chunithm: AliasList, 8 | getAliasList: (game: Game) => AliasList, 9 | fetchAliasList: () => Promise, 10 | } 11 | 12 | const useAliasListStore = create((set, get) => ({ 13 | maimai: new AliasList('maimai'), 14 | chunithm: new AliasList('chunithm'), 15 | getAliasList: (game) => get()[game], 16 | fetchAliasList: async () => { 17 | await Promise.all([get().maimai.fetch(), get().chunithm.fetch()]); 18 | 19 | set({ 20 | getAliasList: (game: Game) => get()[game], 21 | }); 22 | }, 23 | })) 24 | 25 | export default useAliasListStore; -------------------------------------------------------------------------------- /src/hooks/useFixedGame.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Game } from "@/types/game"; 3 | 4 | function useFixedGame(defaultGame = 'maimai'): [Game, (game: Game) => void] { 5 | const [game, setGame] = useState(() => { 6 | const storedGame = localStorage.getItem('game'); 7 | return storedGame ? JSON.parse(storedGame) : defaultGame; 8 | }); 9 | 10 | return [game, setGame]; 11 | } 12 | 13 | export default useFixedGame; -------------------------------------------------------------------------------- /src/hooks/useGame.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "@/types/game"; 2 | import { useLocalStorage } from "@mantine/hooks"; 3 | import { useSearchParams } from "react-router-dom"; 4 | 5 | function useGame(defaultGame: Game = 'maimai'): [Game, (game: Game) => void] { 6 | const [searchParams] = useSearchParams(); 7 | 8 | const gameFromSearch = searchParams.get("game"); 9 | const validGames = ["maimai", "chunithm"]; 10 | const gameFromStorage = localStorage.getItem("game"); 11 | 12 | const initialGame: Game = 13 | validGames.includes(gameFromSearch || "") ? (gameFromSearch as Game) : 14 | (gameFromStorage ? (JSON.parse(gameFromStorage) as Game) : defaultGame); 15 | 16 | if (initialGame !== gameFromStorage) { 17 | localStorage.setItem("game", JSON.stringify(initialGame)); 18 | } 19 | 20 | const [game, setGame] = useLocalStorage({ 21 | key: "game", 22 | defaultValue: initialGame, 23 | }); 24 | 25 | return [game, setGame]; 26 | } 27 | 28 | export default useGame; 29 | -------------------------------------------------------------------------------- /src/hooks/useShellViewportSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function useShellViewportSize(): { width: number, height: number } { 4 | const [size, setSize] = useState({ width: 0, height: 0 }); 5 | 6 | useEffect(() => { 7 | const scrollArea = document.querySelector( 8 | "#shell-root>.mantine-ScrollArea-root>.mantine-ScrollArea-viewport" 9 | ); 10 | 11 | const updateSize = () => { 12 | if (scrollArea) { 13 | setSize({ width: scrollArea.clientWidth, height: scrollArea.clientHeight }); 14 | } 15 | }; 16 | 17 | updateSize(); 18 | 19 | window.addEventListener("resize", updateSize); 20 | 21 | return () => { 22 | window.removeEventListener("resize", updateSize); 23 | }; 24 | }, []); 25 | 26 | return size; 27 | } 28 | 29 | export default useShellViewportSize; 30 | -------------------------------------------------------------------------------- /src/hooks/useSongListStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { MaimaiSongList } from "../utils/api/song/maimai.ts"; 3 | import { ChunithmSongList } from "../utils/api/song/chunithm.ts"; 4 | import { Game } from "@/types/game"; 5 | 6 | type SongListState = { 7 | maimai: MaimaiSongList, 8 | chunithm: ChunithmSongList, 9 | getSongList: (game: Game) => MaimaiSongList | ChunithmSongList, 10 | fetchSongList: (hashes?: { 11 | [key: string]: { 12 | [key: string]: string; 13 | } 14 | }) => Promise, 15 | } 16 | 17 | const useSongListStore = create((set, get) => ({ 18 | maimai: new MaimaiSongList(), 19 | chunithm: new ChunithmSongList(), 20 | getSongList: (game) => get()[game], 21 | fetchSongList: async (hashes) => { 22 | await Promise.all([ 23 | get().maimai.fetch(hashes?.maimai.songs), 24 | get().chunithm.fetch(hashes?.chunithm.songs), 25 | ]); 26 | 27 | set((state) => ({ 28 | getSongList: (game: Game) => state[game], 29 | })); 30 | }, 31 | })); 32 | 33 | export default useSongListStore; -------------------------------------------------------------------------------- /src/hooks/useVersionChecker.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { notifications } from "@mantine/notifications"; 3 | import { Button, Stack } from "@mantine/core"; 4 | import useSWR from 'swr'; 5 | 6 | const fetcher = (url: string) => 7 | fetch(url + '?_t=' + Date.now()).then((res) => res.json()); 8 | 9 | export function useVersionChecker(interval = 10000) { 10 | const currentVersionRef = useRef(null); 11 | const notifiedRef = useRef(false); 12 | 13 | const isProd = import.meta.env.PROD; 14 | const { data } = useSWR<{ version: string }>( 15 | isProd ? '/version.json' : null, 16 | fetcher, 17 | { 18 | refreshInterval: interval, 19 | revalidateOnFocus: true, 20 | shouldRetryOnError: true, 21 | } 22 | ); 23 | 24 | useEffect(() => { 25 | if (!isProd || !data?.version) return; 26 | 27 | if (!currentVersionRef.current) { 28 | currentVersionRef.current = data.version; 29 | } else if ( 30 | currentVersionRef.current !== data.version && 31 | !notifiedRef.current 32 | ) { 33 | notifiedRef.current = true; 34 | 35 | notifications.show({ 36 | title: '新版本可用', 37 | message: ( 38 | 39 | 检测到新版本,请刷新页面以获取最新版本 40 | 49 | 50 | ), 51 | color: 'blue', 52 | autoClose: false, 53 | }); 54 | } 55 | }, [data, isProd]); 56 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | 9 | text-rendering: optimizeLegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | -webkit-text-size-adjust: 100%; 13 | } 14 | 15 | button:focus, 16 | button:focus-visible { 17 | outline: none; 18 | } 19 | 20 | img { 21 | opacity: light-dark(1, 0.8); 22 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import './wdyr'; 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import { RouterProvider } from "react-router-dom"; 6 | import { router } from './router'; 7 | import { Helmet } from "react-helmet"; 8 | 9 | import "@mantine/core/styles.css"; 10 | import "@mantine/dates/styles.css"; 11 | import "@mantine/nprogress/styles.css"; 12 | import "@mantine/notifications/styles.css"; 13 | import '@mantine/carousel/styles.css'; 14 | import '@mantine/tiptap/styles.css'; 15 | import '@mantine/charts/styles.css'; 16 | import "mantine-datatable/styles.css"; 17 | import "./index.css"; 18 | 19 | export const API_URL = import.meta.env.VITE_API_URL; 20 | export const ASSET_URL = import.meta.env.VITE_ASSET_URL; 21 | export const RECAPTCHA_SITE_KEY = import.meta.env.VITE_RECAPTCHA_SITE_KEY; 22 | 23 | window.addEventListener('vite:preloadError', () => { 24 | window.location.reload() 25 | }) 26 | 27 | ReactDOM.createRoot(document.getElementById('root')!).render( 28 | 29 | 30 | maimai DX 查分器 31 | 33 | 35 | 36 | 37 | 38 | , 39 | ) 40 | -------------------------------------------------------------------------------- /src/pages/Form.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding-top: 80px; 3 | padding-bottom: 80px; 4 | } 5 | 6 | .card { 7 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)); 8 | } 9 | 10 | .highlight { 11 | position: relative; 12 | background-color: var(--mantine-primary-color-light); 13 | border-radius: var(--mantine-radius-sm); 14 | padding: 4px 8px; 15 | color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-4)); 16 | } -------------------------------------------------------------------------------- /src/pages/Page.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 16px; 3 | max-width: 600px; 4 | } 5 | 6 | .card { 7 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 8 | } 9 | 10 | .section { 11 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 12 | padding: var(--mantine-spacing-md); 13 | } -------------------------------------------------------------------------------- /src/pages/admin/Panel/Panel.tsx: -------------------------------------------------------------------------------- 1 | import { Page } from "@/components/Page/Page.tsx"; 2 | import { AdminUsersSection } from "@/pages/admin/Panel/users/AdminUsersSection.tsx"; 3 | import { AdminDevelopersSection } from "@/pages/admin/Panel/developers/AdminDevelopersSection.tsx"; 4 | 5 | export function Panel() { 6 | return ( 7 | }, 14 | { id: "developers", name: "开发者列表", children: }, 15 | ]} 16 | /> 17 | ); 18 | } -------------------------------------------------------------------------------- /src/pages/admin/Panel/developers/AdminDevelopersSection.module.css: -------------------------------------------------------------------------------- 1 | @import "@/pages/Page.module.css"; 2 | 3 | .section { 4 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); 5 | padding: var(--mantine-spacing-md); 6 | } 7 | 8 | .user { 9 | display: block; 10 | width: 100%; 11 | padding: var(--mantine-spacing-md); 12 | color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); 13 | 14 | &:hover { 15 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 16 | } 17 | } -------------------------------------------------------------------------------- /src/pages/admin/Panel/index.ts: -------------------------------------------------------------------------------- 1 | import { Panel } from './Panel'; 2 | 3 | export default Panel; -------------------------------------------------------------------------------- /src/pages/public/ErrorPage.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | text-align: center; 3 | font-weight: 800; 4 | letter-spacing: -1px; 5 | margin-bottom: var(--mantine-spacing-xs); 6 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); 7 | 8 | @mixin smaller-than $mantine-breakpoint-xs { 9 | font-size: 28px !important; 10 | } 11 | } 12 | 13 | .description { 14 | text-align: center; 15 | 16 | @mixin smaller-than $mantine-breakpoint-xs { 17 | font-size: var(--mantine-font-size-md) !important; 18 | } 19 | } 20 | 21 | .control { 22 | @mixin smaller-than $mantine-breakpoint-xs { 23 | height: 42px !important; 24 | font-size: var(--mantine-font-size-md) !important; 25 | } 26 | } -------------------------------------------------------------------------------- /src/pages/public/Fallback.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Container, Button, Group, Text, Title, Image, Collapse, Code, ScrollArea } from "@mantine/core"; 2 | import { useDisclosure, useViewportSize } from "@mantine/hooks"; 3 | import classes from "./ErrorPage.module.css"; 4 | 5 | export function Fallback({ error, resetErrorBoundary }: { error: Error, resetErrorBoundary: () => void }) { 6 | const { width } = useViewportSize(); 7 | const [opened, { toggle }] = useDisclosure(false); 8 | 9 | return ( 10 | 11 |
12 | 13 |
14 | 发生意料之外的错误 15 | 16 | {error.message} 17 | 18 | 19 |
20 | 21 | 22 | {error.stack} 23 | 24 | 25 |
26 |
27 | 28 | 31 | 32 | 33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /src/pages/public/Home.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | padding-top: 40px; 4 | padding-bottom: 100px; 5 | 6 | @mixin smaller-than $mantine-breakpoint-xs { 7 | padding-top: 16px; 8 | } 9 | } 10 | 11 | .section { 12 | @mixin smaller-than $mantine-breakpoint-xs { 13 | padding: 0; 14 | } 15 | } 16 | 17 | .title { 18 | text-align: center; 19 | font-weight: 800; 20 | font-size: 40px; 21 | letter-spacing: -1px; 22 | margin-bottom: var(--mantine-spacing-xs); 23 | 24 | @mixin smaller-than $mantine-breakpoint-xs { 25 | font-size: 28px; 26 | text-align: left; 27 | } 28 | } 29 | 30 | .description { 31 | text-align: center; 32 | 33 | @mixin smaller-than $mantine-breakpoint-xs { 34 | text-align: left; 35 | font-size: var(--mantine-font-size-md); 36 | } 37 | } 38 | 39 | .highlight { 40 | display: inline-block; 41 | position: relative; 42 | line-height: 1; 43 | padding: 0 8px; 44 | color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-4)); 45 | } 46 | 47 | .highlight::before { 48 | position: absolute; 49 | content: ''; 50 | height: calc(100% + 12px); 51 | width: 100%; 52 | left: 0; 53 | top: -6px; 54 | background-color: var(--mantine-primary-color-light); 55 | border-radius: var(--mantine-radius-sm); 56 | } 57 | 58 | .controls { 59 | margin-top: var(--mantine-spacing-lg); 60 | display: flex; 61 | justify-content: center; 62 | 63 | @mixin smaller-than $mantine-breakpoint-xs { 64 | flex-direction: column; 65 | } 66 | } 67 | 68 | .control { 69 | &:not(:first-of-type) { 70 | margin-left: var(--mantine-spacing-md); 71 | 72 | @mixin smaller-than $mantine-breakpoint-xs { 73 | margin-left: 0; 74 | margin-top: var(--mantine-spacing-md); 75 | } 76 | } 77 | 78 | @mixin smaller-than $mantine-breakpoint-xs { 79 | height: 42px; 80 | font-size: var(--mantine-font-size-md); 81 | } 82 | } 83 | 84 | .featureTitle { 85 | color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); 86 | } -------------------------------------------------------------------------------- /src/pages/public/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Title, Text, Button, Group, Center, Image } from '@mantine/core'; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useEffect } from "react"; 4 | import classes from "./ErrorPage.module.css"; 5 | 6 | export default function NotFound() { 7 | const navigate = useNavigate(); 8 | 9 | useEffect(() => { 10 | document.title = "页面不存在 | maimai DX 查分器"; 11 | }); 12 | 13 | return ( 14 | 15 |
16 | 17 |
18 | 页面不存在 19 | 20 | 你访问的页面不存在,可能已经被删除或者地址错误。 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | } -------------------------------------------------------------------------------- /src/pages/public/YearInReview.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | padding-top: 100px; 4 | padding-bottom: 100px; 5 | 6 | @mixin smaller-than $mantine-breakpoint-xs { 7 | padding-top: 50px; 8 | padding-bottom: 50px; 9 | } 10 | } 11 | 12 | .section { 13 | @mixin smaller-than $mantine-breakpoint-xs { 14 | padding: 0; 15 | } 16 | } 17 | 18 | .playerTitle > .highlight { 19 | font-size: 32px; 20 | 21 | @mixin smaller-than $mantine-breakpoint-xs { 22 | font-size: 20px; 23 | } 24 | } 25 | 26 | .playerTitle > h1 { 27 | font-size: 48px; 28 | 29 | @mixin smaller-than $mantine-breakpoint-xs { 30 | font-size: 24px; 31 | } 32 | } 33 | 34 | .title { 35 | font-weight: 800; 36 | font-size: 64px; 37 | letter-spacing: -1px; 38 | margin-bottom: var(--mantine-spacing-xs); 39 | 40 | @mixin smaller-than $mantine-breakpoint-xs { 41 | font-size: 32px; 42 | text-align: left; 43 | margin-top: var(--mantine-spacing-md); 44 | } 45 | } 46 | 47 | .description { 48 | @mixin smaller-than $mantine-breakpoint-xs { 49 | text-align: left; 50 | font-size: var(--mantine-font-size-md); 51 | } 52 | } 53 | 54 | .highlight { 55 | display: inline-block; 56 | position: relative; 57 | line-height: 1; 58 | padding: 0 8px; 59 | color: light-dark(var(--mantine-primary-color-6), var(--mantine-primary-color-4)); 60 | } 61 | 62 | .highlight::before { 63 | position: absolute; 64 | content: ''; 65 | height: calc(100% + 12px); 66 | width: 100%; 67 | left: 0; 68 | top: -6px; 69 | background-color: var(--mantine-primary-color-light); 70 | border-radius: var(--mantine-radius-sm); 71 | } 72 | 73 | .arrow { 74 | animation: arrow 1s infinite; 75 | } 76 | 77 | @keyframes arrow { 78 | 0% { 79 | transform: translateY(5px); 80 | } 81 | 82 | 50% { 83 | transform: translateY(-5px); 84 | } 85 | 86 | 100% { 87 | transform: translateY(5px); 88 | } 89 | } -------------------------------------------------------------------------------- /src/pages/user/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Loader, Space } from '@mantine/core'; 2 | import { PlayerSection } from '../../components/Profile/PlayerSection'; 3 | import { UserSection } from '../../components/Profile/UserSection'; 4 | import { UserBindSection } from '../../components/Profile/UserBindSection'; 5 | import { UserTokenSection } from "../../components/Profile/UserTokenSection.tsx"; 6 | import { Page } from "@/components/Page/Page.tsx"; 7 | import { useUser } from "@/hooks/swr/useUser.ts"; 8 | 9 | const ProfileContent = () => { 10 | const { isLoading } = useUser(); 11 | 12 | if (isLoading) { 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | ) 31 | } 32 | 33 | export default function Profile() { 34 | return ( 35 | } 41 | /> 42 | ) 43 | } -------------------------------------------------------------------------------- /src/pages/user/Scores/Scores.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import ScoreContext from "@/utils/context.ts"; 3 | import { Page } from "@/components/Page/Page.tsx"; 4 | import { ScoreBestsSection } from "./bests/ScoreBestsSection.tsx"; 5 | import { ScoreBackupSection } from "./backup/ScoreBackupSection.tsx"; 6 | import { ScoreListSection } from "./list/ScoreListSection.tsx"; 7 | import { ChunithmScoreProps, MaimaiScoreProps } from "@/types/score"; 8 | 9 | export function Scores() { 10 | const [score, setScore] = useState(null); 11 | const [createScoreOpened, setCreateScoreOpened] = useState(false); 12 | 13 | return ( 14 | 15 | }, 22 | { id: "bests", name: "分数构成", children: }, 23 | { id: "backup", name: "备份成绩", children: } 24 | ]} 25 | /> 26 | 27 | ); 28 | } -------------------------------------------------------------------------------- /src/pages/user/Scores/backup/ScoreBackupSection.module.css: -------------------------------------------------------------------------------- 1 | .cardButton { 2 | &:hover { 3 | background-color: var(--mantine-color-default-hover); 4 | } 5 | 6 | &[data-disabled] { 7 | opacity: 0.5; 8 | pointer-events: none; 9 | } 10 | } -------------------------------------------------------------------------------- /src/pages/user/Scores/bests/ScoreBestsSection.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Group, Loader, Space, Text, Title } from "@mantine/core"; 2 | import { ScoreList } from "@/components/Scores/ScoreList.tsx"; 3 | import { useBests } from "@/hooks/swr/useBests.ts"; 4 | import { IconDatabaseOff } from "@tabler/icons-react"; 5 | import { RatingSegments } from "@/components/Scores/RatingSegments.tsx"; 6 | import useGame from "@/hooks/useGame.ts"; 7 | 8 | export const ScoreBestsSection = () => { 9 | const [game] = useGame(); 10 | 11 | const { bests, isLoading } = useBests(game); 12 | 13 | if (isLoading) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | if (!bests) { 22 | return ( 23 | 24 | 25 | 没有获取到任何最佳成绩 26 | 27 | ); 28 | } 29 | 30 | return ( 31 | <> 32 | 33 | 34 | {"dx" in bests && ( 35 | 36 | Best 15 37 | 现版本最佳曲目 38 | 39 | 40 | )} 41 | {"standard" in bests && ( 42 | 43 | Best 35 44 | 旧版本最佳曲目 45 | 46 | 47 | )} 48 | {"bests" in bests && ( 49 | 50 | Best 30 51 | 最佳曲目 52 | 53 | 54 | )} 55 | {"selections" in bests && ( 56 | 57 | Selection 10 58 | 候选最佳曲目 59 | 60 | 61 | )} 62 | {"recents" in bests && ( 63 | 64 | Recent 10 65 | 最近游玩的最佳曲目 66 | 67 | 68 | )} 69 | 70 | ) 71 | } -------------------------------------------------------------------------------- /src/pages/user/Scores/index.ts: -------------------------------------------------------------------------------- 1 | import { Scores } from './Scores'; 2 | 3 | export default Scores; -------------------------------------------------------------------------------- /src/pages/user/Sync.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); 3 | } 4 | 5 | .section { 6 | border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 7 | padding: var(--mantine-spacing-md); 8 | } 9 | 10 | .loaderText { 11 | & + & { 12 | padding-top: var(--mantine-spacing-sm); 13 | margin-top: var(--mantine-spacing-sm); 14 | border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); 15 | } 16 | } 17 | 18 | .subParameters { 19 | background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); 20 | padding: 6px 12px; 21 | border-radius: var(--mantine-radius-md); 22 | } -------------------------------------------------------------------------------- /src/types/alias.d.ts: -------------------------------------------------------------------------------- 1 | export interface AliasListProps { 2 | aliases: AliasProps[]; 3 | page_count: number; 4 | page_size: number; 5 | } 6 | 7 | export interface AliasProps { 8 | alias_id: number; 9 | song: { 10 | id: number; 11 | name: string; 12 | }; 13 | song_type: string; 14 | difficulty: number; 15 | alias: string; 16 | approved: boolean; 17 | weight: { 18 | up: number; 19 | down: number; 20 | total: number; 21 | }; 22 | uploader: { 23 | id: number; 24 | name: string; 25 | }; 26 | upload_time: string; 27 | // extra 28 | vote?: VoteProps; 29 | } 30 | 31 | export interface VoteProps { 32 | alias_id?: number; 33 | vote_id?: number; 34 | weight: number; 35 | } -------------------------------------------------------------------------------- /src/types/game.d.ts: -------------------------------------------------------------------------------- 1 | function strEnum(o: Array): { [K in T]: K } { 2 | return o.reduce((res, key) => { 3 | res[key] = key; 4 | return res; 5 | }, Object.create(null)); 6 | } 7 | 8 | const Game = strEnum(['maimai', 'chunithm']); 9 | 10 | type Game = keyof typeof Game; 11 | 12 | export { Game }; -------------------------------------------------------------------------------- /src/types/player.d.ts: -------------------------------------------------------------------------------- 1 | export interface MaimaiPlayerProps { 2 | name: string; 3 | rating: number; 4 | friend_code: number; 5 | trophy?: { 6 | name: string; 7 | color: string; 8 | }; 9 | course_rank: number; 10 | class_rank: number; 11 | star: number; 12 | icon?: CollectionProps; 13 | name_plate?: CollectionProps; 14 | frame?: CollectionProps; 15 | upload_time: string; 16 | } 17 | 18 | export interface ChunithmPlayerProps { 19 | name: string; 20 | level: number; 21 | rating: number; 22 | friend_code: number; 23 | class_emblem: { 24 | base: number; 25 | medal: number; 26 | }; 27 | reborn_count: number; 28 | trophy: { 29 | name: string; 30 | color: string; 31 | }; 32 | over_power: number; 33 | over_power_progress: number; 34 | currency: number; 35 | total_currency: number; 36 | character?: CollectionProps; 37 | name_plate?: CollectionProps; 38 | map_icon?: CollectionProps; 39 | upload_time: string; 40 | } 41 | 42 | interface CollectionProps { 43 | id: number; 44 | name: string; 45 | level?: number; 46 | } -------------------------------------------------------------------------------- /src/types/score.d.ts: -------------------------------------------------------------------------------- 1 | export interface MaimaiScoreProps { 2 | id: number; 3 | song_name: string; 4 | level: string; 5 | level_index: number; 6 | achievements: number; 7 | fc: string; 8 | fs: string; 9 | dx_score: number; 10 | dx_rating: number; 11 | rate: string; 12 | type: string; 13 | play_time?: string; 14 | upload_time: string; 15 | last_played_time?: string; 16 | } 17 | 18 | export interface ChunithmScoreProps { 19 | id: number; 20 | song_name: string; 21 | level: string; 22 | level_index: number; 23 | score: number; 24 | rating: number; 25 | over_power: number; 26 | clear: string; 27 | full_combo: string; 28 | full_chain: string; 29 | rank: string; 30 | play_time?: string; 31 | upload_time: string; 32 | last_played_time?: string; 33 | } 34 | 35 | export interface MaimaiBestsProps { 36 | standard: MaimaiScoreProps[]; 37 | dx: MaimaiScoreProps[]; 38 | standard_total: number; 39 | dx_total: number; 40 | } 41 | 42 | export interface ChunithmBestsProps { 43 | bests: ChunithmScoreProps[]; 44 | selections: ChunithmScoreProps[]; 45 | recents: ChunithmScoreProps[]; 46 | } -------------------------------------------------------------------------------- /src/types/user.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserProps { 2 | id: number; 3 | name: string; 4 | email: string; 5 | permission: number; 6 | register_time: string; 7 | bind: UserBindProps; 8 | token?: string; 9 | // extra 10 | deleted?: boolean; 11 | } 12 | 13 | export interface UserBindProps { 14 | qq?: number; 15 | } 16 | 17 | export interface ConfigProps { 18 | allow_crawl_scores?: boolean; 19 | allow_crawl_name_plate?: boolean; 20 | allow_crawl_frame?: boolean; 21 | allow_crawl_map_icon?: boolean; 22 | crawl_scores_method?: string; 23 | crawl_scores_difficulty?: string[]; 24 | allow_third_party_fetch_player?: boolean; 25 | allow_third_party_fetch_scores?: boolean; 26 | allow_third_party_write_data?: boolean; 27 | } -------------------------------------------------------------------------------- /src/utils/api/alias.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "./api.ts"; 2 | 3 | export async function createAlias(game: string, data: any) { 4 | return fetchAPI(`user/${game}/alias`, { method: "POST", body: data }); 5 | } 6 | 7 | export async function voteAlias(game: string, aliasId: number, vote: boolean) { 8 | return fetchAPI(`user/${game}/alias/${aliasId}/vote/${vote ? 'up' : 'down'}`, { method: "POST" }); 9 | } 10 | 11 | export async function deleteUserAlias(game: string, aliasId: number) { 12 | return fetchAPI(`user/${game}/alias/${aliasId}`, { method: "DELETE" }); 13 | } 14 | 15 | export async function deleteAlias(game: string, aliasId: number) { 16 | return fetchAPI(`user/admin/${game}/alias/${aliasId}`, { method: "DELETE" }); 17 | } 18 | 19 | export async function approveAlias(game: string, aliasId: number) { 20 | return fetchAPI(`user/admin/${game}/alias/${aliasId}/approve`, { method: "POST" }); 21 | } 22 | 23 | export class AliasList { 24 | game: string = ""; 25 | aliases: any[] = []; 26 | searchMap: any = {}; 27 | 28 | constructor(game: string) { 29 | this.game = game; 30 | } 31 | 32 | private parseSearchMap() { 33 | this.aliases.forEach((alias) => { 34 | alias.aliases.forEach((aliasText: string) => { 35 | this.searchMap[aliasText] = this.searchMap[aliasText] || []; 36 | this.searchMap[aliasText].push(alias.song_id); 37 | }) 38 | }) 39 | } 40 | 41 | async fetch() { 42 | const res = await fetchAPI(`${this.game}/alias/list`, { method: "GET" }); 43 | const data = await res?.json(); 44 | this.aliases = data.aliases; 45 | this.parseSearchMap(); 46 | 47 | return this.aliases; 48 | } 49 | } -------------------------------------------------------------------------------- /src/utils/api/api.ts: -------------------------------------------------------------------------------- 1 | import { isTokenExpired, isTokenUndefined } from "@/utils/session.ts"; 2 | import { mutate } from "swr"; 3 | 4 | export const API_URL = import.meta.env.VITE_API_URL; 5 | 6 | export async function fetchAPI(endpoint: string, options: { method: string, body?: any, headers?: any }) { 7 | if (!isTokenUndefined() && isTokenExpired()) { 8 | if (endpoint !== "user/refresh") { 9 | await mutate("user/refresh"); 10 | } 11 | } 12 | 13 | const { method = "GET", body, headers } = options; 14 | 15 | return await fetch(`${API_URL}/${endpoint}`, { 16 | method, 17 | credentials: "include", 18 | headers: { 19 | "Authorization": `Bearer ${localStorage.getItem("token")}`, 20 | "Content-Type": "application/json", 21 | ...headers, 22 | }, 23 | body: body ? JSON.stringify(body) : undefined, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/api/comment.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "./api.ts"; 2 | 3 | export async function getCommentList(game: string, params: URLSearchParams) { 4 | return fetchAPI(`user/${game}/comment/list?${params.toString()}`, { method: "GET" }); 5 | } 6 | 7 | export async function createComment(game: string, data: any) { 8 | return fetchAPI(`user/${game}/comment`, { method: "POST", body: data }); 9 | } 10 | 11 | export async function deleteComment(game: string, commentId: number) { 12 | return fetchAPI(`user/${game}/comment/${commentId}`, { method: "DELETE" }); 13 | } 14 | 15 | export async function likeComment(game: string, commentId: number) { 16 | return fetchAPI(`user/${game}/comment/${commentId}/like`, { method: "POST" }); 17 | } 18 | 19 | export async function unlikeComment(game: string, commentId: number) { 20 | return fetchAPI(`user/${game}/comment/${commentId}/like`, { method: "DELETE" }); 21 | } -------------------------------------------------------------------------------- /src/utils/api/developer.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "./api.ts"; 2 | 3 | export async function sendDeveloperApply(data: any) { 4 | return fetchAPI(`user/developer/apply`, { method: "POST", body: data }); 5 | } 6 | 7 | export async function getDeveloperApply() { 8 | return fetchAPI("user/developer/apply", { method: "GET" }); 9 | } 10 | 11 | export async function resetDeveloperApiKey() { 12 | return fetchAPI("user/developer/reset", { method: "POST" }); 13 | } 14 | 15 | export async function getDevelopers() { 16 | return fetchAPI("user/admin/developers", { method: "GET" }); 17 | } 18 | 19 | export async function revokeDeveloper(data: any) { 20 | return fetchAPI("user/admin/developer", { method: "DELETE", body: data }); 21 | } -------------------------------------------------------------------------------- /src/utils/api/misc.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "@/utils/api/api.ts"; 2 | import { Game } from "@/types/game"; 3 | 4 | export async function getCrawlStatistic(game: Game) { 5 | return fetchAPI(`${game}/crawl/statistic`, { method: "GET" }); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/api/player.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "./api.ts"; 2 | import { ChunithmPlayerProps, MaimaiPlayerProps } from "@/types/player"; 3 | 4 | export async function getPlayerRatingTrend(game: string, version: number) { 5 | return fetchAPI(`user/${game}/player/trend?version=${version}`, { method: "GET" }); 6 | } 7 | 8 | export async function unbindPlayer(game: string) { 9 | return fetchAPI(`user/${game}/player`, { method: "DELETE" }); 10 | } 11 | 12 | export async function deletePlayerScores(game: string) { 13 | return fetchAPI(`user/${game}/player/scores`, { method: "DELETE" }); 14 | } 15 | 16 | export async function createPlayerScores(game: string, scores: any) { 17 | return fetchAPI(`user/${game}/player/scores`, { method: "POST", body: { scores } }); 18 | } 19 | 20 | export async function getPlayerPlateById(game: string, id: number) { 21 | return fetchAPI(`user/${game}/player/plate/${id}`, { method: "GET" }); 22 | } 23 | 24 | export async function getPlateList(game: string, required: boolean) { 25 | return fetchAPI(`${game}/plate/list?required=${required}`, { method: "GET" }); 26 | } 27 | 28 | export async function getPlateById(game: string, id: number) { 29 | return fetchAPI(`${game}/plate/${id}`, { method: "GET" }); 30 | } 31 | 32 | export function isMaimaiPlayerProps(obj: unknown): obj is MaimaiPlayerProps { 33 | if (!obj) return false; 34 | return typeof obj === 'object' && 'course_rank' in obj; 35 | } 36 | 37 | export function isChunithmPlayerProps(obj: unknown): obj is ChunithmPlayerProps { 38 | if (!obj) return false; 39 | return typeof obj === 'object' && 'over_power' in obj; 40 | } -------------------------------------------------------------------------------- /src/utils/api/song/song.tsx: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "@/utils/api/api.ts"; 2 | import { Game } from "@/types/game"; 3 | 4 | export async function getSong(game: Game, id: number, version: number) { 5 | const res = await fetchAPI(`${game}/song/${id}?version=${version}`, { 6 | method: "GET", 7 | }); 8 | if (res.status === 404) return null; 9 | return await res.json(); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/api/user.ts: -------------------------------------------------------------------------------- 1 | import { fetchAPI } from "./api.ts"; 2 | 3 | export async function updateUserProfile(data: unknown) { 4 | return fetchAPI("user/profile", { method: "POST", body: data }); 5 | } 6 | 7 | export async function getUserCrawlToken() { 8 | return fetchAPI("user/crawl/token", { method: "GET" }); 9 | } 10 | 11 | export async function getCrawlStatus() { 12 | return fetchAPI("user/crawl/status", { method: "GET" }); 13 | } 14 | 15 | export async function updateUserConfig(game: string, data: unknown) { 16 | return fetchAPI(`user/${game}/config`, { method: "POST", body: data }); 17 | } 18 | 19 | export async function updateUserBind(data: unknown) { 20 | return fetchAPI("user/bind", { method: "POST", body: data }); 21 | } 22 | 23 | export async function generateUserToken() { 24 | return fetchAPI("user/token", { method: "POST" }); 25 | } 26 | 27 | export async function logoutUser() { 28 | return fetchAPI("user/logout", { method: "POST" }); 29 | } 30 | 31 | export async function deleteSelfUser() { 32 | return fetchAPI("user", { method: "DELETE" }); 33 | } 34 | 35 | export async function getUsers() { 36 | return fetchAPI("user/admin/users", { method: "GET" }); 37 | } 38 | 39 | export async function deleteUsers(data: unknown) { 40 | return fetchAPI("user/admin/users", { method: "DELETE", body: data }); 41 | } 42 | 43 | export async function sendBatchEmail(data: unknown) { 44 | return fetchAPI("user/admin/email", { method: "POST", body: data }); 45 | } 46 | 47 | export async function updateUser(userId: number, data: unknown) { 48 | return fetchAPI(`user/admin/user/${userId}`, { method: "POST", body: data }); 49 | } 50 | 51 | export async function deleteUser(userId: number) { 52 | return fetchAPI(`user/admin/user/${userId}`, { method: "DELETE" }); 53 | } -------------------------------------------------------------------------------- /src/utils/checkProxy.ts: -------------------------------------------------------------------------------- 1 | export const checkProxy = async () => { 2 | const result = { 3 | proxyAvailable: false, 4 | networkError: false, 5 | }; 6 | try { 7 | await fetch(`https://maimai.wahlap.com/maimai-mobile/error/`, { mode: 'no-cors' }); 8 | } catch (err) { 9 | try { 10 | await fetch(window.location.href, { mode: 'no-cors' }); 11 | result.proxyAvailable = true; 12 | } catch (err) { 13 | result.networkError = true; 14 | } 15 | } 16 | return result; 17 | } -------------------------------------------------------------------------------- /src/utils/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export default createContext({} as any); -------------------------------------------------------------------------------- /src/utils/modal.tsx: -------------------------------------------------------------------------------- 1 | import { modals } from "@mantine/modals"; 2 | import { Text } from "@mantine/core"; 3 | import { ReactNode } from "react"; 4 | 5 | export const openAlertModal = (title: string, content: string | ReactNode, { ...props }: any = {}) => { 6 | modals.openConfirmModal({ 7 | title, 8 | centered: true, 9 | withCloseButton: false, 10 | children: ( 11 | 12 | {content} 13 | 14 | ), 15 | labels: { confirm: '确定', cancel: '取消' }, 16 | onConfirm: () => modals.closeAll(), 17 | ...props, 18 | }); 19 | } 20 | 21 | export const openConfirmModal = (title: string, content: string | ReactNode, onConfirm: () => void, { ...props }: any = {}) => { 22 | modals.openConfirmModal({ 23 | title, 24 | centered: true, 25 | withCloseButton: false, 26 | children: ( 27 | 28 | {content} 29 | 30 | ), 31 | labels: { confirm: '确定', cancel: '取消' }, 32 | onCancel: () => modals.closeAll(), 33 | onConfirm: onConfirm, 34 | ...props, 35 | }); 36 | } 37 | 38 | export const openRetryModal = (title: string, content: string | ReactNode, onConfirm: () => void, { ...props }: any = {}) => { 39 | modals.openConfirmModal({ 40 | title, 41 | centered: true, 42 | withCloseButton: false, 43 | children: ( 44 | 45 | {content} 46 | 47 | ), 48 | labels: { confirm: '重试', cancel: '取消' }, 49 | onCancel: () => modals.closeAll(), 50 | onConfirm: onConfirm, 51 | ...props, 52 | }); 53 | } -------------------------------------------------------------------------------- /src/utils/reCaptcha.ts: -------------------------------------------------------------------------------- 1 | class ReCaptcha { 2 | private readonly siteKey: string; 3 | private action: string; 4 | 5 | constructor(siteKey: string, action: string) { 6 | this.siteKey = siteKey; 7 | this.action = action; 8 | } 9 | 10 | render() { 11 | const scriptId = 'recaptcha-script'; 12 | 13 | if (!document.getElementById(scriptId)) { 14 | const script = document.createElement('script'); 15 | script.id = scriptId; 16 | script.src = `https://recaptcha.google.cn/recaptcha/api.js?render=${this.siteKey}`; 17 | document.body.appendChild(script); 18 | } 19 | } 20 | 21 | async getToken(): Promise { 22 | return new Promise((resolve) => { 23 | window.grecaptcha.ready(() => { 24 | window.grecaptcha.execute(this.siteKey, { action: this.action }).then((token: string) => { 25 | resolve(token); 26 | }); 27 | }); 28 | }); 29 | } 30 | 31 | destroy() { 32 | const scriptId = 'recaptcha-script'; 33 | const script = document.getElementById(scriptId); 34 | if (script) { 35 | script.remove(); 36 | } 37 | 38 | const iframe = document.querySelector('.grecaptcha-badge'); 39 | if (iframe) { 40 | iframe.remove(); 41 | } 42 | } 43 | } 44 | 45 | export default ReCaptcha; -------------------------------------------------------------------------------- /src/utils/session.ts: -------------------------------------------------------------------------------- 1 | import { logoutUser } from "./api/user.ts"; 2 | 3 | const getLoginSessionPayload = () => { 4 | const token = localStorage.getItem('token'); 5 | if (!token) { 6 | return null; 7 | } 8 | 9 | try { 10 | return JSON.parse(atob(token.split('.')[1])); 11 | } catch (error) { 12 | return null; 13 | } 14 | } 15 | 16 | export const getLoginUserId = () => { 17 | const payload = getLoginSessionPayload(); 18 | return payload ? payload.id : null; 19 | } 20 | 21 | export const isTokenExpired = () => { 22 | const token = localStorage.getItem('token'); 23 | if (!token) { 24 | return true; 25 | } 26 | 27 | try { 28 | const payload = JSON.parse(atob(token.split('.')[1])); 29 | const currentTime = Date.now(); 30 | const expirationTime = payload.exp * 1000; 31 | 32 | if (isNaN(expirationTime)) { 33 | return true; 34 | } 35 | 36 | return currentTime > expirationTime; 37 | } catch (error) { 38 | return true; 39 | } 40 | }; 41 | 42 | export const isTokenUndefined = () => { 43 | const token = localStorage.getItem('token'); 44 | return !token; 45 | } 46 | 47 | export const isTokenValid = () => { 48 | return !isTokenExpired(); 49 | } 50 | 51 | export const logout = () => { 52 | localStorage.removeItem('token'); 53 | logoutUser(); 54 | } 55 | 56 | export enum UserPermission { 57 | User = 1 << 0, 58 | Developer = 1 << 1, 59 | Administrator = 1 << 2, 60 | } 61 | 62 | export const checkPermission = (permission: UserPermission) => { 63 | const token = localStorage.getItem('token'); 64 | if (!token) { 65 | return false; 66 | } 67 | 68 | try { 69 | const payload = JSON.parse(atob(token.split('.')[1])); 70 | return (payload.permission & permission) !== 0; 71 | } catch (error) { 72 | return false; 73 | } 74 | } 75 | 76 | export const permissionToList = (permission: number) => { 77 | const list = []; 78 | if ((permission & UserPermission.User) !== 0) { 79 | list.push(UserPermission.User); 80 | } 81 | if ((permission & UserPermission.Developer) !== 0) { 82 | list.push(UserPermission.Developer); 83 | } 84 | if ((permission & UserPermission.Administrator) !== 0) { 85 | list.push(UserPermission.Administrator); 86 | } 87 | return list; 88 | } 89 | 90 | export const listToPermission = (list: UserPermission[]) => { 91 | let permission = 0; 92 | for (const item of list) { 93 | permission |= item; 94 | } 95 | return permission; 96 | } -------------------------------------------------------------------------------- /src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | export const validateEmail = (email: string) => { 2 | return /^([a-zA-Z0-9])(([a-zA-Z0-9])*([._-])?([a-zA-Z0-9]))*@(([a-zA-Z0-9\-])+(\.))+([a-zA-Z]{2,4})+$/.test(email); 3 | } 4 | 5 | export const validateUserName = (name: string) => { 6 | return /^[a-zA-Z0-9_]{4,16}$/.test(name); 7 | } 8 | 9 | export const validatePassword = (password: string) => { 10 | return password.length >= 6 || password.length <= 16; 11 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module "dayjs/locale/*"; 3 | declare module "react-color-extractor"; 4 | declare module 'react-helmet'; 5 | declare module 'wordcloud'; -------------------------------------------------------------------------------- /src/wdyr.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import whyDidYouRender from '@welldone-software/why-did-you-render'; 3 | 4 | if (process.env.NODE_ENV === 'development') { 5 | whyDidYouRender(React, { 6 | trackAllPureComponents: true, 7 | }); 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "node", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "allowSyntheticDefaultImports": true, 23 | 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | }, 28 | "include": ["src"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import * as fs from "node:fs"; 4 | import path from "node:path"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | { 11 | name: 'generate-version', 12 | closeBundle() { 13 | const version = Date.now().toString(); 14 | fs.writeFileSync(path.resolve(__dirname, 'dist/version.json'), JSON.stringify({ version })); 15 | } 16 | } 17 | ], 18 | resolve: { 19 | alias: { 20 | '@': '/src', 21 | } 22 | }, 23 | assetsInclude: ['**/*.md'], 24 | build: { 25 | rollupOptions: { 26 | output: { 27 | manualChunks: { 28 | react: ['react', 'react-dom'], 29 | reactRouter: ['react-router', 'react-router-dom'], 30 | mdi: ['@mdi/js'], 31 | }, 32 | entryFileNames: `assets/[hash].js`, 33 | chunkFileNames: `assets/[hash].js`, 34 | assetFileNames: `assets/[hash].[ext]`, 35 | } 36 | }, 37 | reportCompressedSize: false, 38 | sourcemap: false, 39 | } 40 | }) 41 | --------------------------------------------------------------------------------