feat(authorization): add admin authorization management API and real data integration

Backend (authorization-service):
- Add QueryAuthorizationsDto for query parameters (roleType, keyword, includeRevoked, page, limit)
- Add queryAuthorizations method to fetch all authorizations with user info
- Add GET /admin/authorizations endpoint for listing authorizations
- Add POST /admin/authorizations/:id/revoke endpoint for revoking authorization

Frontend (admin-web):
- Add authorization.types.ts with RoleType, Authorization, and request types
- Add authorizationService.ts for API calls (list, revoke, grant operations)
- Add useAuthorizations.ts React Query hooks
- Update authorization page to use real API data instead of mock data
- Add loading/error states, pagination, and revoke reason display
- Add new styles for loading, error, pagination, and date columns

The authorization management page now displays all authorized users
from the database with support for filtering by role type, status,
and keyword search.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-03 18:50:10 -08:00
parent e08959263a
commit 35a812c058
12 changed files with 972 additions and 263 deletions

View File

@ -1,5 +1,5 @@
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common'
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'
import { Controller, Post, Get, Body, Query, Param, UseGuards, HttpCode, HttpStatus } from '@nestjs/common'
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'
import { AuthorizationApplicationService } from '@/application/services'
import {
GrantCommunityCommand,
@ -7,6 +7,7 @@ import {
GrantCityCompanyCommand,
GrantAuthProvinceCompanyCommand,
GrantAuthCityCompanyCommand,
RevokeAuthorizationCommand,
} from '@/application/commands'
import {
GrantCommunityDto,
@ -14,9 +15,12 @@ import {
GrantCityCompanyDto,
GrantAuthProvinceCompanyDto,
GrantAuthCityCompanyDto,
RevokeAuthorizationDto,
QueryAuthorizationsDto,
} from '@/api/dto/request'
import { CurrentUser } from '@/shared/decorators'
import { JwtAuthGuard } from '@/shared/guards'
import { RoleType } from '@/domain/enums'
@ApiTags('Admin Authorization')
@Controller('admin/authorizations')
@ -25,6 +29,62 @@ import { JwtAuthGuard } from '@/shared/guards'
export class AdminAuthorizationController {
constructor(private readonly applicationService: AuthorizationApplicationService) {}
@Get()
@ApiOperation({ summary: '查询授权列表(管理员)' })
@ApiQuery({ name: 'roleType', required: false, enum: RoleType })
@ApiQuery({ name: 'keyword', required: false })
@ApiQuery({ name: 'includeRevoked', required: false, type: Boolean })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiResponse({ status: 200, description: '授权列表' })
async queryAuthorizations(
@Query() dto: QueryAuthorizationsDto,
): Promise<{
items: Array<{
id: string
accountSequence: string
nickname: string
avatar: string | null
roleType: RoleType
regionName: string
status: string
benefitActive: boolean
createdAt: Date
authorizedAt: Date | null
revokedAt: Date | null
revokeReason: string | null
}>
total: number
page: number
limit: number
}> {
return this.applicationService.queryAuthorizations({
roleType: dto.roleType,
keyword: dto.keyword,
includeRevoked: dto.includeRevoked,
page: dto.page,
limit: dto.limit,
})
}
@Post(':id/revoke')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '撤销授权(管理员)' })
@ApiResponse({ status: 200, description: '撤销成功' })
async revokeAuthorization(
@CurrentUser() user: { userId: string; accountSequence: string },
@Param('id') authorizationId: string,
@Body() dto: RevokeAuthorizationDto,
): Promise<{ message: string }> {
const command = new RevokeAuthorizationCommand(
authorizationId,
dto.reason,
user.accountSequence,
)
await this.applicationService.revokeAuthorization(command)
return { message: '授权已撤销' }
}
@Post('community')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '授权社区(管理员)' })

View File

@ -9,3 +9,4 @@ export * from './grant-auth-city-company.dto'
export * from './revoke-authorization.dto'
export * from './grant-monthly-bypass.dto'
export * from './self-apply-authorization.dto'
export * from './query-authorizations.dto'

