From fc9cfda29ef8bb1a2cf652041fba359d29387a24 Mon Sep 17 00:00:00 2001 From: karlcarlo Date: Wed, 28 Jan 2026 15:54:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20API=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E4=BB=A5=E5=A4=84=E7=90=86=20basePath=20?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=89=80=E6=9C=89=20fetch=20?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 next.config.js 中的 standalone 输出配置,添加 lib/api-client.ts 封装 fetch 请求, 自动为所有 API 调用添加配置的 basePath。更新所有页面和组件中的 fetch 调用以使用新的 fetchApi 函数, 确保在部署到子路径时 API 请求能正确工作。同时调整 tsconfig.json 的格式并修复项目创建时的字段名。 --- app/admin/metrics/page.tsx | 9 +++++---- app/admin/templates/page.tsx | 9 +++++---- app/admin/users/page.tsx | 5 +++-- app/analysis/page.tsx | 9 +++++---- app/krs/[id]/client-page.tsx | 17 +++++++++-------- app/login/page.tsx | 3 ++- app/meeting/page.tsx | 9 +++++---- app/my-krs/page.tsx | 3 ++- app/my-tasks/page.tsx | 7 ++++--- app/page.tsx | 5 +++-- app/projects/[id]/client-page.tsx | 19 ++++++++++--------- app/projects/[id]/krs/new/page.tsx | 9 +++++---- app/projects/[id]/objectives/new/page.tsx | 3 ++- app/projects/new/page.tsx | 5 +++-- app/projects/page.tsx | 3 ++- components/init-wizard.tsx | 3 ++- components/task-edit-dialog.tsx | 13 +++++++------ components/user-context.tsx | 7 ++++--- lib/api-client.ts | 22 ++++++++++++++++++++++ next.config.js | 1 - tsconfig.json | 22 ++++++++++++++++++---- 21 files changed, 118 insertions(+), 65 deletions(-) create mode 100644 lib/api-client.ts diff --git a/app/admin/metrics/page.tsx b/app/admin/metrics/page.tsx index 08dd68c..7631a4d 100644 --- a/app/admin/metrics/page.tsx +++ b/app/admin/metrics/page.tsx @@ -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('指标已删除'); diff --git a/app/admin/templates/page.tsx b/app/admin/templates/page.tsx index 102ec82..31e7ce1 100644 --- a/app/admin/templates/page.tsx +++ b/app/admin/templates/page.tsx @@ -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('模板已删除'); diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index 723fea5..c3fa8da 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -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 || '删除失败'); diff --git a/app/analysis/page.tsx b/app/analysis/page.tsx index faf3834..b357b42 100644 --- a/app/analysis/page.tsx +++ b/app/analysis/page.tsx @@ -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()); diff --git a/app/krs/[id]/client-page.tsx b/app/krs/[id]/client-page.tsx index cb3a6bd..f5ae8d5 100644 --- a/app/krs/[id]/client-page.tsx +++ b/app/krs/[id]/client-page.tsx @@ -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 }), diff --git a/app/login/page.tsx b/app/login/page.tsx index a94f67f..499eb4b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -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 }), diff --git a/app/meeting/page.tsx b/app/meeting/page.tsx index 500d970..0d6cd14 100644 --- a/app/meeting/page.tsx +++ b/app/meeting/page.tsx @@ -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({ diff --git a/app/my-krs/page.tsx b/app/my-krs/page.tsx index c860e2a..320011d 100644 --- a/app/my-krs/page.tsx +++ b/app/my-krs/page.tsx @@ -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); diff --git a/app/my-tasks/page.tsx b/app/my-tasks/page.tsx index 405384a..0d07a23 100644 --- a/app/my-tasks/page.tsx +++ b/app/my-tasks/page.tsx @@ -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 }), diff --git a/app/page.tsx b/app/page.tsx index 7d1e4d8..400f17f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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) { diff --git a/app/projects/[id]/client-page.tsx b/app/projects/[id]/client-page.tsx index eddff86..47cdac8 100644 --- a/app/projects/[id]/client-page.tsx +++ b/app/projects/[id]/client-page.tsx @@ -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 }), diff --git a/app/projects/[id]/krs/new/page.tsx b/app/projects/[id]/krs/new/page.tsx index dca63a0..cae5f4e 100644 --- a/app/projects/[id]/krs/new/page.tsx +++ b/app/projects/[id]/krs/new/page.tsx @@ -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({ diff --git a/app/projects/[id]/objectives/new/page.tsx b/app/projects/[id]/objectives/new/page.tsx index 3ab2a64..ceb891d 100644 --- a/app/projects/[id]/objectives/new/page.tsx +++ b/app/projects/[id]/objectives/new/page.tsx @@ -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({ diff --git a/app/projects/new/page.tsx b/app/projects/new/page.tsx index b37e280..7794bf5 100644 --- a/app/projects/new/page.tsx +++ b/app/projects/new/page.tsx @@ -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, }), }); diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 08f93a5..673cc32 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -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); diff --git a/components/init-wizard.tsx b/components/init-wizard.tsx index 1735748..6a01315 100644 --- a/components/init-wizard.tsx +++ b/components/init-wizard.tsx @@ -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) { diff --git a/components/task-edit-dialog.tsx b/components/task-edit-dialog.tsx index 339ef3a..b0dc0d8 100644 --- a/components/task-edit-dialog.tsx +++ b/components/task-edit-dialog.tsx @@ -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)); } diff --git a/components/user-context.tsx b/components/user-context.tsx index 9b2ff58..ce6d58f 100644 --- a/components/user-context.tsx +++ b/components/user-context.tsx @@ -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 ( diff --git a/lib/api-client.ts b/lib/api-client.ts new file mode 100644 index 0000000..0c89407 --- /dev/null +++ b/lib/api-client.ts @@ -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); +} diff --git a/next.config.js b/next.config.js index b30e346..c158b91 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'standalone', // 输出独立的目录 basePath: '/ok-r', // 部署到子路径时需要设置 eslint: { ignoreDuringBuilds: true, diff --git a/tsconfig.json b/tsconfig.json index c714696..2e14118 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" + ] }