front fix
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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';
|
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// 检测语言
|
setComments(mockComments);
|
||||||
const language = detectLanguage(comment.content);
|
setTotalComments(mockComments.length);
|
||||||
|
setLoading(false);
|
||||||
return {
|
} catch (error) {
|
||||||
id: comment.comment_id,
|
console.error('Error fetching comments:', error);
|
||||||
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);
|
|
||||||
setTotalComments(total);
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch comments:', err);
|
|
||||||
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' => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|
||||||
// Build query parameters
|
// Mock data for posts
|
||||||
const params: Record<string, string | number> = {
|
const mockPosts = [
|
||||||
limit: 50,
|
{
|
||||||
offset: 0
|
id: '1',
|
||||||
};
|
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.',
|
||||||
if (influencerId) {
|
author: 'John Smith',
|
||||||
params.influencer_id = influencerId;
|
date: '2023-05-15',
|
||||||
|
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) {
|
setTotalPosts(mockPosts.length);
|
||||||
params.project_id = projectId;
|
setPosts(mockPosts);
|
||||||
}
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
const response = await postsApi.getPosts(params);
|
console.error('Error fetching posts:', error);
|
||||||
|
setLoading(false);
|
||||||
// Process returned data
|
}
|
||||||
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) => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user