View File

@ -0,0 +1,35 @@
import { IsOptional, IsString, IsEnum, IsInt, Min, Max } from 'class-validator'
import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { RoleType } from '@/domain/enums'
export class QueryAuthorizationsDto {
@ApiPropertyOptional({ description: '授权类型', enum: RoleType })
@IsOptional()
@IsEnum(RoleType)
roleType?: RoleType
@ApiPropertyOptional({ description: '搜索关键词(匹配昵称、账户序列号、地区名称)' })
@IsOptional()
@IsString()
keyword?: string
@ApiPropertyOptional({ description: '是否包含已撤销的授权', default: false })
@IsOptional()
includeRevoked?: boolean
@ApiPropertyOptional({ description: '页码', default: 1 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number
@ApiPropertyOptional({ description: '每页数量', default: 20 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number
}

View File

@ -3430,4 +3430,115 @@ export class AuthorizationApplicationService {
if (name.length === 2) return name[0] + '*'
return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1]
}
// ============ 管理员查询方法 ============
/**
*
*/
async queryAuthorizations(params: {
roleType?: RoleType
keyword?: string
includeRevoked?: boolean
page?: number
limit?: number
}): Promise<{
items: Array<{
id: string
accountSequence: string
nickname: string
avatar: string | null
roleType: RoleType
regionName: string
status: AuthorizationStatus
benefitActive: boolean
createdAt: Date
authorizedAt: Date | null
revokedAt: Date | null
revokeReason: string | null
}>
total: number
page: number
limit: number
}> {
const page = params.page ?? 1
const limit = params.limit ?? 20
const includeRevoked = params.includeRevoked ?? false
// 获取所有授权(根据是否包含已撤销)
let allAuthorizations: AuthorizationRole[]
if (params.roleType) {
allAuthorizations = await this.authorizationRepository.findAllActive(params.roleType)
} else {
allAuthorizations = await this.authorizationRepository.findAllActive()
}
// 如果包含已撤销的,需要添加已撤销的授权
if (includeRevoked) {
const revokedAuths = await this.authorizationRepository.findByStatus(AuthorizationStatus.REVOKED)
// 过滤 roleType
const filteredRevoked = params.roleType
? revokedAuths.filter((a) => a.roleType === params.roleType)
: revokedAuths
allAuthorizations = [...allAuthorizations, ...filteredRevoked]
}
// 获取用户信息
const accountSequences = [...new Set(allAuthorizations.map((a) => a.userId.accountSequence))]
let usersMap = new Map<string, { nickname: string; avatar: string | null }>()
if (accountSequences.length > 0) {
try {
const usersInfoMap = await this.identityServiceClient.batchGetUserInfoBySequence(accountSequences)
usersInfoMap.forEach((userInfo, accountSequence) => {
usersMap.set(accountSequence, { nickname: userInfo.nickname, avatar: userInfo.avatarUrl ?? null })
})
} catch (e) {
this.logger.warn(`获取用户信息失败: ${e}`)
}
}
// 关键词过滤
let filteredAuthorizations = allAuthorizations
if (params.keyword) {
const keyword = params.keyword.toLowerCase()
filteredAuthorizations = allAuthorizations.filter((auth) => {
const userInfo = usersMap.get(auth.userId.accountSequence)
return (
auth.userId.accountSequence.toLowerCase().includes(keyword) ||
auth.regionName.toLowerCase().includes(keyword) ||
(userInfo?.nickname && userInfo.nickname.toLowerCase().includes(keyword))
)
})
}
// 排序:按创建时间降序
filteredAuthorizations.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
// 分页
const total = filteredAuthorizations.length
const startIndex = (page - 1) * limit
const pagedAuthorizations = filteredAuthorizations.slice(startIndex, startIndex + limit)
// 构建响应
const items = pagedAuthorizations.map((auth) => {
const userInfo = usersMap.get(auth.userId.accountSequence)
return {
id: auth.authorizationId.value,
accountSequence: auth.userId.accountSequence,
nickname: userInfo?.nickname ?? auth.userId.accountSequence,
avatar: userInfo?.avatar ?? null,
roleType: auth.roleType,
regionName: auth.regionName,
status: auth.status,
benefitActive: auth.benefitActive,
createdAt: auth.createdAt,
authorizedAt: auth.authorizedAt,
revokedAt: auth.revokedAt,
revokeReason: auth.revokeReason,
}
})
return { items, total, page, limit }
}
}

View File

@ -941,3 +941,100 @@
}
}
}
/* 加载状态 */
.authorization__loading {
align-self: stretch;
padding: 40px 24px;
text-align: center;
color: #6b7280;
font-size: 14px;
}
/* 错误状态 */
.authorization__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;
}
.authorization__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;
}
}
/* 日期列 */
.authorization__tableCell--date {
width: 100px;
flex-shrink: 0;
font-size: 12px;
color: #6b7280;
}
/* 撤销原因显示 */
.authorization__revokeReason {
font-size: 12px;
color: #6b7280;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: help;
}
/* 分页 */
.authorization__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;
}
.authorization__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;
}
}
.authorization__pageInfo {
font-size: 14px;
color: #6b7280;
}

