diff --git a/backend/services/admin-service/src/api/controllers/authorization-photos.controller.ts b/backend/services/admin-service/src/api/controllers/authorization-photos.controller.ts new file mode 100644 index 00000000..d77a30fb --- /dev/null +++ b/backend/services/admin-service/src/api/controllers/authorization-photos.controller.ts @@ -0,0 +1,42 @@ +/** + * 自助申请照片管理控制器 + * [2026-03-02] 纯新增:独立页面展示所有自助申请用户的办公室照片 + * + * === 数据流 === + * admin-web → 本控制器 → AuthorizationProxyService → authorization-service 内部 API + * 绕过 CDC,直接读源头数据,保证 officePhotoUrls 100% 准确 + */ + +import { Controller, Get, Query, Logger, HttpCode, HttpStatus } from '@nestjs/common'; +import { AuthorizationProxyService, SelfApplyPhotosResponse } from '../../authorization/authorization-proxy.service'; + +@Controller('admin/authorization-photos') +export class AuthorizationPhotosController { + private readonly logger = new Logger(AuthorizationPhotosController.name); + + constructor( + private readonly authorizationProxyService: AuthorizationProxyService, + ) {} + + /** + * 获取自助申请照片列表 + * GET /admin/authorization-photos?page=1&limit=20&roleType=COMMUNITY + */ + @Get() + @HttpCode(HttpStatus.OK) + async getSelfApplyPhotos( + @Query('page') page?: string, + @Query('limit') limit?: string, + @Query('roleType') roleType?: string, + ): Promise { + this.logger.debug( + `[getSelfApplyPhotos] page=${page}, limit=${limit}, roleType=${roleType || 'ALL'}`, + ); + + return this.authorizationProxyService.getSelfApplyPhotos({ + page: Number(page) || 1, + limit: Number(limit) || 20, + roleType: roleType || undefined, + }); + } +} diff --git a/backend/services/admin-service/src/app.module.ts b/backend/services/admin-service/src/app.module.ts index 8ac94956..449c019a 100644 --- a/backend/services/admin-service/src/app.module.ts +++ b/backend/services/admin-service/src/app.module.ts @@ -91,6 +91,9 @@ import { PrePlantingConfigService } from './pre-planting/pre-planting-config.ser 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-03-02] 纯新增:授权自助申请照片代理(admin-service → authorization-service 内部 HTTP) +import { AuthorizationProxyService } from './authorization/authorization-proxy.service'; +import { AuthorizationPhotosController } from './api/controllers/authorization-photos.controller'; // [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价) import { AdminTreePricingController, PublicTreePricingController } from './pricing/tree-pricing.controller'; import { TreePricingService } from './pricing/tree-pricing.service'; @@ -144,6 +147,8 @@ import { AutoPriceIncreaseJob } from './infrastructure/jobs/auto-price-increase. // [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价) AdminTreePricingController, PublicTreePricingController, + // [2026-03-02] 纯新增:自助申请照片管理 + AuthorizationPhotosController, ], providers: [ PrismaService, @@ -239,6 +244,8 @@ import { AutoPriceIncreaseJob } from './infrastructure/jobs/auto-price-increase. PrePlantingProxyService, // [2026-03-02] 新增:推荐链预种统计代理 ReferralProxyService, + // [2026-03-02] 纯新增:授权自助申请照片代理 + AuthorizationProxyService, // [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价) TreePricingService, AutoPriceIncreaseJob, diff --git a/backend/services/admin-service/src/authorization/authorization-proxy.service.ts b/backend/services/admin-service/src/authorization/authorization-proxy.service.ts new file mode 100644 index 00000000..9b60895c --- /dev/null +++ b/backend/services/admin-service/src/authorization/authorization-proxy.service.ts @@ -0,0 +1,119 @@ +/** + * 授权服务代理 — 自助申请照片查询 + * [2026-03-02] 纯新增:通过内部 HTTP 调用 authorization-service 获取自助申请照片 + * + * === 架构 === + * admin-web → admin-service (本服务) → authorization-service /authorization/self-apply-photos + * 复用 ReferralProxyService 的 axios 代理模式 + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios, { AxiosInstance } from 'axios'; +import { PrismaService } from '../infrastructure/persistence/prisma/prisma.service'; + +export interface SelfApplyPhotoItem { + id: string; + accountSequence: string; + nickname: string; + avatar: string | null; + roleType: string; + regionName: string; + status: string; + officePhotoUrls: string[]; + createdAt: string; +} + +export interface SelfApplyPhotosResponse { + items: SelfApplyPhotoItem[]; + total: number; + page: number; + limit: number; +} + +@Injectable() +export class AuthorizationProxyService { + private readonly logger = new Logger(AuthorizationProxyService.name); + private readonly httpClient: AxiosInstance; + + constructor( + private readonly configService: ConfigService, + private readonly prisma: PrismaService, + ) { + const authorizationServiceUrl = this.configService.get( + 'AUTHORIZATION_SERVICE_URL', + 'http://rwa-authorization-service:3009', + ); + + this.httpClient = axios.create({ + baseURL: authorizationServiceUrl, + timeout: 30000, + }); + + this.logger.log( + `AuthorizationProxyService initialized, authorization-service URL: ${authorizationServiceUrl}`, + ); + } + + /** + * 获取自助申请照片列表(含用户昵称/头像补充) + */ + async getSelfApplyPhotos(params: { + page?: number; + limit?: number; + roleType?: string; + }): Promise { + const { page = 1, limit = 20, roleType } = params; + + try { + // 1. 从 authorization-service 获取有照片的授权记录 + const queryParams = new URLSearchParams(); + queryParams.set('page', String(page)); + queryParams.set('limit', String(limit)); + if (roleType) { + queryParams.set('roleType', roleType); + } + + const url = `/api/v1/authorization/self-apply-photos?${queryParams.toString()}`; + this.logger.debug(`[getSelfApplyPhotos] 请求: ${url}`); + const response = await this.httpClient.get(url); + const data = response.data; + + if (!data?.items?.length) { + return { items: [], total: data?.total ?? 0, page, limit }; + } + + // 2. 批量查 user_query_view 补充 nickname + avatarUrl + const accountSequences = data.items.map((item: any) => item.accountSequence); + const users = await this.prisma.userQueryView.findMany({ + where: { accountSequence: { in: accountSequences } }, + select: { accountSequence: true, nickname: true, avatarUrl: true }, + }); + + const userMap = new Map( + users.map((u) => [u.accountSequence, { nickname: u.nickname, avatar: u.avatarUrl }]), + ); + + // 3. 合并数据 + const items: SelfApplyPhotoItem[] = data.items.map((item: any) => { + const user = userMap.get(item.accountSequence); + return { + id: item.id, + accountSequence: item.accountSequence, + nickname: user?.nickname ?? item.accountSequence, + avatar: user?.avatar ?? null, + roleType: item.roleType, + regionName: item.regionName, + status: item.status, + officePhotoUrls: item.officePhotoUrls ?? [], + createdAt: item.createdAt, + }; + }); + + return { items, total: data.total, page, limit }; + } catch (error) { + this.logger.error(`[getSelfApplyPhotos] 失败: ${error.message}`); + return { items: [], total: 0, page, limit }; + } + } +} diff --git a/backend/services/authorization-service/src/api/controllers/index.ts b/backend/services/authorization-service/src/api/controllers/index.ts index 11d61abb..6fee8e11 100644 --- a/backend/services/authorization-service/src/api/controllers/index.ts +++ b/backend/services/authorization-service/src/api/controllers/index.ts @@ -2,3 +2,5 @@ export * from './authorization.controller' export * from './admin-authorization.controller' export * from './health.controller' export * from './internal-authorization.controller' +// [2026-03-02] 纯新增:自助申请照片内部 API +export * from './internal-self-apply-photos.controller' diff --git a/backend/services/authorization-service/src/api/controllers/internal-self-apply-photos.controller.ts b/backend/services/authorization-service/src/api/controllers/internal-self-apply-photos.controller.ts new file mode 100644 index 00000000..2cb10142 --- /dev/null +++ b/backend/services/authorization-service/src/api/controllers/internal-self-apply-photos.controller.ts @@ -0,0 +1,94 @@ +/** + * 自助申请照片内部 API + * [2026-03-02] 纯新增:供 admin-service 调用,返回有照片的自助申请授权记录 + * + * === 架构 === + * admin-web → admin-service → 本端点 → authorization_roles 表(源头数据) + * 绕过 CDC 链路,直接读源头,保证 officePhotoUrls 100% 准确 + */ + +import { Controller, Get, Query, Logger } from '@nestjs/common' +import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger' +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service' +import { Prisma } from '@prisma/client' + +interface SelfApplyPhotoRow { + id: string + account_sequence: string + role_type: string + region_name: string + status: string + office_photo_urls: string[] + created_at: Date +} + +@ApiTags('Internal Self-Apply Photos') +@Controller('authorization') +export class InternalSelfApplyPhotosController { + private readonly logger = new Logger(InternalSelfApplyPhotosController.name) + + constructor(private readonly prisma: PrismaService) {} + + /** + * 获取所有自助申请的授权记录(含办公室照片) + * 自助申请的判断依据:officePhotoUrls 非空 + */ + @Get('self-apply-photos') + @ApiOperation({ summary: '获取自助申请照片列表(内部 API)' }) + @ApiQuery({ name: 'page', required: false, type: Number, description: '页码,默认 1' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: '每页数量,默认 20' }) + @ApiQuery({ name: 'roleType', required: false, type: String, description: '授权类型筛选' }) + @ApiResponse({ status: 200, description: '自助申请照片列表' }) + async getSelfApplyPhotos( + @Query('page') pageStr?: string, + @Query('limit') limitStr?: string, + @Query('roleType') roleType?: string, + ) { + const page = Math.max(1, Number(pageStr) || 1) + const limit = Math.min(100, Math.max(1, Number(limitStr) || 20)) + const offset = (page - 1) * limit + + this.logger.debug( + `[INTERNAL] getSelfApplyPhotos: page=${page}, limit=${limit}, roleType=${roleType || 'ALL'}`, + ) + + // 使用 raw SQL 查询,避免本地 Prisma client 类型不同步问题 + const roleTypeCondition = roleType + ? Prisma.sql`AND role_type = ${roleType}` + : Prisma.empty + + const [items, countResult] = await Promise.all([ + this.prisma.$queryRaw` + SELECT id, account_sequence, role_type, region_name, status, office_photo_urls, created_at + FROM authorization_roles + WHERE office_photo_urls != '{}' AND deleted_at IS NULL + ${roleTypeCondition} + ORDER BY created_at DESC + LIMIT ${limit} OFFSET ${offset} + `, + this.prisma.$queryRaw<[{ count: bigint }]>` + SELECT COUNT(*)::bigint as count + FROM authorization_roles + WHERE office_photo_urls != '{}' AND deleted_at IS NULL + ${roleTypeCondition} + `, + ]) + + const total = Number(countResult[0]?.count ?? 0) + + return { + items: items.map((item) => ({ + id: item.id, + accountSequence: item.account_sequence, + roleType: item.role_type, + regionName: item.region_name, + status: item.status, + officePhotoUrls: item.office_photo_urls, + createdAt: item.created_at instanceof Date ? item.created_at.toISOString() : String(item.created_at), + })), + total, + page, + limit, + } + } +} diff --git a/backend/services/authorization-service/src/app.module.ts b/backend/services/authorization-service/src/app.module.ts index e9463c6b..332a7b95 100644 --- a/backend/services/authorization-service/src/app.module.ts +++ b/backend/services/authorization-service/src/app.module.ts @@ -48,6 +48,7 @@ import { AdminAuthorizationController, HealthController, InternalAuthorizationController, + InternalSelfApplyPhotosController, } from '@/api/controllers' // Shared @@ -89,6 +90,8 @@ const MockReferralRepository = { AdminAuthorizationController, HealthController, InternalAuthorizationController, + // [2026-03-02] 纯新增:自助申请照片内部 API + InternalSelfApplyPhotosController, EventConsumerController, ], providers: [ diff --git a/backend/services/docker-compose.yml b/backend/services/docker-compose.yml index ffdf4855..ea26ea90 100644 --- a/backend/services/docker-compose.yml +++ b/backend/services/docker-compose.yml @@ -668,6 +668,8 @@ services: - KAFKA_CDC_CONSUMER_GROUP=admin-service-cdc # File Storage - UPLOAD_DIR=/app/uploads + # [2026-03-02] 纯新增:授权自助申请照片代理 + - AUTHORIZATION_SERVICE_URL=http://rwa-authorization-service:3009 volumes: - admin_uploads_data:/app/uploads depends_on: diff --git a/frontend/admin-web/src/app/(dashboard)/authorization-photos/authorization-photos.module.scss b/frontend/admin-web/src/app/(dashboard)/authorization-photos/authorization-photos.module.scss new file mode 100644 index 00000000..48ba7937 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/authorization-photos/authorization-photos.module.scss @@ -0,0 +1,427 @@ +/* 自助申请照片页面样式 */ +/* [2026-03-02] 纯新增 */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.photos { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + text-align: left; + font-family: 'Noto Sans SC', $font-family-base; +} + +/* 卡片 */ +.photos__card { + align-self: stretch; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.05); + border-radius: 8px; + background-color: #fff; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 24px; + gap: 15px; +} + +.photos__cardHeader { + align-self: stretch; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; +} + +.photos__cardTitle { + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 700; + color: #1e293b; +} + +.photos__count { + font-size: 14px; + color: #6b7280; +} + +/* 筛选 */ +.photos__filters { + align-self: stretch; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.photos__select { + min-width: 160px; + height: 38px; + border-radius: 6px; + background-color: #f3f4f6; + border: 1px solid #e5e7eb; + padding: 9px 13px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + cursor: pointer; + appearance: none; + font-family: inherit; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 8px center; + background-repeat: no-repeat; + background-size: 16px; +} + +/* 表格 */ +.photos__table { + align-self: stretch; + overflow-x: auto; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.photos__tableHeader { + align-self: stretch; + min-width: 750px; + background-color: #f3f4f6; + display: flex; + align-items: center; +} + +.photos__tableRow { + align-self: stretch; + min-width: 750px; + display: flex; + align-items: center; + border-bottom: 1px solid #e5e7eb; + @include transition-fast; + + &:hover { + background-color: #f9fafb; + } +} + +.photos__cell { + display: flex; + align-items: center; + padding: 12px 16px; + font-size: 14px; + line-height: 20px; + color: #1e293b; + + &--header { + font-size: 12px; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; + } + + &--avatar { + width: 64px; + flex-shrink: 0; + justify-content: center; + } + + &--nickname { + width: 120px; + flex-shrink: 0; + font-weight: 500; + } + + &--accountId { + width: 140px; + flex-shrink: 0; + font-size: 13px; + } + + &--type { + width: 100px; + flex-shrink: 0; + } + + &--region { + width: 140px; + flex-shrink: 0; + } + + &--photoCount { + width: 100px; + flex-shrink: 0; + } + + &--date { + flex: 1; + min-width: 90px; + font-size: 12px; + color: #6b7280; + } +} + +/* 头像 */ +.photos__avatar { + width: 36px; + height: 36px; + border-radius: 9999px; + background-size: cover; + background-position: center; + background-color: #e5e7eb; +} + +/* 类型徽章 */ +.photos__typeBadge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + background-color: #e0e7ff; + color: #3730a3; +} + +/* 查看照片按钮 */ +.photos__viewBtn { + 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); + } +} + +/* 空数据 */ +.photos__emptyRow { + align-self: stretch; + padding: 40px 24px; + text-align: center; + color: #6b7280; + font-size: 14px; +} + +/* 帮助文本 */ +.photos__help { + align-self: stretch; + font-size: 12px; + line-height: 16px; + color: #6b7280; + padding-top: 4px; +} + +/* 加载/错误 */ +.photos__loading { + align-self: stretch; + padding: 40px 24px; + text-align: center; + color: #6b7280; + font-size: 14px; +} + +.photos__error { + align-self: stretch; + padding: 24px; + background-color: #fef2f2; + border-radius: 6px; + color: #991b1b; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; +} + +.photos__retryBtn { + cursor: pointer; + border: 1px solid #991b1b; + border-radius: 4px; + background-color: transparent; + padding: 4px 12px; + font-size: 12px; + color: #991b1b; + font-family: inherit; + @include transition-fast; + + &:hover { + background-color: #991b1b; + color: #fff; + } +} + +/* 分页 */ +.photos__pagination { + align-self: stretch; + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + padding: 16px 0; + border-top: 1px solid #e5e7eb; + margin-top: 8px; +} + +.photos__pageBtn { + cursor: pointer; + border: 1px solid #e5e7eb; + border-radius: 6px; + background-color: #fff; + padding: 6px 12px; + font-size: 14px; + color: #1e293b; + font-family: inherit; + @include transition-fast; + + &:hover:not(:disabled) { + background-color: #f3f4f6; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.photos__pageInfo { + font-size: 14px; + color: #6b7280; +} + +/* 模态框 */ +.modal__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; +} + +.modal__content { + background-color: #fff; + border-radius: 12px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 680px; + max-height: 90vh; + overflow-y: auto; + padding: 24px; +} + +.modal__title { + margin: 0 0 12px 0; + font-size: 18px; + font-weight: 700; + color: #1e293b; +} + +.modal__info { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; + font-size: 13px; + color: #6b7280; +} + +.modal__footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid #e5e7eb; +} + +.modal__closeBtn { + cursor: pointer; + border: 1px solid #e5e7eb; + border-radius: 6px; + background-color: #fff; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + color: #6b7280; + font-family: inherit; + @include transition-fast; + + &:hover { + background-color: #f3f4f6; + } +} + +/* 照片网格 */ +.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; +} diff --git a/frontend/admin-web/src/app/(dashboard)/authorization-photos/page.tsx b/frontend/admin-web/src/app/(dashboard)/authorization-photos/page.tsx new file mode 100644 index 00000000..d8cb83b7 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/authorization-photos/page.tsx @@ -0,0 +1,288 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; +import { ROLE_TYPE_LABELS } from '@/types/authorization.types'; +import type { RoleType } from '@/types/authorization.types'; +import styles from './authorization-photos.module.scss'; + +/** 自助申请照片记录 */ +interface SelfApplyPhotoItem { + id: string; + accountSequence: string; + nickname: string; + avatar: string | null; + roleType: RoleType; + regionName: string; + status: string; + officePhotoUrls: string[]; + createdAt: string; +} + +interface SelfApplyPhotosResponse { + items: SelfApplyPhotoItem[]; + total: number; + page: number; + limit: number; +} + +/** + * 自助申请照片页面 + * [2026-03-02] 纯新增:独立展示所有自助申请用户上传的办公室照片 + * 数据直接读取 authorization-service 源头,不经过 CDC + */ +export default function AuthorizationPhotosPage() { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // 筛选 + const [filterType, setFilterType] = useState(''); + const [page, setPage] = useState(1); + const limit = 20; + + // 照片查看 + const [showPhotoModal, setShowPhotoModal] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [lightboxPhoto, setLightboxPhoto] = useState(null); + + const fetchData = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const params: Record = { page, limit }; + if (filterType) { + params.roleType = filterType; + } + const response = await apiClient.get(API_ENDPOINTS.AUTHORIZATION.SELF_APPLY_PHOTOS, { + params, + }); + setData((response as any)?.data ?? { items: [], total: 0, page: 1, limit: 20 }); + } catch (err) { + console.error('获取自助申请照片失败:', err); + setError(err instanceof Error ? err.message : '获取数据失败'); + } finally { + setIsLoading(false); + } + }, [page, limit, filterType]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const openPhotoModal = (item: SelfApplyPhotoItem) => { + setSelectedItem(item); + setShowPhotoModal(true); + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString('zh-CN'); + }; + + const totalPages = data ? Math.ceil(data.total / limit) : 0; + + return ( + +
+
+
+

自助申请照片

+ + 共 {data?.total ?? 0} 条记录 + +
+ + {/* 筛选 */} +
+ +
+ + {/* 加载状态 */} + {isLoading && ( +
加载中...
+ )} + + {/* 错误状态 */} + {error && ( +
+ 加载失败: {error} + +
+ )} + + {/* 表格 */} + {!isLoading && !error && ( + <> +
+
+
+ 头像 +
+
+ 昵称 +
+
+ 账户序列号 +
+
+ 授权类型 +
+
+ 地区 +
+
+ 照片 +
+
+ 申请时间 +
+
+ + {data?.items.map((item) => ( +
+
+
+
+
+ {item.nickname || '-'} +
+
+ {item.accountSequence} +
+
+ + {ROLE_TYPE_LABELS[item.roleType] || item.roleType} + +
+
+ {item.regionName || '-'} +
+
+ +
+
+ {formatDate(item.createdAt)} +
+
+ ))} + + {(!data?.items || data.items.length === 0) && ( +
暂无自助申请照片记录
+ )} +
+ + {/* 分页 */} + {totalPages > 1 && ( +
+ + + 第 {page} 页 / 共 {totalPages} 页 (共 {data?.total ?? 0} 条) + + +
+ )} + + )} + +

+ 此页面展示所有通过 App 自助申请授权时上传的办公室照片。仅显示有照片的申请记录。 +

