From dacefa2b51879f7344d9472d658e8e7994cb5eb2 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 4 Jan 2026 03:35:57 -0800 Subject: [PATCH] feat(leaderboard): add toggle control for mobile-app ranking page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add public /leaderboard/status endpoint (no auth required) - Add LeaderboardService in mobile-app to fetch board status - Update RankingPage to show "待开启" when board is disabled - Connect admin-web leaderboard page to real API - Board toggle now takes effect immediately 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../leaderboard-public.controller.ts | 37 +++ .../src/api/dto/leaderboard-config.dto.ts | 14 ++ .../src/modules/api.module.ts | 2 + .../src/app/(dashboard)/leaderboard/page.tsx | 210 +++++++++++++++--- .../src/infrastructure/api/endpoints.ts | 5 + .../src/services/leaderboardService.ts | 72 ++++++ .../lib/core/di/injection_container.dart | 7 + .../core/services/leaderboard_service.dart | 98 ++++++++ .../presentation/pages/ranking_page.dart | 210 +++++++++++++++--- 9 files changed, 589 insertions(+), 66 deletions(-) create mode 100644 backend/services/leaderboard-service/src/api/controllers/leaderboard-public.controller.ts create mode 100644 frontend/admin-web/src/services/leaderboardService.ts create mode 100644 frontend/mobile-app/lib/core/services/leaderboard_service.dart diff --git a/backend/services/leaderboard-service/src/api/controllers/leaderboard-public.controller.ts b/backend/services/leaderboard-service/src/api/controllers/leaderboard-public.controller.ts new file mode 100644 index 00000000..60354975 --- /dev/null +++ b/backend/services/leaderboard-service/src/api/controllers/leaderboard-public.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { LeaderboardApplicationService } from '../../application/services/leaderboard-application.service'; +import { LeaderboardStatusResponseDto } from '../dto/leaderboard-config.dto'; + +/** + * 龙虎榜公开接口(无需认证) + * 供移动端查询榜单开关状态 + */ +@ApiTags('龙虎榜-公开') +@Controller('leaderboard') +export class LeaderboardPublicController { + constructor( + private readonly leaderboardService: LeaderboardApplicationService, + ) {} + + @Get('status') + @ApiOperation({ summary: '获取榜单开关状态(公开接口)' }) + @ApiResponse({ + status: 200, + description: '榜单开关状态', + type: LeaderboardStatusResponseDto, + }) + async getStatus() { + const config = await this.leaderboardService.getConfig(); + + return { + code: 0, + message: 'success', + data: { + dailyEnabled: config.dailyEnabled, + weeklyEnabled: config.weeklyEnabled, + monthlyEnabled: config.monthlyEnabled, + }, + }; + } +} diff --git a/backend/services/leaderboard-service/src/api/dto/leaderboard-config.dto.ts b/backend/services/leaderboard-service/src/api/dto/leaderboard-config.dto.ts index 63b4f2d8..096cd0f0 100644 --- a/backend/services/leaderboard-service/src/api/dto/leaderboard-config.dto.ts +++ b/backend/services/leaderboard-service/src/api/dto/leaderboard-config.dto.ts @@ -2,6 +2,20 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsInt, IsOptional, Min, Max } from 'class-validator'; import { Type } from 'class-transformer'; +/** + * 公开榜单状态响应 DTO(供移动端使用) + */ +export class LeaderboardStatusResponseDto { + @ApiProperty({ description: '日榜开关' }) + dailyEnabled: boolean; + + @ApiProperty({ description: '周榜开关' }) + weeklyEnabled: boolean; + + @ApiProperty({ description: '月榜开关' }) + monthlyEnabled: boolean; +} + /** * 榜单配置响应 DTO */ diff --git a/backend/services/leaderboard-service/src/modules/api.module.ts b/backend/services/leaderboard-service/src/modules/api.module.ts index 4559b1f6..ddb0e09e 100644 --- a/backend/services/leaderboard-service/src/modules/api.module.ts +++ b/backend/services/leaderboard-service/src/modules/api.module.ts @@ -4,6 +4,7 @@ import { JwtModule } from '@nestjs/jwt'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { HealthController } from '../api/controllers/health.controller'; import { LeaderboardController } from '../api/controllers/leaderboard.controller'; +import { LeaderboardPublicController } from '../api/controllers/leaderboard-public.controller'; import { LeaderboardConfigController } from '../api/controllers/leaderboard-config.controller'; import { VirtualAccountController } from '../api/controllers/virtual-account.controller'; import { JwtStrategy } from '../api/strategies/jwt.strategy'; @@ -31,6 +32,7 @@ import { InfrastructureModule } from './infrastructure.module'; controllers: [ HealthController, LeaderboardController, + LeaderboardPublicController, LeaderboardConfigController, VirtualAccountController, ], diff --git a/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx b/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx index cfdbb4c2..b56f526b 100644 --- a/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/leaderboard/page.tsx @@ -1,11 +1,11 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import Image from 'next/image'; import { toast } from '@/components/common'; import { PageContainer } from '@/components/layout'; import { cn } from '@/utils/helpers'; -import { useLeaderboardStore } from '@/store/zustand/useLeaderboardStore'; +import { leaderboardService, LeaderboardConfig } from '@/services/leaderboardService'; import styles from './leaderboard.module.scss'; /** @@ -59,31 +59,103 @@ const getRankStyle = (rank: number) => { * 基于 UIPro Figma 设计实现 */ export default function LeaderboardPage() { - const { virtualSettings, updateVirtualSettings, displaySettings, updateDisplaySettings } = useLeaderboardStore(); + // 配置状态 + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + // 本地编辑状态 const [boardEnabled, setBoardEnabled] = useState({ - daily: true, + daily: false, weekly: false, monthly: false, }); - + const [virtualSettings, setVirtualSettings] = useState({ + enabled: false, + virtualAccountCount: 0, + }); + const [displayLimit, setDisplayLimit] = useState(20); const [showRules, setShowRules] = useState(false); + // 加载配置 + const loadConfig = useCallback(async () => { + try { + setLoading(true); + const data = await leaderboardService.getConfig(); + setConfig(data); + setBoardEnabled({ + daily: data.dailyEnabled, + weekly: data.weeklyEnabled, + monthly: data.monthlyEnabled, + }); + setVirtualSettings({ + enabled: data.virtualRankingEnabled, + virtualAccountCount: data.virtualAccountCount, + }); + setDisplayLimit(data.displayLimit); + } catch (error) { + console.error('加载配置失败:', error); + toast.error('加载配置失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadConfig(); + }, [loadConfig]); + + // 切换榜单开关 + const handleToggleBoard = async (type: 'daily' | 'weekly' | 'monthly', enabled: boolean) => { + const previousState = boardEnabled[type]; + setBoardEnabled((prev) => ({ ...prev, [type]: enabled })); + + try { + await leaderboardService.updateSwitch({ type, enabled }); + toast.success(`${type === 'daily' ? '日榜' : type === 'weekly' ? '周榜' : '月榜'}已${enabled ? '开启' : '关闭'}`); + } catch (error) { + console.error('切换榜单失败:', error); + toast.error('操作失败,请重试'); + setBoardEnabled((prev) => ({ ...prev, [type]: previousState })); + } + }; + // 导出数据 const handleExport = () => { toast.success('导出功能开发中'); }; - // 保存设置 - const handleSave = () => { - toast.success('设置已保存'); + // 保存虚拟排名和显示设置 + const handleSave = async () => { + try { + setSaving(true); + await Promise.all([ + leaderboardService.updateVirtualRanking({ + enabled: virtualSettings.enabled, + accountCount: virtualSettings.virtualAccountCount, + }), + leaderboardService.updateDisplaySettings({ + displayLimit, + }), + ]); + toast.success('设置已保存'); + } catch (error) { + console.error('保存设置失败:', error); + toast.error('保存失败,请重试'); + } finally { + setSaving(false); + } }; // 渲染切换开关 - const renderToggle = (checked: boolean, onChange: (checked: boolean) => void) => ( + const renderToggle = (checked: boolean, onChange: (checked: boolean) => void, disabled?: boolean) => (
onChange(!checked)} + className={cn( + styles.leaderboard__toggle, + checked ? styles['leaderboard__toggle--on'] : styles['leaderboard__toggle--off'], + disabled && styles['leaderboard__toggle--disabled'] + )} + onClick={() => !disabled && onChange(!checked)} >
+
+
加载中...
+
+ + ); + } + return (
@@ -160,7 +242,7 @@ export default function LeaderboardPage() {
关闭 {renderToggle(boardEnabled.daily, (checked) => - setBoardEnabled({ ...boardEnabled, daily: checked }) + handleToggleBoard('daily', checked) )} 开启
@@ -210,19 +292,44 @@ export default function LeaderboardPage() {
关闭 {renderToggle(boardEnabled.weekly, (checked) => - setBoardEnabled({ ...boardEnabled, weekly: checked }) + handleToggleBoard('weekly', checked) )} 开启
-
-
- 空状态 + {boardEnabled.weekly ? ( +
+
+
+ 排名 +
+
+ 头像 +
+
+ 昵称 +
+
+ 认种数量 +
+
+ 团队数据 +
+
+
+ {dailyRankings.map(renderRankingRow)} +
-

榜单未开启

- 待激活 -
+ ) : ( +
+
+ 空状态 +
+

榜单未开启

+ 待激活 +
+ )}
{/* 月榜 */} @@ -235,19 +342,44 @@ export default function LeaderboardPage() {
关闭 {renderToggle(boardEnabled.monthly, (checked) => - setBoardEnabled({ ...boardEnabled, monthly: checked }) + handleToggleBoard('monthly', checked) )} 开启
-
-
- 空状态 + {boardEnabled.monthly ? ( +
+
+
+ 排名 +
+
+ 头像 +
+
+ 昵称 +
+
+ 认种数量 +
+
+ 团队数据 +
+
+
+ {dailyRankings.map(renderRankingRow)} +
-

榜单未开启

- 待激活 -
+ ) : ( +
+
+ 空状态 +
+

榜单未开启

+ 待激活 +
+ )}
@@ -263,7 +395,7 @@ export default function LeaderboardPage() {
启用虚拟排名 {renderToggle(virtualSettings.enabled, (checked) => - updateVirtualSettings({ enabled: checked }) + setVirtualSettings((prev) => ({ ...prev, enabled: checked })) )}
@@ -275,7 +407,10 @@ export default function LeaderboardPage() { className={styles.leaderboard__numberInput} value={virtualSettings.virtualAccountCount} onChange={(e) => - updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) || 0 }) + setVirtualSettings((prev) => ({ + ...prev, + virtualAccountCount: parseInt(e.target.value) || 0, + })) } /> - updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) }) + setVirtualSettings((prev) => ({ + ...prev, + virtualAccountCount: parseInt(e.target.value), + })) } /> @@ -332,10 +470,8 @@ export default function LeaderboardPage() {