View File

@ -1,127 +1,67 @@
'use client';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { PageContainer } from '@/components/layout';
import { toast } from '@/components/common';
import { cn } from '@/utils/helpers';
import { useAuthorizations, useRevokeAuthorization } from '@/hooks/useAuthorizations';
import type { RoleType, Authorization } from '@/types/authorization.types';
import { ROLE_TYPE_LABELS } from '@/types/authorization.types';
import styles from './authorization.module.scss';
/**
*
*/
type AuthorizationType =
| 'COMMUNITY'
| 'AUTH_PROVINCE_COMPANY'
| 'PROVINCE_COMPANY'
| 'AUTH_CITY_COMPANY'
| 'CITY_COMPANY';
/**
*
*/
const authorizationTypeLabels: Record<AuthorizationType, string> = {
COMMUNITY: '社区',
AUTH_PROVINCE_COMPANY: '省团队',
PROVINCE_COMPANY: '正式省公司',
AUTH_CITY_COMPANY: '市团队',
CITY_COMPANY: '正式市公司',
};
/**
*
*/
interface AuthorizationItem {
id: string;
userId: string;
accountSequence: string;
nickname: string;
avatar?: string;
type: AuthorizationType;
region?: string; // 如: "广东省" 或 "广东省深圳市"
skipAssessment: boolean;
createdAt: string;
status: 'active' | 'revoked';
}
// 模拟授权数据
const mockAuthorizations: AuthorizationItem[] = [
{
id: '1',
userId: '12345',
accountSequence: 'D25122700001',
nickname: '张三',
type: 'PROVINCE_COMPANY',
region: '广东省',
skipAssessment: false,
createdAt: '2025-01-01',
status: 'active',
},
{
id: '2',
userId: '12346',
accountSequence: 'D25122700002',
nickname: '李四',
type: 'AUTH_CITY_COMPANY',
region: '广东省深圳市',
skipAssessment: true,
createdAt: '2025-01-02',
status: 'active',
},
{
id: '3',
userId: '12347',
accountSequence: 'D25122700003',
nickname: '王五',
type: 'COMMUNITY',
region: '榴莲社区A',
skipAssessment: false,
createdAt: '2025-01-03',
status: 'active',
},
];
/**
*
* -
* 使 API
*/
export default function AuthorizationPage() {
// 筛选状态
const [filterType, setFilterType] = useState<AuthorizationType | ''>('');
const [filterStatus, setFilterStatus] = useState<'active' | 'revoked' | ''>('');
const [filterType, setFilterType] = useState<RoleType | ''>('');
const [filterStatus, setFilterStatus] = useState<'ACTIVE' | 'REVOKED' | ''>('');
const [searchKeyword, setSearchKeyword] = useState('');
const [page, setPage] = useState(1);
const limit = 20;
// 创建授权对话框状态
const [showCreateModal, setShowCreateModal] = useState(false);
const [createForm, setCreateForm] = useState({
accountSequence: '',
type: 'COMMUNITY' as AuthorizationType,
type: 'COMMUNITY' as RoleType,
region: '',
skipAssessment: false,
});
// 取消授权对话框状态
const [showRevokeModal, setShowRevokeModal] = useState(false);
const [revokeTarget, setRevokeTarget] = useState<AuthorizationItem | null>(null);
const [revokeTarget, setRevokeTarget] = useState<Authorization | null>(null);
const [revokeReason, setRevokeReason] = useState('');
// 筛选后的数据
const filteredData = mockAuthorizations.filter((item) => {
if (filterType && item.type !== filterType) return false;
if (filterStatus && item.status !== filterStatus) return false;
if (searchKeyword) {
const keyword = searchKeyword.toLowerCase();
return (
item.nickname.toLowerCase().includes(keyword) ||
item.accountSequence.toLowerCase().includes(keyword) ||
(item.region && item.region.toLowerCase().includes(keyword))
);
}
return true;
});
// 查询参数
const queryParams = useMemo(() => ({
roleType: filterType || undefined,
keyword: searchKeyword || undefined,
includeRevoked: filterStatus === 'REVOKED' ? true : filterStatus === '' ? true : false,
page,
limit,
}), [filterType, searchKeyword, filterStatus, page]);
// 获取授权列表
const { data, isLoading, error, refetch } = useAuthorizations(queryParams);
// 撤销授权 mutation
const revokeMutation = useRevokeAuthorization();
// 根据状态筛选数据
const filteredData = useMemo(() => {
if (!data?.items) return [];
if (!filterStatus) return data.items;
return data.items.filter(item => item.status === filterStatus);
}, [data?.items, filterStatus]);
// 处理创建授权
const handleCreate = () => {
// TODO: 调用API创建授权
console.log('创建授权:', createForm);
toast.info('创建授权功能开发中');
setShowCreateModal(false);
setCreateForm({
accountSequence: '',
@ -132,27 +72,37 @@ export default function AuthorizationPage() {
};
// 处理取消授权
const handleRevoke = () => {
// TODO: 调用API取消授权
console.log('取消授权:', revokeTarget, '原因:', revokeReason);
setShowRevokeModal(false);
setRevokeTarget(null);
setRevokeReason('');
const handleRevoke = async () => {
if (!revokeTarget || !revokeReason) return;
try {
await revokeMutation.mutateAsync({
authorizationId: revokeTarget.id,
data: { reason: revokeReason },
});
toast.success('授权已撤销');
setShowRevokeModal(false);
setRevokeTarget(null);
setRevokeReason('');
} catch (err) {
console.error('撤销授权失败:', err);
toast.error('撤销授权失败');
}
};
// 打开取消授权对话框
const openRevokeModal = (item: AuthorizationItem) => {
const openRevokeModal = (item: Authorization) => {
setRevokeTarget(item);
setShowRevokeModal(true);
};
// 判断是否需要地区选择
const needsRegion = (type: AuthorizationType) => {
return type !== 'COMMUNITY' || type === 'COMMUNITY';
const needsRegion = (_type: RoleType) => {
return true; // 所有类型都需要地区/名称
};
// 获取地区输入提示
const getRegionPlaceholder = (type: AuthorizationType) => {
const getRegionPlaceholder = (type: RoleType) => {
switch (type) {
case 'COMMUNITY':
return '输入社区名称';
@ -191,6 +141,12 @@ export default function AuthorizationPage() {
</div>
);
// 格式化日期
const formatDate = (dateStr: string | null) => {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleDateString('zh-CN');
};
return (
<PageContainer title="授权管理">
<div className={styles.authorization}>
@ -211,11 +167,14 @@ export default function AuthorizationPage() {
<select
className={styles.authorization__select}
value={filterType}
onChange={(e) => setFilterType(e.target.value as AuthorizationType | '')}
onChange={(e) => {
setFilterType(e.target.value as RoleType | '');
setPage(1);
}}
aria-label="授权类型"
>
<option value=""></option>
{Object.entries(authorizationTypeLabels).map(([value, label]) => (
{Object.entries(ROLE_TYPE_LABELS).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
@ -224,12 +183,15 @@ export default function AuthorizationPage() {
<select
className={styles.authorization__select}
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as 'active' | 'revoked' | '')}
onChange={(e) => {
setFilterStatus(e.target.value as 'ACTIVE' | 'REVOKED' | '');
setPage(1);
}}
aria-label="授权状态"
>
<option value=""></option>
<option value="active"></option>
<option value="revoked"></option>
<option value="ACTIVE"></option>
<option value="REVOKED"></option>
</select>
<input
className={styles.authorization__input}
@ -237,166 +199,239 @@ export default function AuthorizationPage() {
type="text"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && refetch()}
aria-label="关键词搜索"
/>
<button className={styles.authorization__searchBtn}></button>
<button
className={styles.authorization__searchBtn}
onClick={() => {
setPage(1);
refetch();
}}
>
</button>
</div>
{/* 授权表格 */}
<div className={styles.authorization__table}>
<div className={styles.authorization__tableHeader}>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--avatar']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--nickname']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--accountId']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--type']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--region']
)}
>
/
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--status']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--actions']
)}
>
</div>
{/* 加载状态 */}
{isLoading && (
<div className={styles.authorization__loading}>...</div>
)}
{/* 错误状态 */}
{error && (
<div className={styles.authorization__error}>
: {error instanceof Error ? error.message : '未知错误'}
<button onClick={() => refetch()} className={styles.authorization__retryBtn}>
</button>
</div>
{filteredData.map((item) => (
<div key={item.id} className={styles.authorization__tableRow}>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--avatar']
)}
>
)}
{/* 授权表格 */}
{!isLoading && !error && (
<>
<div className={styles.authorization__table}>
<div className={styles.authorization__tableHeader}>
<div
className={styles.authorization__avatar}
style={item.avatar ? { backgroundImage: `url(${item.avatar})` } : undefined}
/>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--nickname']
)}
>
{item.nickname}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--accountId']
)}
>
{item.accountSequence}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--type']
)}
>
<span className={styles.authorization__typeBadge}>
{authorizationTypeLabels[item.type]}
</span>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--region']
)}
>
{item.region || '-'}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--status']
)}
>
<span
className={cn(
styles.authorization__badge,
item.status === 'active'
? styles['authorization__badge--authorized']
: styles['authorization__badge--revoked']
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--avatar']
)}
>
{item.status === 'active' ? '有效' : '已撤销'}
</span>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--nickname']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--accountId']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--type']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--region']
)}
>
/
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--date']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--status']
)}
>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--header'],
styles['authorization__tableCell--actions']
)}
>
</div>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--actions']
)}
>
{item.status === 'active' && (
<button
{filteredData.map((item) => (
<div key={item.id} className={styles.authorization__tableRow}>
<div
className={cn(
styles.authorization__actionBtn,
styles['authorization__actionBtn--revoke']
styles.authorization__tableCell,
styles['authorization__tableCell--avatar']
)}
onClick={() => openRevokeModal(item)}
>
</button>
)}
</div>
<div
className={styles.authorization__avatar}
style={item.avatar ? { backgroundImage: `url(${item.avatar})` } : undefined}
/>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--nickname']
)}
>
{item.nickname || '-'}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--accountId']
)}
>
{item.accountSequence}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--type']
)}
>
<span className={styles.authorization__typeBadge}>
{ROLE_TYPE_LABELS[item.roleType] || item.roleType}
</span>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--region']
)}
>
{item.regionName || '-'}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--date']
)}
>
{formatDate(item.authorizedAt || item.createdAt)}
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--status']
)}
>
<span
className={cn(
styles.authorization__badge,
item.status === 'ACTIVE'
? styles['authorization__badge--authorized']
: styles['authorization__badge--revoked']
)}
>
{item.status === 'ACTIVE' ? '有效' : '已撤销'}
</span>
</div>
<div
className={cn(
styles.authorization__tableCell,
styles['authorization__tableCell--actions']
)}
>
{item.status === 'ACTIVE' && (
<button
className={cn(
styles.authorization__actionBtn,
styles['authorization__actionBtn--revoke']
)}
onClick={() => openRevokeModal(item)}
>
</button>
)}
{item.status === 'REVOKED' && item.revokeReason && (
<span className={styles.authorization__revokeReason} title={item.revokeReason}>
: {item.revokeReason.length > 10 ? `${item.revokeReason.slice(0, 10)}...` : item.revokeReason}
</span>
)}
</div>
</div>
))}
{filteredData.length === 0 && (
<div className={styles.authorization__emptyRow}></div>
)}
</div>
))}
{filteredData.length === 0 && (
<div className={styles.authorization__emptyRow}></div>
)}
</div>
{/* 分页 */}
{data && data.total > limit && (
<div className={styles.authorization__pagination}>
<button
className={styles.authorization__pageBtn}
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page <= 1}
>
</button>
<span className={styles.authorization__pageInfo}>
{page} / {Math.ceil(data.total / limit)} ( {data.total} )
</span>
<button
className={styles.authorization__pageBtn}
onClick={() => setPage(p => p + 1)}
disabled={page >= Math.ceil(data.total / limit)}
>
</button>
</div>
)}
</>
)}
<p className={styles.authorization__help}>
@ -428,10 +463,10 @@ export default function AuthorizationPage() {
className={styles.modal__select}
value={createForm.type}
onChange={(e) =>
setCreateForm({ ...createForm, type: e.target.value as AuthorizationType })
setCreateForm({ ...createForm, type: e.target.value as RoleType })
}
>
{Object.entries(authorizationTypeLabels).map(([value, label]) => (
{Object.entries(ROLE_TYPE_LABELS).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
@ -490,9 +525,9 @@ export default function AuthorizationPage() {
<h3 className={styles.modal__title}></h3>
<div className={styles.modal__form}>
<p className={styles.modal__warning}>
<strong>{revokeTarget.nickname}</strong> (
<strong>{revokeTarget.nickname || revokeTarget.accountSequence}</strong> (
{revokeTarget.accountSequence}) {' '}
<strong>{authorizationTypeLabels[revokeTarget.type]}</strong>
<strong>{ROLE_TYPE_LABELS[revokeTarget.roleType] || revokeTarget.roleType}</strong>
</p>
<div className={styles.modal__formGroup}>
<label className={styles.modal__label}></label>
@ -515,9 +550,9 @@ export default function AuthorizationPage() {
<button
className={cn(styles.modal__confirmBtn, styles['modal__confirmBtn--danger'])}
onClick={handleRevoke}
disabled={!revokeReason}
disabled={!revokeReason || revokeMutation.isPending}
>
{revokeMutation.isPending ? '处理中...' : '确认撤销'}
</button>
</div>
</div>

View File

@ -2,3 +2,4 @@
export * from './useDashboard';
export * from './useUsers';
export * from './useAuthorizations';

View File

@ -0,0 +1,121 @@
/**
* Hooks
* 使 React Query
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { authorizationService } from '@/services/authorizationService';
import type {
QueryAuthorizationsParams,
RevokeAuthorizationRequest,
GrantCommunityRequest,
GrantProvinceCompanyRequest,
GrantCityCompanyRequest,
GrantAuthProvinceCompanyRequest,
GrantAuthCityCompanyRequest,
} from '@/types/authorization.types';
/** Query Keys */
export const authorizationKeys = {
all: ['authorizations'] as const,
list: (params: QueryAuthorizationsParams) =>
[...authorizationKeys.all, 'list', params] as const,
};
/**
*
*/
export function useAuthorizations(params: QueryAuthorizationsParams = {}) {
return useQuery({
queryKey: authorizationKeys.list(params),
queryFn: () => authorizationService.getList(params),
staleTime: 30 * 1000, // 30秒后标记为过期
gcTime: 5 * 60 * 1000, // 5分钟后垃圾回收
});
}
/**
*
*/
export function useRevokeAuthorization() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ authorizationId, data }: { authorizationId: string; data: RevokeAuthorizationRequest }) =>
authorizationService.revoke(authorizationId, data),
onSuccess: () => {
// 撤销成功后刷新所有相关列表
queryClient.invalidateQueries({ queryKey: authorizationKeys.all });
},
});
}
/**
*
*/
export function useGrantCommunity() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: GrantCommunityRequest) => authorizationService.grantCommunity(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: authorizationKeys.all });
},
});
}
/**
*
*/
export function useGrantProvinceCompany() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: GrantProvinceCompanyRequest) => authorizationService.grantProvinceCompany(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: authorizationKeys.all });
},
});
}
/**
*
*/
export function useGrantCityCompany() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: GrantCityCompanyRequest) => authorizationService.grantCityCompany(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: authorizationKeys.all });
},
});
}
/**
*
*/
export function useGrantAuthProvinceCompany() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: GrantAuthProvinceCompanyRequest) => authorizationService.grantAuthProvinceCompany(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: authorizationKeys.all });
},
});
}
/**
*
*/
export function useGrantAuthCityCompany() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: GrantAuthCityCompanyRequest) => authorizationService.grantAuthCityCompany(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: authorizationKeys.all });
},
});
}