+
+
+ + {/* 照片查看对话框 */} + {showPhotoModal && selectedItem && ( +
setShowPhotoModal(false)}> +
e.stopPropagation()} + > +

+ {selectedItem.nickname || selectedItem.accountSequence} - 申请照片 ({selectedItem.officePhotoUrls.length} 张) +

+
+ 账户: {selectedItem.accountSequence} + 类型: {ROLE_TYPE_LABELS[selectedItem.roleType] || selectedItem.roleType} + 地区: {selectedItem.regionName} +
+
+ {selectedItem.officePhotoUrls.map((url, index) => ( +
+ {`申请照片 setLightboxPhoto(url)} + /> +
+ ))} +
+
+ +
+
+
+ )} + + {/* 全屏灯箱 */} + {lightboxPhoto && ( +
setLightboxPhoto(null)} + > + 照片预览 + 点击任意处关闭 +
+ )} +
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss b/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss index 80628830..937293b9 100644 --- a/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss +++ b/frontend/admin-web/src/app/(dashboard)/authorization/authorization.module.scss @@ -1001,96 +1001,6 @@ 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; diff --git a/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx b/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx index 22af991d..5cd99d3f 100644 --- a/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/authorization/page.tsx @@ -29,11 +29,6 @@ export default function AuthorizationPage() { const [page, setPage] = useState(1); const limit = 20; - // 照片查看状态 - const [showPhotoModal, setShowPhotoModal] = useState(false); - const [selectedPhotos, setSelectedPhotos] = useState([]); - const [lightboxPhoto, setLightboxPhoto] = useState(null); - // 创建授权对话框状态 const [showCreateModal, setShowCreateModal] = useState(false); const [createForm, setCreateForm] = useState({ @@ -191,12 +186,6 @@ export default function AuthorizationPage() { } }; - // 打开照片查看对话框 - const openPhotoModal = (photos: string[]) => { - setSelectedPhotos(photos); - setShowPhotoModal(true); - }; - // 打开取消授权对话框 const openRevokeModal = (item: Authorization) => { setRevokeTarget(item); @@ -403,15 +392,6 @@ export default function AuthorizationPage() { > 状态 -
- 申请照片 -
-
- {item.officePhotoUrls && item.officePhotoUrls.length > 0 ? ( - - ) : ( - - - )} -
)} - {/* 照片查看对话框 */} - {showPhotoModal && selectedPhotos.length > 0 && ( -
setShowPhotoModal(false)}> -
e.stopPropagation()} - > -

申请照片 ({selectedPhotos.length} 张)

-
- {selectedPhotos.map((url, index) => ( -
- {`申请照片 setLightboxPhoto(url)} - /> -
- ))} -
-
- -
-
-
- )} - - {/* 照片全屏查看 (Lightbox) */} - {lightboxPhoto && ( -
setLightboxPhoto(null)} - > - 照片预览 - 点击任意处关闭 -
- )} ); } diff --git a/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx index a35a040a..4cd2d924 100644 --- a/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx +++ b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx @@ -29,6 +29,8 @@ const topMenuItems: MenuItem[] = [ { key: 'contracts', icon: '/images/Container2.svg', label: '合同管理', path: '/contracts' }, { key: 'leaderboard', icon: '/images/Container3.svg', label: '龙虎榜', path: '/leaderboard' }, { key: 'authorization', icon: '/images/Container4.svg', label: '授权管理', path: '/authorization' }, + // [2026-03-02] 纯新增:自助申请照片独立页面 + { key: 'authorization-photos', icon: '/images/Container4.svg', label: '申请照片', path: '/authorization-photos' }, { key: 'co-managed-wallet', icon: '/images/Container4.svg', label: '共管钱包', path: '/co-managed-wallet' }, { key: 'notifications', icon: '/images/Container3.svg', label: '通知管理', path: '/notifications' }, { key: 'pending-actions', icon: '/images/Container3.svg', label: '待办操作', path: '/pending-actions' }, diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index 77ffc27c..d4cc9150 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -55,6 +55,8 @@ export const API_ENDPOINTS = { ADMIN_GRANT_CITY_COMPANY: '/v1/admin/authorizations/city-company', ADMIN_GRANT_AUTH_PROVINCE_COMPANY: '/v1/admin/authorizations/auth-province-company', ADMIN_GRANT_AUTH_CITY_COMPANY: '/v1/admin/authorizations/auth-city-company', + // [2026-03-02] 纯新增:自助申请照片(绕过 CDC,读源头数据) + SELF_APPLY_PHOTOS: '/v1/admin/authorization-photos', // 其他授权端点 PROVINCE_COMPANIES: '/v1/authorizations/province-companies', CITY_COMPANIES: '/v1/authorizations/city-companies',