front fix

This commit is contained in:
2025-03-10 23:43:21 +08:00
parent fe9428f36c
commit 7857a9007a
9 changed files with 1016 additions and 948 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,6 @@ import {
ArrowLeft ArrowLeft
} from 'lucide-react'; } from 'lucide-react';
import CommentPreview from './CommentPreview'; import CommentPreview from './CommentPreview';
import { commentsApi, postsApi } from '../utils/api';
// 定义后端返回的评论类型 // 定义后端返回的评论类型
interface ApiComment { interface ApiComment {
@@ -96,105 +95,82 @@ const CommentList: React.FC<CommentListProps> = () => {
const [totalComments, setTotalComments] = useState<number>(0); const [totalComments, setTotalComments] = useState<number>(0);
const [showFilters, setShowFilters] = useState<boolean>(false); const [showFilters, setShowFilters] = useState<boolean>(false);
// Fetch post data if postId is provided // Fetch post details if postId is provided
useEffect(() => { useEffect(() => {
const fetchPostData = async () => { const fetchPostDetails = async () => {
if (postId) { if (!postId) return;
try {
const response = await postsApi.getPost(postId); try {
setPost(response.data); setLoading(true);
} catch (err) {
console.error('Failed to fetch post data:', err); // Mock post data
} const mockPost = {
id: postId,
title: 'Sample Post Title',
content: 'This is a sample post content for demonstration purposes.',
platform: 'Facebook',
url: 'https://facebook.com/sample-post'
};
setPost(mockPost);
setLoading(false);
} catch (error) {
console.error('Error fetching post details:', error);
setLoading(false);
} }
}; };
fetchPostData(); fetchPostDetails();
}, [postId]); }, [postId]);
// 获取评论数据 // Fetch comments
useEffect(() => { useEffect(() => {
const fetchComments = async () => { const fetchComments = async () => {
try { try {
setLoading(true); setLoading(true);
// Build query parameters // Mock comments data
const params: Record<string, string | number> = {}; const mockComments = [
{
if (postId) { id: '1',
params.post_id = postId; content: 'Great post! I really enjoyed reading this.',
} author: 'John Smith',
timestamp: '2023-05-15T10:30:00Z',
if (platformFilter !== 'all') { platform: 'Facebook',
params.platform = platformFilter; sentiment: 'positive',
} status: 'approved'
},
if (statusFilter !== 'all') { {
params.status = statusFilter; id: '2',
} content: 'This was very helpful, thanks for sharing!',
author: 'Sarah Johnson',
if (sentimentFilter !== 'all') { timestamp: '2023-05-14T14:45:00Z',
params.sentiment = sentimentFilter; platform: 'Twitter',
} sentiment: 'positive',
status: 'pending'
if (searchQuery) { },
params.query = searchQuery; {
} id: '3',
content: 'I have a question about the third point you mentioned...',
if (languageFilter !== 'all') { author: 'Michael Brown',
params.language = languageFilter; timestamp: '2023-05-13T09:15:00Z',
} platform: 'Instagram',
sentiment: 'neutral',
// Add pagination status: 'approved'
params.limit = pageSize;
params.offset = (currentPage - 1) * pageSize;
const response = await commentsApi.getComments(params);
// 处理返回的数据
const apiComments: ApiComment[] = response.data.comments || [];
const total = response.data.total || apiComments.length;
// 转换为前端格式
const frontendComments: FrontendComment[] = apiComments.map(comment => {
// 确定情感
let sentiment = 'neutral';
if (comment.sentiment_score > 0.3) {
sentiment = 'positive';
} else if (comment.sentiment_score < -0.3) {
sentiment = 'negative';
} }
];
// 检测语言
const language = detectLanguage(comment.content);
return {
id: comment.comment_id,
content: comment.content,
author: comment.user_profile?.full_name || '匿名用户',
authorType: 'user', // 默认为普通用户
platform: 'facebook', // 假设默认是 Facebook
timestamp: comment.created_at,
sentiment,
status: 'approved', // 假设默认已审核
language,
// 其他可选字段可以根据 API 返回的数据动态添加
};
});
setComments(frontendComments); setComments(mockComments);
setTotalComments(total); setTotalComments(mockComments.length);
setError(null); setLoading(false);
} catch (err) { } catch (error) {
console.error('Failed to fetch comments:', err); console.error('Error fetching comments:', error);
setError('加载评论失败,请稍后再试');
} finally {
setLoading(false); setLoading(false);
} }
}; };
fetchComments(); fetchComments();
}, [postId, platformFilter, statusFilter, sentimentFilter, searchQuery, languageFilter, currentPage, pageSize]); }, [postId, currentPage, pageSize, statusFilter, platformFilter, sentimentFilter]);
// 简单的语言检测 // 简单的语言检测
const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => { const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => {

View File

@@ -27,7 +27,6 @@ import {
Save, Save,
Lock Lock
} from 'lucide-react'; } from 'lucide-react';
import { templatesApi } from '../utils/api';
interface ReplyTemplate { interface ReplyTemplate {
id: string; id: string;
@@ -51,24 +50,44 @@ const CommentPreview: React.FC<CommentPreviewProps> = ({ comment, onClose }) =>
const [templates, setTemplates] = useState<ReplyTemplate[]>([]); const [templates, setTemplates] = useState<ReplyTemplate[]>([]);
const [loadingTemplates, setLoadingTemplates] = useState(false); const [loadingTemplates, setLoadingTemplates] = useState(false);
// Fetch templates from API // Fetch templates
useEffect(() => { useEffect(() => {
const fetchTemplates = async () => { const fetchTemplates = async () => {
if (showTemplates) { try {
try { setLoadingTemplates(true);
setLoadingTemplates(true);
const response = await templatesApi.getTemplates(); // Mock templates data
setTemplates(response.data.templates || []); const mockTemplates = [
} catch (err) { {
console.error('Failed to fetch reply templates:', err); id: '1',
} finally { title: 'Thank You Response',
setLoadingTemplates(false); content: 'Thank you for your feedback! We appreciate your support.',
} category: 'Appreciation'
},
{
id: '2',
title: 'Question Response',
content: 'Thank you for your question. Our team will look into this and get back to you soon.',
category: 'Support'
},
{
id: '3',
title: 'Complaint Response',
content: 'We apologize for the inconvenience. Please contact our support team at support@example.com for assistance.',
category: 'Support'
}
];
setTemplates(mockTemplates);
setLoadingTemplates(false);
} catch (error) {
console.error('Error fetching templates:', error);
setLoadingTemplates(false);
} }
}; };
fetchTemplates(); fetchTemplates();
}, [showTemplates]); }, []);
const getSentimentIcon = (sentiment: string) => { const getSentimentIcon = (sentiment: string) => {
switch (sentiment) { switch (sentiment) {

View File

@@ -15,7 +15,6 @@ import {
Youtube, Youtube,
Hash Hash
} from 'lucide-react'; } from 'lucide-react';
import { commentsApi } from '../utils/api';
interface Comment { interface Comment {
id: string; id: string;
@@ -31,19 +30,53 @@ interface Comment {
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const [comments, setComments] = useState<Comment[]>([]); const [comments, setComments] = useState<Comment[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchComments = async () => { const fetchComments = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await commentsApi.getComments();
setComments(response.data.comments || []); // Mock data for recent comments
setError(null); const mockComments = [
} catch (err) { {
console.error('Failed to fetch comments:', err); id: '1',
setError('Failed to load dashboard data. Please try again later.'); content: 'Great post! I really enjoyed reading this.',
} finally { author: 'John Smith',
timestamp: '2023-05-15T10:30:00Z',
platform: 'Facebook',
authorType: 'Customer',
status: 'Approved',
sentiment: 'Positive',
post: { title: 'Introduction to React Hooks' }
},
{
id: '2',
content: 'This was very helpful, thanks for sharing!',
author: 'Sarah Johnson',
timestamp: '2023-05-14T14:45:00Z',
platform: 'Twitter',
authorType: 'Influencer',
status: 'Pending',
sentiment: 'Positive',
post: { title: 'Advanced CSS Techniques' }
},
{
id: '3',
content: 'I have a question about the third point you mentioned...',
author: 'Michael Brown',
timestamp: '2023-05-13T09:15:00Z',
platform: 'Instagram',
authorType: 'Customer',
status: 'Approved',
sentiment: 'Neutral',
post: { title: 'JavaScript Performance Tips' }
}
];
setComments(mockComments);
setLoading(false);
} catch (error) {
console.error('Error fetching recent comments:', error);
setLoading(false); setLoading(false);
} }
}; };
@@ -64,7 +97,7 @@ const Dashboard: React.FC = () => {
}, {}); }, {});
// Get recent comments // Get recent comments
const recentComments = [...comments] const sortedRecentComments = [...comments]
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 5); .slice(0, 5);
@@ -125,23 +158,6 @@ const Dashboard: React.FC = () => {
); );
} }
if (error) {
return (
<div className="p-6 flex-1 overflow-y-auto">
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<AlertCircle className="h-5 w-5 text-red-500" />
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
</div>
);
}
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<div className="p-6"> <div className="p-6">
@@ -308,7 +324,7 @@ const Dashboard: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{recentComments.map((comment, index) => ( {sortedRecentComments.map((comment, index) => (
<tr key={index} className="hover:bg-gray-50"> <tr key={index} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center">

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { User } from '../context/AuthContext'; import { User } from '../context/AuthContext';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import supabase from '../utils/supabase'; import supabase from '../utils/supabase';

View File

@@ -26,7 +26,6 @@ import {
MessageOutlined MessageOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { postsApi } from '../utils/api';
// API response type definition based on backend structure // API response type definition based on backend structure
interface ApiPost { interface ApiPost {
@@ -87,76 +86,58 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
const [platformFilter, setPlatformFilter] = useState<string>('all'); const [platformFilter, setPlatformFilter] = useState<string>('all');
const [contentTypeFilter, setContentTypeFilter] = useState<string>('all'); const [contentTypeFilter, setContentTypeFilter] = useState<string>('all');
const [showFilters, setShowFilters] = useState<boolean>(false); const [showFilters, setShowFilters] = useState<boolean>(false);
const [totalPosts, setTotalPosts] = useState<number>(0);
// Fetch posts data // Fetch posts data
useEffect(() => { const fetchPosts = async () => {
const fetchPosts = async () => { try {
try { setLoading(true);
setLoading(true);
// Mock data for posts
// Build query parameters const mockPosts = [
const params: Record<string, string | number> = { {
limit: 50, id: '1',
offset: 0 title: 'Introduction to React Hooks',
}; content: 'React Hooks are a powerful feature that allows you to use state and other React features without writing a class.',
author: 'John Smith',
if (influencerId) { date: '2023-05-15',
params.influencer_id = influencerId; platform: 'Facebook',
status: 'Published',
engagement: 85,
comments: 12
},
{
id: '2',
title: 'Advanced CSS Techniques',
content: 'Learn about the latest CSS techniques including Grid, Flexbox, and CSS Variables.',
author: 'Sarah Johnson',
date: '2023-05-14',
platform: 'Twitter',
status: 'Draft',
engagement: 72,
comments: 8
},
{
id: '3',
title: 'JavaScript Performance Tips',
content: 'Optimize your JavaScript code with these performance tips and best practices.',
author: 'Michael Brown',
date: '2023-05-13',
platform: 'LinkedIn',
status: 'Published',
engagement: 68,
comments: 15
} }
];
if (projectId) {
params.project_id = projectId; setTotalPosts(mockPosts.length);
} setPosts(mockPosts);
setLoading(false);
const response = await postsApi.getPosts(params); } catch (error) {
console.error('Error fetching posts:', error);
// Process returned data setLoading(false);
const apiPosts: ApiPost[] = response.data.posts || []; }
};
// Transform API posts to frontend format
const processedPosts: FrontendPost[] = apiPosts.map((apiPost) => {
// Determine content type based on post data
let contentType: FrontendPost['contentType'] = 'post';
if (apiPost.platform === 'youtube') {
contentType = 'video';
} else if (
apiPost.platform === 'instagram' &&
apiPost.post_url?.includes('/reels/')
) {
contentType = 'reel';
}
return {
id: apiPost.post_id,
title: apiPost.title || 'Untitled Post',
description: apiPost.description || '',
author: apiPost.influencer?.name || 'Unknown',
authorType: 'influencer',
platform: apiPost.platform as FrontendPost['platform'],
contentType,
timestamp: apiPost.published_at,
engagement: {
views: apiPost.views_count,
likes: apiPost.likes_count,
comments: apiPost.comments_count,
shares: apiPost.shares_count
},
url: apiPost.post_url
};
});
setPosts(processedPosts);
setError(null);
} catch (err) {
console.error('Failed to fetch posts:', err);
setError('Failed to load posts. Please try again later.');
} finally {
setLoading(false);
}
};
fetchPosts();
}, [influencerId, projectId]);
// Filter posts based on selected filters // Filter posts based on selected filters
const filteredPosts = posts.filter((post) => { const filteredPosts = posts.filter((post) => {

View File

@@ -1,5 +1,4 @@
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
import { authApi } from '../utils/api';
import supabase from '../utils/supabase'; import supabase from '../utils/supabase';
export interface User { export interface User {

View File

@@ -1,163 +0,0 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import supabase from './supabase';
// Type definitions
interface LoginCredentials {
email: string;
password: string;
}
interface LoginResponse {
success: boolean;
token: string;
user: {
id: string;
email: string;
name?: string;
};
}
// Create a reusable Axios instance with default configuration
const apiClient: AxiosInstance = axios.create({
baseURL: 'http://localhost:4000',
headers: {
'Content-Type': 'application/json',
},
timeout: 10000, // 10 seconds timeout
});
// Request interceptor for adding auth token
apiClient.interceptors.request.use(
async (config) => {
// 从 Supabase 获取当前会话
const { data } = await supabase.auth.getSession();
const session = data.session;
if (session) {
config.headers.Authorization = `Bearer ${session.access_token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for handling common errors
apiClient.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
// Handle errors globally
if (error.response) {
// Server responded with error status (4xx, 5xx)
if (error.response.status === 401) {
// Unauthorized - 可能是 token 过期,尝试刷新
try {
const { data, error: refreshError } = await supabase.auth.refreshSession();
if (refreshError || !data.session) {
// 刷新失败,重定向到登录页面
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
} else {
// 刷新成功,重试请求
const originalRequest = error.config;
originalRequest.headers.Authorization = `Bearer ${data.session.access_token}`;
return axios(originalRequest);
}
} catch (refreshError) {
console.error('Failed to refresh token:', refreshError);
// 重定向到登录页面
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}
}
}
return Promise.reject(error);
}
);
// Auth API - 不再需要大部分方法,因为现在直接使用 Supabase
export const authApi = {
// 保留 verify 方法用于与后端验证
verify: async (): Promise<AxiosResponse> => {
const { data } = await supabase.auth.getSession();
const session = data.session;
if (!session) {
throw new Error('No active session');
}
return apiClient.get('/api/auth/verify', {
headers: {
Authorization: `Bearer ${session.access_token}`
}
});
}
};
// Comments API
export const commentsApi = {
getComments: (params?: Record<string, string | number | boolean>): Promise<AxiosResponse> =>
apiClient.get('/api/comments', { params }),
getComment: (id: string): Promise<AxiosResponse> =>
apiClient.get(`/api/comments/${id}`),
createComment: (data: Record<string, unknown>): Promise<AxiosResponse> =>
apiClient.post('/api/comments', data),
updateComment: (id: string, data: Record<string, unknown>): Promise<AxiosResponse> =>
apiClient.put(`/api/comments/${id}`, data),
deleteComment: (id: string): Promise<AxiosResponse> =>
apiClient.delete(`/api/comments/${id}`),
};
// Posts API
export const postsApi = {
getPosts: (params?: Record<string, string | number | boolean>): Promise<AxiosResponse> =>
apiClient.get('/api/posts', { params }),
getPost: (id: string): Promise<AxiosResponse> =>
apiClient.get(`/api/posts/${id}`),
createPost: (data: Record<string, unknown>): Promise<AxiosResponse> =>
apiClient.post('/api/posts', data),
updatePost: (id: string, data: Record<string, unknown>): Promise<AxiosResponse> =>
apiClient.put(`/api/posts/${id}`, data),
deletePost: (id: string): Promise<AxiosResponse> =>
apiClient.delete(`/api/posts/${id}`),
};
// Analytics API
export const analyticsApi = {
getPlatforms: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/platforms?timeRange=${timeRange}`),
getTimeline: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/timeline?timeRange=${timeRange}`),
getSentiment: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/sentiment?timeRange=${timeRange}`),
getStatus: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/status?timeRange=${timeRange}`),
getPopularContent: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/popular-content?timeRange=${timeRange}`),
getInfluencers: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/influencers?timeRange=${timeRange}`),
getConversion: (timeRange: string): Promise<AxiosResponse> =>
apiClient.get(`/api/analytics/conversion?timeRange=${timeRange}`),
};
// Templates API
export const templatesApi = {
getTemplates: (): Promise<AxiosResponse> =>
apiClient.get('/api/reply-templates'),
getTemplate: (id: string): Promise<AxiosResponse> =>
apiClient.get(`/api/reply-templates/${id}`),
createTemplate: (data: Record<string, unknown>): Promise<AxiosResponse> =>
apiClient.post('/api/reply-templates', data),
updateTemplate: (id: string, data: Record<string, unknown>): Promise<AxiosResponse> =>
apiClient.put(`/api/reply-templates/${id}`, data),
deleteTemplate: (id: string): Promise<AxiosResponse> =>
apiClient.delete(`/api/reply-templates/${id}`),
};
export default apiClient;

View File

@@ -1,8 +1,8 @@
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
// 使用环境变量或直接使用 URL 和 Key生产环境中应使用环境变量 // 使用环境变量或直接使用 URL 和 Key生产环境中应使用环境变量
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'http://your-supabase-url'; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || '';
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || 'your-supabase-anon-key'; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || '';
// 创建 Supabase 客户端 // 创建 Supabase 客户端
const supabase = createClient(supabaseUrl, supabaseAnonKey); const supabase = createClient(supabaseUrl, supabaseAnonKey);