新增 App 建立、資料呈現

This commit is contained in:
2025-08-05 16:13:09 +08:00
parent d0c4adf243
commit 5b407ff29c
51 changed files with 6039 additions and 78 deletions

View File

@@ -77,9 +77,49 @@ const mockNotifications: Notification[] = []
const mockSearchData: SearchResult[] = []
export function AdminLayout({ children, currentPage, onPageChange }: AdminLayoutProps) {
const { user, logout } = useAuth()
const { user, logout, isLoading } = useAuth()
const [sidebarOpen, setSidebarOpen] = useState(true)
// 認證檢查
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-600">...</span>
</div>
</div>
)
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4"></h1>
<p className="text-gray-600 mb-6"></p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
)
}
if (user.role !== 'admin') {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4"></h1>
<p className="text-gray-600 mb-6"></p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
)
}
// Search state
const [searchQuery, setSearchQuery] = useState("")
const [searchResults, setSearchResults] = useState<SearchResult[]>([])

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -84,6 +84,7 @@ const mockApps: any[] = []
export function AppManagement() {
const [apps, setApps] = useState(mockApps)
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [selectedType, setSelectedType] = useState("all")
const [selectedStatus, setSelectedStatus] = useState("all")
@@ -95,6 +96,16 @@ export function AppManagement() {
const [showApprovalDialog, setShowApprovalDialog] = useState(false)
const [approvalAction, setApprovalAction] = useState<"approve" | "reject">("approve")
const [approvalReason, setApprovalReason] = useState("")
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalApps, setTotalApps] = useState(0)
const [stats, setStats] = useState({
published: 0,
pending: 0,
draft: 0,
rejected: 0
})
const itemsPerPage = 10
const [newApp, setNewApp] = useState({
name: "",
type: "文字處理",
@@ -106,15 +117,91 @@ export function AppManagement() {
iconColor: "from-blue-500 to-purple-500",
})
const filteredApps = apps.filter((app) => {
const matchesSearch =
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.creator.toLowerCase().includes(searchTerm.toLowerCase())
const matchesType = selectedType === "all" || app.type === selectedType
const matchesStatus = selectedStatus === "all" || app.status === selectedStatus
// 載入應用程式
useEffect(() => {
const loadApps = async () => {
try {
setLoading(true)
const token = localStorage.getItem('token')
if (!token) {
console.log('未找到 token跳過載入應用程式')
setLoading(false)
return
}
return matchesSearch && matchesType && matchesStatus
})
const params = new URLSearchParams({
page: currentPage.toString(),
limit: itemsPerPage.toString()
})
if (searchTerm) {
params.append('search', searchTerm)
}
if (selectedType !== 'all') {
params.append('type', mapTypeToApiType(selectedType))
}
if (selectedStatus !== 'all') {
params.append('status', selectedStatus)
}
const response = await fetch(`/api/apps?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (!response.ok) {
throw new Error(`載入應用程式失敗: ${response.status}`)
}
const data = await response.json()
console.log('載入的應用程式:', data)
// 轉換 API 資料格式為前端期望的格式
const formattedApps = (data.apps || []).map((app: any) => ({
...app,
creator: app.creator?.name || '未知',
department: app.creator?.department || '未知',
views: app.viewsCount || 0,
likes: app.likesCount || 0,
appUrl: app.demoUrl || '',
type: mapApiTypeToDisplayType(app.type), // 將 API 類型轉換為中文顯示
icon: 'Bot',
iconColor: 'from-blue-500 to-purple-500',
reviews: 0, // API 中沒有評論數,設為 0
createdAt: app.createdAt ? new Date(app.createdAt).toLocaleDateString() : '未知'
}))
console.log('格式化後的應用程式:', formattedApps)
setApps(formattedApps)
// 更新分頁資訊和統計
if (data.pagination) {
setTotalPages(data.pagination.totalPages)
setTotalApps(data.pagination.total)
}
if (data.stats) {
setStats(data.stats)
}
} catch (error) {
console.error('載入應用程式失敗:', error)
} finally {
setLoading(false)
}
}
loadApps()
}, [currentPage, searchTerm, selectedType, selectedStatus])
// 當過濾條件改變時,重置到第一頁
useEffect(() => {
setCurrentPage(1)
}, [searchTerm, selectedType, selectedStatus])
// 使用從 API 返回的應用程式,因為過濾已在服務器端完成
const filteredApps = apps
const handleViewApp = (app: any) => {
setSelectedApp(app)
@@ -169,47 +256,191 @@ export function AppManagement() {
setShowApprovalDialog(true)
}
const confirmApproval = () => {
const confirmApproval = async () => {
if (selectedApp) {
setApps(
apps.map((app) =>
app.id === selectedApp.id
? {
...app,
status: approvalAction === "approve" ? "published" : "rejected",
}
: app,
),
)
setShowApprovalDialog(false)
setSelectedApp(null)
setApprovalReason("")
try {
const token = localStorage.getItem('token')
if (!token) {
throw new Error('未找到認證 token請重新登入')
}
// 準備更新資料
const updateData = {
status: approvalAction === "approve" ? "published" : "rejected"
}
// 如果有備註或原因,可以添加到描述中或創建一個新的欄位
if (approvalReason.trim()) {
// 這裡可以根據需要添加備註欄位
console.log(`${approvalAction === "approve" ? "批准備註" : "拒絕原因"}:`, approvalReason)
}
const response = await fetch(`/api/apps/${selectedApp.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(updateData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || `更新失敗: ${response.status}`)
}
// 更新本地狀態
setApps(
apps.map((app) =>
app.id === selectedApp.id
? {
...app,
status: approvalAction === "approve" ? "published" : "rejected",
}
: app,
),
)
// 顯示成功訊息
alert(`應用程式已${approvalAction === "approve" ? "批准" : "拒絕"}`)
setShowApprovalDialog(false)
setSelectedApp(null)
setApprovalReason("")
} catch (error) {
console.error('更新應用程式狀態失敗:', error)
const errorMessage = error instanceof Error ? error.message : '未知錯誤'
alert(`更新失敗: ${errorMessage}`)
}
}
}
const handleAddApp = () => {
const app = {
id: Date.now().toString(),
...newApp,
status: "pending",
createdAt: new Date().toISOString().split("T")[0],
views: 0,
likes: 0,
rating: 0,
reviews: 0,
const handleAddApp = async () => {
try {
// 準備應用程式資料
const appData = {
name: newApp.name,
description: newApp.description,
type: mapTypeToApiType(newApp.type),
demoUrl: newApp.appUrl || undefined,
version: '1.0.0'
}
console.log('準備提交的應用資料:', appData)
// 調用 API 創建應用程式
const token = localStorage.getItem('token')
console.log('Token:', token ? '存在' : '不存在')
if (!token) {
throw new Error('未找到認證 token請重新登入')
}
const response = await fetch('/api/apps', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(appData)
})
console.log('API 回應狀態:', response.status, response.statusText)
if (!response.ok) {
const errorData = await response.json()
console.error('API 錯誤詳情:', errorData)
throw new Error(errorData.error || `API 錯誤: ${response.status} ${response.statusText}`)
}
const result = await response.json()
console.log('應用程式創建成功:', result)
// 更新本地狀態
const app = {
id: result.appId || Date.now().toString(),
...newApp,
status: result.app?.status || "draft", // 使用 API 返回的狀態
createdAt: new Date().toISOString().split("T")[0],
views: 0,
likes: 0,
rating: 0,
reviews: 0,
}
setApps([...apps, app])
setNewApp({
name: "",
type: "文字處理",
department: "HQBU",
creator: "",
description: "",
appUrl: "",
icon: "Bot",
iconColor: "from-blue-500 to-purple-500",
})
setShowAddApp(false)
} catch (error) {
console.error('創建應用程式失敗:', error)
const errorMessage = error instanceof Error ? error.message : '未知錯誤'
alert(`創建應用程式失敗: ${errorMessage}`)
}
setApps([...apps, app])
setNewApp({
name: "",
type: "文字處理",
department: "HQBU",
creator: "",
description: "",
appUrl: "",
icon: "Bot",
iconColor: "from-blue-500 to-purple-500",
})
setShowAddApp(false)
}
// 將前端類型映射到 API 類型
const mapTypeToApiType = (frontendType: string): string => {
const typeMap: Record<string, string> = {
'文字處理': 'productivity',
'圖像生成': 'ai_model',
'圖像處理': 'ai_model',
'語音辨識': 'ai_model',
'推薦系統': 'ai_model',
'音樂生成': 'ai_model',
'程式開發': 'automation',
'影像處理': 'ai_model',
'對話系統': 'ai_model',
'數據分析': 'data_analysis',
'設計工具': 'productivity',
'語音技術': 'ai_model',
'教育工具': 'educational',
'健康醫療': 'healthcare',
'金融科技': 'finance',
'物聯網': 'iot_device',
'區塊鏈': 'blockchain',
'AR/VR': 'ar_vr',
'機器學習': 'machine_learning',
'電腦視覺': 'computer_vision',
'自然語言處理': 'nlp',
'機器人': 'robotics',
'網路安全': 'cybersecurity',
'雲端服務': 'cloud_service',
'其他': 'other'
}
return typeMap[frontendType] || 'other'
}
// 將 API 類型映射到前端顯示的中文類型
const mapApiTypeToDisplayType = (apiType: string): string => {
const typeMap: Record<string, string> = {
'productivity': '文字處理',
'ai_model': '圖像生成',
'automation': '程式開發',
'data_analysis': '數據分析',
'educational': '教育工具',
'healthcare': '健康醫療',
'finance': '金融科技',
'iot_device': '物聯網',
'blockchain': '區塊鏈',
'ar_vr': 'AR/VR',
'machine_learning': '機器學習',
'computer_vision': '電腦視覺',
'nlp': '自然語言處理',
'robotics': '機器人',
'cybersecurity': '網路安全',
'cloud_service': '雲端服務',
'other': '其他'
}
return typeMap[apiType] || '其他'
}
const handleUpdateApp = () => {
@@ -248,8 +479,29 @@ export function AppManagement() {
const colors = {
: "bg-blue-100 text-blue-800 border-blue-200",
: "bg-purple-100 text-purple-800 border-purple-200",
: "bg-purple-100 text-purple-800 border-purple-200",
: "bg-green-100 text-green-800 border-green-200",
: "bg-orange-100 text-orange-800 border-orange-200",
: "bg-pink-100 text-pink-800 border-pink-200",
: "bg-indigo-100 text-indigo-800 border-indigo-200",
: "bg-purple-100 text-purple-800 border-purple-200",
: "bg-teal-100 text-teal-800 border-teal-200",
: "bg-cyan-100 text-cyan-800 border-cyan-200",
: "bg-blue-100 text-blue-800 border-blue-200",
: "bg-green-100 text-green-800 border-green-200",
: "bg-emerald-100 text-emerald-800 border-emerald-200",
: "bg-red-100 text-red-800 border-red-200",
: "bg-yellow-100 text-yellow-800 border-yellow-200",
: "bg-slate-100 text-slate-800 border-slate-200",
: "bg-violet-100 text-violet-800 border-violet-200",
'AR/VR': "bg-fuchsia-100 text-fuchsia-800 border-fuchsia-200",
: "bg-rose-100 text-rose-800 border-rose-200",
: "bg-purple-100 text-purple-800 border-purple-200",
: "bg-teal-100 text-teal-800 border-teal-200",
: "bg-gray-100 text-gray-800 border-gray-200",
: "bg-red-100 text-red-800 border-red-200",
: "bg-sky-100 text-sky-800 border-sky-200",
: "bg-gray-100 text-gray-800 border-gray-200"
}
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
}
@@ -293,7 +545,7 @@ export function AppManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{apps.length}</p>
<p className="text-2xl font-bold">{totalApps}</p>
</div>
<Bot className="w-8 h-8 text-blue-600" />
</div>
@@ -305,7 +557,7 @@ export function AppManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{apps.filter((a) => a.status === "published").length}</p>
<p className="text-2xl font-bold">{stats.published}</p>
</div>
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
@@ -317,7 +569,7 @@ export function AppManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{apps.filter((a) => a.status === "pending").length}</p>
<p className="text-2xl font-bold">{stats.pending}</p>
</div>
<Clock className="w-8 h-8 text-yellow-600" />
</div>
@@ -347,8 +599,29 @@ export function AppManagement() {
<SelectItem value="all"></SelectItem>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="圖像處理"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
<SelectItem value="健康醫療"></SelectItem>
<SelectItem value="金融科技"></SelectItem>
<SelectItem value="物聯網"></SelectItem>
<SelectItem value="區塊鏈"></SelectItem>
<SelectItem value="AR/VR">AR/VR</SelectItem>
<SelectItem value="機器學習"></SelectItem>
<SelectItem value="電腦視覺"></SelectItem>
<SelectItem value="自然語言處理"></SelectItem>
<SelectItem value="機器人"></SelectItem>
<SelectItem value="網路安全"></SelectItem>
<SelectItem value="雲端服務"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
@@ -372,7 +645,7 @@ export function AppManagement() {
{/* Apps Table */}
<Card>
<CardHeader>
<CardTitle> ({filteredApps.length})</CardTitle>
<CardTitle> ({totalApps})</CardTitle>
<CardDescription> AI </CardDescription>
</CardHeader>
<CardContent>
@@ -390,7 +663,27 @@ export function AppManagement() {
</TableRow>
</TableHeader>
<TableBody>
{filteredApps.map((app) => (
{loading ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8">
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-600">...</span>
</div>
</TableCell>
</TableRow>
) : filteredApps.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8">
<div className="text-gray-500">
<Bot className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p className="text-lg font-medium"></p>
<p className="text-sm"></p>
</div>
</TableCell>
</TableRow>
) : (
filteredApps.map((app) => (
<TableRow key={app.id}>
<TableCell>
<div className="flex items-center space-x-3">
@@ -513,12 +806,60 @@ export function AppManagement() {
</DropdownMenu>
</TableCell>
</TableRow>
))}
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Pagination */}
{totalPages > 1 && (
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
{((currentPage - 1) * itemsPerPage) + 1} {Math.min(currentPage * itemsPerPage, totalApps)} {totalApps}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
>
</Button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = i + 1
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(pageNum)}
className="w-8 h-8"
>
{pageNum}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
</Button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Add App Dialog */}
<Dialog open={showAddApp} onOpenChange={setShowAddApp}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
@@ -559,8 +900,29 @@ export function AppManagement() {
<SelectContent>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="圖像處理"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
<SelectItem value="健康醫療"></SelectItem>
<SelectItem value="金融科技"></SelectItem>
<SelectItem value="物聯網"></SelectItem>
<SelectItem value="區塊鏈"></SelectItem>
<SelectItem value="AR/VR">AR/VR</SelectItem>
<SelectItem value="機器學習"></SelectItem>
<SelectItem value="電腦視覺"></SelectItem>
<SelectItem value="自然語言處理"></SelectItem>
<SelectItem value="機器人"></SelectItem>
<SelectItem value="網路安全"></SelectItem>
<SelectItem value="雲端服務"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -121,33 +121,107 @@ export function AppSubmissionDialog({ open, onOpenChange }: AppSubmissionDialogP
}
const handleSubmit = async () => {
if (!user) {
console.error('用戶未登入')
return
}
setIsSubmitting(true)
// 模擬提交過程
await new Promise((resolve) => setTimeout(resolve, 2000))
try {
// 準備應用程式資料
const appData = {
name: formData.name,
description: formData.description,
type: mapTypeToApiType(formData.type),
demoUrl: formData.appUrl || undefined,
githubUrl: formData.sourceCodeUrl || undefined,
docsUrl: formData.documentation || undefined,
techStack: formData.technicalDetails ? [formData.technicalDetails] : undefined,
tags: formData.features ? [formData.features] : undefined,
version: '1.0.0'
}
setIsSubmitting(false)
setIsSubmitted(true)
// 3秒後關閉對話框
setTimeout(() => {
onOpenChange(false)
setIsSubmitted(false)
setStep(1)
setFormData({
name: "",
type: "文字處理",
description: "",
appUrl: "",
demoFile: null,
sourceCodeUrl: "",
documentation: "",
features: "",
technicalDetails: "",
requestFeatured: false,
agreeTerms: false,
// 調用 API 創建應用程式
const token = localStorage.getItem('token')
const response = await fetch('/api/apps', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(appData)
})
}, 3000)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || '創建應用程式失敗')
}
const result = await response.json()
console.log('應用程式創建成功:', result)
setIsSubmitting(false)
setIsSubmitted(true)
// 3秒後關閉對話框
setTimeout(() => {
onOpenChange(false)
setIsSubmitted(false)
setStep(1)
setFormData({
name: "",
type: "文字處理",
description: "",
appUrl: "",
demoFile: null,
sourceCodeUrl: "",
documentation: "",
features: "",
technicalDetails: "",
requestFeatured: false,
agreeTerms: false,
})
}, 3000)
} catch (error) {
console.error('創建應用程式失敗:', error)
setIsSubmitting(false)
// 這裡可以添加錯誤提示
alert(`創建應用程式失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
}
}
// 將前端類型映射到 API 類型
const mapTypeToApiType = (frontendType: string): string => {
const typeMap: Record<string, string> = {
'文字處理': 'productivity',
'圖像生成': 'ai_model',
'圖像處理': 'ai_model',
'語音辨識': 'ai_model',
'推薦系統': 'ai_model',
'音樂生成': 'ai_model',
'程式開發': 'automation',
'影像處理': 'ai_model',
'對話系統': 'ai_model',
'數據分析': 'data_analysis',
'設計工具': 'productivity',
'語音技術': 'ai_model',
'教育工具': 'educational',
'健康醫療': 'healthcare',
'金融科技': 'finance',
'物聯網': 'iot_device',
'區塊鏈': 'blockchain',
'AR/VR': 'ar_vr',
'機器學習': 'machine_learning',
'電腦視覺': 'computer_vision',
'自然語言處理': 'nlp',
'機器人': 'robotics',
'網路安全': 'cybersecurity',
'雲端服務': 'cloud_service',
'其他': 'other'
}
return typeMap[frontendType] || 'other'
}
const isStep1Valid = formData.name && formData.description && formData.appUrl
@@ -245,9 +319,28 @@ export function AppSubmissionDialog({ open, onOpenChange }: AppSubmissionDialogP
<SelectContent>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="圖像處理"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
<SelectItem value="健康醫療"></SelectItem>
<SelectItem value="金融科技"></SelectItem>
<SelectItem value="物聯網"></SelectItem>
<SelectItem value="區塊鏈"></SelectItem>
<SelectItem value="AR/VR">AR/VR</SelectItem>
<SelectItem value="機器學習"></SelectItem>
<SelectItem value="電腦視覺"></SelectItem>
<SelectItem value="自然語言處理"></SelectItem>
<SelectItem value="機器人"></SelectItem>
<SelectItem value="網路安全"></SelectItem>
<SelectItem value="雲端服務"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>