feat: 添加 API 客户端以处理 basePath 并更新所有 fetch 调用
移除 next.config.js 中的 standalone 输出配置,添加 lib/api-client.ts 封装 fetch 请求, 自动为所有 API 调用添加配置的 basePath。更新所有页面和组件中的 fetch 调用以使用新的 fetchApi 函数, 确保在部署到子路径时 API 请求能正确工作。同时调整 tsconfig.json 的格式并修复项目创建时的字段名。
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const metricSchema = z.object({
|
||||
name: z.string().min(1, '名称不能为空'),
|
||||
@@ -61,7 +62,7 @@ export default function AdminMetricsPage() {
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/metrics');
|
||||
const res = await fetchApi('/api/metrics');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setMetrics(data);
|
||||
@@ -83,7 +84,7 @@ export default function AdminMetricsPage() {
|
||||
const url = editingMetric ? `/api/metrics/${editingMetric.id}` : '/api/metrics';
|
||||
const method = editingMetric ? 'PATCH' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
const res = await fetchApi(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values),
|
||||
@@ -119,7 +120,7 @@ export default function AdminMetricsPage() {
|
||||
|
||||
const handleToggleActive = async (metric: Metric) => {
|
||||
try {
|
||||
const res = await fetch(`/api/metrics/${metric.id}`, {
|
||||
const res = await fetchApi(`/api/metrics/${metric.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ is_active: !metric.is_active }),
|
||||
@@ -143,7 +144,7 @@ export default function AdminMetricsPage() {
|
||||
if (!deleteMetricId) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/metrics/${deleteMetricId}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/metrics/${deleteMetricId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('删除失败');
|
||||
|
||||
toast.success('指标已删除');
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const templateItemSchema = z.object({
|
||||
title: z.string().min(1, '标题不能为空'),
|
||||
@@ -75,7 +76,7 @@ export default function AdminTemplatesPage() {
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/templates');
|
||||
const res = await fetchApi('/api/templates');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setTemplates(data);
|
||||
@@ -97,7 +98,7 @@ export default function AdminTemplatesPage() {
|
||||
const url = editingTemplate ? `/api/templates/${editingTemplate.id}` : '/api/templates';
|
||||
const method = editingTemplate ? 'PATCH' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
const res = await fetchApi(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values),
|
||||
@@ -133,7 +134,7 @@ export default function AdminTemplatesPage() {
|
||||
|
||||
const handleToggleActive = async (template: Template) => {
|
||||
try {
|
||||
const res = await fetch(`/api/templates/${template.id}`, {
|
||||
const res = await fetchApi(`/api/templates/${template.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ is_active: !template.is_active }),
|
||||
@@ -157,7 +158,7 @@ export default function AdminTemplatesPage() {
|
||||
if (!deleteTemplateId) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/templates/${deleteTemplateId}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/templates/${deleteTemplateId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('删除失败');
|
||||
|
||||
toast.success('模板已删除');
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const userSchema = z.object({
|
||||
name: z.string().min(1, '名称不能为空'),
|
||||
@@ -81,7 +82,7 @@ export default function AdminUsersPage() {
|
||||
password: values.password ? values.password : undefined,
|
||||
};
|
||||
|
||||
const res = await fetch(url, {
|
||||
const res = await fetchApi(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -124,7 +125,7 @@ export default function AdminUsersPage() {
|
||||
if (!deleteUserId) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/users/${deleteUserId}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/users/${deleteUserId}`, { method: 'DELETE' });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data?.error || '删除失败');
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
import { BarChart3, PieChart as PieChartIcon, TrendingUp, Users, Target } from 'lucide-react';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface KRWithRelations extends KR {
|
||||
owner: User | null;
|
||||
@@ -48,10 +49,10 @@ export default function AnalysisPage() {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [projectsRes, krsRes, checkinsRes, usersRes] = await Promise.all([
|
||||
fetch('/api/projects'),
|
||||
fetch('/api/krs'),
|
||||
fetch('/api/checkins?limit=100'),
|
||||
fetch('/api/users'),
|
||||
fetchApi('/api/projects'),
|
||||
fetchApi('/api/krs'),
|
||||
fetchApi('/api/checkins?limit=100'),
|
||||
fetchApi('/api/users'),
|
||||
]);
|
||||
|
||||
if (projectsRes.ok) setProjects(await projectsRes.json());
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
|
||||
import { getErrorMessage } from '@/lib/error-utils';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface KRWithRelations extends KR {
|
||||
owner: User | null;
|
||||
@@ -90,9 +91,9 @@ export default function KRDetailPage({ krId }: { krId: string }) {
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [krRes, checkinsRes, tasksRes] = await Promise.all([
|
||||
fetch(`/api/krs/${krId}`),
|
||||
fetch(`/api/checkins?kr_id=${krId}`),
|
||||
fetch(`/api/tasks?kr_id=${krId}`),
|
||||
fetchApi(`/api/krs/${krId}`),
|
||||
fetchApi(`/api/checkins?kr_id=${krId}`),
|
||||
fetchApi(`/api/tasks?kr_id=${krId}`),
|
||||
]);
|
||||
|
||||
if (krRes.status === 403) {
|
||||
@@ -159,7 +160,7 @@ export default function KRDetailPage({ krId }: { krId: string }) {
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/checkins', {
|
||||
const res = await fetchApi('/api/checkins', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -192,7 +193,7 @@ export default function KRDetailPage({ krId }: { krId: string }) {
|
||||
if (!kr) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/krs/${kr.id}`, {
|
||||
const res = await fetchApi(`/api/krs/${kr.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -214,7 +215,7 @@ export default function KRDetailPage({ krId }: { krId: string }) {
|
||||
if (!kr) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/krs/${kr.id}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/krs/${kr.id}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('删除失败');
|
||||
toast.success('KR 已删除');
|
||||
router.push(`/projects/${kr.project_id}`);
|
||||
@@ -233,7 +234,7 @@ export default function KRDetailPage({ krId }: { krId: string }) {
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/krs/${kr.id}`, {
|
||||
const res = await fetchApi(`/api/krs/${kr.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -293,7 +294,7 @@ export default function KRDetailPage({ krId }: { krId: string }) {
|
||||
|
||||
const handleTaskStatusChange = async (taskId: string, newStatus: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}`, {
|
||||
const res = await fetchApi(`/api/tasks/${taskId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { toast } from 'sonner';
|
||||
import { useUser } from '@/components/user-context';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const DEFAULT_USERNAME = 'baicaimiao';
|
||||
const DEFAULT_PASSWORD = 'baicaimiao123';
|
||||
@@ -29,7 +30,7 @@ export default function LoginPage() {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
const res = await fetchApi('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Presentation,
|
||||
} from 'lucide-react';
|
||||
import { getErrorMessage } from '@/lib/error-utils';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface ProjectWithRelations extends Project {
|
||||
owner: User | null;
|
||||
@@ -78,9 +79,9 @@ export default function MeetingPage() {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [projectsRes, krsRes, meetingsRes] = await Promise.all([
|
||||
fetch('/api/projects'),
|
||||
fetch('/api/krs'),
|
||||
fetch('/api/meetings'),
|
||||
fetchApi('/api/projects'),
|
||||
fetchApi('/api/krs'),
|
||||
fetchApi('/api/meetings'),
|
||||
]);
|
||||
|
||||
if (projectsRes.ok) {
|
||||
@@ -182,7 +183,7 @@ export default function MeetingPage() {
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/meetings', {
|
||||
const res = await fetchApi('/api/meetings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -12,6 +12,7 @@ import { calculateProgress, getDaysRemaining, formatDateRange, isKRActive, getTo
|
||||
import { PERIOD_TYPES, REASON_CODES } from '@/lib/constants';
|
||||
import type { KR, User } from '@/types/database';
|
||||
import { Target, Clock, AlertTriangle, CheckCircle, History } from 'lucide-react';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface KRWithRelations extends KR {
|
||||
owner: User | null;
|
||||
@@ -38,7 +39,7 @@ export default function MyKRsPage() {
|
||||
useEffect(() => {
|
||||
async function fetchKRs() {
|
||||
try {
|
||||
const res = await fetch('/api/krs');
|
||||
const res = await fetchApi('/api/krs');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setKRs(data);
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Task, TaskStep, User } from '@/types/database';
|
||||
import { CheckSquare, Clock, Calendar, AlertTriangle, User as UserIcon, Plus } from 'lucide-react';
|
||||
import { TaskEditDialog } from '@/components/task-edit-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface TaskWithRelations extends Task {
|
||||
steps: TaskStep[];
|
||||
@@ -41,7 +42,7 @@ export default function MyTasksPage() {
|
||||
? '/api/tasks'
|
||||
: `/api/tasks?user_id=${currentUser?.id}&include_help=true`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const res = await fetchApi(url);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Enrich with owner info locally since API doesn't join
|
||||
@@ -61,7 +62,7 @@ export default function MyTasksPage() {
|
||||
const fetchHelpSteps = async () => {
|
||||
if (!currentUser?.id || currentUser.role === 'owner') return;
|
||||
try {
|
||||
const res = await fetch(`/api/task-steps?help_requested_to_user_id=${currentUser.id}`);
|
||||
const res = await fetchApi(`/api/task-steps?help_requested_to_user_id=${currentUser.id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setHelpSteps(data);
|
||||
@@ -80,7 +81,7 @@ export default function MyTasksPage() {
|
||||
|
||||
const handleTaskStatusChange = async (taskId: string, newStatus: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}`, {
|
||||
const res = await fetchApi(`/api/tasks/${taskId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
AlertCircle,
|
||||
HandHelping,
|
||||
} from 'lucide-react';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface ProjectWithRelations extends Project {
|
||||
owner: User | null;
|
||||
@@ -40,8 +41,8 @@ export default function DashboardPage() {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [projectsRes, krsRes] = await Promise.all([
|
||||
fetch('/api/projects'),
|
||||
fetch('/api/krs'),
|
||||
fetchApi('/api/projects'),
|
||||
fetchApi('/api/krs'),
|
||||
]);
|
||||
|
||||
if (projectsRes.ok) {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
User as UserIcon,
|
||||
} from 'lucide-react';
|
||||
import { TaskEditDialog } from '@/components/task-edit-dialog';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface ProjectWithRelations extends Project {
|
||||
owner: User | null;
|
||||
@@ -100,10 +101,10 @@ export default function ProjectDetailPage({ projectId }: { projectId: string })
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [projectRes, objectivesRes, krsRes, tasksRes] = await Promise.all([
|
||||
fetch(`/api/projects/${projectId}`),
|
||||
fetch(`/api/objectives?project_id=${projectId}`),
|
||||
fetch(`/api/krs?project_id=${projectId}`),
|
||||
fetch(`/api/tasks?project_id=${projectId}`),
|
||||
fetchApi(`/api/projects/${projectId}`),
|
||||
fetchApi(`/api/objectives?project_id=${projectId}`),
|
||||
fetchApi(`/api/krs?project_id=${projectId}`),
|
||||
fetchApi(`/api/tasks?project_id=${projectId}`),
|
||||
]);
|
||||
|
||||
if (projectRes.status === 403) {
|
||||
@@ -156,7 +157,7 @@ export default function ProjectDetailPage({ projectId }: { projectId: string })
|
||||
const handleDeleteProject = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/projects/${projectId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('删除失败');
|
||||
toast.success('项目已删除');
|
||||
router.push('/projects');
|
||||
@@ -172,7 +173,7 @@ export default function ProjectDetailPage({ projectId }: { projectId: string })
|
||||
setIsEditing(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${projectId}`, {
|
||||
const res = await fetchApi(`/api/projects/${projectId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editForm),
|
||||
@@ -199,7 +200,7 @@ export default function ProjectDetailPage({ projectId }: { projectId: string })
|
||||
if (!deleteObjectiveId) return;
|
||||
setIsDeletingObjective(true);
|
||||
try {
|
||||
const res = await fetch(`/api/objectives/${deleteObjectiveId}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/objectives/${deleteObjectiveId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('删除失败');
|
||||
toast.success('目标已删除');
|
||||
fetchData();
|
||||
@@ -220,7 +221,7 @@ export default function ProjectDetailPage({ projectId }: { projectId: string })
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/objectives/${editingObjective.id}`, {
|
||||
const res = await fetchApi(`/api/objectives/${editingObjective.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -246,7 +247,7 @@ export default function ProjectDetailPage({ projectId }: { projectId: string })
|
||||
|
||||
const handleTaskStatusChange = async (taskId: string, newStatus: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}`, {
|
||||
const res = await fetchApi(`/api/tasks/${taskId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { PERIOD_TYPES, DIRECTION_LABELS } from '@/lib/constants';
|
||||
import type { Objective, Metric, Template } from '@/types/database';
|
||||
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { getErrorMessage } from '@/lib/error-utils';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().min(1, 'KR标题不能为空'),
|
||||
@@ -72,9 +73,9 @@ export default function NewKRPage() {
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const [objRes, metricsRes, templatesRes] = await Promise.all([
|
||||
fetch(`/api/objectives?project_id=${projectId}`),
|
||||
fetch('/api/metrics?active_only=true'),
|
||||
fetch('/api/templates?active_only=true'),
|
||||
fetchApi(`/api/objectives?project_id=${projectId}`),
|
||||
fetchApi('/api/metrics?active_only=true'),
|
||||
fetchApi('/api/templates?active_only=true'),
|
||||
]);
|
||||
|
||||
if (objRes.ok) {
|
||||
@@ -156,7 +157,7 @@ export default function NewKRPage() {
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/krs', {
|
||||
const res = await fetchApi('/api/krs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useUser } from '@/components/user-context';
|
||||
import { toast } from 'sonner';
|
||||
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { getErrorMessage } from '@/lib/error-utils';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().min(1, '目标标题不能为空'),
|
||||
@@ -54,7 +55,7 @@ export default function NewObjectivePage() {
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/objectives', {
|
||||
const res = await fetchApi('/api/objectives', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -18,6 +18,7 @@ import { toast } from 'sonner';
|
||||
import type { User } from '@/types/database';
|
||||
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { getErrorMessage } from '@/lib/error-utils';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, '项目名称不能为空'),
|
||||
@@ -48,12 +49,12 @@ export default function NewProjectPage() {
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/projects', {
|
||||
const res = await fetchApi('/api/projects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...values,
|
||||
owner_id: currentUser?.id,
|
||||
created_by: currentUser?.id,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { StatusBadge, StatusDot } from '@/components/status-badge';
|
||||
import { useUser } from '@/components/user-context';
|
||||
import type { Project, User } from '@/types/database';
|
||||
import { Plus, Users, Target, ArrowRight } from 'lucide-react';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface ProjectWithRelations extends Project {
|
||||
owner: User | null;
|
||||
@@ -22,7 +23,7 @@ export default function ProjectsPage() {
|
||||
useEffect(() => {
|
||||
async function fetchProjects() {
|
||||
try {
|
||||
const res = await fetch('/api/projects');
|
||||
const res = await fetchApi('/api/projects');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setProjects(data);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Target, Loader2, Sparkles, CheckCircle } from 'lucide-react';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface InitWizardProps {
|
||||
onComplete: () => void;
|
||||
@@ -19,7 +20,7 @@ export function InitWizard({ onComplete }: InitWizardProps) {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/seed', { method: 'POST' });
|
||||
const res = await fetchApi('/api/seed', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string().min(1, '计划标题不能为空'),
|
||||
@@ -145,7 +146,7 @@ export function TaskEditDialog({
|
||||
const url = task ? `/api/tasks/${task.id}` : '/api/tasks';
|
||||
const method = task ? 'PATCH' : 'POST';
|
||||
|
||||
const taskRes = await fetch(url, {
|
||||
const taskRes = await fetchApi(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -193,7 +194,7 @@ export function TaskEditDialog({
|
||||
const localSteps = steps.filter(s => s.id.startsWith('temp-'));
|
||||
if (localSteps.length > 0) {
|
||||
await Promise.all(localSteps.map(step =>
|
||||
fetch('/api/task-steps', {
|
||||
fetchApi('/api/task-steps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -223,7 +224,7 @@ export function TaskEditDialog({
|
||||
|
||||
try {
|
||||
setIsDeletingTask(true);
|
||||
await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' });
|
||||
await fetchApi(`/api/tasks/${task.id}`, { method: 'DELETE' });
|
||||
toast.success('计划已删除');
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
@@ -241,7 +242,7 @@ export function TaskEditDialog({
|
||||
// If task exists, save step to DB immediately
|
||||
if (task) {
|
||||
try {
|
||||
const res = await fetch('/api/task-steps', {
|
||||
const res = await fetchApi('/api/task-steps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -295,7 +296,7 @@ export function TaskEditDialog({
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/task-steps', {
|
||||
const res = await fetchApi('/api/task-steps', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -320,7 +321,7 @@ export function TaskEditDialog({
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/task-steps?id=${stepId}`, { method: 'DELETE' });
|
||||
const res = await fetchApi(`/api/task-steps?id=${stepId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
setSteps(steps.filter(s => s.id !== stepId));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
import type { User } from '@/types/database';
|
||||
import { fetchApi } from '@/lib/api-client';
|
||||
|
||||
interface UserContextType {
|
||||
currentUser: User | null;
|
||||
@@ -26,7 +27,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const refreshUsers = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/users');
|
||||
const res = await fetchApi('/api/users');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUsers(data);
|
||||
@@ -40,7 +41,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const refreshMe = useCallback(async () => {
|
||||
try {
|
||||
const meRes = await fetch('/api/auth/me', { cache: 'no-store' });
|
||||
const meRes = await fetchApi('/api/auth/me', { cache: 'no-store' });
|
||||
const meData = await meRes.json().catch(() => ({}));
|
||||
const me = meData?.user as User | null | undefined;
|
||||
|
||||
@@ -109,7 +110,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
|
||||
setCurrentUser(null);
|
||||
setIsAuthenticated(false);
|
||||
setUsers([]);
|
||||
fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
|
||||
fetchApi('/api/auth/logout', { method: 'POST' }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
22
lib/api-client.ts
Normal file
22
lib/api-client.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 必须与 next.config.js 中的 basePath 保持一致
|
||||
const BASE_PATH = '/ok-r';
|
||||
|
||||
/**
|
||||
* 获取带 basePath 的完整 API URL
|
||||
* @param path API 路径,例如 '/api/users'
|
||||
*/
|
||||
export function getApiUrl(path: string) {
|
||||
if (path.startsWith('http')) return path;
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${BASE_PATH}${normalizedPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装 fetch 请求,自动处理 basePath
|
||||
* @param path API 路径,例如 '/api/users'
|
||||
* @param init fetch 配置项
|
||||
*/
|
||||
export async function fetchApi(path: string, init?: RequestInit) {
|
||||
const url = getApiUrl(path);
|
||||
return fetch(url, init);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone', // 输出独立的目录
|
||||
basePath: '/ok-r', // 部署到子路径时需要设置
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -19,9 +23,19 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"dist/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user