新增評分項目設定、資料庫整合
This commit is contained in:
166
app/api/criteria-templates/[id]/route.ts
Normal file
166
app/api/criteria-templates/[id]/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { CriteriaTemplateService, CriteriaItemService } from '@/lib/services/database';
|
||||
|
||||
// GET - 獲取特定評分標準模板
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const templateId = parseInt(params.id);
|
||||
|
||||
if (isNaN(templateId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無效的模板 ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const template = await CriteriaTemplateService.findWithItems(templateId);
|
||||
|
||||
if (!template) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '找不到指定的評分標準模板' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('獲取評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - 更新評分標準模板
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const templateId = parseInt(params.id);
|
||||
|
||||
if (isNaN(templateId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無效的模板 ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { name, description, items } = body;
|
||||
|
||||
if (!name || !items || !Array.isArray(items)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '請提供模板名稱和評分項目' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證權重總和
|
||||
const totalWeight = items.reduce((sum: number, item: any) => sum + (item.weight || 0), 0);
|
||||
if (Math.abs(totalWeight - 100) > 0.01) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '權重總和必須等於 100%' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證所有項目都有名稱
|
||||
if (items.some((item: any) => !item.name?.trim())) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '所有評分項目都必須有名稱' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 使用事務更新模板和項目
|
||||
const { transaction } = await import('@/lib/database');
|
||||
|
||||
await transaction(async (connection) => {
|
||||
// 更新模板
|
||||
await connection.execute(
|
||||
'UPDATE criteria_templates SET name = ?, description = ?, total_weight = ?, updated_at = NOW() WHERE id = ?',
|
||||
[name, description || '', totalWeight, templateId]
|
||||
);
|
||||
|
||||
// 刪除舊的評分項目
|
||||
await connection.execute(
|
||||
'DELETE FROM criteria_items WHERE template_id = ?',
|
||||
[templateId]
|
||||
);
|
||||
|
||||
// 創建新的評分項目
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
await connection.execute(
|
||||
'INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
templateId,
|
||||
item.name,
|
||||
item.description || '',
|
||||
item.weight,
|
||||
item.maxScore || 10,
|
||||
i + 1
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分標準模板更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '更新評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - 刪除評分標準模板
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const templateId = parseInt(params.id);
|
||||
|
||||
if (isNaN(templateId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無效的模板 ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 檢查是否為預設模板
|
||||
const template = await CriteriaTemplateService.findById(templateId);
|
||||
if (template?.is_default) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '無法刪除預設模板' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await CriteriaTemplateService.delete(templateId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '評分標準模板刪除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刪除評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '刪除評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
29
app/api/criteria-templates/default/route.ts
Normal file
29
app/api/criteria-templates/default/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { CriteriaTemplateService } from '@/lib/services/database';
|
||||
|
||||
// GET - 獲取預設評分標準模板
|
||||
export async function GET() {
|
||||
try {
|
||||
const template = await CriteriaTemplateService.findDefault();
|
||||
|
||||
if (!template) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '找不到預設評分標準模板' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const templateWithItems = await CriteriaTemplateService.findWithItems(template.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: templateWithItems
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('獲取預設評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取預設評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
126
app/api/criteria-templates/route.ts
Normal file
126
app/api/criteria-templates/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { CriteriaTemplateService, CriteriaItemService } from '@/lib/services/database';
|
||||
|
||||
// GET - 獲取所有評分標準模板
|
||||
export async function GET() {
|
||||
try {
|
||||
// 這裡暫時使用 user_id = 1,實際應用中應該從 session 獲取
|
||||
const templates = await CriteriaTemplateService.findByUserId(1);
|
||||
|
||||
// 獲取每個模板的評分項目
|
||||
const templatesWithItems = await Promise.all(
|
||||
templates.map(async (template) => {
|
||||
const items = await CriteriaItemService.findByTemplateId(template.id);
|
||||
return { ...template, items };
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: templatesWithItems
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('獲取評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '獲取評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - 創建或更新評分標準模板(單一模板模式)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, description, items } = body;
|
||||
|
||||
if (!name || !items || !Array.isArray(items)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '請提供模板名稱和評分項目' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證權重總和
|
||||
const totalWeight = items.reduce((sum: number, item: any) => sum + (Number(item.weight) || 0), 0);
|
||||
if (Math.abs(totalWeight - 100) > 0.01) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '權重總和必須等於 100%' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證所有項目都有名稱
|
||||
if (items.some((item: any) => !item.name?.trim())) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '所有評分項目都必須有名稱' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 使用事務創建或更新模板
|
||||
const { transaction } = await import('@/lib/database');
|
||||
|
||||
const result = await transaction(async (connection) => {
|
||||
// 檢查是否已存在模板
|
||||
const [existingTemplates] = await connection.execute(
|
||||
'SELECT id FROM criteria_templates WHERE user_id = ? LIMIT 1',
|
||||
[1]
|
||||
);
|
||||
|
||||
let templateId;
|
||||
|
||||
if (existingTemplates.length > 0) {
|
||||
// 更新現有模板
|
||||
templateId = existingTemplates[0].id;
|
||||
await connection.execute(
|
||||
'UPDATE criteria_templates SET name = ?, description = ?, total_weight = ?, updated_at = NOW() WHERE id = ?',
|
||||
[name, description || '', totalWeight, templateId]
|
||||
);
|
||||
|
||||
// 刪除舊的評分項目
|
||||
await connection.execute(
|
||||
'DELETE FROM criteria_items WHERE template_id = ?',
|
||||
[templateId]
|
||||
);
|
||||
} else {
|
||||
// 創建新模板
|
||||
const [templateResult] = await connection.execute(
|
||||
'INSERT INTO criteria_templates (user_id, name, description, is_default, is_public, total_weight) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[1, name, description || '', 1, 1, totalWeight]
|
||||
);
|
||||
templateId = (templateResult as any).insertId;
|
||||
}
|
||||
|
||||
// 創建評分項目
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
await connection.execute(
|
||||
'INSERT INTO criteria_items (template_id, name, description, weight, max_score, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
templateId,
|
||||
item.name,
|
||||
item.description || '',
|
||||
Number(item.weight) || 0,
|
||||
Number(item.maxScore) || 10,
|
||||
i + 1
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return templateId;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { id: result },
|
||||
message: '評分標準模板儲存成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('儲存評分標準模板失敗:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '儲存評分標準模板失敗' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Sidebar } from "@/components/sidebar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -9,60 +9,93 @@ import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Plus, Trash2, Save, RotateCcw, FileText } from "lucide-react"
|
||||
import { Plus, Trash2, Save, RotateCcw, FileText, Loader2 } from "lucide-react"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
|
||||
interface CriteriaItem {
|
||||
id: string
|
||||
id: number | string
|
||||
name: string
|
||||
description: string
|
||||
weight: number
|
||||
maxScore: number
|
||||
}
|
||||
|
||||
const defaultCriteria: CriteriaItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "內容品質",
|
||||
description: "內容的準確性、完整性和專業度",
|
||||
weight: 25,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "視覺設計",
|
||||
description: "版面設計、色彩搭配和視覺效果",
|
||||
weight: 20,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "邏輯結構",
|
||||
description: "內容組織的邏輯性和條理性",
|
||||
weight: 20,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "創新性",
|
||||
description: "創意思維和獨特觀點的展現",
|
||||
weight: 15,
|
||||
maxScore: 10,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "實用性",
|
||||
description: "內容的實際應用價值和可操作性",
|
||||
weight: 20,
|
||||
maxScore: 10,
|
||||
},
|
||||
]
|
||||
interface CriteriaTemplate {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
total_weight: number
|
||||
items: CriteriaItem[]
|
||||
}
|
||||
|
||||
export default function CriteriaPage() {
|
||||
const [criteria, setCriteria] = useState<CriteriaItem[]>(defaultCriteria)
|
||||
const [templateName, setTemplateName] = useState("預設評分標準")
|
||||
const [criteria, setCriteria] = useState<CriteriaItem[]>([])
|
||||
const [templateName, setTemplateName] = useState("")
|
||||
const [templateDescription, setTemplateDescription] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [currentTemplate, setCurrentTemplate] = useState<CriteriaTemplate | null>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
// 載入評分標準
|
||||
useEffect(() => {
|
||||
loadTemplate()
|
||||
}, [])
|
||||
|
||||
const loadTemplate = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await fetch('/api/criteria-templates')
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
// 載入第一個模板(單一模板模式)
|
||||
const template = result.data[0]
|
||||
setCurrentTemplate(template)
|
||||
setTemplateName(template.name)
|
||||
setTemplateDescription(template.description || '')
|
||||
setCriteria(template.items || [])
|
||||
} else {
|
||||
// 如果沒有模板,載入預設模板
|
||||
await loadDefaultTemplate()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入評分標準失敗:', error)
|
||||
// 如果載入失敗,嘗試載入預設模板
|
||||
await loadDefaultTemplate()
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadDefaultTemplate = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/criteria-templates/default')
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && result.data) {
|
||||
const template = result.data
|
||||
setCurrentTemplate(template)
|
||||
setTemplateName(template.name)
|
||||
setTemplateDescription(template.description || '')
|
||||
setCriteria(template.items || [])
|
||||
} else {
|
||||
// 如果連預設模板都沒有,使用空模板
|
||||
setCurrentTemplate(null)
|
||||
setTemplateName('')
|
||||
setTemplateDescription('')
|
||||
setCriteria([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入預設評分標準失敗:', error)
|
||||
// 使用空模板
|
||||
setCurrentTemplate(null)
|
||||
setTemplateName('')
|
||||
setTemplateDescription('')
|
||||
setCriteria([])
|
||||
}
|
||||
}
|
||||
|
||||
const addCriteria = () => {
|
||||
const newCriteria: CriteriaItem = {
|
||||
id: Date.now().toString(),
|
||||
@@ -83,12 +116,12 @@ export default function CriteriaPage() {
|
||||
}
|
||||
|
||||
const updateWeight = (id: string, weight: number[]) => {
|
||||
updateCriteria(id, "weight", weight[0])
|
||||
updateCriteria(id, "weight", Number(weight[0]) || 0)
|
||||
}
|
||||
|
||||
const totalWeight = criteria.reduce((sum, item) => sum + item.weight, 0)
|
||||
const totalWeight = criteria.reduce((sum, item) => sum + (Number(item.weight) || 0), 0)
|
||||
|
||||
const saveCriteria = () => {
|
||||
const saveCriteria = async () => {
|
||||
if (totalWeight !== 100) {
|
||||
toast({
|
||||
title: "權重設定錯誤",
|
||||
@@ -107,20 +140,98 @@ export default function CriteriaPage() {
|
||||
return
|
||||
}
|
||||
|
||||
// 這裡會連接到後端 API 儲存評分標準
|
||||
toast({
|
||||
title: "儲存成功",
|
||||
description: "評分標準已成功儲存",
|
||||
})
|
||||
if (!templateName.trim()) {
|
||||
toast({
|
||||
title: "設定不完整",
|
||||
description: "請填寫模板名稱",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true)
|
||||
|
||||
const templateData = {
|
||||
name: templateName.trim(),
|
||||
description: templateDescription.trim(),
|
||||
items: criteria.map(item => ({
|
||||
name: item.name.trim(),
|
||||
description: item.description.trim(),
|
||||
weight: item.weight,
|
||||
maxScore: item.maxScore
|
||||
}))
|
||||
}
|
||||
|
||||
// 單一模板模式,總是使用 POST 進行覆蓋
|
||||
const url = '/api/criteria-templates'
|
||||
const method = 'POST'
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(templateData),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "儲存成功",
|
||||
description: "評分標準已成功儲存",
|
||||
})
|
||||
|
||||
// 重新載入資料
|
||||
await loadTemplate()
|
||||
} else {
|
||||
throw new Error(result.error || '儲存失敗')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('儲存評分標準失敗:', error)
|
||||
toast({
|
||||
title: "儲存失敗",
|
||||
description: error instanceof Error ? error.message : "無法儲存評分標準",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
setCriteria(defaultCriteria)
|
||||
setTemplateName("預設評分標準")
|
||||
toast({
|
||||
title: "已重置",
|
||||
description: "評分標準已重置為預設值",
|
||||
})
|
||||
const resetToDefault = async () => {
|
||||
try {
|
||||
await loadDefaultTemplate()
|
||||
toast({
|
||||
title: "已重置",
|
||||
description: "評分標準已重置為預設值",
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "重置失敗",
|
||||
description: "無法重置評分標準",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Sidebar />
|
||||
<main className="md:ml-64 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">載入評分標準中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -140,16 +251,29 @@ export default function CriteriaPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
標準模板名稱
|
||||
標準模板資訊
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
placeholder="輸入評分標準名稱"
|
||||
className="max-w-md"
|
||||
/>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="template-name" className="mb-2 block">模板名稱 *</Label>
|
||||
<Input
|
||||
id="template-name"
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
placeholder="輸入評分標準名稱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="template-description" className="mb-2 block">模板描述</Label>
|
||||
<Textarea
|
||||
id="template-description"
|
||||
value={templateDescription}
|
||||
onChange={(e) => setTemplateDescription(e.target.value)}
|
||||
placeholder="輸入模板描述(選填)"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -158,7 +282,9 @@ export default function CriteriaPage() {
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm font-medium">權重總計</span>
|
||||
<Badge variant={totalWeight === 100 ? "default" : "destructive"}>{totalWeight}%</Badge>
|
||||
<Badge variant={totalWeight === 100 ? "default" : "destructive"}>
|
||||
{Number(totalWeight).toFixed(1)}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
@@ -190,7 +316,7 @@ export default function CriteriaPage() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor={`name-${item.id}`}>項目名稱</Label>
|
||||
<Label htmlFor={`name-${item.id}`} className="mb-2 block">項目名稱</Label>
|
||||
<Input
|
||||
id={`name-${item.id}`}
|
||||
value={item.name}
|
||||
@@ -199,7 +325,7 @@ export default function CriteriaPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`maxScore-${item.id}`}>滿分</Label>
|
||||
<Label htmlFor={`maxScore-${item.id}`} className="mb-2 block">滿分</Label>
|
||||
<Input
|
||||
id={`maxScore-${item.id}`}
|
||||
type="number"
|
||||
@@ -212,7 +338,7 @@ export default function CriteriaPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor={`description-${item.id}`}>評分說明</Label>
|
||||
<Label htmlFor={`description-${item.id}`} className="mb-2 block">評分說明</Label>
|
||||
<Textarea
|
||||
id={`description-${item.id}`}
|
||||
value={item.description}
|
||||
@@ -225,10 +351,10 @@ export default function CriteriaPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>權重比例</Label>
|
||||
<Badge variant="outline">{item.weight}%</Badge>
|
||||
<Badge variant="outline">{Number(item.weight).toFixed(1)}%</Badge>
|
||||
</div>
|
||||
<Slider
|
||||
value={[item.weight]}
|
||||
value={[Number(item.weight) || 0]}
|
||||
onValueChange={(value) => updateWeight(item.id, value)}
|
||||
max={100}
|
||||
min={0}
|
||||
@@ -253,11 +379,20 @@ export default function CriteriaPage() {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button onClick={saveCriteria} className="flex-1">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
儲存評分標準
|
||||
<Button onClick={saveCriteria} disabled={isSaving || isLoading} className="flex-1">
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
儲存中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
儲存評分標準
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={resetToDefault} variant="outline">
|
||||
<Button onClick={resetToDefault} variant="outline" disabled={isLoading || isSaving}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
重置為預設
|
||||
</Button>
|
||||
@@ -278,7 +413,7 @@ export default function CriteriaPage() {
|
||||
{item.description && <div className="text-sm text-muted-foreground mt-1">{item.description}</div>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium">權重: {item.weight}%</div>
|
||||
<div className="font-medium">權重: {Number(item.weight).toFixed(1)}%</div>
|
||||
<div className="text-sm text-muted-foreground">滿分: {item.maxScore}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user