feat(admin): 功能8修正 — 自助申请照片独立展示页(纯新增方案)
用户在 App 自助申请社区/市团队/省团队授权时上传的办公室照片,
之前错误放在授权管理页且因 CDC 未同步导致全显示"-"。
本次采用纯新增方案:绕过 CDC,通过内部 HTTP API 直连
authorization-service 源头数据库读取照片,保证数据 100% 准确。
=== 数据流 ===
admin-web 新页面 → admin-service 新 Controller
→ authorization-service 新 Internal API
→ authorization_roles 表 (源头, officePhotoUrls 字段)
=== 后端 — authorization-service ===
- 新建 internal-self-apply-photos.controller.ts
GET /authorization/self-apply-photos?page=1&limit=20&roleType=COMMUNITY
使用 $queryRaw 查询 office_photo_urls != '{}' 的记录
支持 roleType 筛选 + 分页
- index.ts 新增 export, app.module.ts 注册 controller
=== 后端 — admin-service ===
- 新建 authorization/authorization-proxy.service.ts
axios 代理调用 authorization-service 内部 API
批量查 user_query_view 补充 nickname + avatarUrl
- 新建 api/controllers/authorization-photos.controller.ts
GET /admin/authorization-photos (admin-web 调用)
- app.module.ts 注册 controller + provider
- docker-compose.yml 追加 AUTHORIZATION_SERVICE_URL 环境变量
=== 前端 — admin-web ===
- 新建 authorization-photos/ 页面 (page.tsx + SCSS)
表格展示:头像、昵称、账户序列号、授权类型、地区、照片数、申请时间
点击照片弹出 Modal 网格 → 点击单张弹出全屏 Lightbox
支持 roleType 筛选 + 分页
- Sidebar.tsx 追加"申请照片"菜单项 (紧随"授权管理"之后)
- endpoints.ts 追加 SELF_APPLY_PHOTOS 端点
- authorization/page.tsx 移除"申请照片"列、photo modal、lightbox
- authorization.module.scss 清理照片相关样式
=== 风险 ===
- CDC 链路: 零修改
- 现有 API: 零冲突 (新 controller 独立文件)
- 2.0 系统: 零影响
- 所有操作均为只读查询
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
41818eb8e2
commit
59f7bdc137
|
|
@ -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<SelfApplyPhotosResponse> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string>(
|
||||
'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<SelfApplyPhotosResponse> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<SelfApplyPhotoRow[]>`
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<SelfApplyPhotosResponse | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 筛选
|
||||
const [filterType, setFilterType] = useState<RoleType | ''>('');
|
||||
const [page, setPage] = useState(1);
|
||||
const limit = 20;
|
||||
|
||||
// 照片查看
|
||||
const [showPhotoModal, setShowPhotoModal] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<SelfApplyPhotoItem | null>(null);
|
||||
const [lightboxPhoto, setLightboxPhoto] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params: Record<string, string | number> = { 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 (
|
||||
<PageContainer title="自助申请照片">
|
||||
<div className={styles.photos}>
|
||||
<section className={styles.photos__card}>
|
||||
<div className={styles.photos__cardHeader}>
|
||||
<h3 className={styles.photos__cardTitle}>自助申请照片</h3>
|
||||
<span className={styles.photos__count}>
|
||||
共 {data?.total ?? 0} 条记录
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 筛选 */}
|
||||
<div className={styles.photos__filters}>
|
||||
<select
|
||||
className={styles.photos__select}
|
||||
value={filterType}
|
||||
onChange={(e) => {
|
||||
setFilterType(e.target.value as RoleType | '');
|
||||
setPage(1);
|
||||
}}
|
||||
aria-label="授权类型"
|
||||
>
|
||||
<option value="">全部类型</option>
|
||||
{Object.entries(ROLE_TYPE_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{isLoading && (
|
||||
<div className={styles.photos__loading}>加载中...</div>
|
||||
)}
|
||||
|
||||
{/* 错误状态 */}
|
||||
{error && (
|
||||
<div className={styles.photos__error}>
|
||||
加载失败: {error}
|
||||
<button onClick={fetchData} className={styles.photos__retryBtn}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 表格 */}
|
||||
{!isLoading && !error && (
|
||||
<>
|
||||
<div className={styles.photos__table}>
|
||||
<div className={styles.photos__tableHeader}>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--avatar'])}>
|
||||
头像
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--nickname'])}>
|
||||
昵称
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--accountId'])}>
|
||||
账户序列号
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--type'])}>
|
||||
授权类型
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--region'])}>
|
||||
地区
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--photoCount'])}>
|
||||
照片
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--header'], styles['photos__cell--date'])}>
|
||||
申请时间
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data?.items.map((item) => (
|
||||
<div key={item.id} className={styles.photos__tableRow}>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--avatar'])}>
|
||||
<div
|
||||
className={styles.photos__avatar}
|
||||
style={item.avatar ? { backgroundImage: `url(${item.avatar})` } : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--nickname'])}>
|
||||
{item.nickname || '-'}
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--accountId'])}>
|
||||
{item.accountSequence}
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--type'])}>
|
||||
<span className={styles.photos__typeBadge}>
|
||||
{ROLE_TYPE_LABELS[item.roleType] || item.roleType}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--region'])}>
|
||||
{item.regionName || '-'}
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--photoCount'])}>
|
||||
<button
|
||||
className={styles.photos__viewBtn}
|
||||
onClick={() => openPhotoModal(item)}
|
||||
>
|
||||
查看 {item.officePhotoUrls.length} 张
|
||||
</button>
|
||||
</div>
|
||||
<div className={cn(styles.photos__cell, styles['photos__cell--date'])}>
|
||||
{formatDate(item.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(!data?.items || data.items.length === 0) && (
|
||||
<div className={styles.photos__emptyRow}>暂无自助申请照片记录</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{totalPages > 1 && (
|
||||
<div className={styles.photos__pagination}>
|
||||
<button
|
||||
className={styles.photos__pageBtn}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className={styles.photos__pageInfo}>
|
||||
第 {page} 页 / 共 {totalPages} 页 (共 {data?.total ?? 0} 条)
|
||||
</span>
|
||||
<button
|
||||
className={styles.photos__pageBtn}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className={styles.photos__help}>
|
||||
此页面展示所有通过 App 自助申请授权时上传的办公室照片。仅显示有照片的申请记录。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 照片查看对话框 */}
|
||||
{showPhotoModal && selectedItem && (
|
||||
<div className={styles.modal__overlay} onClick={() => setShowPhotoModal(false)}>
|
||||
<div
|
||||
className={styles.modal__content}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className={styles.modal__title}>
|
||||
{selectedItem.nickname || selectedItem.accountSequence} - 申请照片 ({selectedItem.officePhotoUrls.length} 张)
|
||||
</h3>
|
||||
<div className={styles.modal__info}>
|
||||
<span>账户: {selectedItem.accountSequence}</span>
|
||||
<span>类型: {ROLE_TYPE_LABELS[selectedItem.roleType] || selectedItem.roleType}</span>
|
||||
<span>地区: {selectedItem.regionName}</span>
|
||||
</div>
|
||||
<div className={styles.photoGrid}>
|
||||
{selectedItem.officePhotoUrls.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__closeBtn}
|
||||
onClick={() => setShowPhotoModal(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 全屏灯箱 */}
|
||||
{lightboxPhoto && (
|
||||
<div
|
||||
className={styles.lightbox}
|
||||
onClick={() => setLightboxPhoto(null)}
|
||||
>
|
||||
<img
|
||||
src={lightboxPhoto}
|
||||
alt="照片预览"
|
||||
className={styles.lightbox__img}
|
||||
/>
|
||||
<span className={styles.lightbox__close}>点击任意处关闭</span>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string[]>([]);
|
||||
const [lightboxPhoto, setLightboxPhoto] = useState<string | null>(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() {
|
|||
>
|
||||
状态
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.authorization__tableCell,
|
||||
styles['authorization__tableCell--header'],
|
||||
styles['authorization__tableCell--photos']
|
||||
)}
|
||||
>
|
||||
申请照片
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.authorization__tableCell,
|
||||
|
|
@ -494,23 +474,6 @@ 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,
|
||||
|
|
@ -692,52 +655,6 @@ export default function AuthorizationPage() {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue