feat(leaderboard): add toggle control for mobile-app ranking page

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-04 03:35:57 -08:00
parent 52afe72f17
commit dacefa2b51
9 changed files with 589 additions and 66 deletions

View File

@ -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,
},
};
}
}

View File

@ -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
*/

View File

@ -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,
],

View File

@ -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<LeaderboardConfig | null>(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) => (
<div
className={cn(styles.leaderboard__toggle, checked ? styles['leaderboard__toggle--on'] : styles['leaderboard__toggle--off'])}
onClick={() => 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)}
>
<div
className={cn(
@ -134,6 +206,16 @@ export default function LeaderboardPage() {
);
};
if (loading) {
return (
<PageContainer title="龙虎榜管理">
<div className={styles.leaderboard}>
<div className={styles.leaderboard__loading}>...</div>
</div>
</PageContainer>
);
}
return (
<PageContainer title="龙虎榜管理">
<div className={styles.leaderboard}>
@ -160,7 +242,7 @@ export default function LeaderboardPage() {
<div className={styles.leaderboard__boardToggle}>
<span className={styles.leaderboard__toggleLabel}></span>
{renderToggle(boardEnabled.daily, (checked) =>
setBoardEnabled({ ...boardEnabled, daily: checked })
handleToggleBoard('daily', checked)
)}
<span className={styles.leaderboard__toggleLabel}></span>
</div>
@ -210,19 +292,44 @@ export default function LeaderboardPage() {
<div className={styles.leaderboard__boardToggle}>
<span className={styles.leaderboard__toggleLabel}></span>
{renderToggle(boardEnabled.weekly, (checked) =>
setBoardEnabled({ ...boardEnabled, weekly: checked })
handleToggleBoard('weekly', checked)
)}
<span className={styles.leaderboard__toggleLabel}></span>
</div>
</div>
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background.svg" width={64} height={64} alt="空状态" />
{boardEnabled.weekly ? (
<div className={styles.leaderboard__table}>
<div className={styles.leaderboard__tableHeader}>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--rank'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--avatar'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--nickname'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--count'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--team'])}>
</div>
</div>
<div className={styles.leaderboard__tableBody}>
{dailyRankings.map(renderRankingRow)}
</div>
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
) : (
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background.svg" width={64} height={64} alt="空状态" />
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
)}
</div>
{/* 月榜 */}
@ -235,19 +342,44 @@ export default function LeaderboardPage() {
<div className={styles.leaderboard__boardToggle}>
<span className={styles.leaderboard__toggleLabel}></span>
{renderToggle(boardEnabled.monthly, (checked) =>
setBoardEnabled({ ...boardEnabled, monthly: checked })
handleToggleBoard('monthly', checked)
)}
<span className={styles.leaderboard__toggleLabel}></span>
</div>
</div>
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background1.svg" width={64} height={64} alt="空状态" />
{boardEnabled.monthly ? (
<div className={styles.leaderboard__table}>
<div className={styles.leaderboard__tableHeader}>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--rank'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--avatar'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--nickname'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--count'])}>
</div>
<div className={cn(styles.leaderboard__tableHeaderCell, styles['leaderboard__tableHeaderCell--team'])}>
</div>
</div>
<div className={styles.leaderboard__tableBody}>
{dailyRankings.map(renderRankingRow)}
</div>
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
) : (
<div className={styles.leaderboard__empty}>
<div className={styles.leaderboard__emptyIcon}>
<Image src="/images/Background1.svg" width={64} height={64} alt="空状态" />
</div>
<h4 className={styles.leaderboard__emptyTitle}></h4>
<span className={styles.leaderboard__emptyStatus}></span>
</div>
)}
</div>
</div>
@ -263,7 +395,7 @@ export default function LeaderboardPage() {
<div className={styles.leaderboard__settingsItem}>
<span className={styles.leaderboard__settingsLabel}></span>
{renderToggle(virtualSettings.enabled, (checked) =>
updateVirtualSettings({ enabled: checked })
setVirtualSettings((prev) => ({ ...prev, enabled: checked }))
)}
</div>
@ -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,
}))
}
/>
<input
@ -285,7 +420,10 @@ export default function LeaderboardPage() {
max={10}
value={virtualSettings.virtualAccountCount}
onChange={(e) =>
updateVirtualSettings({ virtualAccountCount: parseInt(e.target.value) })
setVirtualSettings((prev) => ({
...prev,
virtualAccountCount: parseInt(e.target.value),
}))
}
/>
</div>
@ -332,10 +470,8 @@ export default function LeaderboardPage() {
</div>
<select
className={styles.leaderboard__select}
value={displaySettings.frontendDisplayCount}
onChange={(e) =>
updateDisplaySettings({ frontendDisplayCount: parseInt(e.target.value) as 10 | 20 | 50 })
}
value={displayLimit}
onChange={(e) => setDisplayLimit(parseInt(e.target.value))}
>
<option value={10}>10</option>
<option value={20}>20</option>
@ -346,8 +482,12 @@ export default function LeaderboardPage() {
{/* 保存按钮 */}
<div className={styles.leaderboard__settingsFooter}>
<button className={styles.leaderboard__saveBtn} onClick={handleSave}>
<button
className={styles.leaderboard__saveBtn}
onClick={handleSave}
disabled={saving}
>
{saving ? '保存中...' : '保存设置'}
</button>
</div>
</div>

View File

@ -31,6 +31,11 @@ export const API_ENDPOINTS = {
MONTHLY: '/v1/leaderboard/monthly',
SETTINGS: '/v1/leaderboard/settings',
EXPORT: '/v1/leaderboard/export',
// 配置管理
CONFIG: '/v1/leaderboard/config',
CONFIG_SWITCH: '/v1/leaderboard/config/switch',
CONFIG_VIRTUAL: '/v1/leaderboard/config/virtual',
CONFIG_DISPLAY: '/v1/leaderboard/config/display',
},
// 授权管理 (authorization-service)

View File

@ -0,0 +1,72 @@
/**
*
* API调用
*/
import apiClient from '@/infrastructure/api/client';
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
/** 龙虎榜配置 */
export interface LeaderboardConfig {
id: string;
configKey: string;
dailyEnabled: boolean;
weeklyEnabled: boolean;
monthlyEnabled: boolean;
virtualRankingEnabled: boolean;
virtualAccountCount: number;
displayLimit: number;
refreshIntervalMinutes: number;
}
/** 更新榜单开关请求 */
export interface UpdateLeaderboardSwitchRequest {
type: 'daily' | 'weekly' | 'monthly';
enabled: boolean;
}
/** 更新虚拟排名设置请求 */
export interface UpdateVirtualRankingRequest {
enabled: boolean;
accountCount: number;
}
/** 更新显示设置请求 */
export interface UpdateDisplaySettingsRequest {
displayLimit: number;
}
/**
*
*/
export const leaderboardService = {
/**
*
*/
async getConfig(): Promise<LeaderboardConfig> {
return apiClient.get(API_ENDPOINTS.LEADERBOARD.CONFIG);
},
/**
*
*/
async updateSwitch(request: UpdateLeaderboardSwitchRequest): Promise<void> {
return apiClient.put(API_ENDPOINTS.LEADERBOARD.CONFIG_SWITCH, request);
},
/**
*
*/
async updateVirtualRanking(request: UpdateVirtualRankingRequest): Promise<void> {
return apiClient.put(API_ENDPOINTS.LEADERBOARD.CONFIG_VIRTUAL, request);
},
/**
*
*/
async updateDisplaySettings(request: UpdateDisplaySettingsRequest): Promise<void> {
return apiClient.put(API_ENDPOINTS.LEADERBOARD.CONFIG_DISPLAY, request);
},
};
export default leaderboardService;

View File

@ -12,6 +12,7 @@ import '../services/planting_service.dart';
import '../services/reward_service.dart';
import '../services/notification_service.dart';
import '../services/system_config_service.dart';
import '../services/leaderboard_service.dart';
import '../services/contract_signing_service.dart';
import '../services/contract_check_service.dart';
import '../services/pending_action_service.dart';
@ -109,6 +110,12 @@ final systemConfigServiceProvider = Provider<SystemConfigService>((ref) {
return SystemConfigService(apiClient: apiClient);
});
// Leaderboard Service Provider ( leaderboard-service)
final leaderboardServiceProvider = Provider<LeaderboardService>((ref) {
final apiClient = ref.watch(apiClientProvider);
return LeaderboardService(apiClient: apiClient);
});
// Contract Signing Service Provider ( planting-service)
final contractSigningServiceProvider = Provider<ContractSigningService>((ref) {
final apiClient = ref.watch(apiClientProvider);

View File

@ -0,0 +1,98 @@
import 'package:flutter/foundation.dart';
import '../network/api_client.dart';
///
class LeaderboardStatus {
///
final bool dailyEnabled;
///
final bool weeklyEnabled;
///
final bool monthlyEnabled;
LeaderboardStatus({
required this.dailyEnabled,
required this.weeklyEnabled,
required this.monthlyEnabled,
});
factory LeaderboardStatus.fromJson(Map<String, dynamic> json) {
return LeaderboardStatus(
dailyEnabled: json['dailyEnabled'] ?? false,
weeklyEnabled: json['weeklyEnabled'] ?? false,
monthlyEnabled: json['monthlyEnabled'] ?? false,
);
}
///
bool get hasAnyEnabled => dailyEnabled || weeklyEnabled || monthlyEnabled;
///
factory LeaderboardStatus.allDisabled() {
return LeaderboardStatus(
dailyEnabled: false,
weeklyEnabled: false,
monthlyEnabled: false,
);
}
}
///
class LeaderboardService {
final ApiClient _apiClient;
///
LeaderboardStatus? _cachedStatus;
///
DateTime? _lastFetchTime;
/// 5
static const Duration _cacheExpiration = Duration(minutes: 5);
LeaderboardService({required ApiClient apiClient}) : _apiClient = apiClient;
///
///
/// [forceRefresh] 使
Future<LeaderboardStatus> getStatus({bool forceRefresh = false}) async {
//
if (!forceRefresh && _cachedStatus != null && _lastFetchTime != null) {
final cacheAge = DateTime.now().difference(_lastFetchTime!);
if (cacheAge < _cacheExpiration) {
return _cachedStatus!;
}
}
try {
final response = await _apiClient.get(
'/leaderboard-service/api/v1/leaderboard/status',
);
if (response.data != null && response.data['data'] != null) {
_cachedStatus = LeaderboardStatus.fromJson(response.data['data']);
} else {
_cachedStatus = LeaderboardStatus.allDisabled();
}
_lastFetchTime = DateTime.now();
debugPrint(
'[LeaderboardService] 获取榜单状态成功: daily=${_cachedStatus!.dailyEnabled}, weekly=${_cachedStatus!.weeklyEnabled}, monthly=${_cachedStatus!.monthlyEnabled}');
return _cachedStatus!;
} catch (e) {
debugPrint('[LeaderboardService] 获取榜单状态失败: $e');
//
return _cachedStatus ?? LeaderboardStatus.allDisabled();
}
}
///
void clearCache() {
_cachedStatus = null;
_lastFetchTime = null;
}
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/leaderboard_service.dart';
///
enum RankingType { daily, weekly, monthly }
@ -26,6 +28,12 @@ class RankingItem {
});
}
/// Provider
final leaderboardStatusProvider = FutureProvider<LeaderboardStatus>((ref) async {
final leaderboardService = ref.watch(leaderboardServiceProvider);
return leaderboardService.getStatus();
});
/// -
///
class RankingPage extends ConsumerStatefulWidget {
@ -102,8 +110,22 @@ class _RankingPageState extends ConsumerState<RankingPage> {
);
}
///
bool _isCurrentBoardEnabled(LeaderboardStatus status) {
switch (_selectedRankingType) {
case RankingType.daily:
return status.dailyEnabled;
case RankingType.weekly:
return status.weeklyEnabled;
case RankingType.monthly:
return status.monthlyEnabled;
}
}
@override
Widget build(BuildContext context) {
final statusAsync = ref.watch(leaderboardStatusProvider);
return Scaffold(
body: Container(
decoration: const BoxDecoration(
@ -116,24 +138,112 @@ class _RankingPageState extends ConsumerState<RankingPage> {
],
),
),
child: Column(
children: [
// Tab栏
_buildHeader(),
//
_buildFilterBar(),
//
Expanded(
child: _buildRankingList(),
),
],
child: statusAsync.when(
data: (status) => _buildContent(status),
loading: () => _buildLoadingContent(),
error: (error, stack) => _buildContent(LeaderboardStatus.allDisabled()),
),
),
);
}
/// Tab栏
Widget _buildHeader() {
///
Widget _buildLoadingContent() {
return Column(
children: [
_buildHeaderWithStatus(null),
const Expanded(
child: Center(
child: CircularProgressIndicator(
color: Color(0xFFD4AF37),
),
),
),
],
);
}
///
Widget _buildContent(LeaderboardStatus status) {
final isCurrentEnabled = _isCurrentBoardEnabled(status);
return Column(
children: [
// Tab栏
_buildHeaderWithStatus(status),
//
if (isCurrentEnabled) ...[
_buildFilterBar(),
Expanded(child: _buildRankingList()),
] else ...[
//
Expanded(child: _buildDisabledState()),
],
],
);
}
///
Widget _buildDisabledState() {
String boardName;
switch (_selectedRankingType) {
case RankingType.daily:
boardName = '日榜';
break;
case RankingType.weekly:
boardName = '周榜';
break;
case RankingType.monthly:
boardName = '月榜';
break;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0x1A8B5A2B),
borderRadius: BorderRadius.circular(40),
),
child: const Icon(
Icons.hourglass_empty,
size: 40,
color: Color(0xFF8B5A2B),
),
),
const SizedBox(height: 24),
//
Text(
'$boardName待开启',
style: const TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 8),
//
const Text(
'该榜单暂未开启,请稍后再来',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
);
}
/// Tab栏
Widget _buildHeaderWithStatus(LeaderboardStatus? status) {
return Container(
color: const Color(0xCCFFF5E6),
child: SafeArea(
@ -159,15 +269,15 @@ class _RankingPageState extends ConsumerState<RankingPage> {
),
),
// Tab栏
_buildTabBar(),
_buildTabBarWithStatus(status),
],
),
),
);
}
/// Tab栏
Widget _buildTabBar() {
/// Tab栏
Widget _buildTabBarWithStatus(LeaderboardStatus? status) {
return Container(
height: 45,
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -183,25 +293,31 @@ class _RankingPageState extends ConsumerState<RankingPage> {
children: [
//
Expanded(
child: _buildTabItem(
child: _buildTabItemWithStatus(
title: '日榜',
type: RankingType.daily,
isSelected: _selectedRankingType == RankingType.daily,
isEnabled: status?.dailyEnabled ?? false,
onTap: () => _selectRankingType(RankingType.daily),
),
),
//
Expanded(
child: _buildTabItem(
child: _buildTabItemWithStatus(
title: '周榜',
type: RankingType.weekly,
isSelected: _selectedRankingType == RankingType.weekly,
isEnabled: status?.weeklyEnabled ?? false,
onTap: () => _selectRankingType(RankingType.weekly),
),
),
//
Expanded(
child: _buildTabItem(
child: _buildTabItemWithStatus(
title: '月榜',
type: RankingType.monthly,
isSelected: _selectedRankingType == RankingType.monthly,
isEnabled: status?.monthlyEnabled ?? false,
onTap: () => _selectRankingType(RankingType.monthly),
),
),
@ -210,12 +326,21 @@ class _RankingPageState extends ConsumerState<RankingPage> {
);
}
/// Tab项
Widget _buildTabItem({
/// Tab项
Widget _buildTabItemWithStatus({
required String title,
required RankingType type,
required bool isSelected,
required bool isEnabled,
required VoidCallback onTap,
}) {
// "待开启"
final textColor = isSelected
? const Color(0xFFD4AF37)
: isEnabled
? const Color(0xFF8B5A2B)
: const Color(0xFFB0A090);
return GestureDetector(
onTap: onTap,
child: Container(
@ -229,16 +354,39 @@ class _RankingPageState extends ConsumerState<RankingPage> {
),
),
child: Center(
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.21,
color: isSelected ? const Color(0xFFD4AF37) : const Color(0xFF8B5A2B),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.21,
color: textColor,
),
),
if (!isEnabled) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: const Color(0x1A8B5A2B),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'待开启',
style: TextStyle(
fontSize: 10,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
),
],
],
),
),
),