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