commit e3e5e89df629c20806dbec637fb2e7c06b4702b4 Author: 李超14pro Date: Sun Jan 25 21:55:29 2026 +0800 fix: force dynamic for meeting page diff --git a/.bolt/config.json b/.bolt/config.json new file mode 100644 index 0000000..f236591 --- /dev/null +++ b/.bolt/config.json @@ -0,0 +1,3 @@ +{ + "template": "nextjs-shadcn" +} diff --git a/.bolt/ignore b/.bolt/ignore new file mode 100644 index 0000000..bbe3a15 --- /dev/null +++ b/.bolt/ignore @@ -0,0 +1,2 @@ +components/ui/* +hooks/use-toast.ts diff --git a/.bolt/prompt b/.bolt/prompt new file mode 100644 index 0000000..88d020b --- /dev/null +++ b/.bolt/prompt @@ -0,0 +1,9 @@ +For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production. + +When using client-side hooks (useState and useEffect) in a component that's being treated as a Server Component by Next.js, always add the "use client" directive at the top of the file. + +Do not write code that will trigger this error: "Warning: Extra attributes from the server: %s%s""class,style" + +By default, this template supports JSX syntax with Tailwind CSS classes, the shadcn/ui library, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them. + +Use icons from lucide-react for logos. diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1913e --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..a48e5d3 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,7 @@ +node_modules +build +dist +.git +.trae +.log +.figma \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..659fa57 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +ok r diff --git a/app/admin/metrics/page.tsx b/app/admin/metrics/page.tsx new file mode 100644 index 0000000..0a86390 --- /dev/null +++ b/app/admin/metrics/page.tsx @@ -0,0 +1,332 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Switch } from '@/components/ui/switch'; +import { useUser } from '@/components/user-context'; +import { DIRECTION_LABELS } from '@/lib/constants'; +import type { Metric } from '@/types/database'; +import { toast } from 'sonner'; +import { Plus, Pencil, Trash2, Gauge, Loader2, TrendingUp, TrendingDown } from 'lucide-react'; +import { getErrorMessage } from '@/lib/error-utils'; + +const metricSchema = z.object({ + name: z.string().min(1, '名称不能为空'), + unit: z.string().min(1, '单位不能为空'), + direction: z.enum(['increase', 'decrease']), + category: z.string().optional(), +}); + +type MetricFormValues = z.infer; + +export default function AdminMetricsPage() { + const { currentUser } = useUser(); + const [metrics, setMetrics] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingMetric, setEditingMetric] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const form = useForm({ + resolver: zodResolver(metricSchema), + defaultValues: { + name: '', + unit: '', + direction: 'increase', + category: '', + }, + }); + + const fetchMetrics = async () => { + try { + const res = await fetch('/api/metrics'); + if (res.ok) { + const data = await res.json(); + setMetrics(data); + } + } catch (error) { + console.error('Failed to fetch metrics:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchMetrics(); + }, []); + + const onSubmit = async (values: MetricFormValues) => { + setIsSubmitting(true); + try { + const url = editingMetric ? `/api/metrics/${editingMetric.id}` : '/api/metrics'; + const method = editingMetric ? 'PATCH' : 'POST'; + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(values), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || '操作失败'); + } + + toast.success(editingMetric ? '指标已更新' : '指标已创建'); + setIsDialogOpen(false); + setEditingMetric(null); + form.reset(); + fetchMetrics(); + } catch (error) { + toast.error(getErrorMessage(error)); + } finally { + setIsSubmitting(false); + } + }; + + const handleEdit = (metric: Metric) => { + setEditingMetric(metric); + form.reset({ + name: metric.name, + unit: metric.unit, + direction: metric.direction, + category: metric.category || '', + }); + setIsDialogOpen(true); + }; + + const handleToggleActive = async (metric: Metric) => { + try { + const res = await fetch(`/api/metrics/${metric.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_active: !metric.is_active }), + }); + + if (!res.ok) throw new Error('操作失败'); + + toast.success(metric.is_active ? '已停用' : '已启用'); + fetchMetrics(); + } catch (error) { + toast.error(getErrorMessage(error)); + } + }; + + const handleDelete = async (metricId: string) => { + if (!confirm('确定要删除此指标吗?')) return; + + try { + const res = await fetch(`/api/metrics/${metricId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('删除失败'); + + toast.success('指标已删除'); + fetchMetrics(); + } catch (error) { + toast.error(getErrorMessage(error)); + } + }; + + const handleDialogClose = () => { + setIsDialogOpen(false); + setEditingMetric(null); + form.reset(); + }; + + if (currentUser?.role !== 'owner') { + return ( +
+

仅老板可访问此页面

+
+ ); + } + + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + const categories = Array.from(new Set(metrics.map(m => m.category).filter(Boolean))); + + return ( +
+
+
+

+ + 指标库管理 +

+

管理可复用的 KR 指标

+
+ + + + + + + + {editingMetric ? '编辑指标' : '新建指标'} + +
+ + ( + + 指标名称 * + + + + + + )} + /> + + ( + + 单位 * + + + + + + )} + /> + + ( + + 方向 + + + + )} + /> + + ( + + 分类 + + + + + + )} + /> + +
+ + +
+ + +
+
+
+ + + + + + + 指标名称 + 单位 + 方向 + 分类 + 状态 + 操作 + + + + {metrics.map(metric => ( + + {metric.name} + {metric.unit} + +
+ {metric.direction === 'increase' ? ( + + ) : ( + + )} + {DIRECTION_LABELS[metric.direction]} +
+
+ + {metric.category || '-'} + + + handleToggleActive(metric)} + /> + + + + + +
+ ))} + + {metrics.length === 0 && ( + + + 暂无指标 + + + )} +
+
+
+
+
+ ); +} diff --git a/app/admin/templates/page.tsx b/app/admin/templates/page.tsx new file mode 100644 index 0000000..80ee337 --- /dev/null +++ b/app/admin/templates/page.tsx @@ -0,0 +1,436 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useForm, useFieldArray } 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Switch } from '@/components/ui/switch'; +import { useUser } from '@/components/user-context'; +import { DIRECTION_LABELS } from '@/lib/constants'; +import type { Template } from '@/types/database'; +import { toast } from 'sonner'; +import { Plus, Pencil, Trash2, Files, Loader2, X } from 'lucide-react'; +import { getErrorMessage } from '@/lib/error-utils'; + +const templateItemSchema = z.object({ + title: z.string().min(1, '标题不能为空'), + metricName: z.string().min(1, '指标名称不能为空'), + unit: z.string().min(1, '单位不能为空'), + direction: z.enum(['increase', 'decrease']), + targetValue: z.number().positive('目标值必须大于0'), +}); + +const templateSchema = z.object({ + name: z.string().min(1, '名称不能为空'), + description: z.string().optional(), + project_type: z.string().optional(), + items: z.array(templateItemSchema).min(1, '至少需要一个模板项'), +}); + +type TemplateFormValues = z.infer; + +export default function AdminTemplatesPage() { + // Force re-compile + const { currentUser } = useUser(); + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingTemplate, setEditingTemplate] = useState