refactor(admin-client): implement 3-layer Clean Architecture for frontend
Refactored admin-client from 1.5-layer to 3-layer architecture using
Feature-Sliced Design pattern with Zustand + TanStack Query.
## Architecture Pattern
Each feature now follows 3-layer structure:
```
features/{feature}/
├── presentation/ # React UI components, pages
├── application/ # Zustand stores, TanStack Query hooks
└── infrastructure/ # API clients (axios calls)
```
## Changes by Feature
### Auth Feature
- infrastructure/auth.api.ts: Login, verify API calls
- application/useAuthStore.ts: Zustand store for auth state
- Updated LoginPage.tsx to use useAuthStore
- shared/hooks/useAuth.ts: Re-exports for backward compatibility
### Knowledge Feature
- infrastructure/knowledge.api.ts: Article CRUD APIs
- application/useKnowledge.ts: TanStack Query hooks
- useKnowledgeArticles, useCreateArticle, useUpdateArticle
- useDeleteArticle, usePublishArticle, useUnpublishArticle
- Updated KnowledgePage.tsx to use application hooks
### Experience Feature
- infrastructure/experience.api.ts: Experience management APIs
- application/useExperience.ts: TanStack Query hooks
- usePendingExperiences, useExperienceStatistics
- useApproveExperience, useRejectExperience, useRunEvolution
- Updated ExperiencePage.tsx to use application hooks
### Dashboard Feature
- infrastructure/dashboard.api.ts: Statistics APIs
- application/useDashboard.ts: TanStack Query hooks
- useEvolutionStatistics, useSystemHealth
- Updated DashboardPage.tsx to use application hooks
## Benefits
- Clear separation of concerns (UI / business logic / data access)
- Better testability (each layer can be tested independently)
- Reusable hooks across components
- Type-safe API interfaces
- Centralized API error handling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
02954f56db
commit
9e1dca25f2
|
|
@ -0,0 +1 @@
|
||||||
|
export { useAuthStore } from './useAuthStore';
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { authApi, AdminInfo } from '../infrastructure/auth.api';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
admin: AdminInfo | null;
|
||||||
|
token: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
checkAuth: () => Promise<boolean>;
|
||||||
|
hasPermission: (permission: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
admin: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
|
||||||
|
login: async (username: string, password: string) => {
|
||||||
|
const data = await authApi.login(username, password);
|
||||||
|
localStorage.setItem('admin_token', data.token);
|
||||||
|
set({
|
||||||
|
admin: data.admin,
|
||||||
|
token: data.token,
|
||||||
|
isAuthenticated: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('admin_token');
|
||||||
|
set({
|
||||||
|
admin: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
checkAuth: async () => {
|
||||||
|
const token = localStorage.getItem('admin_token');
|
||||||
|
if (!token) {
|
||||||
|
set({ isAuthenticated: false, admin: null, token: null });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const admin = await authApi.verify();
|
||||||
|
set({
|
||||||
|
admin,
|
||||||
|
token,
|
||||||
|
isAuthenticated: true,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem('admin_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ isAuthenticated: false, admin: null, token: null });
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermission: (permission: string) => {
|
||||||
|
const { admin } = get();
|
||||||
|
if (!admin) return false;
|
||||||
|
|
||||||
|
const permissions = admin.permissions || [];
|
||||||
|
|
||||||
|
if (permissions.includes('*')) return true;
|
||||||
|
if (permissions.includes(permission)) return true;
|
||||||
|
|
||||||
|
const [resource] = permission.split(':');
|
||||||
|
if (permissions.includes(`${resource}:*`)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'auth-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
admin: state.admin,
|
||||||
|
token: state.token,
|
||||||
|
isAuthenticated: state.isAuthenticated,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface AdminInfo {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
admin: AdminInfo;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
login: async (username: string, password: string): Promise<LoginResponse> => {
|
||||||
|
const response = await api.post('/admin/login', { username, password });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
verify: async (): Promise<AdminInfo> => {
|
||||||
|
const response = await api.get('/admin/verify');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { authApi } from './auth.api';
|
||||||
|
export type { AdminInfo, LoginResponse } from './auth.api';
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Form, Input, Button, Card, message } from 'antd';
|
import { Form, Input, Button, Card, message } from 'antd';
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { useAuth } from '../../../../shared/hooks/useAuth';
|
import { useAuthStore } from '../../application';
|
||||||
|
|
||||||
interface LoginFormValues {
|
interface LoginFormValues {
|
||||||
username: string;
|
username: string;
|
||||||
|
|
@ -12,7 +12,7 @@ interface LoginFormValues {
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const login = useAuth((state) => state.login);
|
const login = useAuthStore((state) => state.login);
|
||||||
|
|
||||||
const onFinish = async (values: LoginFormValues) => {
|
const onFinish = async (values: LoginFormValues) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export {
|
||||||
|
useEvolutionStatistics,
|
||||||
|
useSystemHealth,
|
||||||
|
EVOLUTION_STATS_KEY,
|
||||||
|
SYSTEM_HEALTH_KEY,
|
||||||
|
} from './useDashboard';
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { dashboardApi } from '../infrastructure/dashboard.api';
|
||||||
|
|
||||||
|
export const EVOLUTION_STATS_KEY = 'evolution-stats';
|
||||||
|
export const SYSTEM_HEALTH_KEY = 'system-health';
|
||||||
|
|
||||||
|
export function useEvolutionStatistics() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [EVOLUTION_STATS_KEY],
|
||||||
|
queryFn: () => dashboardApi.getEvolutionStatistics(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSystemHealth() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [SYSTEM_HEALTH_KEY],
|
||||||
|
queryFn: () => dashboardApi.getSystemHealth(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface EvolutionStatistics {
|
||||||
|
totalExperiences: number;
|
||||||
|
activeExperiences: number;
|
||||||
|
pendingExperiences: number;
|
||||||
|
approvedExperiences: number;
|
||||||
|
topExperienceTypes: { type: string; count: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthMetric {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
threshold: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthReport {
|
||||||
|
overall: string;
|
||||||
|
metrics: HealthMetric[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardApi = {
|
||||||
|
getEvolutionStatistics: async (): Promise<EvolutionStatistics> => {
|
||||||
|
const response = await api.get('/evolution/statistics');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSystemHealth: async (): Promise<HealthReport> => {
|
||||||
|
const response = await api.get('/evolution/health');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { dashboardApi } from './dashboard.api';
|
||||||
|
export type { EvolutionStatistics, HealthMetric, HealthReport } from './dashboard.api';
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Card, Row, Col, Statistic, Tag, Progress, List, Typography } from 'antd';
|
import { Card, Row, Col, Statistic, Tag, Progress, List, Typography } from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
|
@ -20,7 +19,8 @@ import {
|
||||||
Pie,
|
Pie,
|
||||||
Cell,
|
Cell,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import api from '../../../../shared/utils/api';
|
import { useEvolutionStatistics, useSystemHealth } from '../../application';
|
||||||
|
import type { HealthMetric } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -45,21 +45,8 @@ const mockCategoryData = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { data: evolutionStats } = useQuery({
|
const { data: evolutionStats } = useEvolutionStatistics();
|
||||||
queryKey: ['evolution-stats'],
|
const { data: healthReport } = useSystemHealth();
|
||||||
queryFn: async () => {
|
|
||||||
const response = await api.get('/evolution/statistics');
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: healthReport } = useQuery({
|
|
||||||
queryKey: ['system-health'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await api.get('/evolution/health');
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const getHealthColor = (status: string) => {
|
const getHealthColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
@ -207,7 +194,7 @@ export function DashboardPage() {
|
||||||
>
|
>
|
||||||
<List
|
<List
|
||||||
dataSource={healthReport?.metrics || []}
|
dataSource={healthReport?.metrics || []}
|
||||||
renderItem={(item: { name: string; value: number; threshold: number; status: string }) => (
|
renderItem={(item: HealthMetric) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between mb-1">
|
||||||
|
|
@ -232,11 +219,11 @@ export function DashboardPage() {
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{healthReport?.recommendations?.length > 0 && (
|
{(healthReport?.recommendations?.length ?? 0) > 0 && (
|
||||||
<div className="mt-4 p-3 bg-yellow-50 rounded">
|
<div className="mt-4 p-3 bg-yellow-50 rounded">
|
||||||
<Text type="warning">建议:</Text>
|
<Text type="warning">建议:</Text>
|
||||||
<ul className="mt-2 ml-4 text-sm text-gray-600">
|
<ul className="mt-2 ml-4 text-sm text-gray-600">
|
||||||
{healthReport.recommendations.map((rec: string, i: number) => (
|
{healthReport?.recommendations?.map((rec: string, i: number) => (
|
||||||
<li key={i}>{rec}</li>
|
<li key={i}>{rec}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export {
|
||||||
|
usePendingExperiences,
|
||||||
|
useExperienceStatistics,
|
||||||
|
useApproveExperience,
|
||||||
|
useRejectExperience,
|
||||||
|
useRunEvolution,
|
||||||
|
EXPERIENCE_QUERY_KEY,
|
||||||
|
EXPERIENCE_STATS_KEY,
|
||||||
|
} from './useExperience';
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { experienceApi } from '../infrastructure/experience.api';
|
||||||
|
|
||||||
|
export const EXPERIENCE_QUERY_KEY = 'pending-experiences';
|
||||||
|
export const EXPERIENCE_STATS_KEY = 'experience-stats';
|
||||||
|
|
||||||
|
export function usePendingExperiences(type?: string, enabled: boolean = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [EXPERIENCE_QUERY_KEY, type],
|
||||||
|
queryFn: () => experienceApi.getPendingExperiences(type),
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExperienceStatistics() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [EXPERIENCE_STATS_KEY],
|
||||||
|
queryFn: () => experienceApi.getStatistics(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApproveExperience() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, adminId }: { id: string; adminId: string }) =>
|
||||||
|
experienceApi.approveExperience(id, adminId),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('经验已批准');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRejectExperience() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, adminId }: { id: string; adminId: string }) =>
|
||||||
|
experienceApi.rejectExperience(id, adminId),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('经验已拒绝');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRunEvolution() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params?: { hoursBack?: number; limit?: number }) =>
|
||||||
|
experienceApi.runEvolution(params?.hoursBack, params?.limit),
|
||||||
|
onSuccess: (result) => {
|
||||||
|
message.success(
|
||||||
|
`进化任务完成:分析了${result.conversationsAnalyzed}个对话,提取了${result.experiencesExtracted}条经验`
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface Experience {
|
||||||
|
id: string;
|
||||||
|
experienceType: string;
|
||||||
|
content: string;
|
||||||
|
scenario: string;
|
||||||
|
confidence: number;
|
||||||
|
relatedCategory: string;
|
||||||
|
sourceConversationIds: string[];
|
||||||
|
verificationStatus: string;
|
||||||
|
usageCount: number;
|
||||||
|
positiveCount: number;
|
||||||
|
negativeCount: number;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceListResponse {
|
||||||
|
items: Experience[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceStatistics {
|
||||||
|
total: number;
|
||||||
|
byStatus: Record<string, number>;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const experienceApi = {
|
||||||
|
getPendingExperiences: async (type?: string): Promise<ExperienceListResponse> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (type) params.append('type', type);
|
||||||
|
const response = await api.get(`/memory/experience/pending?${params}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatistics: async (): Promise<ExperienceStatistics> => {
|
||||||
|
const response = await api.get('/memory/experience/statistics');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
approveExperience: async (id: string, adminId: string): Promise<void> => {
|
||||||
|
await api.post(`/memory/experience/${id}/approve`, { adminId });
|
||||||
|
},
|
||||||
|
|
||||||
|
rejectExperience: async (id: string, adminId: string): Promise<void> => {
|
||||||
|
await api.post(`/memory/experience/${id}/reject`, { adminId });
|
||||||
|
},
|
||||||
|
|
||||||
|
runEvolution: async (hoursBack: number = 24, limit: number = 50): Promise<{
|
||||||
|
conversationsAnalyzed: number;
|
||||||
|
experiencesExtracted: number;
|
||||||
|
}> => {
|
||||||
|
const response = await api.post('/evolution/run', { hoursBack, limit });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { experienceApi } from './experience.api';
|
||||||
|
export type { Experience, ExperienceListResponse, ExperienceStatistics } from './experience.api';
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -8,7 +7,6 @@ import {
|
||||||
Tag,
|
Tag,
|
||||||
Space,
|
Space,
|
||||||
Modal,
|
Modal,
|
||||||
message,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
Statistic,
|
Statistic,
|
||||||
|
|
@ -21,8 +19,15 @@ import {
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import api from '../../../../shared/utils/api';
|
import { useAuthStore } from '../../../auth/application';
|
||||||
import { useAuth } from '../../../../shared/hooks/useAuth';
|
import {
|
||||||
|
usePendingExperiences,
|
||||||
|
useExperienceStatistics,
|
||||||
|
useApproveExperience,
|
||||||
|
useRejectExperience,
|
||||||
|
useRunEvolution,
|
||||||
|
} from '../../application';
|
||||||
|
import type { Experience } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
|
@ -37,86 +42,39 @@ const EXPERIENCE_TYPES = [
|
||||||
{ value: 'OBJECTION_HANDLING', label: '异议处理' },
|
{ value: 'OBJECTION_HANDLING', label: '异议处理' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Experience {
|
|
||||||
id: string;
|
|
||||||
experienceType: string;
|
|
||||||
content: string;
|
|
||||||
scenario: string;
|
|
||||||
confidence: number;
|
|
||||||
relatedCategory: string;
|
|
||||||
sourceConversationIds: string[];
|
|
||||||
verificationStatus: string;
|
|
||||||
usageCount: number;
|
|
||||||
positiveCount: number;
|
|
||||||
negativeCount: number;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExperiencePage() {
|
export function ExperiencePage() {
|
||||||
const [activeTab, setActiveTab] = useState('pending');
|
const [activeTab, setActiveTab] = useState('pending');
|
||||||
const [typeFilter, setTypeFilter] = useState<string>();
|
const [typeFilter, setTypeFilter] = useState<string>();
|
||||||
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const admin = useAuthStore((state) => state.admin);
|
||||||
const admin = useAuth((state) => state.admin);
|
|
||||||
|
|
||||||
const { data: pendingData, isLoading: pendingLoading } = useQuery({
|
const { data: pendingData, isLoading: pendingLoading } = usePendingExperiences(
|
||||||
queryKey: ['pending-experiences', typeFilter],
|
typeFilter,
|
||||||
queryFn: async () => {
|
activeTab === 'pending'
|
||||||
const params = new URLSearchParams();
|
);
|
||||||
if (typeFilter) params.append('type', typeFilter);
|
const { data: stats } = useExperienceStatistics();
|
||||||
const response = await api.get(`/memory/experience/pending?${params}`);
|
const approveMutation = useApproveExperience();
|
||||||
return response.data.data;
|
const rejectMutation = useRejectExperience();
|
||||||
},
|
const runEvolutionMutation = useRunEvolution();
|
||||||
enabled: activeTab === 'pending',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: stats } = useQuery({
|
|
||||||
queryKey: ['experience-stats'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await api.get('/memory/experience/statistics');
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const approveMutation = useMutation({
|
|
||||||
mutationFn: (id: string) =>
|
|
||||||
api.post(`/memory/experience/${id}/approve`, { adminId: admin?.id }),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('经验已批准');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const rejectMutation = useMutation({
|
|
||||||
mutationFn: (id: string) =>
|
|
||||||
api.post(`/memory/experience/${id}/reject`, { adminId: admin?.id }),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('经验已拒绝');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const runEvolutionMutation = useMutation({
|
|
||||||
mutationFn: () => api.post('/evolution/run', { hoursBack: 24, limit: 50 }),
|
|
||||||
onSuccess: (response) => {
|
|
||||||
const result = response.data.data;
|
|
||||||
message.success(
|
|
||||||
`进化任务完成:分析了${result.conversationsAnalyzed}个对话,提取了${result.experiencesExtracted}条经验`
|
|
||||||
);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleView = (exp: Experience) => {
|
const handleView = (exp: Experience) => {
|
||||||
setSelectedExperience(exp);
|
setSelectedExperience(exp);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApprove = (id: string) => {
|
||||||
|
if (admin?.id) {
|
||||||
|
approveMutation.mutate({ id, adminId: admin.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = (id: string) => {
|
||||||
|
if (admin?.id) {
|
||||||
|
rejectMutation.mutate({ id, adminId: admin.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
const getTypeLabel = (type: string) => {
|
||||||
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
||||||
};
|
};
|
||||||
|
|
@ -202,14 +160,14 @@ export function ExperiencePage() {
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CheckOutlined />}
|
icon={<CheckOutlined />}
|
||||||
className="text-green-600"
|
className="text-green-600"
|
||||||
onClick={() => approveMutation.mutate(record.id)}
|
onClick={() => handleApprove(record.id)}
|
||||||
loading={approveMutation.isPending}
|
loading={approveMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CloseOutlined />}
|
icon={<CloseOutlined />}
|
||||||
danger
|
danger
|
||||||
onClick={() => rejectMutation.mutate(record.id)}
|
onClick={() => handleReject(record.id)}
|
||||||
loading={rejectMutation.isPending}
|
loading={rejectMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
@ -226,7 +184,7 @@ export function ExperiencePage() {
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlayCircleOutlined />}
|
||||||
onClick={() => runEvolutionMutation.mutate()}
|
onClick={() => runEvolutionMutation.mutate({})}
|
||||||
loading={runEvolutionMutation.isPending}
|
loading={runEvolutionMutation.isPending}
|
||||||
>
|
>
|
||||||
运行进化任务
|
运行进化任务
|
||||||
|
|
@ -318,7 +276,7 @@ export function ExperiencePage() {
|
||||||
key="reject"
|
key="reject"
|
||||||
danger
|
danger
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
rejectMutation.mutate(selectedExperience.id);
|
handleReject(selectedExperience.id);
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -328,7 +286,7 @@ export function ExperiencePage() {
|
||||||
key="approve"
|
key="approve"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
approveMutation.mutate(selectedExperience.id);
|
handleApprove(selectedExperience.id);
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export {
|
||||||
|
useKnowledgeArticles,
|
||||||
|
useCreateArticle,
|
||||||
|
useUpdateArticle,
|
||||||
|
useDeleteArticle,
|
||||||
|
usePublishArticle,
|
||||||
|
useUnpublishArticle,
|
||||||
|
KNOWLEDGE_QUERY_KEY,
|
||||||
|
} from './useKnowledge';
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { knowledgeApi, CreateArticleParams, UpdateArticleParams } from '../infrastructure/knowledge.api';
|
||||||
|
|
||||||
|
export const KNOWLEDGE_QUERY_KEY = 'knowledge-articles';
|
||||||
|
|
||||||
|
export function useKnowledgeArticles(category?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [KNOWLEDGE_QUERY_KEY, category],
|
||||||
|
queryFn: () => knowledgeApi.getArticles(category),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: CreateArticleParams) => knowledgeApi.createArticle(params),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章创建成功');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: UpdateArticleParams) => knowledgeApi.updateArticle(params),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章更新成功');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => knowledgeApi.deleteArticle(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章已删除');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePublishArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => knowledgeApi.publishArticle(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章已发布');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUnpublishArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => knowledgeApi.unpublishArticle(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章已取消发布');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { knowledgeApi } from './knowledge.api';
|
||||||
|
export type { Article, ArticleListResponse, CreateArticleParams, UpdateArticleParams } from './knowledge.api';
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
summary: string;
|
||||||
|
category: string;
|
||||||
|
tags: string[];
|
||||||
|
source: string;
|
||||||
|
isPublished: boolean;
|
||||||
|
citationCount: number;
|
||||||
|
qualityScore: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleListResponse {
|
||||||
|
items: Article[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateArticleParams {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateArticleParams extends Partial<CreateArticleParams> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const knowledgeApi = {
|
||||||
|
getArticles: async (category?: string): Promise<ArticleListResponse> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (category) params.append('category', category);
|
||||||
|
const response = await api.get(`/knowledge/articles?${params}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
createArticle: async (params: CreateArticleParams): Promise<Article> => {
|
||||||
|
const response = await api.post('/knowledge/articles', params);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateArticle: async ({ id, ...params }: UpdateArticleParams): Promise<Article> => {
|
||||||
|
const response = await api.put(`/knowledge/articles/${id}`, params);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteArticle: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/knowledge/articles/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
publishArticle: async (id: string): Promise<void> => {
|
||||||
|
await api.post(`/knowledge/articles/${id}/publish`);
|
||||||
|
},
|
||||||
|
|
||||||
|
unpublishArticle: async (id: string): Promise<void> => {
|
||||||
|
await api.post(`/knowledge/articles/${id}/unpublish`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -10,7 +9,6 @@ import {
|
||||||
Space,
|
Space,
|
||||||
Modal,
|
Modal,
|
||||||
Form,
|
Form,
|
||||||
message,
|
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Typography,
|
Typography,
|
||||||
Drawer,
|
Drawer,
|
||||||
|
|
@ -24,7 +22,15 @@ import {
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import api from '../../../../shared/utils/api';
|
import {
|
||||||
|
useKnowledgeArticles,
|
||||||
|
useCreateArticle,
|
||||||
|
useUpdateArticle,
|
||||||
|
useDeleteArticle,
|
||||||
|
usePublishArticle,
|
||||||
|
useUnpublishArticle,
|
||||||
|
} from '../../application';
|
||||||
|
import type { Article, CreateArticleParams } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
@ -39,20 +45,6 @@ const CATEGORIES = [
|
||||||
{ value: 'GENERAL', label: '通用知识' },
|
{ value: 'GENERAL', label: '通用知识' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Article {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
summary: string;
|
|
||||||
category: string;
|
|
||||||
tags: string[];
|
|
||||||
source: string;
|
|
||||||
isPublished: boolean;
|
|
||||||
citationCount: number;
|
|
||||||
qualityScore: number;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KnowledgePage() {
|
export function KnowledgePage() {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string>();
|
const [categoryFilter, setCategoryFilter] = useState<string>();
|
||||||
|
|
@ -60,64 +52,13 @@ export function KnowledgePage() {
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useKnowledgeArticles(categoryFilter);
|
||||||
queryKey: ['knowledge-articles', categoryFilter],
|
const createMutation = useCreateArticle();
|
||||||
queryFn: async () => {
|
const updateMutation = useUpdateArticle();
|
||||||
const params = new URLSearchParams();
|
const deleteMutation = useDeleteArticle();
|
||||||
if (categoryFilter) params.append('category', categoryFilter);
|
const publishMutation = usePublishArticle();
|
||||||
const response = await api.get(`/knowledge/articles?${params}`);
|
const unpublishMutation = useUnpublishArticle();
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createMutation = useMutation({
|
|
||||||
mutationFn: (values: Partial<Article>) =>
|
|
||||||
api.post('/knowledge/articles', values),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章创建成功');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
setIsModalOpen(false);
|
|
||||||
form.resetFields();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
|
||||||
mutationFn: ({ id, ...values }: { id: string } & Partial<Article>) =>
|
|
||||||
api.put(`/knowledge/articles/${id}`, values),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章更新成功');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
setIsModalOpen(false);
|
|
||||||
form.resetFields();
|
|
||||||
setSelectedArticle(null);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => api.delete(`/knowledge/articles/${id}`),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章已删除');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const publishMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => api.post(`/knowledge/articles/${id}/publish`),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章已发布');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const unpublishMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => api.post(`/knowledge/articles/${id}/unpublish`),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章已取消发布');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleEdit = (article: Article) => {
|
const handleEdit = (article: Article) => {
|
||||||
setSelectedArticle(article);
|
setSelectedArticle(article);
|
||||||
|
|
@ -135,11 +76,25 @@ export function KnowledgePage() {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (values: Partial<Article>) => {
|
const handleSubmit = (values: CreateArticleParams) => {
|
||||||
if (selectedArticle) {
|
if (selectedArticle) {
|
||||||
updateMutation.mutate({ id: selectedArticle.id, ...values });
|
updateMutation.mutate(
|
||||||
|
{ id: selectedArticle.id, ...values },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
form.resetFields();
|
||||||
|
setSelectedArticle(null);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
createMutation.mutate(values);
|
createMutation.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
form.resetFields();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,105 +1,3 @@
|
||||||
import { create } from 'zustand';
|
// Re-export from auth feature for backward compatibility
|
||||||
import { persist } from 'zustand/middleware';
|
export { useAuthStore as useAuth } from '../../features/auth/application';
|
||||||
import api from '../utils/api';
|
export type { AdminInfo } from '../../features/auth/infrastructure';
|
||||||
|
|
||||||
interface AdminInfo {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthState {
|
|
||||||
admin: AdminInfo | null;
|
|
||||||
token: string | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
login: (username: string, password: string) => Promise<void>;
|
|
||||||
logout: () => void;
|
|
||||||
checkAuth: () => Promise<boolean>;
|
|
||||||
hasPermission: (permission: string) => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAuth = create<AuthState>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
admin: null,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
|
|
||||||
login: async (username: string, password: string) => {
|
|
||||||
const response = await api.post('/admin/login', { username, password });
|
|
||||||
const { data } = response.data;
|
|
||||||
|
|
||||||
localStorage.setItem('admin_token', data.token);
|
|
||||||
|
|
||||||
set({
|
|
||||||
admin: data.admin,
|
|
||||||
token: data.token,
|
|
||||||
isAuthenticated: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
logout: () => {
|
|
||||||
localStorage.removeItem('admin_token');
|
|
||||||
set({
|
|
||||||
admin: null,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
checkAuth: async () => {
|
|
||||||
const token = localStorage.getItem('admin_token');
|
|
||||||
if (!token) {
|
|
||||||
set({ isAuthenticated: false, admin: null, token: null });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await api.get('/admin/verify');
|
|
||||||
if (response.data.success) {
|
|
||||||
set({
|
|
||||||
admin: response.data.data,
|
|
||||||
token,
|
|
||||||
isAuthenticated: true,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem('admin_token');
|
|
||||||
}
|
|
||||||
|
|
||||||
set({ isAuthenticated: false, admin: null, token: null });
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasPermission: (permission: string) => {
|
|
||||||
const { admin } = get();
|
|
||||||
if (!admin) return false;
|
|
||||||
|
|
||||||
const permissions = admin.permissions || [];
|
|
||||||
|
|
||||||
// 超管拥有所有权限
|
|
||||||
if (permissions.includes('*')) return true;
|
|
||||||
|
|
||||||
// 完全匹配
|
|
||||||
if (permissions.includes(permission)) return true;
|
|
||||||
|
|
||||||
// 通配符匹配
|
|
||||||
const [resource] = permission.split(':');
|
|
||||||
if (permissions.includes(`${resource}:*`)) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'auth-storage',
|
|
||||||
partialize: (state) => ({
|
|
||||||
admin: state.admin,
|
|
||||||
token: state.token,
|
|
||||||
isAuthenticated: state.isAuthenticated,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue