Files
ok_r/app/krs/[id]/client-page.tsx
karlcarlo fc9cfda29e feat: 添加 API 客户端以处理 basePath 并更新所有 fetch 调用
移除 next.config.js 中的 standalone 输出配置,添加 lib/api-client.ts 封装 fetch 请求,
自动为所有 API 调用添加配置的 basePath。更新所有页面和组件中的 fetch 调用以使用新的 fetchApi 函数,
确保在部署到子路径时 API 请求能正确工作。同时调整 tsconfig.json 的格式并修复项目创建时的字段名。
2026-01-28 15:54:19 +08:00

820 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Switch } from '@/components/ui/switch';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { StatusBadge, StatusDot } from '@/components/status-badge';
import { ProgressRing } from '@/components/progress-ring';
import { useUser } from '@/components/user-context';
import { Label } from '@/components/ui/label';
import {
calculateProgress,
calculateExpectedProgress,
getDaysRemaining,
formatDateRange,
isKRActive,
} from '@/lib/kr-utils';
import { REASON_CODES, PERIOD_TYPES, DIRECTION_LABELS } from '@/lib/constants';
import { toast } from 'sonner';
import { TaskEditDialog } from '@/components/task-edit-dialog';
import type { KR, User, Checkin, Objective, Task, TaskStep } from '@/types/database';
import {
ArrowLeft,
Plus,
Clock,
TrendingUp,
TrendingDown,
Target,
AlertTriangle,
Calendar,
User as UserIcon,
HandHelping,
Loader2,
Pencil,
Trash2,
CheckSquare,
} from 'lucide-react';
import { getErrorMessage } from '@/lib/error-utils';
import { fetchApi } from '@/lib/api-client';
interface KRWithRelations extends KR {
owner: User | null;
project: { id: string; name: string; status: string } | null;
objective: Objective | null;
}
interface CheckinWithRelations extends Checkin {
user: User | null;
}
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
const checkinSchema = z.object({
current_value: z.number(),
confidence: z.number().min(0).max(100),
reason_code: z.string().optional(),
note: z.string().optional(),
});
type CheckinFormValues = z.infer<typeof checkinSchema>;
export default function KRDetailPage({ krId }: { krId: string }) {
const router = useRouter();
const { currentUser } = useUser();
const [kr, setKR] = useState<KRWithRelations | null>(null);
const [checkins, setCheckins] = useState<CheckinWithRelations[]>([]);
const [tasks, setTasks] = useState<(Task & { steps: TaskStep[] })[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [isCheckinOpen, setIsCheckinOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
const [editingTask, setEditingTask] = useState<(Task & { steps: TaskStep[] }) | null>(null);
const fetchData = useCallback(async () => {
try {
const [krRes, checkinsRes, tasksRes] = await Promise.all([
fetchApi(`/api/krs/${krId}`),
fetchApi(`/api/checkins?kr_id=${krId}`),
fetchApi(`/api/tasks?kr_id=${krId}`),
]);
if (krRes.status === 403) {
setLoadError('你没有权限查看此 KR');
setKR(null);
return;
}
if (krRes.status === 404) {
setLoadError('KR 不存在');
setKR(null);
return;
}
if (krRes.ok) {
const krData = await krRes.json();
setKR(krData);
setLoadError(null);
}
if (checkinsRes.ok) {
const checkinsData = await checkinsRes.json();
setCheckins(checkinsData);
}
if (tasksRes.ok) {
const tasksData = await tasksRes.json();
setTasks(tasksData);
}
} catch (error) {
console.error('Failed to fetch KR data:', error);
setLoadError('加载失败');
} finally {
setIsLoading(false);
}
}, [krId]);
useEffect(() => {
fetchData();
}, [fetchData]);
const form = useForm<CheckinFormValues>({
resolver: zodResolver(checkinSchema),
defaultValues: {
current_value: kr?.current_value || 0,
confidence: kr?.confidence || 80,
reason_code: '',
note: '',
},
});
useEffect(() => {
if (kr) {
form.reset({
current_value: kr.current_value,
confidence: kr.confidence,
reason_code: kr.reason_code || '',
note: '',
});
}
}, [kr, form]);
const onCheckinSubmit = async (values: CheckinFormValues) => {
if (!kr) return;
setIsSubmitting(true);
try {
const res = await fetchApi('/api/checkins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kr_id: kr.id,
project_id: kr.project_id,
user_id: currentUser?.id,
current_value: values.current_value,
confidence: values.confidence,
reason_code: values.reason_code || null,
note: values.note || null,
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || '打卡失败');
}
toast.success('打卡成功');
setIsCheckinOpen(false);
fetchData();
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsSubmitting(false);
}
};
const toggleNeedHelp = async () => {
if (!kr) return;
try {
const res = await fetchApi(`/api/krs/${kr.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
need_owner_help: !kr.need_owner_help,
skipReasonCodeCheck: true,
}),
});
if (res.ok) {
toast.success(kr.need_owner_help ? '已取消求助' : '已发送求助请求');
fetchData();
}
} catch (error) {
toast.error('操作失败');
}
};
const handleDeleteKR = async () => {
if (!kr) return;
setIsDeleting(true);
try {
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}`);
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsDeleting(false);
}
};
const handleEditKR = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!kr) return;
setIsEditing(true);
const formData = new FormData(e.currentTarget);
try {
const res = await fetchApi(`/api/krs/${kr.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: formData.get('title'),
metric_name: formData.get('metric_name'),
unit: formData.get('unit'),
target_value: parseFloat(formData.get('target_value') as string),
period_start: formData.get('period_start'),
period_end: formData.get('period_end'),
}),
});
if (!res.ok) throw new Error('更新失败');
toast.success('KR 已更新');
setIsEditDialogOpen(false);
fetchData();
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsEditing(false);
}
};
if (isLoading) {
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-6 bg-slate-200 rounded w-24" />
<div className="h-10 bg-slate-200 rounded w-64" />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 h-96 bg-slate-200 rounded-lg" />
<div className="h-64 bg-slate-200 rounded-lg" />
</div>
</div>
</div>
);
}
if (!kr) {
return (
<div className="max-w-4xl mx-auto px-4 py-8 text-center">
<p className="text-muted-foreground">{loadError || 'KR 不存在'}</p>
<Button asChild className="mt-4" variant="outline">
<Link href="/my-krs">返回我的 KR</Link>
</Button>
</div>
);
}
const progress = calculateProgress(kr);
const expectedProgress = calculateExpectedProgress(kr);
const daysLeft = getDaysRemaining(kr.period_end);
const isActive = isKRActive(kr);
const isOwner = currentUser?.id === kr.owner_user_id;
const TrendIcon = kr.direction === 'increase' ? TrendingUp : TrendingDown;
const handleTaskStatusChange = async (taskId: string, newStatus: string) => {
try {
const res = await fetchApi(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
});
if (res.ok) {
setTasks(tasks.map(t => t.id === taskId ? { ...t, status: newStatus as any } : t));
toast.success('计划状态已更新');
}
} catch (error) {
toast.error('更新失败');
}
};
const isBooleanMetric = kr?.unit === '状态' && kr?.metric_name === '完成状态';
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="mb-6">
<Link
href="/my-krs"
className="inline-flex items-center text-sm text-muted-foreground hover:text-slate-800 mb-4"
>
<ArrowLeft className="h-4 w-4 mr-1" />
返回我的 KR
</Link>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-2xl font-bold text-slate-800">{kr.title}</h1>
<StatusBadge status={kr.status} />
{kr.need_owner_help && (
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded flex items-center gap-1">
<HandHelping className="h-3 w-3" />
需要帮助
</span>
)}
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<Link href={`/projects/${kr.project_id}`} className="hover:text-slate-800">
{kr.project?.name}
</Link>
<span>|</span>
<span className="flex items-center gap-1">
<UserIcon className="h-4 w-4" />
{kr.owner?.name || '未分配'}
</span>
<span>|</span>
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{formatDateRange(kr.period_start, kr.period_end)}
</span>
</div>
</div>
{isActive && (
<div className="flex gap-2">
{currentUser?.role === 'owner' && (
<>
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Pencil className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>编辑 KR</DialogTitle>
</DialogHeader>
<form onSubmit={handleEditKR} className="space-y-4">
<div>
<Label htmlFor="kr-title">KR 标题</Label>
<Input id="kr-title" name="title" defaultValue={kr.title} required />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="kr-metric">指标名称</Label>
<Input id="kr-metric" name="metric_name" defaultValue={kr.metric_name} required />
</div>
<div>
<Label htmlFor="kr-unit">单位</Label>
<Input id="kr-unit" name="unit" defaultValue={kr.unit} required />
</div>
</div>
<div>
<Label htmlFor="kr-target">目标值</Label>
<Input type="number" step="any" id="kr-target" name="target_value" defaultValue={kr.target_value} required />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="kr-start">开始日期</Label>
<Input type="date" id="kr-start" name="period_start" defaultValue={kr.period_start} required />
</div>
<div>
<Label htmlFor="kr-end">结束日期</Label>
<Input type="date" id="kr-end" name="period_end" defaultValue={kr.period_end} required />
</div>
</div>
<Button type="submit" disabled={isEditing} className="w-full">
{isEditing && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
保存修改
</Button>
</form>
</DialogContent>
</Dialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="icon" className="text-rose-500 hover:text-rose-600">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确定要删除此 KR 吗?</AlertDialogTitle>
<AlertDialogDescription>
此操作无法撤销。这将永久删除 KR 及其所有打卡记录。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteKR}
className="bg-rose-600 hover:bg-rose-700"
disabled={isDeleting}
>
{isDeleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)}
<Dialog open={isCheckinOpen} onOpenChange={setIsCheckinOpen}>
<DialogTrigger asChild>
<Button className="bg-teal-600 hover:bg-teal-700">
<Plus className="h-4 w-4 mr-2" />
打卡更新
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>KR 进度打卡</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onCheckinSubmit)} className="space-y-4">
<FormField
control={form.control}
name="current_value"
render={({ field }) => (
<FormItem>
<FormLabel>当前状态</FormLabel>
<FormControl>
{isBooleanMetric ? (
<RadioGroup
onValueChange={(val) => field.onChange(parseInt(val))}
defaultValue={field.value.toString()}
className="flex flex-col space-y-1"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="0" id="r1" />
<Label htmlFor="r1">待完成</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="1" id="r2" />
<Label htmlFor="r2">已完成</Label>
</div>
</RadioGroup>
) : (
<>
<Input
type="number"
step="any"
{...field}
onChange={e => field.onChange(parseFloat(e.target.value) || 0)}
/>
<p className="text-xs text-muted-foreground">
目标值: {kr.target_value} {kr.unit}
</p>
</>
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confidence"
render={({ field }) => (
<FormItem>
<FormLabel>信心度 ({field.value}%)</FormLabel>
<FormControl>
<Input
type="range"
min="0"
max="100"
{...field}
onChange={e => field.onChange(parseInt(e.target.value))}
className="w-full"
/>
</FormControl>
<div className="flex justify-between text-xs text-muted-foreground">
<span>0%</span>
<span>50%</span>
<span>100%</span>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="reason_code"
render={({ field }) => (
<FormItem>
<FormLabel>问题原因 (如有)</FormLabel>
<Select
onValueChange={(value) => field.onChange(value === "unspecified" ? "" : value)}
value={field.value || "unspecified"}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择原因类型" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="unspecified">无问题</SelectItem>
{Object.entries(REASON_CODES).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="note"
render={({ field }) => (
<FormItem>
<FormLabel>备注</FormLabel>
<FormControl>
<Textarea placeholder="补充说明" rows={2} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
提交打卡
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-6">
<ProgressRing progress={progress} status={kr.status} size={100} strokeWidth={8} />
<div>
<p className="text-3xl font-bold text-slate-800">
{isBooleanMetric
? (kr.current_value >= 1 ? '已完成' : '待完成')
: `${kr.current_value} / ${kr.target_value}`
}
</p>
{!isBooleanMetric && <p className="text-muted-foreground">{kr.unit}</p>}
<div className="flex items-center gap-2 mt-2">
<TrendIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{DIRECTION_LABELS[kr.direction]}
</span>
</div>
</div>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground mb-1">期望进度</p>
<p className="text-2xl font-semibold text-slate-600">
{Math.round(expectedProgress * 100)}%
</p>
{isActive && (
<p className={`text-sm ${daysLeft <= 3 ? 'text-amber-600' : 'text-muted-foreground'}`}>
<Clock className="h-3 w-3 inline mr-1" />
剩余 {daysLeft} 天
</p>
)}
</div>
</div>
{kr.reason_code && kr.status !== 'green' && (
<div className="bg-amber-50 rounded-lg p-4 flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-amber-500 mt-0.5" />
<div>
<p className="font-medium text-amber-800">{REASON_CODES[kr.reason_code]}</p>
{kr.note && <p className="text-sm text-amber-700 mt-1">{kr.note}</p>}
</div>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<CheckSquare className="h-5 w-5 text-indigo-600" />
执行计划 ({tasks.length})
</CardTitle>
{isActive && (
<Button
size="sm"
variant="outline"
className="text-indigo-600 border-indigo-200 hover:bg-indigo-50"
onClick={() => {
setEditingTask(null);
setIsTaskDialogOpen(true);
}}
>
<Plus className="h-4 w-4 mr-1" />
新建计划
</Button>
)}
</CardHeader>
<CardContent>
{tasks.length === 0 ? (
<div className="text-center py-8 border border-dashed rounded-lg bg-slate-50">
<p className="text-muted-foreground text-sm">暂无计划,拆解 KR 为具体执行步骤</p>
</div>
) : (
<div className="space-y-3">
{tasks.map(task => (
<div
key={task.id}
className="group p-3 bg-white border rounded-lg hover:shadow-sm transition-all cursor-pointer"
onClick={() => {
setEditingTask(task);
setIsTaskDialogOpen(true);
}}
>
<div className="flex items-start gap-3">
<div
className="mt-1"
onClick={(e) => {
e.stopPropagation();
handleTaskStatusChange(task.id, task.status === 'done' ? 'todo' : 'done');
}}
>
<div className={`w-5 h-5 rounded border flex items-center justify-center ${task.status === 'done' ? 'bg-indigo-600 border-indigo-600' : 'border-slate-300 hover:border-indigo-500'}`}>
{task.status === 'done' && <CheckSquare className="h-3.5 w-3.5 text-white" />}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<p className={`font-medium text-slate-800 truncate ${task.status === 'done' ? 'line-through text-slate-400' : ''}`}>
{task.title}
</p>
<StatusBadge
status={task.status === 'done' ? 'green' : (task.status === 'in_progress' ? 'yellow' : 'red')}
size="sm"
text={task.status === 'done' ? '已完成' : (task.status === 'in_progress' ? '进行中' : '待办')}
/>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{task.owner_user_id && (
<span className="flex items-center gap-1">
<UserIcon className="h-3 w-3" />
{currentUser?.name}
</span>
)}
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDateRange(task.start_date || '', task.end_date || '')}
</span>
{task.steps.length > 0 && (
<span className="bg-slate-100 px-1.5 rounded">
{task.steps.filter(s => s.is_completed).length}/{task.steps.length} 步骤
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">打卡记录</CardTitle>
</CardHeader>
<CardContent>
{checkins.length === 0 ? (
<p className="text-center text-muted-foreground py-8">暂无打卡记录</p>
) : (
<div className="space-y-4">
{checkins.map((checkin, index) => (
<div key={checkin.id} className="flex items-start gap-4">
<div className="flex flex-col items-center">
<StatusDot status={checkin.status_snapshot} size="md" />
{index < checkins.length - 1 && (
<div className="w-0.5 h-12 bg-slate-200 mt-2" />
)}
</div>
<div className="flex-1 pb-4">
<div className="flex items-center justify-between mb-1">
<span className="font-medium">
{isBooleanMetric
? (checkin.current_value >= 1 ? '已完成' : '待完成')
: `${checkin.current_value} ${kr.unit}`
}
</span>
<span className="text-sm text-muted-foreground">
{new Date(checkin.created_at).toLocaleString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
<div className="text-sm text-muted-foreground">
{checkin.user?.name} | 信心度 {checkin.confidence}%
{checkin.delta_value !== null && checkin.delta_value !== 0 && (
<span className={checkin.delta_value > 0 ? 'text-emerald-600' : 'text-rose-600'}>
{' '}({checkin.delta_value > 0 ? '+' : ''}{checkin.delta_value})
</span>
)}
</div>
{checkin.reason_code && (
<p className="text-sm text-amber-600 mt-1">
{REASON_CODES[checkin.reason_code]}
</p>
)}
{checkin.note && (
<p className="text-sm text-slate-600 mt-1">{checkin.note}</p>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">KR 详情</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-sm text-muted-foreground">周期类型</p>
<p className="font-medium">{PERIOD_TYPES[kr.period_type]}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">指标名称</p>
<p className="font-medium">{kr.metric_name}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">信心度</p>
<p className="font-medium">{kr.confidence}%</p>
</div>
{kr.objective && (
<div>
<p className="text-sm text-muted-foreground">关联目标</p>
<p className="font-medium">{kr.objective.title}</p>
</div>
)}
</CardContent>
</Card>
{isOwner && isActive && (
<Card>
<CardHeader>
<CardTitle className="text-lg">快捷操作</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">请求老板帮助</p>
<p className="text-sm text-muted-foreground">遇到困难时发送求助</p>
</div>
<Switch checked={kr.need_owner_help} onCheckedChange={toggleNeedHelp} />
</div>
</CardContent>
</Card>
)}
</div>
</div>
<TaskEditDialog
open={isTaskDialogOpen}
onOpenChange={setIsTaskDialogOpen}
krId={krId}
projectId={kr.project_id}
projectTitle={kr.project?.name || ''}
task={editingTask}
onSuccess={fetchData}
/>
</div>
);
}