View File

@ -35,6 +35,15 @@ export const API_ENDPOINTS = {
// 授权管理 (authorization-service)
AUTHORIZATION: {
// 管理员授权管理
ADMIN_LIST: '/v1/admin/authorizations',
ADMIN_REVOKE: (id: string) => `/v1/admin/authorizations/${id}/revoke`,
ADMIN_GRANT_COMMUNITY: '/v1/admin/authorizations/community',
ADMIN_GRANT_PROVINCE_COMPANY: '/v1/admin/authorizations/province-company',
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',
// 其他授权端点
PROVINCE_COMPANIES: '/v1/authorizations/province-companies',
CITY_COMPANIES: '/v1/authorizations/city-companies',
PROVINCE_RULES: '/v1/authorizations/province-rules',

View File

@ -0,0 +1,93 @@
/**
*
*
*/
import apiClient from '@/infrastructure/api/client';
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
import type {
AuthorizationListResponse,
QueryAuthorizationsParams,
RevokeAuthorizationRequest,
GrantCommunityRequest,
GrantProvinceCompanyRequest,
GrantCityCompanyRequest,
GrantAuthProvinceCompanyRequest,
GrantAuthCityCompanyRequest,
} from '@/types/authorization.types';
/**
*
*
* API apiClient :
* { success: true, data: { code: "OK", message: "success", data: {...} } }
*
* 访 .data.data
*/
export const authorizationService = {
/**
*
*/
async getList(params: QueryAuthorizationsParams = {}): Promise<AuthorizationListResponse> {
const response = await apiClient.get(API_ENDPOINTS.AUTHORIZATION.ADMIN_LIST, { params });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (response as any)?.data?.data;
return result ?? { items: [], total: 0, page: 1, limit: 20 };
},
/**
*
*/
async revoke(authorizationId: string, data: RevokeAuthorizationRequest): Promise<{ message: string }> {
const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_REVOKE(authorizationId), data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data ?? { message: '授权已撤销' };
},
/**
*
*/
async grantCommunity(data: GrantCommunityRequest): Promise<{ message: string }> {
const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_COMMUNITY, data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data ?? { message: '社区授权成功' };
},
/**
*
*/
async grantProvinceCompany(data: GrantProvinceCompanyRequest): Promise<{ message: string }> {
const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_PROVINCE_COMPANY, data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data ?? { message: '正式省公司授权成功' };
},
/**
*
*/
async grantCityCompany(data: GrantCityCompanyRequest): Promise<{ message: string }> {
const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_CITY_COMPANY, data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data ?? { message: '正式市公司授权成功' };
},
/**
*
*/
async grantAuthProvinceCompany(data: GrantAuthProvinceCompanyRequest): Promise<{ message: string }> {
const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_AUTH_PROVINCE_COMPANY, data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data ?? { message: '省团队授权成功' };
},
/**
*
*/
async grantAuthCityCompany(data: GrantAuthCityCompanyRequest): Promise<{ message: string }> {
const response = await apiClient.post(API_ENDPOINTS.AUTHORIZATION.ADMIN_GRANT_AUTH_CITY_COMPANY, data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data ?? { message: '市团队授权成功' };
},
};
export default authorizationService;

View File

@ -0,0 +1,145 @@
// 授权管理类型定义
/**
*
*/
export type RoleType =
| 'COMMUNITY'
| 'AUTH_PROVINCE_COMPANY'
| 'PROVINCE_COMPANY'
| 'AUTH_CITY_COMPANY'
| 'CITY_COMPANY';
/**
*
*/
export type AuthorizationStatus = 'ACTIVE' | 'REVOKED';
/**
*
*/
export interface Authorization {
id: string;
accountSequence: string;
nickname: string;
avatar: string | null;
roleType: RoleType;
regionName: string;
status: AuthorizationStatus;
benefitActive: boolean;
createdAt: string;
authorizedAt: string | null;
revokedAt: string | null;
revokeReason: string | null;
}
/**
*
*/
export interface QueryAuthorizationsParams {
roleType?: RoleType;
keyword?: string;
includeRevoked?: boolean;
page?: number;
limit?: number;
}
/**
*
*/
export interface AuthorizationListResponse {
items: Authorization[];
total: number;
page: number;
limit: number;
}
/**
*
*/
export interface RevokeAuthorizationRequest {
reason: string;
}
/**
*
*/
export interface GrantCommunityRequest {
userId: string;
accountSequence: string;
communityName: string;
skipAssessment?: boolean;
}
/**
*
*/
export interface GrantProvinceCompanyRequest {
userId: string;
accountSequence: string;
provinceCode: string;
provinceName: string;
skipAssessment?: boolean;
}
/**
*
*/
export interface GrantCityCompanyRequest {
userId: string;
accountSequence: string;
cityCode: string;
cityName: string;
skipAssessment?: boolean;
}
/**
*
*/
export interface GrantAuthProvinceCompanyRequest {
userId: string;
accountSequence: string;
provinceCode: string;
provinceName: string;
skipAssessment?: boolean;
}
/**
*
*/
export interface GrantAuthCityCompanyRequest {
userId: string;
accountSequence: string;
cityCode: string;
cityName: string;
skipAssessment?: boolean;
}
/**
*
*/
export const ROLE_TYPE_LABELS: Record<RoleType, string> = {
COMMUNITY: '社区',
AUTH_PROVINCE_COMPANY: '省团队',
PROVINCE_COMPANY: '正式省公司',
AUTH_CITY_COMPANY: '市团队',
CITY_COMPANY: '正式市公司',
};
/**
*
*/
export function getRoleTypeLabel(roleType: RoleType): string {
return ROLE_TYPE_LABELS[roleType] || roleType;
}
/**
*
*/
export const ROLE_TYPE_OPTIONS = [
{ value: 'COMMUNITY', label: '社区' },
{ value: 'AUTH_PROVINCE_COMPANY', label: '省团队' },
{ value: 'PROVINCE_COMPANY', label: '正式省公司' },
{ value: 'AUTH_CITY_COMPANY', label: '市团队' },
{ value: 'CITY_COMPANY', label: '正式市公司' },
] as const;

View File

@ -8,3 +8,4 @@ export * from './common.types';
export * from './dashboard.types';
export * from './pending-action.types';
export * from './withdrawal.types';
export * from './authorization.types';