feat(pending-actions): add user pending actions system
Add a fully optional pending actions system that allows admins to configure specific tasks that users must complete after login. Backend (identity-service): - Add UserPendingAction model to Prisma schema - Add migration for user_pending_actions table - Add PendingActionService with full CRUD operations - Add user-facing API (GET list, POST complete) - Add admin API (CRUD, batch create) Admin Web: - Add pending actions management page - Support single/batch create, edit, cancel, delete - View action details including completion time - Filter by userId, actionCode, status Flutter Mobile App: - Add PendingActionService and PendingActionCheckService - Add PendingActionsPage for forced task execution - Integrate into splash_page login flow - Users must complete all pending tasks in priority order 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
04a8c56ad6
commit
28e0396a65
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- =============================================================================
|
||||||
|
-- 创建用户待办操作表
|
||||||
|
-- 用于后台配置指定用户登录后需要执行的特定操作
|
||||||
|
-- 完全可选,不影响现有任何功能
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- 创建 user_pending_actions 表
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_pending_actions" (
|
||||||
|
"id" BIGSERIAL NOT NULL,
|
||||||
|
"user_id" BIGINT NOT NULL,
|
||||||
|
"action_code" VARCHAR(50) NOT NULL,
|
||||||
|
"action_params" JSONB,
|
||||||
|
"status" VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"expires_at" TIMESTAMP(3),
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"completed_at" TIMESTAMP(3),
|
||||||
|
"created_by" VARCHAR(50),
|
||||||
|
|
||||||
|
CONSTRAINT "user_pending_actions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建索引
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_user_pending_action" ON "user_pending_actions"("user_id", "status");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_action_code" ON "user_pending_actions"("action_code");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_pending_action_status" ON "user_pending_actions"("status");
|
||||||
|
|
@ -450,3 +450,42 @@ model KycConfig {
|
||||||
|
|
||||||
@@map("kyc_configs")
|
@@map("kyc_configs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 用户待办操作表
|
||||||
|
// 用于后台配置指定用户登录后需要执行的特定操作
|
||||||
|
// 完全可选,不影响现有任何功能
|
||||||
|
// ============================================
|
||||||
|
model UserPendingAction {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
userId BigInt @map("user_id")
|
||||||
|
|
||||||
|
// 操作代码: ADOPTION_WIZARD, SETTLE_REWARDS, BIND_PHONE, FORCE_KYC 等
|
||||||
|
// 前端根据此代码决定跳转到哪个页面/流程
|
||||||
|
actionCode String @map("action_code") @db.VarChar(50)
|
||||||
|
|
||||||
|
// 操作参数 (JSON格式,完全自定义)
|
||||||
|
// 例如: { "treeType": "durian", "rewardIds": ["r1", "r2"], "amount": 100 }
|
||||||
|
actionParams Json? @map("action_params")
|
||||||
|
|
||||||
|
// 操作状态: PENDING(待执行), COMPLETED(已完成), CANCELLED(已取消)
|
||||||
|
status String @default("PENDING") @db.VarChar(20)
|
||||||
|
|
||||||
|
// 优先级 (数值越大优先级越高,前端按此排序执行)
|
||||||
|
priority Int @default(0)
|
||||||
|
|
||||||
|
// 可选过期时间 (过期后前端可忽略)
|
||||||
|
expiresAt DateTime? @map("expires_at")
|
||||||
|
|
||||||
|
// 时间戳
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
completedAt DateTime? @map("completed_at")
|
||||||
|
|
||||||
|
// 创建人 (后台管理员)
|
||||||
|
createdBy String? @map("created_by") @db.VarChar(50)
|
||||||
|
|
||||||
|
@@index([userId, status], name: "idx_user_pending_action")
|
||||||
|
@@index([actionCode], name: "idx_action_code")
|
||||||
|
@@index([status], name: "idx_pending_action_status")
|
||||||
|
@@map("user_pending_actions")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Get,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { PendingActionService } from '@/application/services/pending-action.service';
|
||||||
|
import {
|
||||||
|
JwtAuthGuard,
|
||||||
|
CurrentUser,
|
||||||
|
CurrentUserData,
|
||||||
|
} from '@/shared/guards/jwt-auth.guard';
|
||||||
|
import {
|
||||||
|
CreatePendingActionDto,
|
||||||
|
UpdatePendingActionDto,
|
||||||
|
QueryPendingActionsDto,
|
||||||
|
BatchCreatePendingActionDto,
|
||||||
|
CompletePendingActionDto,
|
||||||
|
} from '@/api/dto/request/pending-action.dto';
|
||||||
|
|
||||||
|
// ========== 用户端控制器 ==========
|
||||||
|
|
||||||
|
@ApiTags('用户待办操作 - 用户端')
|
||||||
|
@Controller('user/pending-actions')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class PendingActionController {
|
||||||
|
constructor(private readonly pendingActionService: PendingActionService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的待办操作列表
|
||||||
|
* 前端登录成功后调用此接口,获取需要执行的特定操作
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取我的待办操作列表',
|
||||||
|
description: '获取当前用户待执行的操作列表,按优先级降序排列。前端根据 actionCode 决定跳转到哪个页面。',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '成功',
|
||||||
|
schema: {
|
||||||
|
example: {
|
||||||
|
code: 'OK',
|
||||||
|
message: 'success',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
userId: '12345',
|
||||||
|
actionCode: 'ADOPTION_WIZARD',
|
||||||
|
actionParams: { treeType: 'durian', location: '马来西亚' },
|
||||||
|
status: 'PENDING',
|
||||||
|
priority: 10,
|
||||||
|
expiresAt: null,
|
||||||
|
createdAt: '2025-01-01T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async getMyPendingActions(@CurrentUser() user: CurrentUserData) {
|
||||||
|
const result = await this.pendingActionService.getMyPendingActions(user.userId);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: 'success',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记操作完成
|
||||||
|
*/
|
||||||
|
@Post(':id/complete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '标记操作完成',
|
||||||
|
description: '用户完成操作后调用此接口标记完成',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '操作ID' })
|
||||||
|
async completeAction(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@Param('id') actionId: string,
|
||||||
|
@Body() dto: CompletePendingActionDto,
|
||||||
|
) {
|
||||||
|
const result = await this.pendingActionService.completeAction(
|
||||||
|
user.userId,
|
||||||
|
actionId,
|
||||||
|
dto.resultData,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: '操作已完成',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 后台管理控制器 ==========
|
||||||
|
|
||||||
|
@ApiTags('用户待办操作 - 后台管理')
|
||||||
|
@Controller('admin/pending-actions')
|
||||||
|
export class AdminPendingActionController {
|
||||||
|
constructor(private readonly pendingActionService: PendingActionService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户待办操作
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '创建用户待办操作',
|
||||||
|
description: '为指定用户创建一个待办操作,用户登录后会看到此操作',
|
||||||
|
})
|
||||||
|
async createAction(@Body() dto: CreatePendingActionDto) {
|
||||||
|
const result = await this.pendingActionService.createAction(dto, 'admin');
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: '操作已创建',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建用户待办操作
|
||||||
|
*/
|
||||||
|
@Post('batch')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '批量创建用户待办操作',
|
||||||
|
description: '为多个用户批量创建相同的待办操作',
|
||||||
|
})
|
||||||
|
async batchCreateActions(@Body() dto: BatchCreatePendingActionDto) {
|
||||||
|
const result = await this.pendingActionService.batchCreateActions(dto, 'admin');
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: `成功创建 ${result.successCount} 个操作`,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户待办操作列表
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '查询用户待办操作列表',
|
||||||
|
description: '支持按用户ID、操作代码、状态筛选,分页返回',
|
||||||
|
})
|
||||||
|
async queryActions(@Query() dto: QueryPendingActionsDto) {
|
||||||
|
const result = await this.pendingActionService.queryActions(dto);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: 'success',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个操作详情
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '获取操作详情',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '操作ID' })
|
||||||
|
async getAction(@Param('id') actionId: string) {
|
||||||
|
const result = await this.pendingActionService.getAction(actionId);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: 'success',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户待办操作
|
||||||
|
*/
|
||||||
|
@Put(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '更新用户待办操作',
|
||||||
|
description: '更新操作的参数、状态、优先级、过期时间等',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '操作ID' })
|
||||||
|
async updateAction(
|
||||||
|
@Param('id') actionId: string,
|
||||||
|
@Body() dto: UpdatePendingActionDto,
|
||||||
|
) {
|
||||||
|
const result = await this.pendingActionService.updateAction(actionId, dto);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: '操作已更新',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消用户待办操作 (软删除)
|
||||||
|
*/
|
||||||
|
@Post(':id/cancel')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '取消用户待办操作',
|
||||||
|
description: '将操作状态改为 CANCELLED,用户将不再看到此操作',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '操作ID' })
|
||||||
|
async cancelAction(@Param('id') actionId: string) {
|
||||||
|
const result = await this.pendingActionService.cancelAction(actionId);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: '操作已取消',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户待办操作 (硬删除)
|
||||||
|
*/
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '删除用户待办操作',
|
||||||
|
description: '彻底删除操作记录',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: '操作ID' })
|
||||||
|
async deleteAction(@Param('id') actionId: string) {
|
||||||
|
await this.pendingActionService.deleteAction(actionId);
|
||||||
|
return {
|
||||||
|
code: 'OK',
|
||||||
|
message: '操作已删除',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,3 +19,4 @@ export * from './bind-email.dto';
|
||||||
export * from './unbind-email.dto';
|
export * from './unbind-email.dto';
|
||||||
export * from './register-without-sms-verify.dto';
|
export * from './register-without-sms-verify.dto';
|
||||||
export * from './change-phone.dto';
|
export * from './change-phone.dto';
|
||||||
|
export * from './pending-action.dto';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsInt,
|
||||||
|
IsObject,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
Min,
|
||||||
|
MaxLength,
|
||||||
|
IsArray,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作状态枚举
|
||||||
|
*/
|
||||||
|
export enum PendingActionStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
COMPLETED = 'COMPLETED',
|
||||||
|
CANCELLED = 'CANCELLED',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户待办操作 DTO (后台管理)
|
||||||
|
*/
|
||||||
|
export class CreatePendingActionDto {
|
||||||
|
@ApiProperty({ example: '12345', description: '目标用户ID' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '用户ID不能为空' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'ADOPTION_WIZARD',
|
||||||
|
description: '操作代码,如: ADOPTION_WIZARD, SETTLE_REWARDS, BIND_PHONE, FORCE_KYC',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '操作代码不能为空' })
|
||||||
|
@MaxLength(50)
|
||||||
|
actionCode: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: { treeType: 'durian', location: '马来西亚' },
|
||||||
|
description: '操作参数 (JSON对象)',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
actionParams?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 10, description: '优先级 (数值越大优先级越高)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: '2025-12-31T23:59:59Z',
|
||||||
|
description: '过期时间 (ISO 8601格式)',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建用户待办操作 DTO
|
||||||
|
*/
|
||||||
|
export class BatchCreatePendingActionDto {
|
||||||
|
@ApiProperty({ type: [String], description: '目标用户ID列表' })
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsNotEmpty({ each: true })
|
||||||
|
userIds: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'ADOPTION_WIZARD', description: '操作代码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '操作代码不能为空' })
|
||||||
|
@MaxLength(50)
|
||||||
|
actionCode: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '操作参数' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
actionParams?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 0, description: '优先级' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '过期时间' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户待办操作 DTO
|
||||||
|
*/
|
||||||
|
export class UpdatePendingActionDto {
|
||||||
|
@ApiPropertyOptional({ description: '操作参数' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
actionParams?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: PendingActionStatus, description: '状态' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(PendingActionStatus)
|
||||||
|
status?: PendingActionStatus;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '优先级' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '过期时间' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户待办操作 DTO (后台管理)
|
||||||
|
*/
|
||||||
|
export class QueryPendingActionsDto {
|
||||||
|
@ApiPropertyOptional({ description: '用户ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '操作代码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
actionCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: PendingActionStatus, description: '状态' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(PendingActionStatus)
|
||||||
|
status?: PendingActionStatus;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 1, description: '页码' })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 20, description: '每页数量' })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户标记操作完成 DTO
|
||||||
|
*/
|
||||||
|
export class CompletePendingActionDto {
|
||||||
|
@ApiPropertyOptional({ description: '完成时附带的数据' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
resultData?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './user-profile.dto';
|
export * from './user-profile.dto';
|
||||||
export * from './device.dto';
|
export * from './device.dto';
|
||||||
|
export * from './pending-action.dto';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户待办操作响应 DTO
|
||||||
|
*/
|
||||||
|
export class PendingActionResponseDto {
|
||||||
|
@ApiProperty({ example: '1', description: '操作ID' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '12345', description: '用户ID' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'ADOPTION_WIZARD', description: '操作代码' })
|
||||||
|
actionCode: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: { treeType: 'durian', location: '马来西亚' },
|
||||||
|
description: '操作参数',
|
||||||
|
})
|
||||||
|
actionParams: Record<string, any> | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'PENDING', description: '状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10, description: '优先级' })
|
||||||
|
priority: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '过期时间' })
|
||||||
|
expiresAt: Date | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '完成时间' })
|
||||||
|
completedAt: Date | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '创建人' })
|
||||||
|
createdBy: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户待办操作列表响应 DTO (带分页)
|
||||||
|
*/
|
||||||
|
export class PendingActionListResponseDto {
|
||||||
|
@ApiProperty({ type: [PendingActionResponseDto], description: '操作列表' })
|
||||||
|
items: PendingActionResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100, description: '总数' })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1, description: '当前页' })
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 20, description: '每页数量' })
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建结果响应 DTO
|
||||||
|
*/
|
||||||
|
export class BatchCreateResultDto {
|
||||||
|
@ApiProperty({ example: 10, description: '成功创建数量' })
|
||||||
|
successCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 2, description: '失败数量' })
|
||||||
|
failedCount: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
type: [String],
|
||||||
|
description: '失败的用户ID列表',
|
||||||
|
})
|
||||||
|
failedUserIds?: string[];
|
||||||
|
}
|
||||||
|
|
@ -24,12 +24,17 @@ import { AuthController } from '@/api/controllers/auth.controller';
|
||||||
import { TotpController } from '@/api/controllers/totp.controller';
|
import { TotpController } from '@/api/controllers/totp.controller';
|
||||||
import { InternalController } from '@/api/controllers/internal.controller';
|
import { InternalController } from '@/api/controllers/internal.controller';
|
||||||
import { KycController, AdminKycController } from '@/api/controllers/kyc.controller';
|
import { KycController, AdminKycController } from '@/api/controllers/kyc.controller';
|
||||||
|
import {
|
||||||
|
PendingActionController,
|
||||||
|
AdminPendingActionController,
|
||||||
|
} from '@/api/controllers/pending-action.controller';
|
||||||
|
|
||||||
// Application Services
|
// Application Services
|
||||||
import { UserApplicationService } from '@/application/services/user-application.service';
|
import { UserApplicationService } from '@/application/services/user-application.service';
|
||||||
import { TokenService } from '@/application/services/token.service';
|
import { TokenService } from '@/application/services/token.service';
|
||||||
import { TotpService } from '@/application/services/totp.service';
|
import { TotpService } from '@/application/services/totp.service';
|
||||||
import { KycApplicationService } from '@/application/services/kyc-application.service';
|
import { KycApplicationService } from '@/application/services/kyc-application.service';
|
||||||
|
import { PendingActionService } from '@/application/services/pending-action.service';
|
||||||
import { BlockchainWalletHandler } from '@/application/event-handlers/blockchain-wallet.handler';
|
import { BlockchainWalletHandler } from '@/application/event-handlers/blockchain-wallet.handler';
|
||||||
import { MpcKeygenCompletedHandler } from '@/application/event-handlers/mpc-keygen-completed.handler';
|
import { MpcKeygenCompletedHandler } from '@/application/event-handlers/mpc-keygen-completed.handler';
|
||||||
import { WalletRetryTask } from '@/application/tasks/wallet-retry.task';
|
import { WalletRetryTask } from '@/application/tasks/wallet-retry.task';
|
||||||
|
|
@ -134,13 +139,14 @@ export class DomainModule {}
|
||||||
TokenService,
|
TokenService,
|
||||||
TotpService,
|
TotpService,
|
||||||
KycApplicationService,
|
KycApplicationService,
|
||||||
|
PendingActionService,
|
||||||
// Event Handlers - 通过注入到 UserApplicationService 来确保它们被初始化
|
// Event Handlers - 通过注入到 UserApplicationService 来确保它们被初始化
|
||||||
BlockchainWalletHandler,
|
BlockchainWalletHandler,
|
||||||
MpcKeygenCompletedHandler,
|
MpcKeygenCompletedHandler,
|
||||||
// Tasks - 定时任务
|
// Tasks - 定时任务
|
||||||
WalletRetryTask,
|
WalletRetryTask,
|
||||||
],
|
],
|
||||||
exports: [UserApplicationService, TokenService, TotpService, KycApplicationService],
|
exports: [UserApplicationService, TokenService, TotpService, KycApplicationService, PendingActionService],
|
||||||
})
|
})
|
||||||
export class ApplicationModule {}
|
export class ApplicationModule {}
|
||||||
|
|
||||||
|
|
@ -156,6 +162,8 @@ export class ApplicationModule {}
|
||||||
InternalController,
|
InternalController,
|
||||||
KycController,
|
KycController,
|
||||||
AdminKycController,
|
AdminKycController,
|
||||||
|
PendingActionController,
|
||||||
|
AdminPendingActionController,
|
||||||
],
|
],
|
||||||
providers: [UserAccountRepositoryImpl],
|
providers: [UserAccountRepositoryImpl],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||||
|
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
||||||
|
import {
|
||||||
|
PendingActionStatus,
|
||||||
|
CreatePendingActionDto,
|
||||||
|
UpdatePendingActionDto,
|
||||||
|
QueryPendingActionsDto,
|
||||||
|
BatchCreatePendingActionDto,
|
||||||
|
} from '@/api/dto/request/pending-action.dto';
|
||||||
|
import {
|
||||||
|
PendingActionResponseDto,
|
||||||
|
PendingActionListResponseDto,
|
||||||
|
BatchCreateResultDto,
|
||||||
|
} from '@/api/dto/response/pending-action.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户待办操作服务
|
||||||
|
*
|
||||||
|
* 提供用户待办操作的增删改查功能,用于后台配置指定用户登录后需要执行的特定操作。
|
||||||
|
* 完全独立,不影响现有任何功能。
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PendingActionService {
|
||||||
|
private readonly logger = new Logger(PendingActionService.name);
|
||||||
|
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// ==================== 用户端接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户的待办操作列表
|
||||||
|
* 返回 PENDING 状态且未过期的操作,按优先级降序排列
|
||||||
|
*/
|
||||||
|
async getMyPendingActions(userId: string): Promise<PendingActionResponseDto[]> {
|
||||||
|
this.logger.log(`[PendingAction] Getting pending actions for user: ${userId}`);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const actions = await this.prisma.userPendingAction.findMany({
|
||||||
|
where: {
|
||||||
|
userId: BigInt(userId),
|
||||||
|
status: PendingActionStatus.PENDING,
|
||||||
|
OR: [
|
||||||
|
{ expiresAt: null },
|
||||||
|
{ expiresAt: { gt: now } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ priority: 'desc' },
|
||||||
|
{ createdAt: 'asc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions.map((action) => this.toResponseDto(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户标记操作完成
|
||||||
|
*/
|
||||||
|
async completeAction(
|
||||||
|
userId: string,
|
||||||
|
actionId: string,
|
||||||
|
resultData?: Record<string, any>,
|
||||||
|
): Promise<PendingActionResponseDto> {
|
||||||
|
this.logger.log(`[PendingAction] Completing action: ${actionId} for user: ${userId}`);
|
||||||
|
|
||||||
|
const action = await this.prisma.userPendingAction.findFirst({
|
||||||
|
where: {
|
||||||
|
id: BigInt(actionId),
|
||||||
|
userId: BigInt(userId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
throw new ApplicationError('操作不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.status !== PendingActionStatus.PENDING) {
|
||||||
|
throw new ApplicationError('操作已完成或已取消');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.prisma.userPendingAction.update({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
data: {
|
||||||
|
status: PendingActionStatus.COMPLETED,
|
||||||
|
completedAt: new Date(),
|
||||||
|
// 如果有结果数据,合并到 actionParams 中
|
||||||
|
...(resultData && {
|
||||||
|
actionParams: {
|
||||||
|
...(action.actionParams as object || {}),
|
||||||
|
_result: resultData,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[PendingAction] Action completed: ${actionId}`);
|
||||||
|
|
||||||
|
return this.toResponseDto(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 后台管理接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户待办操作 (后台管理)
|
||||||
|
*/
|
||||||
|
async createAction(
|
||||||
|
dto: CreatePendingActionDto,
|
||||||
|
createdBy?: string,
|
||||||
|
): Promise<PendingActionResponseDto> {
|
||||||
|
this.logger.log(`[PendingAction] Creating action for user: ${dto.userId}, code: ${dto.actionCode}`);
|
||||||
|
|
||||||
|
// 验证用户是否存在
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: BigInt(dto.userId) },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ApplicationError('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = await this.prisma.userPendingAction.create({
|
||||||
|
data: {
|
||||||
|
userId: BigInt(dto.userId),
|
||||||
|
actionCode: dto.actionCode,
|
||||||
|
actionParams: dto.actionParams ?? undefined,
|
||||||
|
priority: dto.priority ?? 0,
|
||||||
|
expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null,
|
||||||
|
createdBy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[PendingAction] Action created: ${action.id}`);
|
||||||
|
|
||||||
|
return this.toResponseDto(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建用户待办操作 (后台管理)
|
||||||
|
*/
|
||||||
|
async batchCreateActions(
|
||||||
|
dto: BatchCreatePendingActionDto,
|
||||||
|
createdBy?: string,
|
||||||
|
): Promise<BatchCreateResultDto> {
|
||||||
|
this.logger.log(`[PendingAction] Batch creating actions for ${dto.userIds.length} users, code: ${dto.actionCode}`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
const failedUserIds: string[] = [];
|
||||||
|
|
||||||
|
for (const userId of dto.userIds) {
|
||||||
|
try {
|
||||||
|
// 验证用户是否存在
|
||||||
|
const user = await this.prisma.userAccount.findUnique({
|
||||||
|
where: { userId: BigInt(userId) },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
failedUserIds.push(userId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.userPendingAction.create({
|
||||||
|
data: {
|
||||||
|
userId: BigInt(userId),
|
||||||
|
actionCode: dto.actionCode,
|
||||||
|
actionParams: dto.actionParams ?? undefined,
|
||||||
|
priority: dto.priority ?? 0,
|
||||||
|
expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null,
|
||||||
|
createdBy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`[PendingAction] Failed to create action for user: ${userId}, error: ${error.message}`);
|
||||||
|
failedUserIds.push(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`[PendingAction] Batch create result: success=${successCount}, failed=${failedUserIds.length}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
successCount,
|
||||||
|
failedCount: failedUserIds.length,
|
||||||
|
failedUserIds: failedUserIds.length > 0 ? failedUserIds : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户待办操作列表 (后台管理)
|
||||||
|
*/
|
||||||
|
async queryActions(dto: QueryPendingActionsDto): Promise<PendingActionListResponseDto> {
|
||||||
|
const page = dto.page ?? 1;
|
||||||
|
const limit = dto.limit ?? 20;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
if (dto.userId) {
|
||||||
|
where.userId = BigInt(dto.userId);
|
||||||
|
}
|
||||||
|
if (dto.actionCode) {
|
||||||
|
where.actionCode = dto.actionCode;
|
||||||
|
}
|
||||||
|
if (dto.status) {
|
||||||
|
where.status = dto.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
this.prisma.userPendingAction.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: [
|
||||||
|
{ createdAt: 'desc' },
|
||||||
|
],
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
this.prisma.userPendingAction.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items.map((action) => this.toResponseDto(action)),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个操作详情 (后台管理)
|
||||||
|
*/
|
||||||
|
async getAction(actionId: string): Promise<PendingActionResponseDto> {
|
||||||
|
const action = await this.prisma.userPendingAction.findUnique({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
throw new ApplicationError('操作不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.toResponseDto(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户待办操作 (后台管理)
|
||||||
|
*/
|
||||||
|
async updateAction(
|
||||||
|
actionId: string,
|
||||||
|
dto: UpdatePendingActionDto,
|
||||||
|
): Promise<PendingActionResponseDto> {
|
||||||
|
this.logger.log(`[PendingAction] Updating action: ${actionId}`);
|
||||||
|
|
||||||
|
const action = await this.prisma.userPendingAction.findUnique({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
throw new ApplicationError('操作不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: any = {};
|
||||||
|
|
||||||
|
if (dto.actionParams !== undefined) {
|
||||||
|
updateData.actionParams = dto.actionParams;
|
||||||
|
}
|
||||||
|
if (dto.status !== undefined) {
|
||||||
|
updateData.status = dto.status;
|
||||||
|
if (dto.status === PendingActionStatus.COMPLETED) {
|
||||||
|
updateData.completedAt = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dto.priority !== undefined) {
|
||||||
|
updateData.priority = dto.priority;
|
||||||
|
}
|
||||||
|
if (dto.expiresAt !== undefined) {
|
||||||
|
updateData.expiresAt = dto.expiresAt ? new Date(dto.expiresAt) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.prisma.userPendingAction.update({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[PendingAction] Action updated: ${actionId}`);
|
||||||
|
|
||||||
|
return this.toResponseDto(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户待办操作 (后台管理)
|
||||||
|
*/
|
||||||
|
async deleteAction(actionId: string): Promise<void> {
|
||||||
|
this.logger.log(`[PendingAction] Deleting action: ${actionId}`);
|
||||||
|
|
||||||
|
const action = await this.prisma.userPendingAction.findUnique({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
throw new ApplicationError('操作不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.userPendingAction.delete({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[PendingAction] Action deleted: ${actionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消用户待办操作 (后台管理)
|
||||||
|
* 软删除,将状态改为 CANCELLED
|
||||||
|
*/
|
||||||
|
async cancelAction(actionId: string): Promise<PendingActionResponseDto> {
|
||||||
|
this.logger.log(`[PendingAction] Cancelling action: ${actionId}`);
|
||||||
|
|
||||||
|
const action = await this.prisma.userPendingAction.findUnique({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
throw new ApplicationError('操作不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.status !== PendingActionStatus.PENDING) {
|
||||||
|
throw new ApplicationError('只能取消待处理的操作');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.prisma.userPendingAction.update({
|
||||||
|
where: { id: BigInt(actionId) },
|
||||||
|
data: {
|
||||||
|
status: PendingActionStatus.CANCELLED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[PendingAction] Action cancelled: ${actionId}`);
|
||||||
|
|
||||||
|
return this.toResponseDto(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Helper Methods ====================
|
||||||
|
|
||||||
|
private toResponseDto(action: any): PendingActionResponseDto {
|
||||||
|
return {
|
||||||
|
id: action.id.toString(),
|
||||||
|
userId: action.userId.toString(),
|
||||||
|
actionCode: action.actionCode,
|
||||||
|
actionParams: action.actionParams as Record<string, any> | null,
|
||||||
|
status: action.status,
|
||||||
|
priority: action.priority,
|
||||||
|
expiresAt: action.expiresAt,
|
||||||
|
createdAt: action.createdAt,
|
||||||
|
completedAt: action.completedAt,
|
||||||
|
createdBy: action.createdBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,688 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Modal, toast, Button } from '@/components/common';
|
||||||
|
import { PageContainer } from '@/components/layout';
|
||||||
|
import { cn } from '@/utils/helpers';
|
||||||
|
import { formatDateTime } from '@/utils/formatters';
|
||||||
|
import {
|
||||||
|
usePendingActions,
|
||||||
|
useCreatePendingAction,
|
||||||
|
useBatchCreatePendingActions,
|
||||||
|
useUpdatePendingAction,
|
||||||
|
useCancelPendingAction,
|
||||||
|
useDeletePendingAction,
|
||||||
|
} from '@/hooks/usePendingActions';
|
||||||
|
import {
|
||||||
|
PendingAction,
|
||||||
|
PendingActionStatus,
|
||||||
|
ACTION_CODE_OPTIONS,
|
||||||
|
STATUS_OPTIONS,
|
||||||
|
getStatusInfo,
|
||||||
|
getActionCodeLabel,
|
||||||
|
} from '@/types/pending-action.types';
|
||||||
|
import styles from './pending-actions.module.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户待办操作管理页面
|
||||||
|
*/
|
||||||
|
export default function PendingActionsPage() {
|
||||||
|
// 筛选状态
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
userId: '',
|
||||||
|
actionCode: '',
|
||||||
|
status: '' as PendingActionStatus | '',
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [showBatchModal, setShowBatchModal] = useState(false);
|
||||||
|
const [editingAction, setEditingAction] = useState<PendingAction | null>(null);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||||
|
const [viewingAction, setViewingAction] = useState<PendingAction | null>(null);
|
||||||
|
|
||||||
|
// 表单状态
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
userId: '',
|
||||||
|
actionCode: 'ADOPTION_WIZARD',
|
||||||
|
actionParams: '',
|
||||||
|
priority: 0,
|
||||||
|
expiresAt: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量创建表单
|
||||||
|
const [batchFormData, setBatchFormData] = useState({
|
||||||
|
userIds: '',
|
||||||
|
actionCode: 'ADOPTION_WIZARD',
|
||||||
|
actionParams: '',
|
||||||
|
priority: 0,
|
||||||
|
expiresAt: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据查询
|
||||||
|
const { data, isLoading, error, refetch } = usePendingActions({
|
||||||
|
userId: filters.userId || undefined,
|
||||||
|
actionCode: filters.actionCode || undefined,
|
||||||
|
status: filters.status || undefined,
|
||||||
|
page: filters.page,
|
||||||
|
limit: filters.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
const createMutation = useCreatePendingAction();
|
||||||
|
const batchCreateMutation = useBatchCreatePendingActions();
|
||||||
|
const updateMutation = useUpdatePendingAction();
|
||||||
|
const cancelMutation = useCancelPendingAction();
|
||||||
|
const deleteMutation = useDeletePendingAction();
|
||||||
|
|
||||||
|
// 打开创建弹窗
|
||||||
|
const handleCreate = () => {
|
||||||
|
setEditingAction(null);
|
||||||
|
setFormData({
|
||||||
|
userId: '',
|
||||||
|
actionCode: 'ADOPTION_WIZARD',
|
||||||
|
actionParams: '',
|
||||||
|
priority: 0,
|
||||||
|
expiresAt: '',
|
||||||
|
});
|
||||||
|
setShowCreateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开编辑弹窗
|
||||||
|
const handleEdit = (action: PendingAction) => {
|
||||||
|
setEditingAction(action);
|
||||||
|
setFormData({
|
||||||
|
userId: action.userId,
|
||||||
|
actionCode: action.actionCode,
|
||||||
|
actionParams: action.actionParams ? JSON.stringify(action.actionParams, null, 2) : '',
|
||||||
|
priority: action.priority,
|
||||||
|
expiresAt: action.expiresAt ? action.expiresAt.slice(0, 16) : '',
|
||||||
|
});
|
||||||
|
setShowCreateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开批量创建弹窗
|
||||||
|
const handleBatchCreate = () => {
|
||||||
|
setBatchFormData({
|
||||||
|
userIds: '',
|
||||||
|
actionCode: 'ADOPTION_WIZARD',
|
||||||
|
actionParams: '',
|
||||||
|
priority: 0,
|
||||||
|
expiresAt: '',
|
||||||
|
});
|
||||||
|
setShowBatchModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交创建/编辑表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!editingAction && !formData.userId.trim()) {
|
||||||
|
toast.error('请输入用户ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionParams: Record<string, unknown> | undefined;
|
||||||
|
if (formData.actionParams.trim()) {
|
||||||
|
try {
|
||||||
|
actionParams = JSON.parse(formData.actionParams);
|
||||||
|
} catch {
|
||||||
|
toast.error('操作参数格式错误,请输入有效的 JSON');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingAction) {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
id: editingAction.id,
|
||||||
|
data: {
|
||||||
|
actionParams,
|
||||||
|
priority: formData.priority,
|
||||||
|
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
toast.success('操作已更新');
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
userId: formData.userId.trim(),
|
||||||
|
actionCode: formData.actionCode,
|
||||||
|
actionParams,
|
||||||
|
priority: formData.priority,
|
||||||
|
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined,
|
||||||
|
});
|
||||||
|
toast.success('操作已创建');
|
||||||
|
}
|
||||||
|
setShowCreateModal(false);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交批量创建
|
||||||
|
const handleBatchSubmit = async () => {
|
||||||
|
const userIds = batchFormData.userIds
|
||||||
|
.split(/[\n,]/)
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
toast.error('请输入用户ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionParams: Record<string, unknown> | undefined;
|
||||||
|
if (batchFormData.actionParams.trim()) {
|
||||||
|
try {
|
||||||
|
actionParams = JSON.parse(batchFormData.actionParams);
|
||||||
|
} catch {
|
||||||
|
toast.error('操作参数格式错误,请输入有效的 JSON');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await batchCreateMutation.mutateAsync({
|
||||||
|
userIds,
|
||||||
|
actionCode: batchFormData.actionCode,
|
||||||
|
actionParams,
|
||||||
|
priority: batchFormData.priority,
|
||||||
|
expiresAt: batchFormData.expiresAt
|
||||||
|
? new Date(batchFormData.expiresAt).toISOString()
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
toast.success(`成功创建 ${result.successCount} 个操作${result.failedCount > 0 ? `,${result.failedCount} 个失败` : ''}`);
|
||||||
|
setShowBatchModal(false);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消操作
|
||||||
|
const handleCancel = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await cancelMutation.mutateAsync(id);
|
||||||
|
toast.success('操作已取消');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '取消失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除操作
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await deleteMutation.mutateAsync(id);
|
||||||
|
toast.success('操作已删除');
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
setFilters((prev) => ({ ...prev, page: 1 }));
|
||||||
|
refetch();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
// 翻页
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setFilters((prev) => ({ ...prev, page }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="用户待办操作">
|
||||||
|
<div className={styles.pendingActions}>
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className={styles.pendingActions__header}>
|
||||||
|
<h1 className={styles.pendingActions__title}>用户待办操作管理</h1>
|
||||||
|
<p className={styles.pendingActions__subtitle}>
|
||||||
|
配置指定用户登录后需要执行的特定操作,如认种向导、结算奖励等
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主内容卡片 */}
|
||||||
|
<div className={styles.pendingActions__card}>
|
||||||
|
{/* 筛选区域 */}
|
||||||
|
<div className={styles.pendingActions__filters}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.pendingActions__input}
|
||||||
|
placeholder="用户ID"
|
||||||
|
value={filters.userId}
|
||||||
|
onChange={(e) => setFilters({ ...filters, userId: e.target.value })}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className={styles.pendingActions__select}
|
||||||
|
value={filters.actionCode}
|
||||||
|
onChange={(e) => setFilters({ ...filters, actionCode: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">全部操作类型</option>
|
||||||
|
{ACTION_CODE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className={styles.pendingActions__select}
|
||||||
|
value={filters.status}
|
||||||
|
onChange={(e) => setFilters({ ...filters, status: e.target.value as PendingActionStatus | '' })}
|
||||||
|
>
|
||||||
|
<option value="">全部状态</option>
|
||||||
|
{STATUS_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleSearch}>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<Button variant="outline" onClick={handleBatchCreate}>
|
||||||
|
批量创建
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleCreate}>
|
||||||
|
+ 新建操作
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 列表 */}
|
||||||
|
<div className={styles.pendingActions__list}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={styles.pendingActions__loading}>加载中...</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className={styles.pendingActions__error}>
|
||||||
|
<span>{(error as Error).message || '加载失败'}</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : !data || data.items.length === 0 ? (
|
||||||
|
<div className={styles.pendingActions__empty}>
|
||||||
|
暂无数据,点击"新建操作"创建第一条待办操作
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 表格 */}
|
||||||
|
<table className={styles.pendingActions__table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>用户ID</th>
|
||||||
|
<th>操作类型</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>优先级</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>过期时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.items.map((action) => {
|
||||||
|
const statusInfo = getStatusInfo(action.status);
|
||||||
|
return (
|
||||||
|
<tr key={action.id}>
|
||||||
|
<td>{action.id}</td>
|
||||||
|
<td>{action.userId}</td>
|
||||||
|
<td>
|
||||||
|
<span className={styles.pendingActions__codeTag}>
|
||||||
|
{getActionCodeLabel(action.actionCode)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={styles.pendingActions__statusTag}
|
||||||
|
style={{ backgroundColor: statusInfo.color, color: 'white' }}
|
||||||
|
>
|
||||||
|
{statusInfo.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{action.priority}</td>
|
||||||
|
<td>{formatDateTime(action.createdAt)}</td>
|
||||||
|
<td>{action.expiresAt ? formatDateTime(action.expiresAt) : '-'}</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.pendingActions__actions}>
|
||||||
|
<button
|
||||||
|
className={styles.pendingActions__actionBtn}
|
||||||
|
onClick={() => setViewingAction(action)}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</button>
|
||||||
|
{action.status === 'PENDING' && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles.pendingActions__actionBtn}
|
||||||
|
onClick={() => handleEdit(action)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(styles.pendingActions__actionBtn, styles['pendingActions__actionBtn--warning'])}
|
||||||
|
onClick={() => handleCancel(action.id)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={cn(styles.pendingActions__actionBtn, styles['pendingActions__actionBtn--danger'])}
|
||||||
|
onClick={() => setDeleteConfirm(action.id)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{data.total > filters.limit && (
|
||||||
|
<div className={styles.pendingActions__pagination}>
|
||||||
|
<span>
|
||||||
|
共 {data.total} 条,第 {data.page} / {Math.ceil(data.total / data.limit)} 页
|
||||||
|
</span>
|
||||||
|
<div className={styles.pendingActions__pageButtons}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={data.page <= 1}
|
||||||
|
onClick={() => handlePageChange(data.page - 1)}
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={data.page >= Math.ceil(data.total / data.limit)}
|
||||||
|
onClick={() => handlePageChange(data.page + 1)}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 创建/编辑弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={showCreateModal}
|
||||||
|
title={editingAction ? '编辑操作' : '新建操作'}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
footer={
|
||||||
|
<div className={styles.pendingActions__modalFooter}>
|
||||||
|
<Button variant="outline" onClick={() => setShowCreateModal(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={createMutation.isPending || updateMutation.isPending}
|
||||||
|
>
|
||||||
|
{editingAction ? '保存' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<div className={styles.pendingActions__form}>
|
||||||
|
{!editingAction && (
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>用户ID *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.userId}
|
||||||
|
onChange={(e) => setFormData({ ...formData, userId: e.target.value })}
|
||||||
|
placeholder="请输入目标用户ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>操作类型</label>
|
||||||
|
<select
|
||||||
|
value={formData.actionCode}
|
||||||
|
onChange={(e) => setFormData({ ...formData, actionCode: e.target.value })}
|
||||||
|
disabled={!!editingAction}
|
||||||
|
>
|
||||||
|
{ACTION_CODE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>操作参数 (JSON格式,可选)</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.actionParams}
|
||||||
|
onChange={(e) => setFormData({ ...formData, actionParams: e.target.value })}
|
||||||
|
placeholder='{"key": "value"}'
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<span className={styles.pendingActions__formHint}>
|
||||||
|
例如:{`{"treeType": "durian", "rewardIds": ["r1", "r2"]}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formRow}>
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>优先级</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.priority}
|
||||||
|
onChange={(e) => setFormData({ ...formData, priority: parseInt(e.target.value) || 0 })}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
<span className={styles.pendingActions__formHint}>数值越大优先级越高</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>过期时间</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={formData.expiresAt}
|
||||||
|
onChange={(e) => setFormData({ ...formData, expiresAt: e.target.value })}
|
||||||
|
/>
|
||||||
|
<span className={styles.pendingActions__formHint}>留空表示永不过期</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 批量创建弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={showBatchModal}
|
||||||
|
title="批量创建操作"
|
||||||
|
onClose={() => setShowBatchModal(false)}
|
||||||
|
footer={
|
||||||
|
<div className={styles.pendingActions__modalFooter}>
|
||||||
|
<Button variant="outline" onClick={() => setShowBatchModal(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleBatchSubmit}
|
||||||
|
loading={batchCreateMutation.isPending}
|
||||||
|
>
|
||||||
|
批量创建
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<div className={styles.pendingActions__form}>
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>用户ID列表 *</label>
|
||||||
|
<textarea
|
||||||
|
value={batchFormData.userIds}
|
||||||
|
onChange={(e) => setBatchFormData({ ...batchFormData, userIds: e.target.value })}
|
||||||
|
placeholder="请输入用户ID,每行一个或用逗号分隔"
|
||||||
|
rows={6}
|
||||||
|
className={styles.pendingActions__userIdsInput}
|
||||||
|
/>
|
||||||
|
<span className={styles.pendingActions__formHint}>
|
||||||
|
当前 {batchFormData.userIds.split(/[\n,]/).filter((s) => s.trim()).length} 个用户
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>操作类型</label>
|
||||||
|
<select
|
||||||
|
value={batchFormData.actionCode}
|
||||||
|
onChange={(e) => setBatchFormData({ ...batchFormData, actionCode: e.target.value })}
|
||||||
|
>
|
||||||
|
{ACTION_CODE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>操作参数 (JSON格式,可选)</label>
|
||||||
|
<textarea
|
||||||
|
value={batchFormData.actionParams}
|
||||||
|
onChange={(e) => setBatchFormData({ ...batchFormData, actionParams: e.target.value })}
|
||||||
|
placeholder='{"key": "value"}'
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formRow}>
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>优先级</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={batchFormData.priority}
|
||||||
|
onChange={(e) => setBatchFormData({ ...batchFormData, priority: parseInt(e.target.value) || 0 })}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>过期时间</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={batchFormData.expiresAt}
|
||||||
|
onChange={(e) => setBatchFormData({ ...batchFormData, expiresAt: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 查看详情弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={!!viewingAction}
|
||||||
|
title="操作详情"
|
||||||
|
onClose={() => setViewingAction(null)}
|
||||||
|
footer={
|
||||||
|
<div className={styles.pendingActions__modalFooter}>
|
||||||
|
<Button variant="outline" onClick={() => setViewingAction(null)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{viewingAction && (
|
||||||
|
<div className={styles.pendingActions__detail}>
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>ID:</span>
|
||||||
|
<span>{viewingAction.id}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>用户ID:</span>
|
||||||
|
<span>{viewingAction.userId}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>操作类型:</span>
|
||||||
|
<span>{getActionCodeLabel(viewingAction.actionCode)} ({viewingAction.actionCode})</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>状态:</span>
|
||||||
|
<span
|
||||||
|
className={styles.pendingActions__statusTag}
|
||||||
|
style={{ backgroundColor: getStatusInfo(viewingAction.status).color, color: 'white' }}
|
||||||
|
>
|
||||||
|
{getStatusInfo(viewingAction.status).label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>优先级:</span>
|
||||||
|
<span>{viewingAction.priority}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>创建时间:</span>
|
||||||
|
<span>{formatDateTime(viewingAction.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
{viewingAction.completedAt && (
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>完成时间:</span>
|
||||||
|
<span>{formatDateTime(viewingAction.completedAt)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewingAction.expiresAt && (
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>过期时间:</span>
|
||||||
|
<span>{formatDateTime(viewingAction.expiresAt)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewingAction.createdBy && (
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>创建人:</span>
|
||||||
|
<span>{viewingAction.createdBy}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewingAction.actionParams && (
|
||||||
|
<div className={styles.pendingActions__detailRow}>
|
||||||
|
<span className={styles.pendingActions__detailLabel}>操作参数:</span>
|
||||||
|
<pre className={styles.pendingActions__json}>
|
||||||
|
{JSON.stringify(viewingAction.actionParams, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 删除确认弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={!!deleteConfirm}
|
||||||
|
title="确认删除"
|
||||||
|
onClose={() => setDeleteConfirm(null)}
|
||||||
|
footer={
|
||||||
|
<div className={styles.pendingActions__modalFooter}>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => deleteConfirm && handleDelete(deleteConfirm)}
|
||||||
|
loading={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<p>确定要删除这条操作记录吗?此操作无法撤销。</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
|
.pendingActions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
background: $card-background;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: $shadow-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading,
|
||||||
|
&__empty,
|
||||||
|
&__error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
color: $error-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格样式
|
||||||
|
&__table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-secondary;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__codeTag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #e6f4ff;
|
||||||
|
color: #1677ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__statusTag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actionBtn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-secondary;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
&:hover {
|
||||||
|
color: #faad14;
|
||||||
|
border-color: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
&:hover {
|
||||||
|
color: $error-color;
|
||||||
|
border-color: $error-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
&__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid $border-color;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pageButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单样式
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__formRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__formGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__formHint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $text-disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__userIdsInput {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__modalFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情样式
|
||||||
|
&__detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__detailRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__detailLabel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 80px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__json {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ const topMenuItems: MenuItem[] = [
|
||||||
{ key: 'leaderboard', icon: '/images/Container3.svg', label: '龙虎榜', path: '/leaderboard' },
|
{ key: 'leaderboard', icon: '/images/Container3.svg', label: '龙虎榜', path: '/leaderboard' },
|
||||||
{ key: 'authorization', icon: '/images/Container4.svg', label: '授权管理', path: '/authorization' },
|
{ key: 'authorization', icon: '/images/Container4.svg', label: '授权管理', path: '/authorization' },
|
||||||
{ key: 'notifications', icon: '/images/Container3.svg', label: '通知管理', path: '/notifications' },
|
{ key: 'notifications', icon: '/images/Container3.svg', label: '通知管理', path: '/notifications' },
|
||||||
|
{ key: 'pending-actions', icon: '/images/Container3.svg', label: '待办操作', path: '/pending-actions' },
|
||||||
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
|
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
|
||||||
{ key: 'maintenance', icon: '/images/Container6.svg', label: '系统维护', path: '/maintenance' },
|
{ key: 'maintenance', icon: '/images/Container6.svg', label: '系统维护', path: '/maintenance' },
|
||||||
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
/**
|
||||||
|
* 用户待办操作 Hooks
|
||||||
|
* 使用 React Query 进行数据获取和缓存管理
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { pendingActionService } from '@/services/pendingActionService';
|
||||||
|
import type {
|
||||||
|
QueryPendingActionsParams,
|
||||||
|
CreatePendingActionRequest,
|
||||||
|
BatchCreatePendingActionRequest,
|
||||||
|
UpdatePendingActionRequest,
|
||||||
|
} from '@/types/pending-action.types';
|
||||||
|
|
||||||
|
/** Query Keys */
|
||||||
|
export const pendingActionKeys = {
|
||||||
|
all: ['pendingActions'] as const,
|
||||||
|
list: (params: QueryPendingActionsParams) => [...pendingActionKeys.all, 'list', params] as const,
|
||||||
|
detail: (id: string) => [...pendingActionKeys.all, 'detail', id] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待办操作列表
|
||||||
|
*/
|
||||||
|
export function usePendingActions(params: QueryPendingActionsParams = {}) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: pendingActionKeys.list(params),
|
||||||
|
queryFn: () => pendingActionService.getList(params),
|
||||||
|
staleTime: 30 * 1000, // 30秒后标记为过期
|
||||||
|
gcTime: 5 * 60 * 1000, // 5分钟后垃圾回收
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个待办操作详情
|
||||||
|
*/
|
||||||
|
export function usePendingActionDetail(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: pendingActionKeys.detail(id),
|
||||||
|
queryFn: () => pendingActionService.getDetail(id),
|
||||||
|
enabled: !!id,
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建待办操作
|
||||||
|
*/
|
||||||
|
export function useCreatePendingAction() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreatePendingActionRequest) => pendingActionService.create(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
// 创建成功后刷新列表
|
||||||
|
queryClient.invalidateQueries({ queryKey: pendingActionKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建待办操作
|
||||||
|
*/
|
||||||
|
export function useBatchCreatePendingActions() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: BatchCreatePendingActionRequest) => pendingActionService.batchCreate(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: pendingActionKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新待办操作
|
||||||
|
*/
|
||||||
|
export function useUpdatePendingAction() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: UpdatePendingActionRequest }) =>
|
||||||
|
pendingActionService.update(id, data),
|
||||||
|
onSuccess: (_, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: pendingActionKeys.all });
|
||||||
|
queryClient.invalidateQueries({ queryKey: pendingActionKeys.detail(id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消待办操作
|
||||||
|
*/
|
||||||
|
export function useCancelPendingAction() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => pendingActionService.cancel(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: pendingActionKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除待办操作
|
||||||
|
*/
|
||||||
|
export function useDeletePendingAction() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => pendingActionService.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: pendingActionKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -164,4 +164,15 @@ export const API_ENDPOINTS = {
|
||||||
DELETE: (id: string) => `/v1/admin/maintenance/${id}`,
|
DELETE: (id: string) => `/v1/admin/maintenance/${id}`,
|
||||||
CURRENT_STATUS: '/v1/admin/maintenance/status/current',
|
CURRENT_STATUS: '/v1/admin/maintenance/status/current',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 用户待办操作 (identity-service)
|
||||||
|
PENDING_ACTIONS: {
|
||||||
|
LIST: '/v1/admin/pending-actions',
|
||||||
|
CREATE: '/v1/admin/pending-actions',
|
||||||
|
BATCH_CREATE: '/v1/admin/pending-actions/batch',
|
||||||
|
DETAIL: (id: string) => `/v1/admin/pending-actions/${id}`,
|
||||||
|
UPDATE: (id: string) => `/v1/admin/pending-actions/${id}`,
|
||||||
|
CANCEL: (id: string) => `/v1/admin/pending-actions/${id}/cancel`,
|
||||||
|
DELETE: (id: string) => `/v1/admin/pending-actions/${id}`,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* 用户待办操作服务
|
||||||
|
* 用于后台管理指定用户登录后需要执行的特定操作
|
||||||
|
*/
|
||||||
|
|
||||||
|
import apiClient from '@/infrastructure/api/client';
|
||||||
|
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
|
||||||
|
import type {
|
||||||
|
PendingAction,
|
||||||
|
PendingActionListResponse,
|
||||||
|
CreatePendingActionRequest,
|
||||||
|
BatchCreatePendingActionRequest,
|
||||||
|
UpdatePendingActionRequest,
|
||||||
|
QueryPendingActionsParams,
|
||||||
|
BatchCreateResult,
|
||||||
|
} from '@/types/pending-action.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办操作管理服务
|
||||||
|
* 注意:apiClient 响应拦截器已解包 response.data,直接返回数据
|
||||||
|
*/
|
||||||
|
export const pendingActionService = {
|
||||||
|
/**
|
||||||
|
* 查询待办操作列表
|
||||||
|
*/
|
||||||
|
async getList(params: QueryPendingActionsParams = {}): Promise<PendingActionListResponse> {
|
||||||
|
const response = await apiClient.get(API_ENDPOINTS.PENDING_ACTIONS.LIST, { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个待办操作详情
|
||||||
|
*/
|
||||||
|
async getDetail(id: string): Promise<PendingAction> {
|
||||||
|
const response = await apiClient.get(API_ENDPOINTS.PENDING_ACTIONS.DETAIL(id));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建待办操作
|
||||||
|
*/
|
||||||
|
async create(data: CreatePendingActionRequest): Promise<PendingAction> {
|
||||||
|
const response = await apiClient.post(API_ENDPOINTS.PENDING_ACTIONS.CREATE, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建待办操作
|
||||||
|
*/
|
||||||
|
async batchCreate(data: BatchCreatePendingActionRequest): Promise<BatchCreateResult> {
|
||||||
|
const response = await apiClient.post(API_ENDPOINTS.PENDING_ACTIONS.BATCH_CREATE, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新待办操作
|
||||||
|
*/
|
||||||
|
async update(id: string, data: UpdatePendingActionRequest): Promise<PendingAction> {
|
||||||
|
const response = await apiClient.put(API_ENDPOINTS.PENDING_ACTIONS.UPDATE(id), data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消待办操作(软删除)
|
||||||
|
*/
|
||||||
|
async cancel(id: string): Promise<PendingAction> {
|
||||||
|
const response = await apiClient.post(API_ENDPOINTS.PENDING_ACTIONS.CANCEL(id));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除待办操作(硬删除)
|
||||||
|
*/
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await apiClient.delete(API_ENDPOINTS.PENDING_ACTIONS.DELETE(id));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pendingActionService;
|
||||||
|
|
@ -6,3 +6,4 @@ export * from './company.types';
|
||||||
export * from './statistics.types';
|
export * from './statistics.types';
|
||||||
export * from './common.types';
|
export * from './common.types';
|
||||||
export * from './dashboard.types';
|
export * from './dashboard.types';
|
||||||
|
export * from './pending-action.types';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
// 用户待办操作类型定义
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作状态
|
||||||
|
*/
|
||||||
|
export type PendingActionStatus = 'PENDING' | 'COMPLETED' | 'CANCELLED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 常用操作代码
|
||||||
|
*/
|
||||||
|
export const ACTION_CODES = {
|
||||||
|
ADOPTION_WIZARD: 'ADOPTION_WIZARD', // 认种向导
|
||||||
|
SETTLE_REWARDS: 'SETTLE_REWARDS', // 结算奖励
|
||||||
|
BIND_PHONE: 'BIND_PHONE', // 绑定手机
|
||||||
|
FORCE_KYC: 'FORCE_KYC', // 强制 KYC
|
||||||
|
CUSTOM_NOTICE: 'CUSTOM_NOTICE', // 自定义通知
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ActionCode = (typeof ACTION_CODES)[keyof typeof ACTION_CODES] | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办操作
|
||||||
|
*/
|
||||||
|
export interface PendingAction {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
actionCode: string;
|
||||||
|
actionParams: Record<string, unknown> | null;
|
||||||
|
status: PendingActionStatus;
|
||||||
|
priority: number;
|
||||||
|
expiresAt: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
completedAt: string | null;
|
||||||
|
createdBy: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建待办操作请求
|
||||||
|
*/
|
||||||
|
export interface CreatePendingActionRequest {
|
||||||
|
userId: string;
|
||||||
|
actionCode: string;
|
||||||
|
actionParams?: Record<string, unknown>;
|
||||||
|
priority?: number;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建待办操作请求
|
||||||
|
*/
|
||||||
|
export interface BatchCreatePendingActionRequest {
|
||||||
|
userIds: string[];
|
||||||
|
actionCode: string;
|
||||||
|
actionParams?: Record<string, unknown>;
|
||||||
|
priority?: number;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新待办操作请求
|
||||||
|
*/
|
||||||
|
export interface UpdatePendingActionRequest {
|
||||||
|
actionParams?: Record<string, unknown>;
|
||||||
|
status?: PendingActionStatus;
|
||||||
|
priority?: number;
|
||||||
|
expiresAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询待办操作参数
|
||||||
|
*/
|
||||||
|
export interface QueryPendingActionsParams {
|
||||||
|
userId?: string;
|
||||||
|
actionCode?: string;
|
||||||
|
status?: PendingActionStatus;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办操作列表响应
|
||||||
|
*/
|
||||||
|
export interface PendingActionListResponse {
|
||||||
|
items: PendingAction[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建结果
|
||||||
|
*/
|
||||||
|
export interface BatchCreateResult {
|
||||||
|
successCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
failedUserIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作代码选项(用于下拉选择)
|
||||||
|
*/
|
||||||
|
export const ACTION_CODE_OPTIONS = [
|
||||||
|
{ value: ACTION_CODES.ADOPTION_WIZARD, label: '认种向导' },
|
||||||
|
{ value: ACTION_CODES.SETTLE_REWARDS, label: '结算奖励' },
|
||||||
|
{ value: ACTION_CODES.BIND_PHONE, label: '绑定手机' },
|
||||||
|
{ value: ACTION_CODES.FORCE_KYC, label: '强制 KYC' },
|
||||||
|
{ value: ACTION_CODES.CUSTOM_NOTICE, label: '自定义通知' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态选项(用于筛选和显示)
|
||||||
|
*/
|
||||||
|
export const STATUS_OPTIONS = [
|
||||||
|
{ value: 'PENDING', label: '待执行', color: '#faad14' },
|
||||||
|
{ value: 'COMPLETED', label: '已完成', color: '#52c41a' },
|
||||||
|
{ value: 'CANCELLED', label: '已取消', color: '#999999' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态显示信息
|
||||||
|
*/
|
||||||
|
export function getStatusInfo(status: PendingActionStatus) {
|
||||||
|
return STATUS_OPTIONS.find((s) => s.value === status) || STATUS_OPTIONS[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作代码标签
|
||||||
|
*/
|
||||||
|
export function getActionCodeLabel(code: string): string {
|
||||||
|
const option = ACTION_CODE_OPTIONS.find((o) => o.value === code);
|
||||||
|
return option?.label || code;
|
||||||
|
}
|
||||||
|
|
@ -92,4 +92,8 @@ class ApiEndpoints {
|
||||||
// System Config (-> Admin Service)
|
// System Config (-> Admin Service)
|
||||||
static const String systemConfig = '/system-config';
|
static const String systemConfig = '/system-config';
|
||||||
static const String displaySettings = '$systemConfig/display/settings';
|
static const String displaySettings = '$systemConfig/display/settings';
|
||||||
|
|
||||||
|
// Pending Actions (-> Identity Service)
|
||||||
|
static const String pendingActions = '/user/pending-actions';
|
||||||
|
static const String pendingActionsComplete = '/user/pending-actions'; // POST /:id/complete
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import '../services/notification_service.dart';
|
||||||
import '../services/system_config_service.dart';
|
import '../services/system_config_service.dart';
|
||||||
import '../services/contract_signing_service.dart';
|
import '../services/contract_signing_service.dart';
|
||||||
import '../services/contract_check_service.dart';
|
import '../services/contract_check_service.dart';
|
||||||
|
import '../services/pending_action_service.dart';
|
||||||
|
import '../services/pending_action_check_service.dart';
|
||||||
import '../telemetry/storage/telemetry_storage.dart';
|
import '../telemetry/storage/telemetry_storage.dart';
|
||||||
import '../../features/kyc/data/kyc_service.dart';
|
import '../../features/kyc/data/kyc_service.dart';
|
||||||
|
|
||||||
|
|
@ -125,6 +127,18 @@ final kycServiceProvider = Provider<KycService>((ref) {
|
||||||
return KycService(apiClient);
|
return KycService(apiClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pending Action Service Provider (调用 identity-service)
|
||||||
|
final pendingActionServiceProvider = Provider<PendingActionService>((ref) {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
return PendingActionService(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pending Action Check Service Provider (用于启动时检查待办操作)
|
||||||
|
final pendingActionCheckServiceProvider = Provider<PendingActionCheckService>((ref) {
|
||||||
|
final pendingActionService = ref.watch(pendingActionServiceProvider);
|
||||||
|
return PendingActionCheckService(pendingActionService: pendingActionService);
|
||||||
|
});
|
||||||
|
|
||||||
// Override provider with initialized instance
|
// Override provider with initialized instance
|
||||||
ProviderContainer createProviderContainer(LocalStorage localStorage) {
|
ProviderContainer createProviderContainer(LocalStorage localStorage) {
|
||||||
return ProviderContainer(
|
return ProviderContainer(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'pending_action_service.dart';
|
||||||
|
|
||||||
|
/// 待办操作检查结果
|
||||||
|
class PendingActionCheckResult {
|
||||||
|
/// 是否有待办操作
|
||||||
|
final bool hasPendingActions;
|
||||||
|
|
||||||
|
/// 待办操作数量
|
||||||
|
final int pendingCount;
|
||||||
|
|
||||||
|
/// 下一个待办操作
|
||||||
|
final PendingAction? nextAction;
|
||||||
|
|
||||||
|
PendingActionCheckResult({
|
||||||
|
required this.hasPendingActions,
|
||||||
|
required this.pendingCount,
|
||||||
|
this.nextAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 待办操作检查服务
|
||||||
|
/// 用于在 App 启动时检查用户是否有待办操作需要执行
|
||||||
|
class PendingActionCheckService {
|
||||||
|
final PendingActionService _pendingActionService;
|
||||||
|
|
||||||
|
PendingActionCheckService({
|
||||||
|
required PendingActionService pendingActionService,
|
||||||
|
}) : _pendingActionService = pendingActionService;
|
||||||
|
|
||||||
|
/// 检查是否有待办操作
|
||||||
|
/// 返回 true 表示有待办操作,需要强制执行
|
||||||
|
Future<bool> hasPendingActions() async {
|
||||||
|
try {
|
||||||
|
final response = await _pendingActionService.getMyPendingActions();
|
||||||
|
return response.hasPendingActions;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PendingActionCheckService] 检查待办操作失败: $e');
|
||||||
|
// 检查失败时不阻止用户使用 App
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 综合检查待办操作
|
||||||
|
Future<PendingActionCheckResult> check() async {
|
||||||
|
try {
|
||||||
|
final response = await _pendingActionService.getMyPendingActions();
|
||||||
|
final pendingActions = response.pendingActionsSorted;
|
||||||
|
|
||||||
|
debugPrint('[PendingActionCheckService] 检测到 ${pendingActions.length} 个待办操作');
|
||||||
|
|
||||||
|
return PendingActionCheckResult(
|
||||||
|
hasPendingActions: pendingActions.isNotEmpty,
|
||||||
|
pendingCount: pendingActions.length,
|
||||||
|
nextAction: response.nextAction,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PendingActionCheckService] 综合检查失败: $e');
|
||||||
|
// 检查失败时不阻止用户使用 App
|
||||||
|
return PendingActionCheckResult(
|
||||||
|
hasPendingActions: false,
|
||||||
|
pendingCount: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取待办操作数量
|
||||||
|
Future<int> getPendingActionCount() async {
|
||||||
|
try {
|
||||||
|
final response = await _pendingActionService.getMyPendingActions();
|
||||||
|
return response.pendingActionsSorted.length;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PendingActionCheckService] 获取待办操作数量失败: $e');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../network/api_client.dart';
|
||||||
|
import '../constants/api_endpoints.dart';
|
||||||
|
|
||||||
|
/// 待办操作动作代码枚举
|
||||||
|
enum PendingActionCode {
|
||||||
|
adoptionWizard('ADOPTION_WIZARD', '认种向导'),
|
||||||
|
settleRewards('SETTLE_REWARDS', '结算奖励'),
|
||||||
|
bindPhone('BIND_PHONE', '绑定手机'),
|
||||||
|
forceKyc('FORCE_KYC', '强制实名'),
|
||||||
|
signContract('SIGN_CONTRACT', '签署合同'),
|
||||||
|
updateProfile('UPDATE_PROFILE', '更新资料');
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final String label;
|
||||||
|
const PendingActionCode(this.code, this.label);
|
||||||
|
|
||||||
|
static PendingActionCode? fromCode(String code) {
|
||||||
|
for (final action in PendingActionCode.values) {
|
||||||
|
if (action.code == code) return action;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 待办操作状态枚举
|
||||||
|
enum PendingActionStatus {
|
||||||
|
pending('PENDING'),
|
||||||
|
completed('COMPLETED'),
|
||||||
|
cancelled('CANCELLED');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
const PendingActionStatus(this.value);
|
||||||
|
|
||||||
|
static PendingActionStatus fromValue(String value) {
|
||||||
|
for (final status in PendingActionStatus.values) {
|
||||||
|
if (status.value == value) return status;
|
||||||
|
}
|
||||||
|
return PendingActionStatus.pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 待办操作模型
|
||||||
|
class PendingAction {
|
||||||
|
final int id;
|
||||||
|
final int userId;
|
||||||
|
final String actionCode;
|
||||||
|
final Map<String, dynamic>? actionParams;
|
||||||
|
final PendingActionStatus status;
|
||||||
|
final int priority;
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? completedAt;
|
||||||
|
|
||||||
|
PendingAction({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.actionCode,
|
||||||
|
this.actionParams,
|
||||||
|
required this.status,
|
||||||
|
required this.priority,
|
||||||
|
this.expiresAt,
|
||||||
|
required this.createdAt,
|
||||||
|
this.completedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PendingAction.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PendingAction(
|
||||||
|
id: _parseInt(json['id']),
|
||||||
|
userId: _parseInt(json['userId']),
|
||||||
|
actionCode: json['actionCode'] as String,
|
||||||
|
actionParams: json['actionParams'] as Map<String, dynamic>?,
|
||||||
|
status: PendingActionStatus.fromValue(json['status'] as String),
|
||||||
|
priority: json['priority'] as int? ?? 0,
|
||||||
|
expiresAt: json['expiresAt'] != null
|
||||||
|
? DateTime.parse(json['expiresAt'] as String)
|
||||||
|
: null,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
completedAt: json['completedAt'] != null
|
||||||
|
? DateTime.parse(json['completedAt'] as String)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _parseInt(dynamic value) {
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is String) return int.parse(value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取动作代码枚举
|
||||||
|
PendingActionCode? get actionCodeEnum => PendingActionCode.fromCode(actionCode);
|
||||||
|
|
||||||
|
/// 获取显示标签
|
||||||
|
String get displayLabel => actionCodeEnum?.label ?? actionCode;
|
||||||
|
|
||||||
|
/// 是否已过期
|
||||||
|
bool get isExpired {
|
||||||
|
if (expiresAt == null) return false;
|
||||||
|
return DateTime.now().isAfter(expiresAt!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 是否待处理
|
||||||
|
bool get isPending => status == PendingActionStatus.pending && !isExpired;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'PendingAction(id: $id, actionCode: $actionCode, status: ${status.value}, priority: $priority)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 待办操作列表响应
|
||||||
|
class PendingActionsResponse {
|
||||||
|
final List<PendingAction> actions;
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
PendingActionsResponse({
|
||||||
|
required this.actions,
|
||||||
|
required this.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PendingActionsResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
final actionsJson = json['actions'] as List<dynamic>? ?? [];
|
||||||
|
return PendingActionsResponse(
|
||||||
|
actions: actionsJson
|
||||||
|
.map((e) => PendingAction.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
count: json['count'] as int? ?? actionsJson.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取按优先级排序的待处理操作(优先级高的在前)
|
||||||
|
List<PendingAction> get pendingActionsSorted {
|
||||||
|
final pending = actions.where((a) => a.isPending).toList();
|
||||||
|
pending.sort((a, b) => b.priority.compareTo(a.priority));
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 是否有待处理的操作
|
||||||
|
bool get hasPendingActions => pendingActionsSorted.isNotEmpty;
|
||||||
|
|
||||||
|
/// 获取下一个需要执行的操作
|
||||||
|
PendingAction? get nextAction =>
|
||||||
|
pendingActionsSorted.isNotEmpty ? pendingActionsSorted.first : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 待办操作服务
|
||||||
|
class PendingActionService {
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
PendingActionService({required ApiClient apiClient}) : _apiClient = apiClient;
|
||||||
|
|
||||||
|
/// 获取当前用户的待办操作列表
|
||||||
|
Future<PendingActionsResponse> getMyPendingActions() async {
|
||||||
|
debugPrint('[PendingActionService] 获取待办操作列表');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.get(ApiEndpoints.pendingActions);
|
||||||
|
debugPrint('[PendingActionService] 响应: $response');
|
||||||
|
|
||||||
|
if (response is Map<String, dynamic>) {
|
||||||
|
return PendingActionsResponse.fromJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果返回的是列表
|
||||||
|
if (response is List) {
|
||||||
|
return PendingActionsResponse(
|
||||||
|
actions: response
|
||||||
|
.map((e) => PendingAction.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
count: response.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingActionsResponse(actions: [], count: 0);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PendingActionService] 获取待办操作失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完成待办操作
|
||||||
|
Future<void> completeAction(int actionId, {Map<String, dynamic>? resultData}) async {
|
||||||
|
debugPrint('[PendingActionService] 完成待办操作: actionId=$actionId');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _apiClient.post(
|
||||||
|
'${ApiEndpoints.pendingActions}/$actionId/complete',
|
||||||
|
data: resultData != null ? {'resultData': resultData} : null,
|
||||||
|
);
|
||||||
|
debugPrint('[PendingActionService] 待办操作完成成功');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PendingActionService] 完成待办操作失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
import '../../../../bootstrap.dart';
|
import '../../../../bootstrap.dart';
|
||||||
import '../../../../core/providers/maintenance_provider.dart';
|
import '../../../../core/providers/maintenance_provider.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
|
|
||||||
/// 开屏页面 - 应用启动时显示的第一个页面
|
/// 开屏页面 - 应用启动时显示的第一个页面
|
||||||
|
|
@ -145,13 +146,33 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
||||||
|
|
||||||
// 根据认证状态决定跳转目标
|
// 根据认证状态决定跳转目标
|
||||||
// 优先级:
|
// 优先级:
|
||||||
// 1. 账号已创建 → 主页面(龙虎榜)
|
// 1. 账号已创建 → 检查待办操作 → 有待办则跳转待办页 → 无待办则跳转主页面
|
||||||
// 2. 首次打开 → 向导页
|
// 2. 首次打开 → 向导页
|
||||||
// 3. 已看过向导但账号未创建(退出登录后) → 登录页面
|
// 3. 已看过向导但账号未创建(退出登录后) → 登录页面
|
||||||
if (authState.isAccountCreated) {
|
if (authState.isAccountCreated) {
|
||||||
// 账号已创建,进入主页面(龙虎榜)
|
// 账号已创建,检查是否有待办操作
|
||||||
debugPrint('[SplashPage] 账号已创建 → 跳转到龙虎榜');
|
debugPrint('[SplashPage] 账号已创建 → 检查待办操作');
|
||||||
context.go(RoutePaths.ranking);
|
|
||||||
|
try {
|
||||||
|
final pendingActionCheckService = ref.read(pendingActionCheckServiceProvider);
|
||||||
|
final hasPending = await pendingActionCheckService.hasPendingActions();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (hasPending) {
|
||||||
|
// 有待办操作,跳转到待办操作页面
|
||||||
|
debugPrint('[SplashPage] 有待办操作 → 跳转到待办操作页面');
|
||||||
|
context.go(RoutePaths.pendingActions);
|
||||||
|
} else {
|
||||||
|
// 无待办操作,跳转到主页面(龙虎榜)
|
||||||
|
debugPrint('[SplashPage] 无待办操作 → 跳转到龙虎榜');
|
||||||
|
context.go(RoutePaths.ranking);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 检查失败,不阻止用户使用,直接跳转到主页面
|
||||||
|
debugPrint('[SplashPage] 检查待办操作失败: $e → 跳转到龙虎榜');
|
||||||
|
context.go(RoutePaths.ranking);
|
||||||
|
}
|
||||||
} else if (authState.isFirstLaunch) {
|
} else if (authState.isFirstLaunch) {
|
||||||
// 首次打开,进入向导页
|
// 首次打开,进入向导页
|
||||||
debugPrint('[SplashPage] 首次打开 → 跳转到向导页');
|
debugPrint('[SplashPage] 首次打开 → 跳转到向导页');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,496 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/pending_action_service.dart';
|
||||||
|
import '../../../../routes/route_paths.dart';
|
||||||
|
|
||||||
|
/// 待办操作页面
|
||||||
|
/// 强制用户按后端配置执行待办任务,不可跳过
|
||||||
|
class PendingActionsPage extends ConsumerStatefulWidget {
|
||||||
|
const PendingActionsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PendingActionsPage> createState() => _PendingActionsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
|
List<PendingAction> _pendingActions = [];
|
||||||
|
int _currentIndex = 0;
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isExecuting = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadPendingActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载待办操作列表
|
||||||
|
Future<void> _loadPendingActions() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final service = ref.read(pendingActionServiceProvider);
|
||||||
|
final response = await service.getMyPendingActions();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_pendingActions = response.pendingActionsSorted;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有待办操作,直接完成
|
||||||
|
if (_pendingActions.isEmpty) {
|
||||||
|
_completeAllActions();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = '加载失败: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行当前待办操作
|
||||||
|
Future<void> _executeCurrentAction() async {
|
||||||
|
if (_currentIndex >= _pendingActions.length) {
|
||||||
|
_completeAllActions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final action = _pendingActions[_currentIndex];
|
||||||
|
setState(() => _isExecuting = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据不同的操作类型执行不同的逻辑
|
||||||
|
final result = await _executeAction(action);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// 操作执行成功,标记为完成
|
||||||
|
final service = ref.read(pendingActionServiceProvider);
|
||||||
|
await service.completeAction(action.id);
|
||||||
|
|
||||||
|
// 移动到下一个操作
|
||||||
|
setState(() {
|
||||||
|
_currentIndex++;
|
||||||
|
_isExecuting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否所有操作都完成
|
||||||
|
if (_currentIndex >= _pendingActions.length) {
|
||||||
|
_completeAllActions();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 用户取消或操作失败,保持在当前操作
|
||||||
|
setState(() => _isExecuting = false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isExecuting = false;
|
||||||
|
_errorMessage = '操作失败: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行具体的操作,返回 true 表示成功
|
||||||
|
Future<bool> _executeAction(PendingAction action) async {
|
||||||
|
switch (action.actionCode) {
|
||||||
|
case 'ADOPTION_WIZARD':
|
||||||
|
// 跳转到认种向导
|
||||||
|
final result = await context.push<bool>(RoutePaths.plantingQuantity);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
|
case 'SETTLE_REWARDS':
|
||||||
|
// 跳转到交易页面进行结算
|
||||||
|
// 可以从 actionParams 获取具体的结算参数
|
||||||
|
final result = await context.push<bool>(RoutePaths.trading);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
|
case 'BIND_PHONE':
|
||||||
|
// 跳转到手机号绑定页面
|
||||||
|
final result = await context.push<bool>(RoutePaths.kycPhone);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
|
case 'FORCE_KYC':
|
||||||
|
// 跳转到实名认证页面
|
||||||
|
final orderNo = action.actionParams?['orderNo'] as String?;
|
||||||
|
final result = await context.push<bool>(RoutePaths.kycEntry, extra: orderNo);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
|
case 'SIGN_CONTRACT':
|
||||||
|
// 跳转到合同签署页面
|
||||||
|
final orderNo = action.actionParams?['orderNo'] as String?;
|
||||||
|
if (orderNo != null) {
|
||||||
|
final result = await context.push<bool>(
|
||||||
|
'${RoutePaths.contractSigning}/$orderNo',
|
||||||
|
);
|
||||||
|
return result == true;
|
||||||
|
}
|
||||||
|
// 跳转到待签合同列表
|
||||||
|
final result = await context.push<bool>(
|
||||||
|
RoutePaths.pendingContracts,
|
||||||
|
extra: true, // forceSign = true
|
||||||
|
);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
|
case 'UPDATE_PROFILE':
|
||||||
|
// 跳转到编辑资料页面
|
||||||
|
final result = await context.push<bool>(RoutePaths.editProfile);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 未知操作类型,显示提示并标记为完成
|
||||||
|
debugPrint('[PendingActionsPage] 未知操作类型: ${action.actionCode}');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 所有操作完成,跳转到主页
|
||||||
|
void _completeAllActions() {
|
||||||
|
if (mounted) {
|
||||||
|
debugPrint('[PendingActionsPage] 所有待办操作完成,跳转到主页');
|
||||||
|
context.go(RoutePaths.ranking);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
// 禁止返回
|
||||||
|
canPop: false,
|
||||||
|
child: Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFFF7E6),
|
||||||
|
Color(0xFFEAE0C8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(),
|
||||||
|
Expanded(child: _buildContent()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.assignment_outlined,
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'待办事项',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'请完成以下任务后继续使用',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_pendingActions.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildProgress(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProgress() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'进度',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$_currentIndex / ${_pendingActions.length}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: _pendingActions.isEmpty
|
||||||
|
? 0
|
||||||
|
: _currentIndex / _pendingActions.length,
|
||||||
|
backgroundColor: Colors.grey[300],
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(color: Color(0xFFD4AF37)),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'正在加载待办事项...',
|
||||||
|
style: TextStyle(fontSize: 14, color: Color(0xFF666666)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage != null) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _loadPendingActions,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFD4AF37),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingActions.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle_outline, color: Color(0xFF4CAF50), size: 64),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'所有待办事项已完成',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Color(0xFF4CAF50),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentIndex >= _pendingActions.length) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle_outline, color: Color(0xFF4CAF50), size: 64),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'恭喜!所有任务已完成',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Color(0xFF4CAF50),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: _pendingActions.length,
|
||||||
|
itemBuilder: (context, index) => _buildActionCard(index),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionCard(int index) {
|
||||||
|
final action = _pendingActions[index];
|
||||||
|
final isCompleted = index < _currentIndex;
|
||||||
|
final isCurrent = index == _currentIndex;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: isCurrent
|
||||||
|
? Border.all(color: const Color(0xFFD4AF37), width: 2)
|
||||||
|
: isCompleted
|
||||||
|
? Border.all(color: const Color(0xFF4CAF50), width: 1)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 状态图标
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCompleted
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: isCurrent
|
||||||
|
? const Color(0xFFD4AF37)
|
||||||
|
: Colors.grey[300],
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isCompleted
|
||||||
|
? Icons.check
|
||||||
|
: _getActionIcon(action.actionCode),
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// 操作信息
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
action.displayLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isCompleted
|
||||||
|
? Colors.grey
|
||||||
|
: const Color(0xFF5D4037),
|
||||||
|
decoration: isCompleted
|
||||||
|
? TextDecoration.lineThrough
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
isCompleted
|
||||||
|
? '已完成'
|
||||||
|
: isCurrent
|
||||||
|
? '当前任务'
|
||||||
|
: '待处理',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isCompleted
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: isCurrent
|
||||||
|
? const Color(0xFFD4AF37)
|
||||||
|
: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 操作按钮
|
||||||
|
if (isCurrent)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isExecuting ? null : _executeCurrentAction,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFD4AF37),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _isExecuting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('开始'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getActionIcon(String actionCode) {
|
||||||
|
switch (actionCode) {
|
||||||
|
case 'ADOPTION_WIZARD':
|
||||||
|
return Icons.nature;
|
||||||
|
case 'SETTLE_REWARDS':
|
||||||
|
return Icons.monetization_on;
|
||||||
|
case 'BIND_PHONE':
|
||||||
|
return Icons.phone_android;
|
||||||
|
case 'FORCE_KYC':
|
||||||
|
return Icons.person_outline;
|
||||||
|
case 'SIGN_CONTRACT':
|
||||||
|
return Icons.description;
|
||||||
|
case 'UPDATE_PROFILE':
|
||||||
|
return Icons.edit;
|
||||||
|
default:
|
||||||
|
return Icons.assignment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ import '../features/kyc/presentation/pages/kyc_id_card_page.dart';
|
||||||
import '../features/kyc/presentation/pages/change_phone_page.dart';
|
import '../features/kyc/presentation/pages/change_phone_page.dart';
|
||||||
import '../features/contract_signing/presentation/pages/contract_signing_page.dart';
|
import '../features/contract_signing/presentation/pages/contract_signing_page.dart';
|
||||||
import '../features/contract_signing/presentation/pages/pending_contracts_page.dart';
|
import '../features/contract_signing/presentation/pages/pending_contracts_page.dart';
|
||||||
|
import '../features/pending_actions/presentation/pages/pending_actions_page.dart';
|
||||||
import 'route_paths.dart';
|
import 'route_paths.dart';
|
||||||
import 'route_names.dart';
|
import 'route_names.dart';
|
||||||
|
|
||||||
|
|
@ -435,6 +436,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Pending Actions Page (待办操作)
|
||||||
|
GoRoute(
|
||||||
|
path: RoutePaths.pendingActions,
|
||||||
|
name: RouteNames.pendingActions,
|
||||||
|
builder: (context, state) => const PendingActionsPage(),
|
||||||
|
),
|
||||||
|
|
||||||
// Main Shell with Bottom Navigation
|
// Main Shell with Bottom Navigation
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
navigatorKey: _shellNavigatorKey,
|
navigatorKey: _shellNavigatorKey,
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,7 @@ class RouteNames {
|
||||||
// Contract Signing (合同签署)
|
// Contract Signing (合同签署)
|
||||||
static const contractSigning = 'contract-signing';
|
static const contractSigning = 'contract-signing';
|
||||||
static const pendingContracts = 'pending-contracts';
|
static const pendingContracts = 'pending-contracts';
|
||||||
|
|
||||||
|
// Pending Actions (待办操作)
|
||||||
|
static const pendingActions = 'pending-actions';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,7 @@ class RoutePaths {
|
||||||
// Contract Signing (合同签署)
|
// Contract Signing (合同签署)
|
||||||
static const contractSigning = '/contract-signing';
|
static const contractSigning = '/contract-signing';
|
||||||
static const pendingContracts = '/contract-signing/pending';
|
static const pendingContracts = '/contract-signing/pending';
|
||||||
|
|
||||||
|
// Pending Actions (待办操作)
|
||||||
|
static const pendingActions = '/pending-actions';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue