feat(admin): 用户预种数量展示 & 授权申请照片查看
## 功能7:用户管理展示预种数量
### 需求
在用户管理(列表+详情页)展示个人及团队预种份数,
并支持跳转预种管理页查看团队中哪些ID购买了预种。
### 后端变更
referral-service — 新增内部 API:
- GET /internal/referral/pre-planting-stats/:accountSequence
- POST /internal/referral/pre-planting-stats/batch
从 TeamStatistics 表查询 selfPrePlantingPortions/teamPrePlantingPortions
admin-service:
- 新建 ReferralProxyService (src/referral/) 代理 referral-service 内部 API
- UserFullDetailDto 新增 selfPrePlantingPortions, teamPrePlantingPortions
- user-detail.controller 并行调用获取预种统计
- user.controller 批量获取用户列表预种统计
- UserListItemDto 新增 selfPrePlantingPortions, teamPrePlantingPortions
- pre-planting-config.controller 新增 teamOf 查询参数
→ 先从 referralQueryView 获取团队成员列表
→ 将 accountSequences 传递给 planting-service 过滤
planting-service:
- internal-pre-planting.controller 的 admin orders/positions 端点
新增 accountSequences 查询参数,支持按用户列表过滤
### 前端变更
admin-web:
- 用户列表页表格新增"个人预种(份)"、"团队预种(份)"两列
- 用户详情页新增预种统计卡片(个人/团队预种份数)
- 团队预种数字可点击,跳转 /pre-planting?teamOf={accountSequence}
- 预种管理页支持 teamOf URL 参数,团队过滤模式下显示提示条+返回链接
---
## 功能8:授权管理查看申请照片
### 需求
管理后台无法查看社区/市公司/省公司申请时提供的办公室照片。
原因:authorization-service 收到 officePhotoUrls 后只验证未持久化。
照片文件本身已存储在 MinIO,只需持久化 URL 到数据库。
### 后端变更
authorization-service:
- schema.prisma: AuthorizationRole 新增 officePhotoUrls String[] 字段
- authorization-role.aggregate: props/字段/getter/构造/toPersistence/所有工厂方法
- repository: save() create/update 块 + toDomain() 映射
- application service: 3个 self-apply 方法传递 officePhotoUrls 到 aggregate
- admin controller: queryAuthorizations 响应包含 officePhotoUrls
admin-service:
- schema.prisma: AuthorizationRoleQueryView 新增 officePhotoUrls 字段
- user-detail-query.repository 接口 + impl: 包含 officePhotoUrls
- user-detail.dto: AuthorizationRoleDto 新增 officePhotoUrls
### 前端变更
admin-web:
- 授权管理页表格新增"申请照片"列,点击弹出照片网格 Modal
- 照片支持点击放大(全屏 lightbox 覆盖层)
- 用户详情页授权信息 Tab 同样支持查看照片
- authorization.types.ts / userDetail.types.ts 类型更新
---
## 部署注意事项
1. authorization-service 需运行: npx prisma migrate dev --name add-office-photo-urls
2. admin-service 需运行: npx prisma migrate dev --name add-auth-role-office-photos
3. 需部署: authorization-service, admin-service, referral-service, planting-service, admin-web
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d5d61f4f68
commit
ac15d6682a
|
|
@ -796,6 +796,9 @@ model AuthorizationRoleQueryView {
|
|||
lastAssessmentMonth String? @map("last_assessment_month")
|
||||
monthlyTreesAdded Int @default(0) @map("monthly_trees_added")
|
||||
|
||||
// 申请时提供的办公室照片(MinIO URL)
|
||||
officePhotoUrls String[] @default([]) @map("office_photo_urls")
|
||||
|
||||
// 时间戳
|
||||
createdAt DateTime @map("created_at")
|
||||
syncedAt DateTime @default(now()) @map("synced_at")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
IUserDetailQueryRepository,
|
||||
USER_DETAIL_QUERY_REPOSITORY,
|
||||
} from '../../domain/repositories/user-detail-query.repository';
|
||||
import { ReferralProxyService } from '../../referral/referral-proxy.service';
|
||||
|
||||
/**
|
||||
* 用户详情控制器
|
||||
|
|
@ -40,6 +41,7 @@ export class UserDetailController {
|
|||
private readonly userQueryRepository: IUserQueryRepository,
|
||||
@Inject(USER_DETAIL_QUERY_REPOSITORY)
|
||||
private readonly userDetailRepository: IUserDetailQueryRepository,
|
||||
private readonly referralProxyService: ReferralProxyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -58,11 +60,12 @@ export class UserDetailController {
|
|||
}
|
||||
|
||||
// 并行获取所有相关数据
|
||||
const [referralInfo, personalAdoptions, teamStats, directReferralCount] = await Promise.all([
|
||||
const [referralInfo, personalAdoptions, teamStats, directReferralCount, prePlantingStats] = await Promise.all([
|
||||
this.userDetailRepository.getReferralInfo(accountSequence),
|
||||
this.userDetailRepository.getPersonalAdoptionCount(accountSequence),
|
||||
this.userDetailRepository.getTeamStats(accountSequence),
|
||||
this.userDetailRepository.getDirectReferralCount(accountSequence),
|
||||
this.referralProxyService.getPrePlantingStats(accountSequence),
|
||||
]);
|
||||
|
||||
// 获取推荐人昵称
|
||||
|
|
@ -87,6 +90,8 @@ export class UserDetailController {
|
|||
registeredAt: user.registeredAt.toISOString(),
|
||||
lastActiveAt: user.lastActiveAt?.toISOString() || null,
|
||||
personalAdoptions: personalAdoptions,
|
||||
selfPrePlantingPortions: prePlantingStats.selfPrePlantingPortions,
|
||||
teamPrePlantingPortions: prePlantingStats.teamPrePlantingPortions,
|
||||
teamAddresses: teamStats.teamAddressCount,
|
||||
teamAdoptions: teamStats.teamAdoptionCount,
|
||||
provincialAdoptions: {
|
||||
|
|
@ -371,6 +376,7 @@ export class UserDetailController {
|
|||
monthlyTargetType: role.monthlyTargetType,
|
||||
lastAssessmentMonth: role.lastAssessmentMonth,
|
||||
monthlyTreesAdded: role.monthlyTreesAdded,
|
||||
officePhotoUrls: role.officePhotoUrls,
|
||||
createdAt: role.createdAt.toISOString(),
|
||||
})),
|
||||
assessments: assessments.map((assessment) => ({
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
IUserDetailQueryRepository,
|
||||
USER_DETAIL_QUERY_REPOSITORY,
|
||||
} from '../../domain/repositories/user-detail-query.repository';
|
||||
import { ReferralProxyService } from '../../referral/referral-proxy.service';
|
||||
|
||||
/**
|
||||
* 用户管理控制器
|
||||
|
|
@ -34,6 +35,7 @@ export class UserController {
|
|||
private readonly userQueryRepository: IUserQueryRepository,
|
||||
@Inject(USER_DETAIL_QUERY_REPOSITORY)
|
||||
private readonly userDetailRepository: IUserDetailQueryRepository,
|
||||
private readonly referralProxyService: ReferralProxyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -70,7 +72,10 @@ export class UserController {
|
|||
|
||||
// 批量获取实时统计数据
|
||||
const accountSequences = result.items.map(item => item.accountSequence);
|
||||
const statsMap = await this.userDetailRepository.getBatchUserStats(accountSequences);
|
||||
const [statsMap, prePlantingStatsMap] = await Promise.all([
|
||||
this.userDetailRepository.getBatchUserStats(accountSequences),
|
||||
this.referralProxyService.batchGetPrePlantingStats(accountSequences),
|
||||
]);
|
||||
|
||||
// 获取所有用户的团队总认种数用于计算百分比(使用实时数据)
|
||||
let totalTeamAdoptions = 0;
|
||||
|
|
@ -79,7 +84,7 @@ export class UserController {
|
|||
}
|
||||
|
||||
return {
|
||||
items: result.items.map((item) => this.mapToListItem(item, totalTeamAdoptions, statsMap.get(item.accountSequence))),
|
||||
items: result.items.map((item) => this.mapToListItem(item, totalTeamAdoptions, statsMap.get(item.accountSequence), prePlantingStatsMap[item.accountSequence])),
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
|
|
@ -157,6 +162,10 @@ export class UserController {
|
|||
provinceAdoptionCount: number;
|
||||
cityAdoptionCount: number;
|
||||
},
|
||||
prePlantingStats?: {
|
||||
selfPrePlantingPortions: number;
|
||||
teamPrePlantingPortions: number;
|
||||
},
|
||||
): UserListItemDto {
|
||||
// 使用实时统计数据(如果有),否则使用预计算数据
|
||||
const personalAdoptions = realTimeStats?.personalAdoptionCount ?? item.personalAdoptionCount;
|
||||
|
|
@ -181,6 +190,8 @@ export class UserController {
|
|||
nickname: item.nickname,
|
||||
phoneNumberMasked: item.phoneNumberMasked,
|
||||
personalAdoptions,
|
||||
selfPrePlantingPortions: prePlantingStats?.selfPrePlantingPortions ?? 0,
|
||||
teamPrePlantingPortions: prePlantingStats?.teamPrePlantingPortions ?? 0,
|
||||
teamAddresses,
|
||||
teamAdoptions,
|
||||
provincialAdoptions: {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ export class UserFullDetailDto {
|
|||
percentage: number;
|
||||
};
|
||||
|
||||
// 预种统计
|
||||
selfPrePlantingPortions!: number;
|
||||
teamPrePlantingPortions!: number;
|
||||
|
||||
// 排名
|
||||
ranking!: number | null;
|
||||
|
||||
|
|
@ -215,6 +219,7 @@ export class AuthorizationRoleDto {
|
|||
monthlyTargetType!: string;
|
||||
lastAssessmentMonth!: string | null;
|
||||
monthlyTreesAdded!: number;
|
||||
officePhotoUrls!: string[];
|
||||
createdAt!: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export class UserListItemDto {
|
|||
nickname!: string | null;
|
||||
phoneNumberMasked!: string | null;
|
||||
personalAdoptions!: number;
|
||||
selfPrePlantingPortions!: number;
|
||||
teamPrePlantingPortions!: number;
|
||||
teamAddresses!: number;
|
||||
teamAdoptions!: number;
|
||||
provincialAdoptions!: {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ import { PrePlantingConfigController, PublicPrePlantingConfigController } from '
|
|||
import { PrePlantingConfigService } from './pre-planting/pre-planting-config.service';
|
||||
// [2026-02-27] 新增:预种计划数据代理(admin-service → planting-service 内部 HTTP)
|
||||
import { PrePlantingProxyService } from './pre-planting/pre-planting-proxy.service';
|
||||
// [2026-03-02] 新增:推荐链预种统计代理(admin-service → referral-service 内部 HTTP)
|
||||
import { ReferralProxyService } from './referral/referral-proxy.service';
|
||||
// [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价)
|
||||
import { AdminTreePricingController, PublicTreePricingController } from './pricing/tree-pricing.controller';
|
||||
import { TreePricingService } from './pricing/tree-pricing.service';
|
||||
|
|
@ -235,6 +237,8 @@ import { AutoPriceIncreaseJob } from './infrastructure/jobs/auto-price-increase.
|
|||
PrePlantingConfigService,
|
||||
// [2026-02-27] 新增:预种计划数据代理
|
||||
PrePlantingProxyService,
|
||||
// [2026-03-02] 新增:推荐链预种统计代理
|
||||
ReferralProxyService,
|
||||
// [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价)
|
||||
TreePricingService,
|
||||
AutoPriceIncreaseJob,
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ export interface AuthorizationRole {
|
|||
monthlyTargetType: string;
|
||||
lastAssessmentMonth: string | null;
|
||||
monthlyTreesAdded: number;
|
||||
officePhotoUrls: string[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -448,6 +448,7 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
|
|||
monthlyTargetType: role.monthlyTargetType,
|
||||
lastAssessmentMonth: role.lastAssessmentMonth,
|
||||
monthlyTreesAdded: role.monthlyTreesAdded,
|
||||
officePhotoUrls: role.officePhotoUrls ?? [],
|
||||
createdAt: role.createdAt,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import {
|
|||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { PrePlantingConfigService } from './pre-planting-config.service';
|
||||
import { PrePlantingProxyService } from './pre-planting-proxy.service';
|
||||
import { PrismaService } from '../infrastructure/persistence/prisma/prisma.service';
|
||||
|
||||
class UpdatePrePlantingConfigDto {
|
||||
@IsBoolean()
|
||||
|
|
@ -35,11 +37,44 @@ class UpdatePrePlantingAgreementDto {
|
|||
@ApiTags('预种计划配置')
|
||||
@Controller('admin/pre-planting')
|
||||
export class PrePlantingConfigController {
|
||||
private readonly logger = new Logger(PrePlantingConfigController.name);
|
||||
|
||||
constructor(
|
||||
private readonly configService: PrePlantingConfigService,
|
||||
private readonly proxyService: PrePlantingProxyService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 根据 accountSequence 查找该用户的团队成员 accountSequence 列表
|
||||
* 逻辑:找到该用户的 userId,然后查找 ancestorPath 包含该 userId 的所有用户
|
||||
* 返回的列表包含该用户本人
|
||||
*/
|
||||
private async resolveTeamAccountSequences(teamOfAccountSeq: string): Promise<string[]> {
|
||||
// 1. 找到 teamOf 用户的 userId
|
||||
const leader = await this.prisma.referralQueryView.findUnique({
|
||||
where: { accountSequence: teamOfAccountSeq },
|
||||
select: { userId: true, accountSequence: true },
|
||||
});
|
||||
if (!leader) {
|
||||
this.logger.warn(`[resolveTeamAccountSequences] 未找到用户: ${teamOfAccountSeq}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. 查找 ancestorPath 包含该 userId 的所有下级用户(PostgreSQL array contains)
|
||||
const teamMembers = await this.prisma.referralQueryView.findMany({
|
||||
where: {
|
||||
ancestorPath: { has: leader.userId },
|
||||
},
|
||||
select: { accountSequence: true },
|
||||
});
|
||||
|
||||
// 3. 包含团队领导本人
|
||||
const sequences = [leader.accountSequence, ...teamMembers.map((m) => m.accountSequence)];
|
||||
this.logger.debug(`[resolveTeamAccountSequences] ${teamOfAccountSeq} 团队成员数: ${sequences.length}`);
|
||||
return sequences;
|
||||
}
|
||||
|
||||
@Get('config')
|
||||
@ApiOperation({ summary: '获取预种计划开关状态(含协议文本)' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '开关状态' })
|
||||
|
|
@ -75,17 +110,28 @@ export class PrePlantingConfigController {
|
|||
@ApiQuery({ name: 'pageSize', required: false })
|
||||
@ApiQuery({ name: 'keyword', required: false })
|
||||
@ApiQuery({ name: 'status', required: false })
|
||||
@ApiQuery({ name: 'teamOf', required: false, description: '团队筛选:指定用户 accountSequence,只显示其团队成员的订单' })
|
||||
async getOrders(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('teamOf') teamOf?: string,
|
||||
) {
|
||||
let accountSequences: string[] | undefined;
|
||||
if (teamOf) {
|
||||
accountSequences = await this.resolveTeamAccountSequences(teamOf);
|
||||
if (accountSequences.length === 0) {
|
||||
return { items: [], total: 0, page: page ? parseInt(page, 10) : 1, pageSize: pageSize ? parseInt(pageSize, 10) : 20 };
|
||||
}
|
||||
}
|
||||
|
||||
return this.proxyService.getOrders({
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||||
keyword: keyword || undefined,
|
||||
status: status || undefined,
|
||||
accountSequences,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -94,15 +140,26 @@ export class PrePlantingConfigController {
|
|||
@ApiQuery({ name: 'page', required: false })
|
||||
@ApiQuery({ name: 'pageSize', required: false })
|
||||
@ApiQuery({ name: 'keyword', required: false })
|
||||
@ApiQuery({ name: 'teamOf', required: false, description: '团队筛选:指定用户 accountSequence,只显示其团队成员的持仓' })
|
||||
async getPositions(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('teamOf') teamOf?: string,
|
||||
) {
|
||||
let accountSequences: string[] | undefined;
|
||||
if (teamOf) {
|
||||
accountSequences = await this.resolveTeamAccountSequences(teamOf);
|
||||
if (accountSequences.length === 0) {
|
||||
return { items: [], total: 0, page: page ? parseInt(page, 10) : 1, pageSize: pageSize ? parseInt(pageSize, 10) : 20 };
|
||||
}
|
||||
}
|
||||
|
||||
return this.proxyService.getPositions({
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||||
keyword: keyword || undefined,
|
||||
accountSequences,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export class PrePlantingProxyService {
|
|||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
accountSequences?: string[];
|
||||
}) {
|
||||
try {
|
||||
const qp = new URLSearchParams();
|
||||
|
|
@ -44,6 +45,7 @@ export class PrePlantingProxyService {
|
|||
if (params.pageSize) qp.append('pageSize', params.pageSize.toString());
|
||||
if (params.keyword) qp.append('keyword', params.keyword);
|
||||
if (params.status) qp.append('status', params.status);
|
||||
if (params.accountSequences?.length) qp.append('accountSequences', params.accountSequences.join(','));
|
||||
|
||||
const url = `/api/v1/internal/pre-planting/admin/orders?${qp.toString()}`;
|
||||
this.logger.debug(`[getOrders] 请求: ${url}`);
|
||||
|
|
@ -59,12 +61,14 @@ export class PrePlantingProxyService {
|
|||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
accountSequences?: string[];
|
||||
}) {
|
||||
try {
|
||||
const qp = new URLSearchParams();
|
||||
if (params.page) qp.append('page', params.page.toString());
|
||||
if (params.pageSize) qp.append('pageSize', params.pageSize.toString());
|
||||
if (params.keyword) qp.append('keyword', params.keyword);
|
||||
if (params.accountSequences?.length) qp.append('accountSequences', params.accountSequences.join(','));
|
||||
|
||||
const url = `/api/v1/internal/pre-planting/admin/positions?${qp.toString()}`;
|
||||
this.logger.debug(`[getPositions] 请求: ${url}`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* 推荐链预种统计代理服务
|
||||
* [2026-03-02] 新增:通过内部 HTTP 调用 referral-service 获取预种统计数据
|
||||
*
|
||||
* === 架构 ===
|
||||
* admin-web → admin-service (本服务) → referral-service /internal/referral/pre-planting-stats/*
|
||||
* 复用现有 PrePlantingProxyService 的 axios 代理模式
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export interface PrePlantingStats {
|
||||
selfPrePlantingPortions: number;
|
||||
teamPrePlantingPortions: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReferralProxyService {
|
||||
private readonly logger = new Logger(ReferralProxyService.name);
|
||||
private readonly httpClient: AxiosInstance;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const referralServiceUrl = this.configService.get<string>(
|
||||
'REFERRAL_SERVICE_URL',
|
||||
'http://rwa-referral-service:3004',
|
||||
);
|
||||
|
||||
this.httpClient = axios.create({
|
||||
baseURL: referralServiceUrl,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`ReferralProxyService initialized, referral-service URL: ${referralServiceUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个用户的预种统计(个人 + 团队预种份数)
|
||||
*/
|
||||
async getPrePlantingStats(accountSequence: string): Promise<PrePlantingStats> {
|
||||
try {
|
||||
const url = `/api/v1/internal/referral/pre-planting-stats/${accountSequence}`;
|
||||
this.logger.debug(`[getPrePlantingStats] 请求: ${url}`);
|
||||
const response = await this.httpClient.get(url);
|
||||
return {
|
||||
selfPrePlantingPortions: response.data?.selfPrePlantingPortions ?? 0,
|
||||
teamPrePlantingPortions: response.data?.teamPrePlantingPortions ?? 0,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`[getPrePlantingStats] 失败 (${accountSequence}): ${error.message}`);
|
||||
return { selfPrePlantingPortions: 0, teamPrePlantingPortions: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取多个用户的预种统计
|
||||
*/
|
||||
async batchGetPrePlantingStats(
|
||||
accountSequences: string[],
|
||||
): Promise<Record<string, PrePlantingStats>> {
|
||||
if (accountSequences.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const url = '/api/v1/internal/referral/pre-planting-stats/batch';
|
||||
this.logger.debug(`[batchGetPrePlantingStats] 请求: ${url}, 数量: ${accountSequences.length}`);
|
||||
const response = await this.httpClient.post(url, { accountSequences });
|
||||
return response.data ?? {};
|
||||
} catch (error) {
|
||||
this.logger.error(`[batchGetPrePlantingStats] 失败: ${error.message}`);
|
||||
// 返回所有用户的零值默认
|
||||
const defaults: Record<string, PrePlantingStats> = {};
|
||||
for (const seq of accountSequences) {
|
||||
defaults[seq] = { selfPrePlantingPortions: 0, teamPrePlantingPortions: 0 };
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@ model AuthorizationRole {
|
|||
// 当前考核月份索引
|
||||
currentMonthIndex Int @default(0) @map("current_month_index")
|
||||
|
||||
// 申请时提供的办公室照片(MinIO URL)
|
||||
officePhotoUrls String[] @default([]) @map("office_photo_urls")
|
||||
|
||||
// 时间戳
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export class AdminAuthorizationController {
|
|||
authorizedAt: Date | null
|
||||
revokedAt: Date | null
|
||||
revokeReason: string | null
|
||||
officePhotoUrls: string[]
|
||||
}>
|
||||
total: number
|
||||
page: number
|
||||
|
|
|
|||
|
|
@ -3404,6 +3404,7 @@ export class AuthorizationApplicationService {
|
|||
const authorization = AuthorizationRole.createSelfAppliedCommunity({
|
||||
userId,
|
||||
communityName: command.communityName!,
|
||||
officePhotoUrls: command.officePhotoUrls,
|
||||
})
|
||||
|
||||
// 检查初始考核
|
||||
|
|
@ -3465,6 +3466,7 @@ export class AuthorizationApplicationService {
|
|||
userId,
|
||||
cityCode: command.cityCode!,
|
||||
cityName: command.cityName!,
|
||||
officePhotoUrls: command.officePhotoUrls,
|
||||
})
|
||||
|
||||
// 检查初始考核(500棵)
|
||||
|
|
@ -3526,6 +3528,7 @@ export class AuthorizationApplicationService {
|
|||
userId,
|
||||
provinceCode: command.provinceCode!,
|
||||
provinceName: command.provinceName!,
|
||||
officePhotoUrls: command.officePhotoUrls,
|
||||
})
|
||||
|
||||
// 检查初始考核(500棵)
|
||||
|
|
@ -3941,6 +3944,7 @@ export class AuthorizationApplicationService {
|
|||
authorizedAt: auth.authorizedAt,
|
||||
revokedAt: auth.revokedAt,
|
||||
revokeReason: auth.revokeReason,
|
||||
officePhotoUrls: auth.officePhotoUrls,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export interface AuthorizationRoleProps {
|
|||
lastMonthTreesAdded: number // 上月新增树数(考核用存档)
|
||||
currentMonthIndex: number
|
||||
deletedAt: Date | null // 软删除时间 (大厂通用做法)
|
||||
officePhotoUrls: string[] // 办公室照片 URL 列表
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
|
@ -93,6 +94,9 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
// 软删除 (大厂通用做法)
|
||||
private _deletedAt: Date | null
|
||||
|
||||
// 办公室照片
|
||||
private _officePhotoUrls: string[]
|
||||
|
||||
private _createdAt: Date
|
||||
private _updatedAt: Date
|
||||
|
||||
|
|
@ -175,6 +179,9 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
get deletedAt(): Date | null {
|
||||
return this._deletedAt
|
||||
}
|
||||
get officePhotoUrls(): string[] {
|
||||
return this._officePhotoUrls
|
||||
}
|
||||
get isActive(): boolean {
|
||||
return this._status === AuthorizationStatus.AUTHORIZED
|
||||
}
|
||||
|
|
@ -206,6 +213,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
this._lastMonthTreesAdded = props.lastMonthTreesAdded
|
||||
this._currentMonthIndex = props.currentMonthIndex
|
||||
this._deletedAt = props.deletedAt
|
||||
this._officePhotoUrls = props.officePhotoUrls
|
||||
this._createdAt = props.createdAt
|
||||
this._updatedAt = props.updatedAt
|
||||
}
|
||||
|
|
@ -268,6 +276,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
|
@ -284,7 +293,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
}
|
||||
|
||||
// 工厂方法 - 自助申请社区授权(直接生效,无需审核)
|
||||
static createSelfAppliedCommunity(params: { userId: UserId; communityName: string }): AuthorizationRole {
|
||||
static createSelfAppliedCommunity(params: { userId: UserId; communityName: string; officePhotoUrls?: string[] }): AuthorizationRole {
|
||||
const now = new Date()
|
||||
const auth = new AuthorizationRole({
|
||||
authorizationId: AuthorizationId.generate(),
|
||||
|
|
@ -311,6 +320,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: params.officePhotoUrls ?? [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -361,6 +371,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: skipAssessment ? 1 : 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -409,6 +420,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -430,6 +442,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
userId: UserId
|
||||
provinceCode: string
|
||||
provinceName: string
|
||||
officePhotoUrls?: string[]
|
||||
}): AuthorizationRole {
|
||||
const now = new Date()
|
||||
const auth = new AuthorizationRole({
|
||||
|
|
@ -457,6 +470,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: params.officePhotoUrls ?? [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -509,6 +523,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 1, // 从第1个月开始考核
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -558,6 +573,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -579,6 +595,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
userId: UserId
|
||||
cityCode: string
|
||||
cityName: string
|
||||
officePhotoUrls?: string[]
|
||||
}): AuthorizationRole {
|
||||
const now = new Date()
|
||||
const auth = new AuthorizationRole({
|
||||
|
|
@ -606,6 +623,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: params.officePhotoUrls ?? [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -658,6 +676,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 1, // 从第1个月开始考核
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -708,6 +727,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 1, // 从第1个月开始考核(与手动授权一致)
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -758,6 +778,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: 1, // 从第1个月开始考核(与手动授权一致)
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -810,6 +831,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: skipAssessment ? 1 : 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -862,6 +884,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
lastMonthTreesAdded: 0,
|
||||
currentMonthIndex: skipAssessment ? 1 : 0,
|
||||
deletedAt: null,
|
||||
officePhotoUrls: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
|
@ -1170,6 +1193,7 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
monthlyTreesAdded: this._monthlyTreesAdded,
|
||||
lastMonthTreesAdded: this._lastMonthTreesAdded,
|
||||
currentMonthIndex: this._currentMonthIndex,
|
||||
officePhotoUrls: this._officePhotoUrls,
|
||||
deletedAt: this._deletedAt,
|
||||
createdAt: this._createdAt,
|
||||
updatedAt: this._updatedAt,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
monthlyTreesAdded: data.monthlyTreesAdded,
|
||||
lastMonthTreesAdded: data.lastMonthTreesAdded,
|
||||
currentMonthIndex: data.currentMonthIndex,
|
||||
officePhotoUrls: data.officePhotoUrls,
|
||||
deletedAt: data.deletedAt,
|
||||
},
|
||||
update: {
|
||||
|
|
@ -72,6 +73,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
monthlyTreesAdded: data.monthlyTreesAdded,
|
||||
lastMonthTreesAdded: data.lastMonthTreesAdded,
|
||||
currentMonthIndex: data.currentMonthIndex,
|
||||
officePhotoUrls: data.officePhotoUrls,
|
||||
deletedAt: data.deletedAt,
|
||||
},
|
||||
})
|
||||
|
|
@ -518,6 +520,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
monthlyTreesAdded: record.monthlyTreesAdded ?? 0,
|
||||
lastMonthTreesAdded: record.lastMonthTreesAdded ?? 0,
|
||||
currentMonthIndex: record.currentMonthIndex,
|
||||
officePhotoUrls: record.officePhotoUrls ?? [],
|
||||
deletedAt: record.deletedAt,
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
|
|
|
|||
|
|
@ -47,22 +47,37 @@ export class InternalPrePlantingController {
|
|||
@ApiQuery({ name: 'pageSize', required: false })
|
||||
@ApiQuery({ name: 'keyword', required: false })
|
||||
@ApiQuery({ name: 'status', required: false })
|
||||
@ApiQuery({ name: 'accountSequences', required: false, description: '逗号分隔的 accountSequence 列表,用于团队筛选' })
|
||||
async getAdminOrders(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('accountSequences') accountSequences?: string,
|
||||
) {
|
||||
const p = Math.max(1, parseInt(page || '1', 10));
|
||||
const ps = Math.min(100, Math.max(1, parseInt(pageSize || '20', 10)));
|
||||
|
||||
const where: any = {};
|
||||
if (status) where.status = status;
|
||||
if (accountSequences) {
|
||||
const seqList = accountSequences.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
if (seqList.length > 0) {
|
||||
where.accountSequence = { in: seqList };
|
||||
}
|
||||
}
|
||||
if (keyword) {
|
||||
where.OR = [
|
||||
{ orderNo: { contains: keyword, mode: 'insensitive' } },
|
||||
{ accountSequence: { contains: keyword, mode: 'insensitive' } },
|
||||
];
|
||||
// 当同时存在 accountSequences 时,keyword 只搜 orderNo(accountSequence 已被限定)
|
||||
if (accountSequences) {
|
||||
where.OR = [
|
||||
{ orderNo: { contains: keyword, mode: 'insensitive' } },
|
||||
];
|
||||
} else {
|
||||
where.OR = [
|
||||
{ orderNo: { contains: keyword, mode: 'insensitive' } },
|
||||
{ accountSequence: { contains: keyword, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
|
|
@ -103,17 +118,34 @@ export class InternalPrePlantingController {
|
|||
@ApiQuery({ name: 'page', required: false })
|
||||
@ApiQuery({ name: 'pageSize', required: false })
|
||||
@ApiQuery({ name: 'keyword', required: false })
|
||||
@ApiQuery({ name: 'accountSequences', required: false, description: '逗号分隔的 accountSequence 列表,用于团队筛选' })
|
||||
async getAdminPositions(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('accountSequences') accountSequences?: string,
|
||||
) {
|
||||
const p = Math.max(1, parseInt(page || '1', 10));
|
||||
const ps = Math.min(100, Math.max(1, parseInt(pageSize || '20', 10)));
|
||||
|
||||
const where: any = {};
|
||||
if (accountSequences) {
|
||||
const seqList = accountSequences.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
if (seqList.length > 0) {
|
||||
where.accountSequence = { in: seqList };
|
||||
}
|
||||
}
|
||||
if (keyword) {
|
||||
where.accountSequence = { contains: keyword, mode: 'insensitive' };
|
||||
// 当同时存在 accountSequences 时,用 AND 组合两个条件
|
||||
if (accountSequences && where.accountSequence) {
|
||||
where.AND = [
|
||||
{ accountSequence: where.accountSequence },
|
||||
{ accountSequence: { contains: keyword, mode: 'insensitive' } },
|
||||
];
|
||||
delete where.accountSequence;
|
||||
} else {
|
||||
where.accountSequence = { contains: keyword, mode: 'insensitive' };
|
||||
}
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ export * from './referral.controller';
|
|||
export * from './team-statistics.controller';
|
||||
export * from './internal-team-statistics.controller';
|
||||
export * from './internal-referral-chain.controller';
|
||||
export * from './internal-pre-planting-stats.controller';
|
||||
export * from './health.controller';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
Body,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
|
||||
/**
|
||||
* 内部预种统计API - 供其他微服务调用
|
||||
* 无需JWT认证(服务间内部通信)
|
||||
*/
|
||||
@ApiTags('Internal Pre-Planting Stats API')
|
||||
@Controller('internal/referral/pre-planting-stats')
|
||||
export class InternalPrePlantingStatsController {
|
||||
private readonly logger = new Logger(InternalPrePlantingStatsController.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 根据 accountSequence 获取单个用户的预种统计
|
||||
*/
|
||||
@Get(':accountSequence')
|
||||
@ApiOperation({ summary: '获取用户预种统计(内部API)' })
|
||||
@ApiParam({ name: 'accountSequence', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '预种统计数据',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
selfPrePlantingPortions: { type: 'number', description: '个人预种份数' },
|
||||
teamPrePlantingPortions: { type: 'number', description: '团队预种份数(含自己和所有下级)' },
|
||||
},
|
||||
},
|
||||
})
|
||||
async getPrePlantingStats(@Param('accountSequence') accountSequence: string) {
|
||||
this.logger.debug(`[INTERNAL] getPrePlantingStats: ${accountSequence}`);
|
||||
|
||||
const relationship = await this.prisma.referralRelationship.findUnique({
|
||||
where: { accountSequence },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!relationship) {
|
||||
this.logger.debug(`[INTERNAL] No referral found for accountSequence: ${accountSequence}`);
|
||||
return {
|
||||
selfPrePlantingPortions: 0,
|
||||
teamPrePlantingPortions: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await this.prisma.teamStatistics.findUnique({
|
||||
where: { userId: relationship.userId },
|
||||
select: {
|
||||
selfPrePlantingPortions: true,
|
||||
teamPrePlantingPortions: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!stats) {
|
||||
this.logger.debug(`[INTERNAL] No team statistics found for userId: ${relationship.userId}`);
|
||||
return {
|
||||
selfPrePlantingPortions: 0,
|
||||
teamPrePlantingPortions: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
selfPrePlantingPortions: stats.selfPrePlantingPortions,
|
||||
teamPrePlantingPortions: stats.teamPrePlantingPortions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取多个用户的预种统计
|
||||
*/
|
||||
@Post('batch')
|
||||
@ApiOperation({ summary: '批量获取用户预种统计(内部API)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '批量预种统计数据 (Map: accountSequence → stats)',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
selfPrePlantingPortions: { type: 'number' },
|
||||
teamPrePlantingPortions: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
async getBatchPrePlantingStats(
|
||||
@Body() body: { accountSequences: string[] },
|
||||
) {
|
||||
const { accountSequences } = body;
|
||||
this.logger.debug(`[INTERNAL] getBatchPrePlantingStats: ${accountSequences.length} accounts`);
|
||||
|
||||
// 批量查询所有 referral relationships
|
||||
const relationships = await this.prisma.referralRelationship.findMany({
|
||||
where: { accountSequence: { in: accountSequences } },
|
||||
select: { accountSequence: true, userId: true },
|
||||
});
|
||||
|
||||
const accountToUserId = new Map<string, bigint>();
|
||||
for (const rel of relationships) {
|
||||
accountToUserId.set(rel.accountSequence, rel.userId);
|
||||
}
|
||||
|
||||
// 批量查询所有 team statistics
|
||||
const userIds = relationships.map((r) => r.userId);
|
||||
const allStats = await this.prisma.teamStatistics.findMany({
|
||||
where: { userId: { in: userIds } },
|
||||
select: {
|
||||
userId: true,
|
||||
selfPrePlantingPortions: true,
|
||||
teamPrePlantingPortions: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userIdToStats = new Map<bigint, { selfPrePlantingPortions: number; teamPrePlantingPortions: number }>();
|
||||
for (const s of allStats) {
|
||||
userIdToStats.set(s.userId, {
|
||||
selfPrePlantingPortions: s.selfPrePlantingPortions,
|
||||
teamPrePlantingPortions: s.teamPrePlantingPortions,
|
||||
});
|
||||
}
|
||||
|
||||
// 组装结果
|
||||
const result: Record<string, { selfPrePlantingPortions: number; teamPrePlantingPortions: number }> = {};
|
||||
|
||||
for (const seq of accountSequences) {
|
||||
const userId = accountToUserId.get(seq);
|
||||
if (userId) {
|
||||
const stats = userIdToStats.get(userId);
|
||||
result[seq] = stats ?? { selfPrePlantingPortions: 0, teamPrePlantingPortions: 0 };
|
||||
} else {
|
||||
result[seq] = { selfPrePlantingPortions: 0, teamPrePlantingPortions: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TeamStatisticsController,
|
||||
InternalTeamStatisticsController,
|
||||
InternalReferralChainController,
|
||||
InternalPrePlantingStatsController,
|
||||
HealthController,
|
||||
} from '../api';
|
||||
import { InternalReferralController } from '../api/controllers/referral.controller';
|
||||
|
|
@ -18,6 +19,7 @@ import { InternalReferralController } from '../api/controllers/referral.controll
|
|||
TeamStatisticsController,
|
||||
InternalTeamStatisticsController,
|
||||
InternalReferralChainController,
|
||||
InternalPrePlantingStatsController,
|
||||
HealthController,
|
||||
InternalReferralController,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1001,6 +1001,96 @@
|
|||
cursor: help;
|
||||
}
|
||||
|
||||
/* 照片列 */
|
||||
.authorization__tableCell--photos {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 查看照片按钮 */
|
||||
.authorization__photoBtn {
|
||||
cursor: pointer;
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #2563eb;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(59, 130, 246, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
/* 宽模态框(用于照片查看) */
|
||||
.modal__content--wide {
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
/* 照片网格 */
|
||||
.photoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.photoGrid__item {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.photoGrid__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 全屏灯箱 */
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lightbox__img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.lightbox__close {
|
||||
margin-top: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.authorization__pagination {
|
||||
align-self: stretch;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ export default function AuthorizationPage() {
|
|||
const [page, setPage] = useState(1);
|
||||
const limit = 20;
|
||||
|
||||
// 照片查看状态
|
||||
const [showPhotoModal, setShowPhotoModal] = useState(false);
|
||||
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([]);
|
||||
const [lightboxPhoto, setLightboxPhoto] = useState<string | null>(null);
|
||||
|
||||
// 创建授权对话框状态
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [createForm, setCreateForm] = useState({
|
||||
|
|
@ -186,6 +191,12 @@ export default function AuthorizationPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// 打开照片查看对话框
|
||||
const openPhotoModal = (photos: string[]) => {
|
||||
setSelectedPhotos(photos);
|
||||
setShowPhotoModal(true);
|
||||
};
|
||||
|
||||
// 打开取消授权对话框
|
||||
const openRevokeModal = (item: Authorization) => {
|
||||
setRevokeTarget(item);
|
||||
|
|
@ -392,6 +403,15 @@ export default function AuthorizationPage() {
|
|||
>
|
||||
状态
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.authorization__tableCell,
|
||||
styles['authorization__tableCell--header'],
|
||||
styles['authorization__tableCell--photos']
|
||||
)}
|
||||
>
|
||||
申请照片
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.authorization__tableCell,
|
||||
|
|
@ -474,6 +494,23 @@ export default function AuthorizationPage() {
|
|||
{item.status === 'AUTHORIZED' ? '有效' : '已撤销'}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.authorization__tableCell,
|
||||
styles['authorization__tableCell--photos']
|
||||
)}
|
||||
>
|
||||
{item.officePhotoUrls && item.officePhotoUrls.length > 0 ? (
|
||||
<button
|
||||
className={styles.authorization__photoBtn}
|
||||
onClick={() => openPhotoModal(item.officePhotoUrls)}
|
||||
>
|
||||
查看 {item.officePhotoUrls.length} 张照片
|
||||
</button>
|
||||
) : (
|
||||
<span style={{ color: '#9ca3af', fontSize: '12px' }}>-</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.authorization__tableCell,
|
||||
|
|
@ -654,6 +691,53 @@ export default function AuthorizationPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 照片查看对话框 */}
|
||||
{showPhotoModal && selectedPhotos.length > 0 && (
|
||||
<div className={styles.modal__overlay} onClick={() => setShowPhotoModal(false)}>
|
||||
<div
|
||||
className={cn(styles.modal__content, styles['modal__content--wide'])}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className={styles.modal__title}>申请照片 ({selectedPhotos.length} 张)</h3>
|
||||
<div className={styles.photoGrid}>
|
||||
{selectedPhotos.map((url, index) => (
|
||||
<div key={index} className={styles.photoGrid__item}>
|
||||
<img
|
||||
src={url}
|
||||
alt={`申请照片 ${index + 1}`}
|
||||
className={styles.photoGrid__img}
|
||||
onClick={() => setLightboxPhoto(url)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.modal__footer}>
|
||||
<button
|
||||
className={styles.modal__cancelBtn}
|
||||
onClick={() => setShowPhotoModal(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 照片全屏查看 (Lightbox) */}
|
||||
{lightboxPhoto && (
|
||||
<div
|
||||
className={styles.lightbox}
|
||||
onClick={() => setLightboxPhoto(null)}
|
||||
>
|
||||
<img
|
||||
src={lightboxPhoto}
|
||||
alt="照片预览"
|
||||
className={styles.lightbox__img}
|
||||
/>
|
||||
<span className={styles.lightbox__close}>点击任意处关闭</span>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/common';
|
||||
import { PageContainer } from '@/components/layout';
|
||||
import { cn } from '@/utils/helpers';
|
||||
|
|
@ -62,6 +64,10 @@ const CONTRACT_STATUS_MAP: Record<string, { label: string; style: string }> = {
|
|||
* 预种计划管理页面
|
||||
*/
|
||||
export default function PrePlantingPage() {
|
||||
// === URL 参数:团队过滤 ===
|
||||
const searchParams = useSearchParams();
|
||||
const teamOf = searchParams.get('teamOf') || undefined;
|
||||
|
||||
// === Tab 与分页状态 ===
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('orders');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
|
@ -77,16 +83,19 @@ export default function PrePlantingPage() {
|
|||
page,
|
||||
pageSize,
|
||||
keyword: activeTab === 'orders' ? keyword : undefined,
|
||||
teamOf,
|
||||
});
|
||||
const positionsQuery = usePrePlantingPositions({
|
||||
page,
|
||||
pageSize,
|
||||
keyword: activeTab === 'positions' ? keyword : undefined,
|
||||
teamOf,
|
||||
});
|
||||
const mergesQuery = usePrePlantingMerges({
|
||||
page,
|
||||
pageSize,
|
||||
keyword: activeTab === 'merges' ? keyword : undefined,
|
||||
teamOf,
|
||||
});
|
||||
|
||||
// === 协议文本管理 ===
|
||||
|
|
@ -141,9 +150,23 @@ export default function PrePlantingPage() {
|
|||
<div className={styles.prePlanting}>
|
||||
{/* 页面标题 */}
|
||||
<div className={styles.prePlanting__header}>
|
||||
<h1 className={styles.prePlanting__title}>预种计划管理</h1>
|
||||
<h1 className={styles.prePlanting__title}>
|
||||
{teamOf ? `${teamOf} 的团队预种数据` : '预种计划管理'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* 团队过滤提示 */}
|
||||
{teamOf && (
|
||||
<div className={styles.prePlanting__teamBanner}>
|
||||
<span>
|
||||
正在查看 <strong>{teamOf}</strong> 的团队预种数据
|
||||
</span>
|
||||
<Link href={`/users/${teamOf}`} className={styles.prePlanting__teamBannerLink}>
|
||||
返回用户详情
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 预种开关卡片 */}
|
||||
<div
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,35 @@
|
|||
color: $text-primary;
|
||||
}
|
||||
|
||||
// 团队过滤提示横幅
|
||||
&__teamBanner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #d6eaf8 100%);
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
&__teamBannerLink {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1565c0;
|
||||
text-decoration: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(21, 101, 192, 0.08);
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(21, 101, 192, 0.16);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// 开关状态卡片
|
||||
&__switchCard {
|
||||
background: linear-gradient(135deg, #fff9e6 0%, #fff3cc 100%);
|
||||
|
|
|
|||
|
|
@ -184,6 +184,11 @@ export default function UserDetailPage() {
|
|||
const [contractsData, setContractsData] = useState<ContractsListResponse | null>(null);
|
||||
const [contractsLoading, setContractsLoading] = useState(false);
|
||||
|
||||
// 授权照片查看状态
|
||||
const [showAuthPhotoModal, setShowAuthPhotoModal] = useState(false);
|
||||
const [authSelectedPhotos, setAuthSelectedPhotos] = useState<string[]>([]);
|
||||
const [authLightboxPhoto, setAuthLightboxPhoto] = useState<string | null>(null);
|
||||
|
||||
// 获取用户完整信息
|
||||
const { data: userDetail, isLoading: detailLoading, error: detailError } = useUserFullDetail(accountSequence);
|
||||
|
||||
|
|
@ -372,6 +377,19 @@ export default function UserDetailPage() {
|
|||
<span className={styles.userDetail__statLabel}>团队认种量</span>
|
||||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.teamAdoptions)}</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>个人预种</span>
|
||||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.selfPrePlantingPortions)} 份</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>团队预种</span>
|
||||
<Link
|
||||
href={`/pre-planting?teamOf=${userDetail.accountSequence}`}
|
||||
className={styles.userDetail__statValueLink}
|
||||
>
|
||||
{formatNumber(userDetail.teamPrePlantingPortions)} 份
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>团队地址数</span>
|
||||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.teamAddresses)}</span>
|
||||
|
|
@ -775,6 +793,20 @@ export default function UserDetailPage() {
|
|||
<p><strong>初始目标:</strong> {formatNumber(role.initialTargetTreeCount)} 棵</p>
|
||||
<p><strong>月度目标类型:</strong> {role.monthlyTargetType}</p>
|
||||
<p><strong>授权时间:</strong> {formatDate(role.authorizedAt)}</p>
|
||||
{role.officePhotoUrls && role.officePhotoUrls.length > 0 && (
|
||||
<div className={styles.authTab__rolePhotos}>
|
||||
<strong>申请照片:</strong>
|
||||
<button
|
||||
className={styles.authTab__photoBtn}
|
||||
onClick={() => {
|
||||
setAuthSelectedPhotos(role.officePhotoUrls);
|
||||
setShowAuthPhotoModal(true);
|
||||
}}
|
||||
>
|
||||
查看 {role.officePhotoUrls.length} 张照片
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -1069,6 +1101,56 @@ export default function UserDetailPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 授权照片查看对话框 */}
|
||||
{showAuthPhotoModal && authSelectedPhotos.length > 0 && (
|
||||
<div
|
||||
className={styles.authPhotoModal__overlay}
|
||||
onClick={() => setShowAuthPhotoModal(false)}
|
||||
>
|
||||
<div
|
||||
className={styles.authPhotoModal__content}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className={styles.authPhotoModal__title}>申请照片 ({authSelectedPhotos.length} 张)</h3>
|
||||
<div className={styles.authPhotoModal__grid}>
|
||||
{authSelectedPhotos.map((url, index) => (
|
||||
<div key={index} className={styles.authPhotoModal__item}>
|
||||
<img
|
||||
src={url}
|
||||
alt={`申请照片 ${index + 1}`}
|
||||
className={styles.authPhotoModal__img}
|
||||
onClick={() => setAuthLightboxPhoto(url)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.authPhotoModal__footer}>
|
||||
<button
|
||||
className={styles.authPhotoModal__closeBtn}
|
||||
onClick={() => setShowAuthPhotoModal(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 授权照片全屏查看 (Lightbox) */}
|
||||
{authLightboxPhoto && (
|
||||
<div
|
||||
className={styles.authLightbox}
|
||||
onClick={() => setAuthLightboxPhoto(null)}
|
||||
>
|
||||
<img
|
||||
src={authLightboxPhoto}
|
||||
alt="照片预览"
|
||||
className={styles.authLightbox__img}
|
||||
/>
|
||||
<span className={styles.authLightbox__close}>点击任意处关闭</span>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@
|
|||
|
||||
.userDetail__statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: $spacing-base;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
|
|
@ -201,6 +201,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.userDetail__statValueLink {
|
||||
font-family: $font-family-number;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $primary-color;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐人信息
|
||||
// ============================================================================
|
||||
|
|
@ -734,6 +749,157 @@
|
|||
color: $text-disabled;
|
||||
}
|
||||
|
||||
// 角色卡片中的照片区域
|
||||
.authTab__rolePhotos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
margin-top: $spacing-xs;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
|
||||
strong {
|
||||
color: $text-primary;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.authTab__photoBtn {
|
||||
cursor: pointer;
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
padding: 2px 8px;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
color: #2563eb;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(59, 130, 246, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 授权照片模态框 & 灯箱
|
||||
// ============================================================================
|
||||
|
||||
.authPhotoModal__overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.authPhotoModal__content {
|
||||
background-color: $card-background;
|
||||
border-radius: $border-radius-lg;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: $spacing-xl;
|
||||
}
|
||||
|
||||
.authPhotoModal__title {
|
||||
margin: 0 0 $spacing-lg 0;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.authPhotoModal__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-base;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: $spacing-xs;
|
||||
}
|
||||
|
||||
.authPhotoModal__item {
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
border: 1px solid $border-color;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.authPhotoModal__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.authPhotoModal__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: $spacing-lg;
|
||||
padding-top: $spacing-base;
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.authPhotoModal__closeBtn {
|
||||
cursor: pointer;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-base;
|
||||
background-color: $card-background;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-secondary;
|
||||
font-family: inherit;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: $background-color;
|
||||
}
|
||||
}
|
||||
|
||||
.authLightbox {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.authLightbox__img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: $border-radius-sm;
|
||||
}
|
||||
|
||||
.authLightbox__close {
|
||||
margin-top: $spacing-base;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 通用分类账表格
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import styles from './users.module.scss';
|
|||
// 骨架屏组件
|
||||
const TableRowSkeleton = () => (
|
||||
<div className={styles.users__tableRow}>
|
||||
{Array.from({ length: 13 }).map((_, i) => (
|
||||
{Array.from({ length: 15 }).map((_, i) => (
|
||||
<div key={i} className={styles.users__tableCellSkeleton}>
|
||||
<div className={styles.users__skeleton} />
|
||||
</div>
|
||||
|
|
@ -361,6 +361,12 @@ export default function UsersPage() {
|
|||
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--teamTotal'])}>
|
||||
<b>团队总认种量</b>
|
||||
</div>
|
||||
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--prePlanting'])}>
|
||||
<b>个人预种(份)</b>
|
||||
</div>
|
||||
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--prePlanting'])}>
|
||||
<b>团队预种(份)</b>
|
||||
</div>
|
||||
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--province'])}>
|
||||
<b>团队本省认种量及占比</b>
|
||||
</div>
|
||||
|
|
@ -455,6 +461,16 @@ export default function UsersPage() {
|
|||
{formatNumber(user.teamAdoptions)}
|
||||
</div>
|
||||
|
||||
{/* 个人预种(份) */}
|
||||
<div className={cn(styles.users__tableCell, styles['users__tableCell--prePlanting'])}>
|
||||
{formatNumber(user.selfPrePlantingPortions)}
|
||||
</div>
|
||||
|
||||
{/* 团队预种(份) */}
|
||||
<div className={cn(styles.users__tableCell, styles['users__tableCell--prePlanting'])}>
|
||||
{formatNumber(user.teamPrePlantingPortions)}
|
||||
</div>
|
||||
|
||||
{/* 团队本省认种量及占比 */}
|
||||
<div className={cn(styles.users__tableCell, styles['users__tableCell--province'])}>
|
||||
<span>{formatNumber(user.provincialAdoptions.count)}</span>
|
||||
|
|
|
|||
|
|
@ -273,6 +273,11 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--prePlanting {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--province {
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -385,6 +390,11 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--prePlanting {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--province {
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export interface PrePlantingListParams {
|
|||
status?: string;
|
||||
accountSequence?: string;
|
||||
keyword?: string;
|
||||
teamOf?: string; // 按团队过滤:传入 accountSequence,仅返回该用户团队成员的数据
|
||||
}
|
||||
|
||||
/** 分页列表响应 */
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export interface UserListItem {
|
|||
};
|
||||
referrerId: string | null;
|
||||
ranking: number | null;
|
||||
selfPrePlantingPortions: number;
|
||||
teamPrePlantingPortions: number;
|
||||
status: 'active' | 'frozen' | 'deactivated';
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface Authorization {
|
|||
regionName: string;
|
||||
status: AuthorizationStatus;
|
||||
benefitActive: boolean;
|
||||
officePhotoUrls: string[];
|
||||
createdAt: string;
|
||||
authorizedAt: string | null;
|
||||
revokedAt: string | null;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export interface UserListItem {
|
|||
};
|
||||
referrerId: string;
|
||||
ranking: number | null;
|
||||
selfPrePlantingPortions: number;
|
||||
teamPrePlantingPortions: number;
|
||||
status: 'active' | 'inactive';
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ export interface UserFullDetail {
|
|||
percentage: number;
|
||||
};
|
||||
|
||||
// 预种统计
|
||||
selfPrePlantingPortions: number;
|
||||
teamPrePlantingPortions: number;
|
||||
|
||||
// 排名
|
||||
ranking: number | null;
|
||||
|
||||
|
|
@ -192,6 +196,7 @@ export interface AuthorizationRole {
|
|||
monthlyTargetType: string;
|
||||
lastAssessmentMonth: string | null;
|
||||
monthlyTreesAdded: number;
|
||||
officePhotoUrls: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue