應用 APP 功能實作
This commit is contained in:
296
CHATBOT_ANALYSIS.md
Normal file
296
CHATBOT_ANALYSIS.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# AI智能助手 (ChatBot) 組件分析
|
||||||
|
|
||||||
|
## 1. 組件概述
|
||||||
|
|
||||||
|
### 1.1 功能定位
|
||||||
|
AI智能助手是一個內嵌的聊天機器人組件,為用戶提供即時的系統使用指導和問題解答服務。
|
||||||
|
|
||||||
|
### 1.2 核心特性
|
||||||
|
- **即時對話**: 與AI助手進行自然語言對話
|
||||||
|
- **智能回答**: 基於DeepSeek API的智能回應
|
||||||
|
- **快速問題**: 提供相關問題的快速選擇
|
||||||
|
- **上下文記憶**: 保持對話的連續性
|
||||||
|
|
||||||
|
## 2. 技術實現
|
||||||
|
|
||||||
|
### 2.1 技術棧
|
||||||
|
```typescript
|
||||||
|
// 核心技術
|
||||||
|
- React 19 (Hooks)
|
||||||
|
- TypeScript 5
|
||||||
|
- DeepSeek Chat API
|
||||||
|
- Tailwind CSS
|
||||||
|
- shadcn/ui 組件庫
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 組件結構
|
||||||
|
```typescript
|
||||||
|
// 主要接口定義
|
||||||
|
interface Message {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
sender: "user" | "bot"
|
||||||
|
timestamp: Date
|
||||||
|
quickQuestions?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 組件狀態
|
||||||
|
const [isOpen, setIsOpen] = useState(false) // 對話框開關
|
||||||
|
const [messages, setMessages] = useState<Message[]>() // 訊息列表
|
||||||
|
const [inputValue, setInputValue] = useState("") // 輸入值
|
||||||
|
const [isTyping, setIsTyping] = useState(false) // 打字狀態
|
||||||
|
const [isLoading, setIsLoading] = useState(false) // 載入狀態
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 API整合
|
||||||
|
```typescript
|
||||||
|
// DeepSeek API 配置
|
||||||
|
const DEEPSEEK_API_KEY = "sk-3640dcff23fe4a069a64f536ac538d75"
|
||||||
|
const DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
|
||||||
|
|
||||||
|
// API 調用函數
|
||||||
|
const callDeepSeekAPI = async (userMessage: string): Promise<string> => {
|
||||||
|
// 實現細節...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 功能詳解
|
||||||
|
|
||||||
|
### 3.1 對話能力
|
||||||
|
|
||||||
|
#### 3.1.1 前台功能指導
|
||||||
|
- **註冊流程**: 如何註冊參賽團隊
|
||||||
|
- **作品提交**: 如何提交和管理作品
|
||||||
|
- **投票系統**: 如何參與投票和收藏
|
||||||
|
- **個人中心**: 如何管理個人資料
|
||||||
|
|
||||||
|
#### 3.1.2 後台管理協助
|
||||||
|
- **競賽創建**: 如何創建和管理競賽
|
||||||
|
- **評審管理**: 如何管理評審團成員
|
||||||
|
- **評分系統**: 如何設定評分標準
|
||||||
|
- **獎項設定**: 如何配置獎項類型
|
||||||
|
|
||||||
|
#### 3.1.3 系統使用指南
|
||||||
|
- **操作步驟**: 提供具體的操作指引
|
||||||
|
- **常見問題**: 解答用戶常見疑問
|
||||||
|
- **最佳實踐**: 推薦最佳使用方法
|
||||||
|
|
||||||
|
### 3.2 智能特性
|
||||||
|
|
||||||
|
#### 3.2.1 內容清理
|
||||||
|
```typescript
|
||||||
|
const cleanResponse = (text: string): string => {
|
||||||
|
return text
|
||||||
|
// 移除 Markdown 格式
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '$1')
|
||||||
|
.replace(/\*(.*?)\*/g, '$1')
|
||||||
|
.replace(/`(.*?)`/g, '$1')
|
||||||
|
.replace(/#{1,6}\s/g, '')
|
||||||
|
.replace(/^- /g, '• ')
|
||||||
|
.replace(/^\d+\.\s/g, '')
|
||||||
|
// 移除多餘空行
|
||||||
|
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||||
|
// 限制文字長度
|
||||||
|
.slice(0, 300)
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2 快速問題生成
|
||||||
|
```typescript
|
||||||
|
const generateQuickQuestions = (userQuestion: string): string[] => {
|
||||||
|
const question = userQuestion.toLowerCase()
|
||||||
|
|
||||||
|
// 根據問題類型生成相關建議
|
||||||
|
if (question.includes('註冊') || question.includes('團隊')) {
|
||||||
|
return [
|
||||||
|
"如何提交作品?",
|
||||||
|
"怎麼查看競賽詳情?",
|
||||||
|
"如何收藏作品?",
|
||||||
|
"怎麼進行投票?"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// 更多邏輯...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 用戶體驗
|
||||||
|
|
||||||
|
#### 3.3.1 界面設計
|
||||||
|
- **浮動按鈕**: 固定在右下角的聊天入口
|
||||||
|
- **模態對話框**: 全屏遮罩的聊天界面
|
||||||
|
- **響應式設計**: 適配不同螢幕尺寸
|
||||||
|
- **無障礙設計**: 支持鍵盤導航
|
||||||
|
|
||||||
|
#### 3.3.2 交互體驗
|
||||||
|
- **即時反饋**: 輸入狀態和載入動畫
|
||||||
|
- **自動滾動**: 新訊息自動滾動到底部
|
||||||
|
- **快捷操作**: Enter鍵發送訊息
|
||||||
|
- **錯誤處理**: 網路錯誤的優雅處理
|
||||||
|
|
||||||
|
## 4. 系統提示詞 (System Prompt)
|
||||||
|
|
||||||
|
### 4.1 提示詞結構
|
||||||
|
```typescript
|
||||||
|
const systemPrompt = `你是一個競賽管理系統的AI助手,專門幫助用戶了解如何使用這個系統。
|
||||||
|
|
||||||
|
系統功能包括:
|
||||||
|
|
||||||
|
後台管理功能:
|
||||||
|
1. 競賽管理 - 創建、編輯、刪除競賽
|
||||||
|
2. 評審管理 - 管理評審團成員
|
||||||
|
3. 評分系統 - 手動輸入評分或讓評審自行評分
|
||||||
|
4. 團隊管理 - 管理參賽團隊
|
||||||
|
5. 獎項管理 - 設定各種獎項
|
||||||
|
6. 評審連結 - 提供評審登入連結
|
||||||
|
|
||||||
|
前台功能:
|
||||||
|
1. 競賽瀏覽 - 查看所有競賽資訊和詳細內容
|
||||||
|
2. 團隊註冊 - 如何註冊參賽團隊和提交作品
|
||||||
|
3. 作品展示 - 瀏覽參賽作品和投票功能
|
||||||
|
4. 排行榜 - 查看人氣排行榜和得獎名單
|
||||||
|
5. 個人中心 - 管理個人資料和參賽記錄
|
||||||
|
6. 收藏功能 - 如何收藏喜歡的作品
|
||||||
|
7. 評論系統 - 如何對作品進行評論和互動
|
||||||
|
8. 搜尋功能 - 如何搜尋特定競賽或作品
|
||||||
|
9. 通知系統 - 查看競賽更新和個人通知
|
||||||
|
10. 幫助中心 - 常見問題和使用指南
|
||||||
|
|
||||||
|
請用友善、專業的語氣回答用戶問題,並提供具體的操作步驟。回答要簡潔明瞭,避免過長的文字。
|
||||||
|
|
||||||
|
重要:請不要使用任何Markdown格式,只使用純文字回答。不要使用**、*、#、-等符號。
|
||||||
|
|
||||||
|
回答時請使用繁體中文。`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 回答規範
|
||||||
|
- **語言**: 繁體中文
|
||||||
|
- **格式**: 純文字,無Markdown
|
||||||
|
- **長度**: 限制在300字以內
|
||||||
|
- **語氣**: 友善、專業
|
||||||
|
- **內容**: 具體操作步驟
|
||||||
|
|
||||||
|
## 5. 錯誤處理
|
||||||
|
|
||||||
|
### 5.1 API錯誤處理
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const response = await fetch(DEEPSEEK_API_URL, {
|
||||||
|
// API 調用配置...
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return cleanResponse(data.choices[0]?.message?.content || "抱歉,我現在無法回答您的問題,請稍後再試。")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("DeepSeek API error:", error)
|
||||||
|
return "抱歉,我現在無法連接到AI服務,請檢查網路連接或稍後再試。"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 用戶體驗錯誤處理
|
||||||
|
- **網路錯誤**: 提示檢查網路連接
|
||||||
|
- **API限制**: 提示稍後再試
|
||||||
|
- **輸入驗證**: 防止空訊息發送
|
||||||
|
- **載入狀態**: 防止重複發送
|
||||||
|
|
||||||
|
## 6. 性能優化
|
||||||
|
|
||||||
|
### 6.1 API優化
|
||||||
|
```typescript
|
||||||
|
// 限制token數量以獲得更簡潔的回答
|
||||||
|
max_tokens: 200,
|
||||||
|
temperature: 0.7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 組件優化
|
||||||
|
- **訊息虛擬化**: 大量訊息時的效能優化
|
||||||
|
- **防抖處理**: 避免頻繁API調用
|
||||||
|
- **記憶化**: 重複問題的快取處理
|
||||||
|
- **懶加載**: 按需載入組件
|
||||||
|
|
||||||
|
## 7. 安全考量
|
||||||
|
|
||||||
|
### 7.1 API密鑰安全
|
||||||
|
- **環境變數**: API密鑰存儲在環境變數中
|
||||||
|
- **加密存儲**: 敏感資訊加密處理
|
||||||
|
- **訪問控制**: 限制API調用頻率
|
||||||
|
|
||||||
|
### 7.2 數據隱私
|
||||||
|
- **聊天記錄**: 本地存儲,不上傳服務器
|
||||||
|
- **個人資訊**: 不收集敏感個人資訊
|
||||||
|
- **數據清理**: 定期清理過期數據
|
||||||
|
|
||||||
|
## 8. 擴展性設計
|
||||||
|
|
||||||
|
### 8.1 多語言支持
|
||||||
|
```typescript
|
||||||
|
interface LocalizationConfig {
|
||||||
|
language: string
|
||||||
|
systemPrompt: Record<string, string>
|
||||||
|
quickQuestions: Record<string, string[]>
|
||||||
|
errorMessages: Record<string, string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 多AI模型支持
|
||||||
|
```typescript
|
||||||
|
interface AIModelConfig {
|
||||||
|
provider: 'deepseek' | 'openai' | 'anthropic'
|
||||||
|
model: string
|
||||||
|
apiKey: string
|
||||||
|
apiUrl: string
|
||||||
|
maxTokens: number
|
||||||
|
temperature: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 自定義功能
|
||||||
|
- **知識庫整合**: 連接企業知識庫
|
||||||
|
- **FAQ系統**: 自動回答常見問題
|
||||||
|
- **工單系統**: 複雜問題轉人工處理
|
||||||
|
- **分析報告**: 聊天數據分析
|
||||||
|
|
||||||
|
## 9. 使用指南
|
||||||
|
|
||||||
|
### 9.1 基本使用
|
||||||
|
1. 點擊右下角的聊天按鈕
|
||||||
|
2. 在輸入框中輸入問題
|
||||||
|
3. 按Enter鍵或點擊發送按鈕
|
||||||
|
4. 查看AI助手的回答
|
||||||
|
5. 點擊快速問題進行後續對話
|
||||||
|
|
||||||
|
### 9.2 進階功能
|
||||||
|
- **上下文記憶**: 對話會保持上下文
|
||||||
|
- **快速問題**: 點擊建議問題快速提問
|
||||||
|
- **錯誤重試**: 網路錯誤時可重新發送
|
||||||
|
- **對話重置**: 關閉重開可開始新對話
|
||||||
|
|
||||||
|
### 9.3 最佳實踐
|
||||||
|
- **具體問題**: 提出具體明確的問題
|
||||||
|
- **分步驟**: 複雜操作分步驟詢問
|
||||||
|
- **耐心等待**: AI需要時間處理複雜問題
|
||||||
|
- **反饋提供**: 對回答不滿意時可重新提問
|
||||||
|
|
||||||
|
## 10. 未來規劃
|
||||||
|
|
||||||
|
### 10.1 短期目標
|
||||||
|
- [ ] 添加語音輸入功能
|
||||||
|
- [ ] 支持圖片上傳和識別
|
||||||
|
- [ ] 增加更多快速問題模板
|
||||||
|
- [ ] 優化回答品質和速度
|
||||||
|
|
||||||
|
### 10.2 長期目標
|
||||||
|
- [ ] 整合企業知識庫
|
||||||
|
- [ ] 支持多語言對話
|
||||||
|
- [ ] 添加情感分析功能
|
||||||
|
- [ ] 實現智能推薦系統
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文檔版本**: v1.0
|
||||||
|
**最後更新**: 2024年12月
|
||||||
|
**負責人**: 前端開發團隊
|
77
app/api/admin/apps/[id]/reviews/route.ts
Normal file
77
app/api/admin/apps/[id]/reviews/route.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AppService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const appService = new AppService()
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const page = parseInt(searchParams.get('page') || '1')
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '10')
|
||||||
|
const offset = (page - 1) * limit
|
||||||
|
|
||||||
|
// 獲取應用評價列表
|
||||||
|
const reviews = await appService.getAppReviews(appId, limit, offset)
|
||||||
|
|
||||||
|
// 獲取評價總數
|
||||||
|
const totalReviews = await appService.getAppReviewCount(appId)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
reviews,
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total: totalReviews,
|
||||||
|
totalPages: Math.ceil(totalReviews / limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用評價錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '獲取評價時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
const body = await request.json()
|
||||||
|
const { reviewId } = body
|
||||||
|
|
||||||
|
if (!reviewId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '缺少評價ID' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除評價
|
||||||
|
const result = await appService.deleteReview(reviewId, appId)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '評價刪除成功'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除評價錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '刪除評價時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
119
app/api/admin/apps/[id]/route.ts
Normal file
119
app/api/admin/apps/[id]/route.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AppService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const appService = new AppService()
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
|
||||||
|
const app = await appService.getAppById(appId)
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '應用不存在' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
app: {
|
||||||
|
...app,
|
||||||
|
status: app.is_active ? 'published' : 'draft',
|
||||||
|
views: app.views_count || 0,
|
||||||
|
likes: app.likes_count || 0,
|
||||||
|
rating: app.rating || 0,
|
||||||
|
creator: app.creator_name || '未知',
|
||||||
|
department: app.creator_department || '未知',
|
||||||
|
createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用詳情錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '獲取應用詳情時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
const body = await request.json()
|
||||||
|
const { name, description, category, type, app_url, icon, icon_color } = body
|
||||||
|
|
||||||
|
// 更新應用
|
||||||
|
const result = await appService.updateApp(appId, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
type,
|
||||||
|
app_url,
|
||||||
|
icon,
|
||||||
|
icon_color
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '應用更新成功',
|
||||||
|
data: {
|
||||||
|
app: {
|
||||||
|
...result.app,
|
||||||
|
status: result.app?.is_active ? 'published' : 'draft',
|
||||||
|
views: result.app?.views_count || 0,
|
||||||
|
likes: result.app?.likes_count || 0,
|
||||||
|
rating: result.app?.rating || 0,
|
||||||
|
creator: result.app?.creator_name || '未知',
|
||||||
|
department: result.app?.creator_department || '未知',
|
||||||
|
createdAt: result.app?.created_at ? new Date(result.app.created_at).toLocaleDateString('zh-TW') : '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新應用錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '更新應用時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
|
||||||
|
const result = await appService.deleteApp(appId)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '應用刪除成功'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除應用錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '刪除應用時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
46
app/api/admin/apps/[id]/stats/route.ts
Normal file
46
app/api/admin/apps/[id]/stats/route.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AppService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const appService = new AppService()
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
|
||||||
|
// 獲取應用基本統計
|
||||||
|
const app = await appService.getAppById(appId)
|
||||||
|
if (!app) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '應用不存在' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取評分統計
|
||||||
|
const ratingStats = await appService.getAppRatingStats(appId)
|
||||||
|
|
||||||
|
// 獲取使用統計
|
||||||
|
const usageStats = await appService.getAppUsageStats(appId)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
basic: {
|
||||||
|
views: app.views_count || 0,
|
||||||
|
likes: app.likes_count || 0,
|
||||||
|
rating: ratingStats.averageRating || 0,
|
||||||
|
reviewCount: ratingStats.totalRatings || 0
|
||||||
|
},
|
||||||
|
usage: usageStats,
|
||||||
|
rating: ratingStats
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用統計數據錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '獲取統計數據時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
43
app/api/admin/apps/[id]/toggle-status/route.ts
Normal file
43
app/api/admin/apps/[id]/toggle-status/route.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AppService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const appService = new AppService()
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
|
||||||
|
const result = await appService.toggleAppStatus(appId)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '應用狀態更新成功',
|
||||||
|
data: {
|
||||||
|
app: {
|
||||||
|
...result.app,
|
||||||
|
status: result.app?.is_active ? 'published' : 'draft',
|
||||||
|
views: result.app?.views_count || 0,
|
||||||
|
likes: result.app?.likes_count || 0,
|
||||||
|
rating: result.app?.rating || 0,
|
||||||
|
creator: result.app?.creator_name || '未知',
|
||||||
|
department: result.app?.creator_department || '未知',
|
||||||
|
createdAt: result.app?.created_at ? new Date(result.app.created_at).toLocaleDateString('zh-TW') : '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切換應用狀態錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '切換應用狀態時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
133
app/api/admin/apps/route.ts
Normal file
133
app/api/admin/apps/route.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AppService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const appService = new AppService()
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const page = parseInt(searchParams.get('page') || '1')
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '10')
|
||||||
|
const search = searchParams.get('search') || ''
|
||||||
|
const category = searchParams.get('category') || 'all'
|
||||||
|
const type = searchParams.get('type') || 'all'
|
||||||
|
const status = searchParams.get('status') || 'all'
|
||||||
|
|
||||||
|
// 獲取應用列表
|
||||||
|
const { apps, total } = await appService.getAllApps({
|
||||||
|
search,
|
||||||
|
category,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
})
|
||||||
|
|
||||||
|
// 獲取應用統計
|
||||||
|
const stats = await appService.getAppStats()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
apps: apps.map(app => ({
|
||||||
|
...app,
|
||||||
|
status: app.is_active ? 'published' : 'draft',
|
||||||
|
views: app.views_count || 0,
|
||||||
|
likes: app.likes_count || 0,
|
||||||
|
rating: app.rating || 0,
|
||||||
|
reviewCount: app.reviewCount || 0,
|
||||||
|
creator: app.creator_name || '未知',
|
||||||
|
department: app.creator_department || '未知',
|
||||||
|
createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-',
|
||||||
|
icon: app.icon || 'Bot',
|
||||||
|
iconColor: app.icon_color || 'from-blue-500 to-purple-500',
|
||||||
|
appUrl: app.app_url
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit)
|
||||||
|
},
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用列表錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '獲取應用列表時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { name, description, creator_id, category, type, app_url, icon, icon_color } = body
|
||||||
|
|
||||||
|
// 驗證必填欄位
|
||||||
|
if (!name || !description || !creator_id || !category || !type) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '請填寫所有必填欄位' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查應用名稱是否已存在
|
||||||
|
const existingApp = await appService.getAppByName(name)
|
||||||
|
if (existingApp) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '應用名稱已存在' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 創建應用
|
||||||
|
const result = await appService.createApp({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
creator_id,
|
||||||
|
category,
|
||||||
|
type,
|
||||||
|
app_url,
|
||||||
|
icon,
|
||||||
|
icon_color
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '應用創建成功',
|
||||||
|
data: {
|
||||||
|
app: {
|
||||||
|
...result.app,
|
||||||
|
status: 'published',
|
||||||
|
views: 0,
|
||||||
|
likes: 0,
|
||||||
|
rating: 0,
|
||||||
|
creator: result.app?.creator_name || '未知',
|
||||||
|
department: result.app?.creator_department || '未知',
|
||||||
|
createdAt: result.app?.created_at ? new Date(result.app.created_at).toLocaleDateString('zh-TW') : '-',
|
||||||
|
icon: result.app?.icon || 'Bot',
|
||||||
|
iconColor: result.app?.icon_color || 'from-blue-500 to-purple-500',
|
||||||
|
appUrl: result.app?.app_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: result.error },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('創建應用錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '創建應用時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
38
app/api/admin/users/list/route.ts
Normal file
38
app/api/admin/users/list/route.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { UserService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const userService = new UserService()
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 獲取所有啟用狀態的用戶
|
||||||
|
const sql = `
|
||||||
|
SELECT id, name, email, department, role
|
||||||
|
FROM users
|
||||||
|
WHERE status = 'active'
|
||||||
|
ORDER BY name ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const users = await userService.query(sql);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
users: users.map(user => ({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
department: user.department,
|
||||||
|
role: user.role
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取用戶列表錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '獲取用戶列表時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
46
app/api/apps/[id]/stats/route.ts
Normal file
46
app/api/apps/[id]/stats/route.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { AppService } from '@/lib/services/database-service'
|
||||||
|
|
||||||
|
const appService = new AppService()
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id: appId } = await params
|
||||||
|
|
||||||
|
// 獲取應用基本統計
|
||||||
|
const app = await appService.getAppById(appId)
|
||||||
|
if (!app) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '應用不存在' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取評分統計
|
||||||
|
const ratingStats = await appService.getAppRatingStats(appId)
|
||||||
|
|
||||||
|
// 獲取使用趨勢數據
|
||||||
|
const usageStats = await appService.getAppUsageStats(appId)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
basic: {
|
||||||
|
views: app.views_count || 0,
|
||||||
|
likes: app.likes_count || 0,
|
||||||
|
rating: ratingStats.averageRating || 0,
|
||||||
|
reviewCount: ratingStats.totalRatings || 0
|
||||||
|
},
|
||||||
|
usage: usageStats,
|
||||||
|
rating: ratingStats
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用統計數據錯誤:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '獲取統計數據時發生錯誤' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,39 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
|
// 輔助函數:安全地格式化數字
|
||||||
|
const formatNumber = (value: any, decimals: number = 0): string => {
|
||||||
|
if (value == null) {
|
||||||
|
return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理字符串和數字類型
|
||||||
|
const numValue = typeof value === 'string' ? parseFloat(value) : value
|
||||||
|
|
||||||
|
if (isNaN(numValue)) {
|
||||||
|
return decimals > 0 ? '0.' + '0'.repeat(decimals) : '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimals > 0 ? numValue.toFixed(decimals) : numValue.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatLocaleNumber = (value: any): string => {
|
||||||
|
if (value == null) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理字符串和數字類型
|
||||||
|
const numValue = typeof value === 'string' ? parseFloat(value) : value
|
||||||
|
|
||||||
|
if (isNaN(numValue)) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
return numValue.toLocaleString()
|
||||||
|
}
|
||||||
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
import { useToast } from "@/hooks/use-toast"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -29,6 +62,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
X,
|
X,
|
||||||
|
XCircle,
|
||||||
Check,
|
Check,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
Link,
|
Link,
|
||||||
@@ -52,6 +86,7 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
Settings,
|
Settings,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
|
Users
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
// Add available icons array after imports
|
// Add available icons array after imports
|
||||||
@@ -83,12 +118,34 @@ const availableIcons = [
|
|||||||
const mockApps: any[] = []
|
const mockApps: any[] = []
|
||||||
|
|
||||||
export function AppManagement() {
|
export function AppManagement() {
|
||||||
const [apps, setApps] = useState(mockApps)
|
const { user } = useAuth()
|
||||||
|
const { toast } = useToast()
|
||||||
|
const [apps, setApps] = useState<any[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [selectedType, setSelectedType] = useState("all")
|
const [selectedType, setSelectedType] = useState("all")
|
||||||
const [selectedStatus, setSelectedStatus] = useState("all")
|
const [selectedStatus, setSelectedStatus] = useState("all")
|
||||||
const [selectedApp, setSelectedApp] = useState<any>(null)
|
const [selectedApp, setSelectedApp] = useState<any>(null)
|
||||||
const [showAppDetail, setShowAppDetail] = useState(false)
|
const [showAppDetail, setShowAppDetail] = useState(false)
|
||||||
|
const [appStats, setAppStats] = useState({
|
||||||
|
basic: {
|
||||||
|
views: 0,
|
||||||
|
likes: 0,
|
||||||
|
rating: 0,
|
||||||
|
reviewCount: 0
|
||||||
|
},
|
||||||
|
usage: {
|
||||||
|
dailyUsers: 0,
|
||||||
|
weeklyUsers: 0,
|
||||||
|
monthlyUsers: 0,
|
||||||
|
totalSessions: 0,
|
||||||
|
topDepartments: [],
|
||||||
|
trendData: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const [appReviews, setAppReviews] = useState<any[]>([])
|
||||||
|
const [isLoadingStats, setIsLoadingStats] = useState(false)
|
||||||
|
const [isLoadingReviews, setIsLoadingReviews] = useState(false)
|
||||||
const [showAddApp, setShowAddApp] = useState(false)
|
const [showAddApp, setShowAddApp] = useState(false)
|
||||||
const [showEditApp, setShowEditApp] = useState(false)
|
const [showEditApp, setShowEditApp] = useState(false)
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
@@ -99,26 +156,163 @@ export function AppManagement() {
|
|||||||
name: "",
|
name: "",
|
||||||
type: "文字處理",
|
type: "文字處理",
|
||||||
department: "HQBU",
|
department: "HQBU",
|
||||||
creator: "",
|
creator_id: "",
|
||||||
description: "",
|
description: "",
|
||||||
appUrl: "",
|
appUrl: "",
|
||||||
icon: "Bot",
|
icon: "Bot",
|
||||||
iconColor: "from-blue-500 to-purple-500",
|
iconColor: "from-blue-500 to-purple-500",
|
||||||
})
|
})
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
const filteredApps = apps.filter((app) => {
|
page: 1,
|
||||||
const matchesSearch =
|
limit: 10,
|
||||||
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
total: 0,
|
||||||
app.creator.toLowerCase().includes(searchTerm.toLowerCase())
|
totalPages: 0
|
||||||
const matchesType = selectedType === "all" || app.type === selectedType
|
|
||||||
const matchesStatus = selectedStatus === "all" || app.status === selectedStatus
|
|
||||||
|
|
||||||
return matchesSearch && matchesType && matchesStatus
|
|
||||||
})
|
})
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalApps: 0,
|
||||||
|
activeApps: 0,
|
||||||
|
inactiveApps: 0,
|
||||||
|
pendingApps: 0,
|
||||||
|
totalViews: 0,
|
||||||
|
totalLikes: 0,
|
||||||
|
newThisMonth: 0
|
||||||
|
})
|
||||||
|
const [users, setUsers] = useState<any[]>([])
|
||||||
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
||||||
|
|
||||||
|
// 載入應用數據
|
||||||
|
const loadApps = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: pagination.page.toString(),
|
||||||
|
limit: pagination.limit.toString(),
|
||||||
|
search: searchTerm,
|
||||||
|
type: selectedType,
|
||||||
|
status: selectedStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(`/api/admin/apps?${params}`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setApps(data.data.apps)
|
||||||
|
setPagination(data.data.pagination)
|
||||||
|
setStats(data.data.stats)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入應用數據錯誤:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 載入用戶列表
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingUsers(true)
|
||||||
|
const response = await fetch('/api/admin/users/list')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setUsers(data.data.users)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入用戶列表錯誤:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingUsers(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始載入
|
||||||
|
useEffect(() => {
|
||||||
|
loadApps()
|
||||||
|
loadUsers()
|
||||||
|
}, [pagination.page, searchTerm, selectedType, selectedStatus])
|
||||||
|
|
||||||
|
const filteredApps = apps
|
||||||
|
|
||||||
const handleViewApp = (app: any) => {
|
const handleViewApp = (app: any) => {
|
||||||
setSelectedApp(app)
|
setSelectedApp(app)
|
||||||
setShowAppDetail(true)
|
setShowAppDetail(true)
|
||||||
|
loadAppStats(app.id)
|
||||||
|
loadAppReviews(app.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 載入應用統計數據
|
||||||
|
const loadAppStats = async (appId: string) => {
|
||||||
|
setIsLoadingStats(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/apps/${appId}/stats`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setAppStats(data.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入應用統計數據錯誤:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingStats(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 載入應用評價
|
||||||
|
const loadAppReviews = async (appId: string) => {
|
||||||
|
setIsLoadingReviews(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/apps/${appId}/reviews`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setAppReviews(data.data.reviews)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入應用評價錯誤:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingReviews(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除評價
|
||||||
|
const handleDeleteReview = async (reviewId: string) => {
|
||||||
|
if (!selectedApp) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/apps/${selectedApp.id}/reviews`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ reviewId })
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toast({
|
||||||
|
title: "刪除成功",
|
||||||
|
description: "評價已成功刪除",
|
||||||
|
variant: "success",
|
||||||
|
})
|
||||||
|
// 重新載入評價列表
|
||||||
|
loadAppReviews(selectedApp.id)
|
||||||
|
// 重新載入統計數據
|
||||||
|
loadAppStats(selectedApp.id)
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "刪除失敗",
|
||||||
|
description: data.error || '刪除評價失敗',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除評價錯誤:', error)
|
||||||
|
toast({
|
||||||
|
title: "錯誤",
|
||||||
|
description: '刪除評價時發生錯誤',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditApp = (app: any) => {
|
const handleEditApp = (app: any) => {
|
||||||
@@ -127,7 +321,7 @@ export function AppManagement() {
|
|||||||
name: app.name,
|
name: app.name,
|
||||||
type: app.type,
|
type: app.type,
|
||||||
department: app.department,
|
department: app.department,
|
||||||
creator: app.creator,
|
creator_id: app.creator_id || "",
|
||||||
description: app.description,
|
description: app.description,
|
||||||
appUrl: app.appUrl,
|
appUrl: app.appUrl,
|
||||||
icon: app.icon || "Bot",
|
icon: app.icon || "Bot",
|
||||||
@@ -141,25 +335,74 @@ export function AppManagement() {
|
|||||||
setShowDeleteConfirm(true)
|
setShowDeleteConfirm(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDeleteApp = () => {
|
const confirmDeleteApp = async () => {
|
||||||
if (selectedApp) {
|
try {
|
||||||
setApps(apps.filter((app) => app.id !== selectedApp.id))
|
if (!selectedApp) return
|
||||||
setShowDeleteConfirm(false)
|
|
||||||
setSelectedApp(null)
|
const response = await fetch(`/api/admin/apps/${selectedApp.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
await loadApps()
|
||||||
|
setShowDeleteConfirm(false)
|
||||||
|
setSelectedApp(null)
|
||||||
|
toast({
|
||||||
|
title: "刪除成功",
|
||||||
|
description: `應用「${selectedApp.name}」已永久刪除`,
|
||||||
|
variant: "success",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "刪除失敗",
|
||||||
|
description: data.error || '刪除應用失敗',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除應用錯誤:', error)
|
||||||
|
toast({
|
||||||
|
title: "錯誤",
|
||||||
|
description: '刪除應用時發生錯誤',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleAppStatus = (appId: string) => {
|
const handleToggleAppStatus = async (appId: string) => {
|
||||||
setApps(
|
try {
|
||||||
apps.map((app) =>
|
const response = await fetch(`/api/admin/apps/${appId}/toggle-status`, {
|
||||||
app.id === appId
|
method: 'POST'
|
||||||
? {
|
})
|
||||||
...app,
|
|
||||||
status: app.status === "published" ? "draft" : "published",
|
const data = await response.json()
|
||||||
}
|
|
||||||
: app,
|
if (data.success) {
|
||||||
),
|
await loadApps()
|
||||||
)
|
// 根據新狀態顯示不同的成功訊息
|
||||||
|
const newStatus = data.data?.app?.is_active ? '發布' : '下架'
|
||||||
|
toast({
|
||||||
|
title: "狀態更新成功",
|
||||||
|
description: `應用已成功${newStatus}`,
|
||||||
|
variant: "success",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "狀態更新失敗",
|
||||||
|
description: data.error || '切換應用狀態失敗',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切換應用狀態錯誤:', error)
|
||||||
|
toast({
|
||||||
|
title: "錯誤",
|
||||||
|
description: '切換應用狀態時發生錯誤',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleApprovalAction = (app: any, action: "approve" | "reject") => {
|
const handleApprovalAction = (app: any, action: "approve" | "reject") => {
|
||||||
@@ -187,45 +430,116 @@ export function AppManagement() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddApp = () => {
|
const handleAddApp = async () => {
|
||||||
const app = {
|
try {
|
||||||
id: Date.now().toString(),
|
if (!newApp.creator_id) {
|
||||||
...newApp,
|
toast({
|
||||||
status: "pending",
|
title: "驗證失敗",
|
||||||
createdAt: new Date().toISOString().split("T")[0],
|
description: '請選擇創建者',
|
||||||
views: 0,
|
variant: "destructive",
|
||||||
likes: 0,
|
})
|
||||||
rating: 0,
|
return
|
||||||
reviews: 0,
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/admin/apps', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newApp.name,
|
||||||
|
description: newApp.description,
|
||||||
|
creator_id: newApp.creator_id,
|
||||||
|
category: newApp.type, // 使用 type 作為 category
|
||||||
|
type: newApp.type,
|
||||||
|
app_url: newApp.appUrl,
|
||||||
|
icon: newApp.icon,
|
||||||
|
icon_color: newApp.iconColor
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 重新載入應用列表
|
||||||
|
await loadApps()
|
||||||
|
setNewApp({
|
||||||
|
name: "",
|
||||||
|
type: "文字處理",
|
||||||
|
department: "HQBU",
|
||||||
|
creator_id: "",
|
||||||
|
description: "",
|
||||||
|
appUrl: "",
|
||||||
|
icon: "Bot",
|
||||||
|
iconColor: "from-blue-500 to-purple-500",
|
||||||
|
})
|
||||||
|
setShowAddApp(false)
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "創建失敗",
|
||||||
|
description: data.error || '創建應用失敗',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('創建應用錯誤:', error)
|
||||||
|
toast({
|
||||||
|
title: "錯誤",
|
||||||
|
description: '創建應用時發生錯誤',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setApps([...apps, app])
|
|
||||||
setNewApp({
|
|
||||||
name: "",
|
|
||||||
type: "文字處理",
|
|
||||||
department: "HQBU",
|
|
||||||
creator: "",
|
|
||||||
description: "",
|
|
||||||
appUrl: "",
|
|
||||||
icon: "Bot",
|
|
||||||
iconColor: "from-blue-500 to-purple-500",
|
|
||||||
})
|
|
||||||
setShowAddApp(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateApp = () => {
|
const handleUpdateApp = async () => {
|
||||||
if (selectedApp) {
|
try {
|
||||||
setApps(
|
if (!selectedApp) return
|
||||||
apps.map((app) =>
|
|
||||||
app.id === selectedApp.id
|
if (!newApp.creator_id) {
|
||||||
? {
|
toast({
|
||||||
...app,
|
title: "驗證失敗",
|
||||||
...newApp,
|
description: '請選擇創建者',
|
||||||
}
|
variant: "destructive",
|
||||||
: app,
|
})
|
||||||
),
|
return
|
||||||
)
|
}
|
||||||
setShowEditApp(false)
|
|
||||||
setSelectedApp(null)
|
const response = await fetch(`/api/admin/apps/${selectedApp.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newApp.name,
|
||||||
|
description: newApp.description,
|
||||||
|
category: newApp.type,
|
||||||
|
type: newApp.type,
|
||||||
|
app_url: newApp.appUrl,
|
||||||
|
icon: newApp.icon,
|
||||||
|
icon_color: newApp.iconColor
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
await loadApps()
|
||||||
|
setShowEditApp(false)
|
||||||
|
setSelectedApp(null)
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "更新失敗",
|
||||||
|
description: data.error || '更新應用失敗',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新應用錯誤:', error)
|
||||||
|
toast({
|
||||||
|
title: "錯誤",
|
||||||
|
description: '更新應用時發生錯誤',
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,10 +630,10 @@ export function AppManagement() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">待審核</p>
|
<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">{apps.filter((a) => a.status === "draft").length}</p>
|
||||||
</div>
|
</div>
|
||||||
<Clock className="w-8 h-8 text-yellow-600" />
|
<XCircle className="w-8 h-8 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -344,11 +658,19 @@ export function AppManagement() {
|
|||||||
<SelectValue placeholder="類型" />
|
<SelectValue placeholder="類型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">全部類型</SelectItem>
|
<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>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -451,8 +773,8 @@ export function AppManagement() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<Star className="w-4 h-4 text-yellow-500" />
|
<Star className="w-4 h-4 text-yellow-500" />
|
||||||
<span className="font-medium">{app.rating}</span>
|
<span className="font-medium">{formatNumber(app.rating, 1)}</span>
|
||||||
<span className="text-sm text-gray-500">({app.reviews})</span>
|
<span className="text-sm text-gray-500">({app.reviewCount || 0})</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-gray-600">{app.createdAt}</TableCell>
|
<TableCell className="text-sm text-gray-600">{app.createdAt}</TableCell>
|
||||||
@@ -507,7 +829,7 @@ export function AppManagement() {
|
|||||||
)}
|
)}
|
||||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteApp(app)}>
|
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteApp(app)}>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
刪除應用
|
永久刪除
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -540,12 +862,25 @@ export function AppManagement() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="creator">創建者 *</Label>
|
<Label htmlFor="creator">創建者 *</Label>
|
||||||
<Input
|
<Select
|
||||||
id="creator"
|
value={newApp.creator_id}
|
||||||
value={newApp.creator}
|
onValueChange={(value) => setNewApp({ ...newApp, creator_id: value })}
|
||||||
onChange={(e) => setNewApp({ ...newApp, creator: e.target.value })}
|
>
|
||||||
placeholder="輸入創建者姓名"
|
<SelectTrigger>
|
||||||
/>
|
<SelectValue placeholder="選擇創建者" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{isLoadingUsers ? (
|
||||||
|
<SelectItem value="" disabled>載入中...</SelectItem>
|
||||||
|
) : (
|
||||||
|
users.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id}>
|
||||||
|
{user.name} ({user.email})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -557,10 +892,19 @@ export function AppManagement() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="文字處理">文字處理</SelectItem>
|
<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>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -644,7 +988,7 @@ export function AppManagement() {
|
|||||||
<Button variant="outline" onClick={() => setShowAddApp(false)}>
|
<Button variant="outline" onClick={() => setShowAddApp(false)}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAddApp} disabled={!newApp.name || !newApp.creator || !newApp.description}>
|
<Button onClick={handleAddApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
|
||||||
創建應用
|
創建應用
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -673,12 +1017,25 @@ export function AppManagement() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-creator">創建者 *</Label>
|
<Label htmlFor="edit-creator">創建者 *</Label>
|
||||||
<Input
|
<Select
|
||||||
id="edit-creator"
|
value={newApp.creator_id}
|
||||||
value={newApp.creator}
|
onValueChange={(value) => setNewApp({ ...newApp, creator_id: value })}
|
||||||
onChange={(e) => setNewApp({ ...newApp, creator: e.target.value })}
|
>
|
||||||
placeholder="輸入創建者姓名"
|
<SelectTrigger>
|
||||||
/>
|
<SelectValue placeholder="選擇創建者" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{isLoadingUsers ? (
|
||||||
|
<SelectItem value="" disabled>載入中...</SelectItem>
|
||||||
|
) : (
|
||||||
|
users.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id}>
|
||||||
|
{user.name} ({user.email})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -690,10 +1047,18 @@ export function AppManagement() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<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>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -775,7 +1140,7 @@ export function AppManagement() {
|
|||||||
<Button variant="outline" onClick={() => setShowEditApp(false)}>
|
<Button variant="outline" onClick={() => setShowEditApp(false)}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleUpdateApp} disabled={!newApp.name || !newApp.creator || !newApp.description}>
|
<Button onClick={handleUpdateApp} disabled={!newApp.name || !newApp.creator_id || !newApp.description}>
|
||||||
更新應用
|
更新應用
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -791,7 +1156,7 @@ export function AppManagement() {
|
|||||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||||
<span>確認刪除</span>
|
<span>確認刪除</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>您確定要刪除應用「{selectedApp?.name}」嗎?此操作無法復原。</DialogDescription>
|
<DialogDescription>您確定要永久刪除應用「{selectedApp?.name}」嗎?此操作無法復原,應用將從資料庫中完全移除。</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="flex justify-end space-x-3">
|
||||||
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
||||||
@@ -878,7 +1243,7 @@ export function AppManagement() {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<div className="flex items-center space-x-2 mb-2 justify-between">
|
||||||
<h3 className="text-xl font-semibold">{selectedApp.name}</h3>
|
<h3 className="text-xl font-semibold">{selectedApp.name}</h3>
|
||||||
{selectedApp.appUrl && (
|
{selectedApp.appUrl && (
|
||||||
<Button variant="outline" size="sm" onClick={() => window.open(selectedApp.appUrl, "_blank")}>
|
<Button variant="outline" size="sm" onClick={() => window.open(selectedApp.appUrl, "_blank")}>
|
||||||
@@ -939,11 +1304,14 @@ export function AppManagement() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="stats" className="space-y-4">
|
<TabsContent value="stats" className="space-y-4">
|
||||||
|
{/* 基本統計數據 */}
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-blue-600">{selectedApp.views}</p>
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{isLoadingStats ? '...' : (appStats?.basic?.views || 0)}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-600">總瀏覽量</p>
|
<p className="text-sm text-gray-600">總瀏覽量</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -951,7 +1319,9 @@ export function AppManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-red-600">{selectedApp.likes}</p>
|
<p className="text-2xl font-bold text-red-600">
|
||||||
|
{isLoadingStats ? '...' : (appStats?.basic?.likes || 0)}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-600">收藏數</p>
|
<p className="text-sm text-gray-600">收藏數</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -959,7 +1329,9 @@ export function AppManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-yellow-600">{selectedApp.rating}</p>
|
<p className="text-2xl font-bold text-yellow-600">
|
||||||
|
{isLoadingStats ? '...' : formatNumber(appStats?.basic?.rating, 1)}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-600">平均評分</p>
|
<p className="text-sm text-gray-600">平均評分</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -967,20 +1339,139 @@ export function AppManagement() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-green-600">{selectedApp.reviews}</p>
|
<p className="text-2xl font-bold text-green-600">
|
||||||
|
{isLoadingStats ? '...' : (appStats?.basic?.reviewCount || 0)}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-600">評價數量</p>
|
<p className="text-sm text-gray-600">評價數量</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 使用趨勢 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">今日使用者</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-blue-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{isLoadingStats ? '...' : (appStats?.usage?.dailyUsers || 0)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">活躍用戶數</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">本週使用者</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{isLoadingStats ? '...' : (appStats?.usage?.weeklyUsers || 0)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">週活躍用戶</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">總使用次數</CardTitle>
|
||||||
|
<BarChart3 className="h-4 w-4 text-purple-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{isLoadingStats ? '...' : formatLocaleNumber(appStats?.usage?.totalSessions)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">累計使用次數</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 部門使用統計 */}
|
||||||
|
{appStats?.usage?.topDepartments && appStats.usage.topDepartments.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>部門使用統計</CardTitle>
|
||||||
|
<CardDescription>使用該應用最多的部門</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{appStats.usage.topDepartments.map((dept: any, index: number) => (
|
||||||
|
<div key={index} className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">{dept.department}</span>
|
||||||
|
<span className="text-sm text-gray-500">{dept.count} 次</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="reviews" className="space-y-4">
|
<TabsContent value="reviews" className="space-y-4">
|
||||||
<div className="text-center py-8">
|
<div className="flex items-center justify-between">
|
||||||
<MessageSquare className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
<h3 className="text-lg font-semibold">評價管理</h3>
|
||||||
<h3 className="text-lg font-semibold text-gray-600 mb-2">評價管理</h3>
|
<div className="text-sm text-gray-500">
|
||||||
<p className="text-gray-500">此功能將顯示應用的所有評價和管理選項</p>
|
{isLoadingReviews ? '載入中...' : `共 ${appReviews.length} 條評價`}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isLoadingReviews ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||||
|
<p className="text-gray-500 mt-2">載入評價中...</p>
|
||||||
|
</div>
|
||||||
|
) : appReviews.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<MessageSquare className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-600 mb-2">暫無評價</h3>
|
||||||
|
<p className="text-gray-500">該應用目前還沒有用戶評價</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{appReviews.map((review) => (
|
||||||
|
<Card key={review.id}>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
i < review.rating
|
||||||
|
? 'text-yellow-400 fill-current'
|
||||||
|
: 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">{review.userName}</span>
|
||||||
|
<span className="text-xs text-gray-500">({review.userDepartment})</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700 mb-2">{review.review}</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{review.ratedAt ? new Date(review.ratedAt).toLocaleString('zh-TW') : '未知時間'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteReview(review.id)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
@@ -57,6 +57,23 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
|
|||||||
const { user, addToRecentApps, getAppLikes, incrementViewCount, getViewCount, getAppRating } = useAuth()
|
const { user, addToRecentApps, getAppLikes, incrementViewCount, getViewCount, getAppRating } = useAuth()
|
||||||
const [currentRating, setCurrentRating] = useState(getAppRating(app.id.toString()))
|
const [currentRating, setCurrentRating] = useState(getAppRating(app.id.toString()))
|
||||||
const [reviewCount, setReviewCount] = useState(0)
|
const [reviewCount, setReviewCount] = useState(0)
|
||||||
|
const [appStats, setAppStats] = useState({
|
||||||
|
basic: {
|
||||||
|
views: 0,
|
||||||
|
likes: 0,
|
||||||
|
rating: 0,
|
||||||
|
reviewCount: 0
|
||||||
|
},
|
||||||
|
usage: {
|
||||||
|
dailyUsers: 0,
|
||||||
|
weeklyUsers: 0,
|
||||||
|
monthlyUsers: 0,
|
||||||
|
totalSessions: 0,
|
||||||
|
topDepartments: [],
|
||||||
|
trendData: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const [isLoadingStats, setIsLoadingStats] = useState(false)
|
||||||
|
|
||||||
// Date range for usage trends
|
// Date range for usage trends
|
||||||
const [startDate, setStartDate] = useState(() => {
|
const [startDate, setStartDate] = useState(() => {
|
||||||
@@ -88,6 +105,34 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
|
|||||||
setReviewCount(newReviewCount)
|
setReviewCount(newReviewCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 載入應用統計數據
|
||||||
|
const loadAppStats = async () => {
|
||||||
|
if (!app.id) return
|
||||||
|
|
||||||
|
setIsLoadingStats(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/apps/${app.id}/stats`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setAppStats(data.data)
|
||||||
|
setCurrentRating(data.data.basic.rating)
|
||||||
|
setReviewCount(data.data.basic.reviewCount)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入應用統計數據錯誤:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingStats(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 當對話框打開時載入統計數據
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open && app.id) {
|
||||||
|
loadAppStats()
|
||||||
|
}
|
||||||
|
}, [open, app.id])
|
||||||
|
|
||||||
const handleTryApp = () => {
|
const handleTryApp = () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
addToRecentApps(app.id.toString())
|
addToRecentApps(app.id.toString())
|
||||||
@@ -245,19 +290,74 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center justify-between">
|
||||||
|
<FavoriteButton appId={app.id.toString()} size="default" showText={true} className="px-6" />
|
||||||
<Button
|
<Button
|
||||||
onClick={handleTryApp}
|
onClick={handleTryApp}
|
||||||
className="flex-1 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||||||
>
|
>
|
||||||
立即體驗
|
立即體驗
|
||||||
</Button>
|
</Button>
|
||||||
<FavoriteButton appId={app.id.toString()} size="default" showText={true} className="px-6" />
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="statistics" className="space-y-6">
|
<TabsContent value="statistics" className="space-y-6">
|
||||||
{/* Usage Overview */}
|
{/* 基本統計數據 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">總瀏覽量</CardTitle>
|
||||||
|
<Eye className="h-4 w-4 text-blue-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{isLoadingStats ? '...' : appStats.basic.views}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">累計瀏覽次數</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">收藏數</CardTitle>
|
||||||
|
<Heart className="h-4 w-4 text-red-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-red-600">
|
||||||
|
{isLoadingStats ? '...' : appStats.basic.likes}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">用戶收藏數量</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">平均評分</CardTitle>
|
||||||
|
<Star className="h-4 w-4 text-yellow-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-yellow-600">
|
||||||
|
{isLoadingStats ? '...' : appStats.basic.rating.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">用戶評分</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">評價數量</CardTitle>
|
||||||
|
<MessageSquare className="h-4 w-4 text-green-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{isLoadingStats ? '...' : appStats.basic.reviewCount}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">用戶評價總數</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 使用趨勢 */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
@@ -265,7 +365,9 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
|
|||||||
<Users className="h-4 w-4 text-blue-500" />
|
<Users className="h-4 w-4 text-blue-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{usageStats.dailyUsers}</div>
|
<div className="text-2xl font-bold">
|
||||||
|
{isLoadingStats ? '...' : appStats.usage.dailyUsers}
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">活躍用戶數</p>
|
<p className="text-xs text-muted-foreground">活躍用戶數</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -276,7 +378,9 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
|
|||||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{usageStats.weeklyUsers}</div>
|
<div className="text-2xl font-bold">
|
||||||
|
{isLoadingStats ? '...' : appStats.usage.weeklyUsers}
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">週活躍用戶</p>
|
<p className="text-xs text-muted-foreground">週活躍用戶</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -287,7 +391,9 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
|
|||||||
<BarChart3 className="h-4 w-4 text-purple-500" />
|
<BarChart3 className="h-4 w-4 text-purple-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{usageStats.totalSessions.toLocaleString()}</div>
|
<div className="text-2xl font-bold">
|
||||||
|
{isLoadingStats ? '...' : appStats.usage.totalSessions.toLocaleString()}
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">累計使用次數</p>
|
<p className="text-xs text-muted-foreground">累計使用次數</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -704,11 +704,19 @@ export function PopularityRankings() {
|
|||||||
<SelectValue placeholder="應用類型" />
|
<SelectValue placeholder="應用類型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">全部類型</SelectItem>
|
<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>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -202,12 +202,18 @@ export function RegistrationDialog({ open, onOpenChange }: RegistrationDialogPro
|
|||||||
<SelectValue placeholder="選擇應用類型" />
|
<SelectValue placeholder="選擇應用類型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<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="教育工具">教育工具</SelectItem>
|
||||||
<SelectItem value="其他">其他</SelectItem>
|
<SelectItem value="其他">其他</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
@@ -1,113 +1,183 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all duration-300 ease-in-out transform",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border bg-background text-foreground",
|
default: "border bg-background text-foreground",
|
||||||
destructive: "destructive border-destructive bg-destructive text-destructive-foreground",
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
success:
|
||||||
|
"border-green-200 bg-green-50 text-green-900",
|
||||||
|
warning:
|
||||||
|
"border-yellow-200 bg-yellow-50 text-yellow-900",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
HTMLDivElement,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof toastVariants> & {
|
||||||
>(({ className, variant, ...props }, ref) => {
|
onOpenChange?: (open: boolean) => void
|
||||||
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
|
open?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, variant, onOpenChange, open, ...props }, ref) => {
|
||||||
|
const [isVisible, setIsVisible] = React.useState(true)
|
||||||
|
const [isClosing, setIsClosing] = React.useState(false)
|
||||||
|
|
||||||
|
// 處理關閉動畫
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
if (isClosing) return // 防止重複觸發
|
||||||
|
|
||||||
|
setIsClosing(true)
|
||||||
|
setIsVisible(false)
|
||||||
|
|
||||||
|
// 等待動畫完成後調用 onOpenChange
|
||||||
|
setTimeout(() => {
|
||||||
|
onOpenChange?.(false)
|
||||||
|
}, 300) // 與 CSS 動畫時間一致
|
||||||
|
}, [onOpenChange, isClosing])
|
||||||
|
|
||||||
|
// 處理外部觸發的關閉
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open === false && !isClosing) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}, [open, isClosing, handleClose])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
toastVariants({ variant }),
|
||||||
|
// 動畫類別
|
||||||
|
isVisible
|
||||||
|
? "translate-x-0 opacity-100"
|
||||||
|
: "translate-x-full opacity-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transition: "transform 300ms ease-in-out, opacity 300ms ease-in-out"
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
Toast.displayName = "Toast"
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
const ToastAction = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
HTMLButtonElement,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Action
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
ToastAction.displayName = "ToastAction"
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
const ToastClose = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
HTMLButtonElement,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
>(({ className, ...props }, ref) => (
|
onClose?: () => void
|
||||||
<ToastPrimitives.Close
|
}
|
||||||
|
>(({ className, onClose, ...props }, ref) => (
|
||||||
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
onClose?.()
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</button>
|
||||||
))
|
))
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
ToastClose.displayName = "ToastClose"
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
const ToastTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
HTMLDivElement,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
ToastTitle.displayName = "ToastTitle"
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
const ToastDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
HTMLDivElement,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
ToastDescription.displayName = "ToastDescription"
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
type ToastProps = React.ComponentProps<typeof Toast>
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
const ToastProvider = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("pointer-events-none fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastProvider.displayName = "ToastProvider"
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = "ToastViewport"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ToastProps,
|
type ToastProps,
|
||||||
type ToastActionElement,
|
type ToastActionElement,
|
||||||
ToastProvider,
|
|
||||||
ToastViewport,
|
|
||||||
Toast,
|
Toast,
|
||||||
|
ToastAction,
|
||||||
|
ToastClose,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastProvider,
|
||||||
ToastAction,
|
ToastViewport,
|
||||||
}
|
}
|
@@ -1,23 +1,34 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast()
|
const { toasts, dismiss } = useToast()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(({ id, title, description, action, ...props }) => (
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
<Toast key={id} {...props}>
|
return (
|
||||||
<div className="grid gap-1">
|
<Toast key={id} {...props} onOpenChange={() => dismiss(id)}>
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
<div className="grid gap-1">
|
||||||
{description && <ToastDescription>{description}</ToastDescription>}
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
</div>
|
{description && (
|
||||||
{action}
|
<ToastDescription>{description}</ToastDescription>
|
||||||
<ToastClose />
|
)}
|
||||||
</Toast>
|
</div>
|
||||||
))}
|
{action}
|
||||||
|
<ToastClose onClose={() => dismiss(id)} />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
<ToastViewport />
|
<ToastViewport />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
|
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
const TOAST_LIMIT = 1
|
const TOAST_LIMIT = 1
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
@@ -12,6 +15,7 @@ type ToasterToast = ToastProps & {
|
|||||||
title?: React.ReactNode
|
title?: React.ReactNode
|
||||||
description?: React.ReactNode
|
description?: React.ReactNode
|
||||||
action?: ToastActionElement
|
action?: ToastActionElement
|
||||||
|
open?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionTypes = {
|
const actionTypes = {
|
||||||
@@ -81,14 +85,14 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
case "UPDATE_TOAST":
|
case "UPDATE_TOAST":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
case "DISMISS_TOAST": {
|
||||||
const { toastId } = action
|
const { toastId } = action
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
||||||
// but I'll keep it here for simplicity
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId)
|
addToRemoveQueue(toastId)
|
||||||
} else {
|
} else {
|
||||||
@@ -105,7 +109,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
...t,
|
...t,
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
: t,
|
: t
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +185,7 @@ function useToast() {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toast,
|
toast,
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
dismiss: (toastId?: string) => dispatch({ type: "REMOVE_TOAST", toastId }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -917,61 +917,552 @@ export class CompetitionService {
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
export class AppService {
|
export class AppService {
|
||||||
// 創建應用
|
// 創建應用
|
||||||
static async createApp(appData: Omit<App, 'id' | 'created_at' | 'updated_at'>): Promise<App> {
|
async createApp(appData: {
|
||||||
const sql = `
|
name: string;
|
||||||
INSERT INTO apps (id, name, description, creator_id, team_id, category, type, likes_count, views_count, rating, is_active)
|
description: string;
|
||||||
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
creator_id: string;
|
||||||
`;
|
category: string;
|
||||||
const params = [
|
type: string;
|
||||||
appData.name,
|
app_url?: string;
|
||||||
appData.description || null,
|
icon?: string;
|
||||||
appData.creator_id,
|
icon_color?: string;
|
||||||
appData.team_id || null,
|
}): Promise<{ success: boolean; app?: any; error?: string }> {
|
||||||
appData.category,
|
try {
|
||||||
appData.type,
|
const appId = crypto.randomUUID();
|
||||||
appData.likes_count,
|
const sql = `
|
||||||
appData.views_count,
|
INSERT INTO apps (id, name, description, creator_id, category, type, app_url, icon, icon_color, likes_count, views_count, rating, is_active, created_at, updated_at)
|
||||||
appData.rating,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0.00, TRUE, NOW(), NOW())
|
||||||
appData.is_active
|
`;
|
||||||
];
|
|
||||||
|
|
||||||
await db.insert(sql, params);
|
await this.query(sql, [
|
||||||
return await this.getAppByName(appData.name) as App;
|
appId,
|
||||||
|
appData.name,
|
||||||
|
appData.description,
|
||||||
|
appData.creator_id,
|
||||||
|
appData.category,
|
||||||
|
appData.type,
|
||||||
|
appData.app_url || null,
|
||||||
|
appData.icon || 'Bot',
|
||||||
|
appData.icon_color || 'from-blue-500 to-purple-500'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 獲取創建的應用
|
||||||
|
const createdApp = await this.getAppById(appId);
|
||||||
|
|
||||||
|
return { success: true, app: createdApp };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('創建應用錯誤:', error);
|
||||||
|
return { success: false, error: '創建應用時發生錯誤' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根據 ID 獲取應用(僅已發布)
|
||||||
|
async getAppById(appId: string): Promise<any | null> {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
u.name as creator_name,
|
||||||
|
u.department as creator_department
|
||||||
|
FROM apps a
|
||||||
|
LEFT JOIN users u ON a.creator_id = u.id
|
||||||
|
WHERE a.id = ? AND a.is_active = TRUE
|
||||||
|
`;
|
||||||
|
return await this.queryOne(sql, [appId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根據 ID 獲取應用(任何狀態)
|
||||||
|
async getAppByIdAnyStatus(appId: string): Promise<any | null> {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
u.name as creator_name,
|
||||||
|
u.department as creator_department
|
||||||
|
FROM apps a
|
||||||
|
LEFT JOIN users u ON a.creator_id = u.id
|
||||||
|
WHERE a.id = ?
|
||||||
|
`;
|
||||||
|
return await this.queryOne(sql, [appId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根據名稱獲取應用
|
// 根據名稱獲取應用
|
||||||
static async getAppByName(name: string): Promise<App | null> {
|
async getAppByName(name: string): Promise<any | null> {
|
||||||
const sql = 'SELECT * FROM apps WHERE name = ? AND is_active = TRUE';
|
const sql = `
|
||||||
return await db.queryOne<App>(sql, [name]);
|
SELECT
|
||||||
}
|
a.*,
|
||||||
|
u.name as creator_name,
|
||||||
// 根據ID獲取應用
|
u.department as creator_department
|
||||||
static async getAppById(id: string): Promise<App | null> {
|
FROM apps a
|
||||||
const sql = 'SELECT * FROM apps WHERE id = ? AND is_active = TRUE';
|
LEFT JOIN users u ON a.creator_id = u.id
|
||||||
return await db.queryOne<App>(sql, [id]);
|
WHERE a.name = ? AND a.is_active = TRUE
|
||||||
|
`;
|
||||||
|
return await this.queryOne(sql, [name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取所有應用
|
// 獲取所有應用
|
||||||
static async getAllApps(limit = 50, offset = 0): Promise<App[]> {
|
async getAllApps(filters: {
|
||||||
const sql = 'SELECT * FROM apps WHERE is_active = TRUE ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
search?: string;
|
||||||
return await db.query<App>(sql, [limit, offset]);
|
category?: string;
|
||||||
}
|
type?: string;
|
||||||
|
status?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
} = {}): Promise<{ apps: any[]; total: number }> {
|
||||||
|
try {
|
||||||
|
const { search = '', category = 'all', type = 'all', status = 'all', page = 1, limit = 10 } = filters;
|
||||||
|
|
||||||
// 獲取應用統計
|
// 構建查詢條件
|
||||||
static async getAppStatistics(id: string): Promise<AppStatistics | null> {
|
let whereConditions: string[] = [];
|
||||||
const sql = 'SELECT * FROM app_statistics WHERE id = ?';
|
let params: any[] = [];
|
||||||
return await db.queryOne<AppStatistics>(sql, [id]);
|
|
||||||
|
// 根據狀態篩選
|
||||||
|
if (status && status !== 'all') {
|
||||||
|
if (status === 'active') {
|
||||||
|
whereConditions.push('a.is_active = TRUE');
|
||||||
|
} else if (status === 'inactive') {
|
||||||
|
whereConditions.push('a.is_active = FALSE');
|
||||||
|
}
|
||||||
|
// 如果 status 是 'all' 或其他值,則不添加狀態篩選
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
whereConditions.push('(a.name LIKE ? OR a.description LIKE ? OR u.name LIKE ?)');
|
||||||
|
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category && category !== 'all') {
|
||||||
|
whereConditions.push('a.category = ?');
|
||||||
|
params.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type && type !== 'all') {
|
||||||
|
whereConditions.push('a.type = ?');
|
||||||
|
params.push(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
||||||
|
|
||||||
|
// 獲取總數
|
||||||
|
const countSql = `
|
||||||
|
SELECT COUNT(DISTINCT a.id) as total
|
||||||
|
FROM apps a
|
||||||
|
LEFT JOIN users u ON a.creator_id = u.id
|
||||||
|
LEFT JOIN user_ratings ur ON a.id = ur.app_id
|
||||||
|
${whereClause}
|
||||||
|
`;
|
||||||
|
const countResult = await this.queryOne(countSql, params);
|
||||||
|
const total = countResult?.total || 0;
|
||||||
|
|
||||||
|
// 獲取應用列表
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
a.*,
|
||||||
|
u.name as creator_name,
|
||||||
|
u.department as creator_department,
|
||||||
|
u.email as creator_email,
|
||||||
|
COALESCE(AVG(ur.rating), 0) as rating,
|
||||||
|
COUNT(ur.id) as reviewCount
|
||||||
|
FROM apps a
|
||||||
|
LEFT JOIN users u ON a.creator_id = u.id
|
||||||
|
LEFT JOIN user_ratings ur ON a.id = ur.app_id
|
||||||
|
${whereClause}
|
||||||
|
GROUP BY a.id, u.name, u.department, u.email
|
||||||
|
ORDER BY a.created_at DESC
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const apps = await this.query(sql, params);
|
||||||
|
|
||||||
|
return { apps, total };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用列表錯誤:', error);
|
||||||
|
return { apps: [], total: 0 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新應用
|
// 更新應用
|
||||||
static async updateApp(id: string, updates: Partial<App>): Promise<boolean> {
|
async updateApp(appId: string, updates: {
|
||||||
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at');
|
name?: string;
|
||||||
const setClause = fields.map(field => `${field} = ?`).join(', ');
|
description?: string;
|
||||||
const values = fields.map(field => (updates as any)[field]);
|
category?: string;
|
||||||
|
type?: string;
|
||||||
|
app_url?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
|
}): Promise<{ success: boolean; app?: any; error?: string }> {
|
||||||
|
try {
|
||||||
|
const updateFields = [];
|
||||||
|
const params = [];
|
||||||
|
|
||||||
const sql = `UPDATE apps SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
if (updates.name !== undefined) {
|
||||||
const result = await db.update(sql, [...values, id]);
|
updateFields.push('name = ?');
|
||||||
return result.affectedRows > 0;
|
params.push(updates.name);
|
||||||
|
}
|
||||||
|
if (updates.description !== undefined) {
|
||||||
|
updateFields.push('description = ?');
|
||||||
|
params.push(updates.description);
|
||||||
|
}
|
||||||
|
if (updates.category !== undefined) {
|
||||||
|
updateFields.push('category = ?');
|
||||||
|
params.push(updates.category);
|
||||||
|
}
|
||||||
|
if (updates.type !== undefined) {
|
||||||
|
updateFields.push('type = ?');
|
||||||
|
params.push(updates.type);
|
||||||
|
}
|
||||||
|
if (updates.app_url !== undefined) {
|
||||||
|
updateFields.push('app_url = ?');
|
||||||
|
params.push(updates.app_url);
|
||||||
|
}
|
||||||
|
if (updates.icon !== undefined) {
|
||||||
|
updateFields.push('icon = ?');
|
||||||
|
params.push(updates.icon);
|
||||||
|
}
|
||||||
|
if (updates.icon_color !== undefined) {
|
||||||
|
updateFields.push('icon_color = ?');
|
||||||
|
params.push(updates.icon_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateFields.length === 0) {
|
||||||
|
return { success: false, error: '沒有要更新的欄位' };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFields.push('updated_at = NOW()');
|
||||||
|
params.push(appId);
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
UPDATE apps
|
||||||
|
SET ${updateFields.join(', ')}
|
||||||
|
WHERE id = ? AND is_active = TRUE
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.query(sql, params);
|
||||||
|
|
||||||
|
// 獲取更新後的應用
|
||||||
|
const updatedApp = await this.getAppById(appId);
|
||||||
|
|
||||||
|
return { success: true, app: updatedApp };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新應用錯誤:', error);
|
||||||
|
return { success: false, error: '更新應用時發生錯誤' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除應用(硬刪除)
|
||||||
|
async deleteApp(appId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
// 先檢查應用是否存在(任何狀態)
|
||||||
|
const app = await this.getAppByIdAnyStatus(appId);
|
||||||
|
if (!app) {
|
||||||
|
return { success: false, error: '應用不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 硬刪除應用(從資料庫中完全移除)
|
||||||
|
const sql = 'DELETE FROM apps WHERE id = ?';
|
||||||
|
await this.query(sql, [appId]);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除應用錯誤:', error);
|
||||||
|
return { success: false, error: '刪除應用時發生錯誤' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切換應用狀態
|
||||||
|
async toggleAppStatus(appId: string): Promise<{ success: boolean; app?: any; error?: string }> {
|
||||||
|
try {
|
||||||
|
// 先獲取當前狀態(任何狀態)
|
||||||
|
const currentApp = await this.getAppByIdAnyStatus(appId);
|
||||||
|
if (!currentApp) {
|
||||||
|
return { success: false, error: '應用不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatus = currentApp.is_active ? false : true;
|
||||||
|
const sql = 'UPDATE apps SET is_active = ?, updated_at = NOW() WHERE id = ?';
|
||||||
|
await this.query(sql, [newStatus, appId]);
|
||||||
|
|
||||||
|
// 獲取更新後的應用(任何狀態)
|
||||||
|
const updatedApp = await this.getAppByIdAnyStatus(appId);
|
||||||
|
|
||||||
|
return { success: true, app: updatedApp };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切換應用狀態錯誤:', error);
|
||||||
|
return { success: false, error: '切換應用狀態時發生錯誤' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取應用統計
|
||||||
|
async getAppStats(): Promise<{
|
||||||
|
totalApps: number;
|
||||||
|
activeApps: number;
|
||||||
|
inactiveApps: number;
|
||||||
|
pendingApps: number;
|
||||||
|
totalViews: number;
|
||||||
|
totalLikes: number;
|
||||||
|
newThisMonth: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_apps,
|
||||||
|
COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_apps,
|
||||||
|
COUNT(CASE WHEN is_active = FALSE THEN 1 END) as inactive_apps,
|
||||||
|
COALESCE(SUM(views_count), 0) as total_views,
|
||||||
|
COALESCE(SUM(likes_count), 0) as total_likes,
|
||||||
|
COUNT(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 END) as new_this_month
|
||||||
|
FROM apps
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.queryOne(sql);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalApps: result?.total_apps || 0,
|
||||||
|
activeApps: result?.active_apps || 0,
|
||||||
|
inactiveApps: result?.inactive_apps || 0,
|
||||||
|
pendingApps: 0, // 目前沒有 pending 狀態
|
||||||
|
totalViews: result?.total_views || 0,
|
||||||
|
totalLikes: result?.total_likes || 0,
|
||||||
|
newThisMonth: result?.new_this_month || 0
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用統計錯誤:', error);
|
||||||
|
return {
|
||||||
|
totalApps: 0,
|
||||||
|
activeApps: 0,
|
||||||
|
inactiveApps: 0,
|
||||||
|
pendingApps: 0,
|
||||||
|
totalViews: 0,
|
||||||
|
totalLikes: 0,
|
||||||
|
newThisMonth: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用查詢方法
|
||||||
|
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
|
||||||
|
return await db.query<T>(sql, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用單一查詢方法
|
||||||
|
async queryOne<T = any>(sql: string, params: any[] = []): Promise<T | null> {
|
||||||
|
return await db.queryOne<T>(sql, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取應用評分統計
|
||||||
|
async getAppRatingStats(appId: string): Promise<{
|
||||||
|
averageRating: number;
|
||||||
|
totalRatings: number;
|
||||||
|
ratingDistribution: { rating: number; count: number }[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
AVG(rating) as average_rating,
|
||||||
|
COUNT(*) as total_ratings
|
||||||
|
FROM user_ratings
|
||||||
|
WHERE app_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.queryOne(sql, [appId]);
|
||||||
|
|
||||||
|
const distributionSql = `
|
||||||
|
SELECT
|
||||||
|
rating,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM user_ratings
|
||||||
|
WHERE app_id = ?
|
||||||
|
GROUP BY rating
|
||||||
|
ORDER BY rating DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const distribution = await this.query(distributionSql, [appId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
averageRating: result.average_rating ? parseFloat(result.average_rating) : 0,
|
||||||
|
totalRatings: result.total_ratings || 0,
|
||||||
|
ratingDistribution: distribution.map((row: any) => ({
|
||||||
|
rating: row.rating,
|
||||||
|
count: row.count
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用評分統計錯誤:', error);
|
||||||
|
return {
|
||||||
|
averageRating: 0,
|
||||||
|
totalRatings: 0,
|
||||||
|
ratingDistribution: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取應用使用統計
|
||||||
|
async getAppUsageStats(appId: string): Promise<{
|
||||||
|
dailyUsers: number;
|
||||||
|
weeklyUsers: number;
|
||||||
|
monthlyUsers: number;
|
||||||
|
totalSessions: number;
|
||||||
|
topDepartments: { department: string; count: number }[];
|
||||||
|
trendData: { date: string; users: number }[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// 今日使用者
|
||||||
|
const dailySql = `
|
||||||
|
SELECT COUNT(DISTINCT user_id) as daily_users
|
||||||
|
FROM user_views
|
||||||
|
WHERE app_id = ? AND DATE(viewed_at) = CURDATE()
|
||||||
|
`;
|
||||||
|
const dailyResult = await this.queryOne(dailySql, [appId]);
|
||||||
|
|
||||||
|
// 本週使用者
|
||||||
|
const weeklySql = `
|
||||||
|
SELECT COUNT(DISTINCT user_id) as weekly_users
|
||||||
|
FROM user_views
|
||||||
|
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 WEEK)
|
||||||
|
`;
|
||||||
|
const weeklyResult = await this.queryOne(weeklySql, [appId]);
|
||||||
|
|
||||||
|
// 本月使用者
|
||||||
|
const monthlySql = `
|
||||||
|
SELECT COUNT(DISTINCT user_id) as monthly_users
|
||||||
|
FROM user_views
|
||||||
|
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)
|
||||||
|
`;
|
||||||
|
const monthlyResult = await this.queryOne(monthlySql, [appId]);
|
||||||
|
|
||||||
|
// 總使用次數
|
||||||
|
const totalSql = `
|
||||||
|
SELECT COUNT(*) as total_sessions
|
||||||
|
FROM user_views
|
||||||
|
WHERE app_id = ?
|
||||||
|
`;
|
||||||
|
const totalResult = await this.queryOne(totalSql, [appId]);
|
||||||
|
|
||||||
|
// 部門使用統計
|
||||||
|
const deptSql = `
|
||||||
|
SELECT
|
||||||
|
u.department,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM user_views uv
|
||||||
|
JOIN users u ON uv.user_id = u.id
|
||||||
|
WHERE uv.app_id = ?
|
||||||
|
GROUP BY u.department
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 5
|
||||||
|
`;
|
||||||
|
const deptResult = await this.query(deptSql, [appId]);
|
||||||
|
|
||||||
|
// 使用趨勢(過去7天)
|
||||||
|
const trendSql = `
|
||||||
|
SELECT
|
||||||
|
DATE(viewed_at) as date,
|
||||||
|
COUNT(DISTINCT user_id) as users
|
||||||
|
FROM user_views
|
||||||
|
WHERE app_id = ? AND viewed_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||||
|
GROUP BY DATE(viewed_at)
|
||||||
|
ORDER BY date ASC
|
||||||
|
`;
|
||||||
|
const trendResult = await this.query(trendSql, [appId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dailyUsers: dailyResult.daily_users || 0,
|
||||||
|
weeklyUsers: weeklyResult.weekly_users || 0,
|
||||||
|
monthlyUsers: monthlyResult.monthly_users || 0,
|
||||||
|
totalSessions: totalResult.total_sessions || 0,
|
||||||
|
topDepartments: deptResult.map((row: any) => ({
|
||||||
|
department: row.department,
|
||||||
|
count: row.count
|
||||||
|
})),
|
||||||
|
trendData: trendResult.map((row: any) => ({
|
||||||
|
date: row.date,
|
||||||
|
users: row.users
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用使用統計錯誤:', error);
|
||||||
|
return {
|
||||||
|
dailyUsers: 0,
|
||||||
|
weeklyUsers: 0,
|
||||||
|
monthlyUsers: 0,
|
||||||
|
totalSessions: 0,
|
||||||
|
topDepartments: [],
|
||||||
|
trendData: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取應用評價列表
|
||||||
|
async getAppReviews(appId: string, limit: number = 10, offset: number = 0): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
ur.id,
|
||||||
|
ur.rating,
|
||||||
|
ur.rated_at,
|
||||||
|
u.name as user_name,
|
||||||
|
u.department as user_department,
|
||||||
|
u.avatar as user_avatar
|
||||||
|
FROM user_ratings ur
|
||||||
|
JOIN users u ON ur.user_id = u.id
|
||||||
|
WHERE ur.app_id = ?
|
||||||
|
ORDER BY ur.rated_at DESC
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const reviews = await this.query(sql, [appId]);
|
||||||
|
return reviews.map((review: any) => ({
|
||||||
|
id: review.id,
|
||||||
|
rating: review.rating,
|
||||||
|
review: '用戶評價', // 暫時使用固定文字,因為資料庫中沒有 review 欄位
|
||||||
|
ratedAt: review.rated_at,
|
||||||
|
userName: review.user_name,
|
||||||
|
userDepartment: review.user_department,
|
||||||
|
userAvatar: review.user_avatar
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用評價列表錯誤:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取應用評價總數
|
||||||
|
async getAppReviewCount(appId: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM user_ratings
|
||||||
|
WHERE app_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.queryOne(sql, [appId]);
|
||||||
|
return result.count || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取應用評價總數錯誤:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除評價
|
||||||
|
async deleteReview(reviewId: string, appId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
// 檢查評價是否存在且屬於該應用
|
||||||
|
const checkSql = `
|
||||||
|
SELECT id FROM user_ratings
|
||||||
|
WHERE id = ? AND app_id = ?
|
||||||
|
`;
|
||||||
|
const review = await this.queryOne(checkSql, [reviewId, appId]);
|
||||||
|
|
||||||
|
if (!review) {
|
||||||
|
return { success: false, error: '評價不存在或無權限刪除' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除評價
|
||||||
|
const deleteSql = 'DELETE FROM user_ratings WHERE id = ?';
|
||||||
|
await this.query(deleteSql, [reviewId]);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刪除評價錯誤:', error);
|
||||||
|
return { success: false, error: '刪除評價時發生錯誤' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
80
scripts/README.md
Normal file
80
scripts/README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 測試數據插入說明
|
||||||
|
|
||||||
|
## 問題修復
|
||||||
|
|
||||||
|
✅ **已修復重複約束錯誤**:原來的腳本會因為同一個用戶對同一個應用重複評分而出現 `Duplicate entry` 錯誤。現在已修復為使用不同的用戶 ID。
|
||||||
|
|
||||||
|
✅ **已修復 null 值錯誤**:修復了 `user_id` 不能為 null 的問題,提供了簡化版本的腳本。
|
||||||
|
|
||||||
|
## 腳本版本
|
||||||
|
|
||||||
|
### 版本 1:完整版本 (`insert-test-data.sql`)
|
||||||
|
- 使用不同的用戶 ID 避免重複約束
|
||||||
|
- 需要資料庫中有至少 10 個活躍用戶
|
||||||
|
|
||||||
|
### 版本 2:簡化版本 (`insert-test-data-simple.sql`)
|
||||||
|
- 使用單一用戶 ID,避免 null 值問題
|
||||||
|
- 只需要資料庫中有至少 1 個活躍用戶
|
||||||
|
- 只有 1 條評分記錄
|
||||||
|
|
||||||
|
### 版本 3:多用戶版本 (`insert-test-data-multi-user.sql`) ⭐ 推薦
|
||||||
|
- 使用 5 個不同用戶,避免重複約束
|
||||||
|
- 需要資料庫中有至少 5 個活躍用戶
|
||||||
|
- 包含 25 條瀏覽、8 條按讚、5 條評分記錄
|
||||||
|
- 平衡了數據豐富度和穩定性
|
||||||
|
|
||||||
|
## 方法一:使用 MySQL 命令行
|
||||||
|
|
||||||
|
1. 打開命令提示符或 PowerShell
|
||||||
|
2. 連接到 MySQL:
|
||||||
|
```bash
|
||||||
|
mysql -u root -p
|
||||||
|
```
|
||||||
|
3. 選擇資料庫:
|
||||||
|
```sql
|
||||||
|
USE ai_showcase_platform;
|
||||||
|
```
|
||||||
|
4. 執行 SQL 腳本(推薦使用多用戶版本):
|
||||||
|
```sql
|
||||||
|
source E:/ai-showcase-platform/scripts/insert-test-data-multi-user.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 方法二:使用 MySQL Workbench 或其他 GUI 工具
|
||||||
|
|
||||||
|
1. 打開 MySQL Workbench
|
||||||
|
2. 連接到您的 MySQL 服務器
|
||||||
|
3. 選擇 `ai_showcase_platform` 資料庫
|
||||||
|
4. 打開 `scripts/insert-test-data-multi-user.sql` 文件(推薦)
|
||||||
|
5. 執行整個腳本
|
||||||
|
|
||||||
|
## 方法三:使用 phpMyAdmin
|
||||||
|
|
||||||
|
1. 打開 phpMyAdmin
|
||||||
|
2. 選擇 `ai_showcase_platform` 資料庫
|
||||||
|
3. 點擊 "SQL" 標籤
|
||||||
|
4. 複製 `scripts/insert-test-data-multi-user.sql` 的內容(推薦)
|
||||||
|
5. 貼上並執行
|
||||||
|
|
||||||
|
## 預期結果
|
||||||
|
|
||||||
|
執行成功後,您應該看到:
|
||||||
|
- 25 條瀏覽記錄(使用 5 個不同用戶)
|
||||||
|
- 8 條按讚記錄(使用 5 個不同用戶)
|
||||||
|
- 5 條評分記錄(使用 5 個不同用戶)
|
||||||
|
- 應用統計數據更新為:25 瀏覽、8 讚、4.2 平均評分
|
||||||
|
|
||||||
|
## 驗證
|
||||||
|
|
||||||
|
執行完成後,您可以:
|
||||||
|
1. 重新載入應用管理頁面
|
||||||
|
2. 點擊任何應用的「查看詳情」
|
||||||
|
3. 切換到「統計數據」標籤頁查看真實數據
|
||||||
|
4. 切換到「評價管理」標籤頁查看評價列表
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- 腳本會先清空現有的測試數據,避免重複
|
||||||
|
- **簡化版本**:使用單一用戶 ID,只有 1 條評分記錄
|
||||||
|
- **多用戶版本**:使用 5 個不同用戶,平衡數據豐富度和穩定性 ⭐ 推薦
|
||||||
|
- **完整版本**:使用不同用戶 ID,需要至少 10 個活躍用戶
|
||||||
|
- 如果您的資料庫中用戶數量少於 5 個,建議使用簡化版本
|
180
scripts/insert-real-test-data.js
Normal file
180
scripts/insert-real-test-data.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
async function insertTestData() {
|
||||||
|
console.log('📊 在資料庫中插入真實測試數據...\n');
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
// 連接資料庫
|
||||||
|
connection = await mysql.createConnection({
|
||||||
|
host: 'localhost',
|
||||||
|
user: 'root',
|
||||||
|
password: '123456',
|
||||||
|
database: 'ai_showcase_platform'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 資料庫連接成功');
|
||||||
|
|
||||||
|
// 1. 獲取現有的應用和用戶
|
||||||
|
const [apps] = await connection.execute('SELECT id FROM apps WHERE is_active = TRUE LIMIT 3');
|
||||||
|
const [users] = await connection.execute('SELECT id FROM users WHERE status = "active" LIMIT 10');
|
||||||
|
|
||||||
|
if (apps.length === 0) {
|
||||||
|
console.log('❌ 沒有找到現有應用,無法創建測試數據');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📱 找到 ${apps.length} 個應用`);
|
||||||
|
console.log(`👥 找到 ${users.length} 個用戶`);
|
||||||
|
|
||||||
|
const targetAppId = apps[0].id; // 使用第一個應用作為目標
|
||||||
|
console.log(`🎯 目標應用 ID: ${targetAppId}`);
|
||||||
|
|
||||||
|
// 2. 清空現有的測試數據(如果有的話)
|
||||||
|
console.log('\n🧹 清空現有測試數據...');
|
||||||
|
await connection.execute('DELETE FROM user_views WHERE app_id = ?', [targetAppId]);
|
||||||
|
await connection.execute('DELETE FROM user_likes WHERE app_id = ?', [targetAppId]);
|
||||||
|
await connection.execute('DELETE FROM user_ratings WHERE app_id = ?', [targetAppId]);
|
||||||
|
console.log('✅ 現有測試數據已清空');
|
||||||
|
|
||||||
|
// 3. 插入用戶瀏覽記錄
|
||||||
|
console.log('\n👀 插入用戶瀏覽記錄...');
|
||||||
|
const views = [];
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
const userId = users[Math.floor(Math.random() * users.length)].id;
|
||||||
|
const viewedAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000); // 過去30天內
|
||||||
|
const ipAddress = `192.168.1.${Math.floor(Math.random() * 255)}`;
|
||||||
|
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
|
||||||
|
|
||||||
|
views.push([targetAppId, userId, viewedAt, ipAddress, userAgent]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
await connection.execute(`
|
||||||
|
INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent)
|
||||||
|
VALUES (UUID(), ?, ?, ?, ?, ?)
|
||||||
|
`, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 插入了 ${views.length} 個瀏覽記錄`);
|
||||||
|
|
||||||
|
// 4. 插入用戶按讚記錄
|
||||||
|
console.log('\n❤️ 插入用戶按讚記錄...');
|
||||||
|
const likes = [];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const userId = users[Math.floor(Math.random() * users.length)].id;
|
||||||
|
const likedAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
likes.push([targetAppId, userId, likedAt]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const like of likes) {
|
||||||
|
await connection.execute(`
|
||||||
|
INSERT INTO user_likes (id, app_id, user_id, liked_at)
|
||||||
|
VALUES (UUID(), ?, ?, ?)
|
||||||
|
`, like);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 插入了 ${likes.length} 個按讚記錄`);
|
||||||
|
|
||||||
|
// 5. 插入用戶評分記錄
|
||||||
|
console.log('\n⭐ 插入用戶評分記錄...');
|
||||||
|
const ratings = [
|
||||||
|
{ rating: 5, review: '這個應用非常實用,界面設計得很棒!' },
|
||||||
|
{ rating: 4, review: '功能強大,處理速度很快,推薦使用。' },
|
||||||
|
{ rating: 5, review: '非常好用的工具,節省了很多時間。' },
|
||||||
|
{ rating: 3, review: '還不錯,但還有改進空間。' },
|
||||||
|
{ rating: 4, review: '界面簡潔,操作簡單,很滿意。' },
|
||||||
|
{ rating: 5, review: '功能齊全,使用體驗很好。' },
|
||||||
|
{ rating: 4, review: '整體不錯,希望能增加更多功能。' },
|
||||||
|
{ rating: 5, review: '非常推薦,已經推薦給同事了。' },
|
||||||
|
{ rating: 3, review: '還可以,但界面可以再優化。' },
|
||||||
|
{ rating: 4, review: '實用性很高,值得使用。' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < ratings.length; i++) {
|
||||||
|
const userId = users[Math.floor(Math.random() * users.length)].id;
|
||||||
|
const ratedAt = new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
await connection.execute(`
|
||||||
|
INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at)
|
||||||
|
VALUES (UUID(), ?, ?, ?, ?)
|
||||||
|
`, [targetAppId, userId, ratings[i].rating, ratedAt]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 插入了 ${ratings.length} 個評分記錄`);
|
||||||
|
|
||||||
|
// 6. 更新應用的統計數據
|
||||||
|
console.log('\n📈 更新應用統計數據...');
|
||||||
|
const [viewCount] = await connection.execute(
|
||||||
|
'SELECT COUNT(*) as count FROM user_views WHERE app_id = ?',
|
||||||
|
[targetAppId]
|
||||||
|
);
|
||||||
|
const [likeCount] = await connection.execute(
|
||||||
|
'SELECT COUNT(*) as count FROM user_likes WHERE app_id = ?',
|
||||||
|
[targetAppId]
|
||||||
|
);
|
||||||
|
|
||||||
|
await connection.execute(`
|
||||||
|
UPDATE apps
|
||||||
|
SET views_count = ?, likes_count = ?, updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
`, [viewCount[0].count, likeCount[0].count, targetAppId]);
|
||||||
|
|
||||||
|
console.log(` - 應用 ${targetAppId}: ${viewCount[0].count} 瀏覽, ${likeCount[0].count} 讚`);
|
||||||
|
|
||||||
|
// 7. 顯示最終統計
|
||||||
|
console.log('\n📊 數據插入完成!最終統計:');
|
||||||
|
|
||||||
|
const [appStats] = await connection.execute(`
|
||||||
|
SELECT
|
||||||
|
views_count,
|
||||||
|
likes_count
|
||||||
|
FROM apps
|
||||||
|
WHERE id = ?
|
||||||
|
`, [targetAppId]);
|
||||||
|
|
||||||
|
const [ratingStats] = await connection.execute(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_ratings,
|
||||||
|
AVG(rating) as avg_rating
|
||||||
|
FROM user_ratings
|
||||||
|
WHERE app_id = ?
|
||||||
|
`, [targetAppId]);
|
||||||
|
|
||||||
|
const [viewStats] = await connection.execute(`
|
||||||
|
SELECT COUNT(*) as total_views FROM user_views WHERE app_id = ?
|
||||||
|
`, [targetAppId]);
|
||||||
|
|
||||||
|
const [likeStats] = await connection.execute(`
|
||||||
|
SELECT COUNT(*) as total_likes FROM user_likes WHERE app_id = ?
|
||||||
|
`, [targetAppId]);
|
||||||
|
|
||||||
|
console.log(` - 應用瀏覽數: ${appStats[0].views_count}`);
|
||||||
|
console.log(` - 應用讚數: ${appStats[0].likes_count}`);
|
||||||
|
console.log(` - 總瀏覽數: ${viewStats[0].total_views}`);
|
||||||
|
console.log(` - 總讚數: ${likeStats[0].total_likes}`);
|
||||||
|
console.log(` - 評分總數: ${ratingStats[0].total_ratings}`);
|
||||||
|
console.log(` - 平均評分: ${ratingStats[0].avg_rating.toFixed(1)}`);
|
||||||
|
|
||||||
|
console.log('\n🎉 真實測試數據插入完成!');
|
||||||
|
console.log('\n💡 現在您可以重新載入應用管理頁面查看真實的資料庫數據');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 插入測試數據時發生錯誤:', error);
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
console.log('\n💡 請確保 MySQL 服務正在運行,並且連接參數正確');
|
||||||
|
console.log(' - Host: localhost');
|
||||||
|
console.log(' - User: root');
|
||||||
|
console.log(' - Password: 123456');
|
||||||
|
console.log(' - Database: ai_showcase_platform');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
console.log('\n🔌 資料庫連接已關閉');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertTestData();
|
80
scripts/insert-test-data-multi-user.sql
Normal file
80
scripts/insert-test-data-multi-user.sql
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
-- 插入真實測試數據到資料庫(多用戶版本)
|
||||||
|
-- 請在 MySQL 中執行此腳本
|
||||||
|
|
||||||
|
-- 1. 檢查是否有足夠的用戶
|
||||||
|
SELECT COUNT(*) as user_count FROM users WHERE status = 'active';
|
||||||
|
|
||||||
|
-- 2. 清空現有的測試數據(如果有的話)
|
||||||
|
DELETE FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
DELETE FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
DELETE FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
|
||||||
|
-- 3. 獲取多個用戶的 ID
|
||||||
|
SET @user1_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0);
|
||||||
|
SET @user2_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1);
|
||||||
|
SET @user3_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2);
|
||||||
|
SET @user4_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3);
|
||||||
|
SET @user5_id = (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4);
|
||||||
|
|
||||||
|
-- 4. 插入用戶瀏覽記錄 (25條) - 使用多個用戶
|
||||||
|
INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-20 10:30:00', '192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-19 14:20:00', '192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-18 09:15:00', '192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-17 16:45:00', '192.168.1.103', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-16 11:30:00', '192.168.1.104', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-15 08:20:00', '192.168.1.105', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-14 15:10:00', '192.168.1.106', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-13 12:45:00', '192.168.1.107', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-12 17:30:00', '192.168.1.108', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-11 09:15:00', '192.168.1.109', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-10 14:25:00', '192.168.1.110', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-09 11:40:00', '192.168.1.111', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-08 16:55:00', '192.168.1.112', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-07 13:20:00', '192.168.1.113', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-06 10:35:00', '192.168.1.114', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-05 15:50:00', '192.168.1.115', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-04 12:15:00', '192.168.1.116', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-03 09:30:00', '192.168.1.117', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-02 17:45:00', '192.168.1.118', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-01 14:20:00', '192.168.1.119', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2023-12-31 11:10:00', '192.168.1.120', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2023-12-30 16:25:00', '192.168.1.121', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2023-12-29 13:40:00', '192.168.1.122', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2023-12-28 10:55:00', '192.168.1.123', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2023-12-27 15:15:00', '192.168.1.124', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||||
|
|
||||||
|
-- 5. 插入用戶按讚記錄 (8條) - 使用多個用戶
|
||||||
|
INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-20 10:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-19 14:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-18 09:20:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, '2024-01-17 16:50:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, '2024-01-16 11:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, '2024-01-15 08:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, '2024-01-14 15:15:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, '2024-01-13 12:50:00');
|
||||||
|
|
||||||
|
-- 6. 插入用戶評分記錄 (5條) - 使用不同用戶,避免重複約束
|
||||||
|
INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user1_id, 5, '2024-01-20 10:40:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user2_id, 4, '2024-01-19 14:30:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user3_id, 5, '2024-01-18 09:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user4_id, 3, '2024-01-17 16:55:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @user5_id, 4, '2024-01-16 11:40:00');
|
||||||
|
|
||||||
|
-- 7. 更新應用的統計數據
|
||||||
|
UPDATE apps
|
||||||
|
SET
|
||||||
|
views_count = (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'),
|
||||||
|
likes_count = (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
|
||||||
|
-- 8. 顯示統計摘要
|
||||||
|
SELECT
|
||||||
|
'統計摘要' as 項目,
|
||||||
|
(SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總瀏覽數,
|
||||||
|
(SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總讚數,
|
||||||
|
(SELECT COUNT(*) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總評分數,
|
||||||
|
(SELECT AVG(rating) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 平均評分;
|
72
scripts/insert-test-data-simple.sql
Normal file
72
scripts/insert-test-data-simple.sql
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
-- 插入真實測試數據到資料庫(簡化版本)
|
||||||
|
-- 請在 MySQL 中執行此腳本
|
||||||
|
|
||||||
|
-- 1. 檢查是否有足夠的用戶
|
||||||
|
SELECT COUNT(*) as user_count FROM users WHERE status = 'active';
|
||||||
|
|
||||||
|
-- 2. 清空現有的測試數據(如果有的話)
|
||||||
|
DELETE FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
DELETE FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
DELETE FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
|
||||||
|
-- 3. 獲取第一個用戶的 ID 作為默認用戶
|
||||||
|
SET @default_user_id = (SELECT id FROM users WHERE status = 'active' LIMIT 1);
|
||||||
|
|
||||||
|
-- 4. 插入用戶瀏覽記錄 (25條) - 使用默認用戶
|
||||||
|
INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-20 10:30:00', '192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-19 14:20:00', '192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-18 09:15:00', '192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-17 16:45:00', '192.168.1.103', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-16 11:30:00', '192.168.1.104', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-15 08:20:00', '192.168.1.105', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-14 15:10:00', '192.168.1.106', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-13 12:45:00', '192.168.1.107', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-12 17:30:00', '192.168.1.108', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-11 09:15:00', '192.168.1.109', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-10 14:25:00', '192.168.1.110', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-09 11:40:00', '192.168.1.111', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-08 16:55:00', '192.168.1.112', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-07 13:20:00', '192.168.1.113', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-06 10:35:00', '192.168.1.114', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-05 15:50:00', '192.168.1.115', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-04 12:15:00', '192.168.1.116', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-03 09:30:00', '192.168.1.117', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-02 17:45:00', '192.168.1.118', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-01 14:20:00', '192.168.1.119', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-31 11:10:00', '192.168.1.120', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-30 16:25:00', '192.168.1.121', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-29 13:40:00', '192.168.1.122', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-28 10:55:00', '192.168.1.123', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2023-12-27 15:15:00', '192.168.1.124', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||||
|
|
||||||
|
-- 5. 插入用戶按讚記錄 (8條) - 使用默認用戶
|
||||||
|
INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-20 10:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-19 14:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-18 09:20:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-17 16:50:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-16 11:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-15 08:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-14 15:15:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, '2024-01-13 12:50:00');
|
||||||
|
|
||||||
|
-- 6. 插入用戶評分記錄 (1條) - 使用默認用戶,避免重複約束
|
||||||
|
INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', @default_user_id, 5, '2024-01-20 10:40:00');
|
||||||
|
|
||||||
|
-- 7. 更新應用的統計數據
|
||||||
|
UPDATE apps
|
||||||
|
SET
|
||||||
|
views_count = (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'),
|
||||||
|
likes_count = (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
|
||||||
|
-- 8. 顯示統計摘要
|
||||||
|
SELECT
|
||||||
|
'統計摘要' as 項目,
|
||||||
|
(SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總瀏覽數,
|
||||||
|
(SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總讚數,
|
||||||
|
(SELECT COUNT(*) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總評分數,
|
||||||
|
(SELECT AVG(rating) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 平均評分;
|
78
scripts/insert-test-data.sql
Normal file
78
scripts/insert-test-data.sql
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
-- 插入真實測試數據到資料庫
|
||||||
|
-- 請在 MySQL 中執行此腳本
|
||||||
|
|
||||||
|
-- 1. 檢查是否有足夠的用戶
|
||||||
|
SELECT COUNT(*) as user_count FROM users WHERE status = 'active';
|
||||||
|
|
||||||
|
-- 2. 清空現有的測試數據(如果有的話)
|
||||||
|
DELETE FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
DELETE FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
DELETE FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
|
||||||
|
-- 2. 插入用戶瀏覽記錄 (25條) - 使用不同的用戶避免重複約束
|
||||||
|
INSERT INTO user_views (id, app_id, user_id, viewed_at, ip_address, user_agent) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2024-01-20 10:30:00', '192.168.1.100', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2024-01-19 14:20:00', '192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2024-01-18 09:15:00', '192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2024-01-17 16:45:00', '192.168.1.103', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2024-01-16 11:30:00', '192.168.1.104', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), '2024-01-15 08:20:00', '192.168.1.105', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), '2024-01-14 15:10:00', '192.168.1.106', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), '2024-01-13 12:45:00', '192.168.1.107', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 8), '2024-01-12 17:30:00', '192.168.1.108', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 9), '2024-01-11 09:15:00', '192.168.1.109', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2024-01-10 14:25:00', '192.168.1.110', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2024-01-09 11:40:00', '192.168.1.111', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2024-01-08 16:55:00', '192.168.1.112', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2024-01-07 13:20:00', '192.168.1.113', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2024-01-06 10:35:00', '192.168.1.114', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), '2024-01-05 15:50:00', '192.168.1.115', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), '2024-01-04 12:15:00', '192.168.1.116', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), '2024-01-03 09:30:00', '192.168.1.117', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 8), '2024-01-02 17:45:00', '192.168.1.118', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 9), '2024-01-01 14:20:00', '192.168.1.119', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2023-12-31 11:10:00', '192.168.1.120', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2023-12-30 16:25:00', '192.168.1.121', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2023-12-29 13:40:00', '192.168.1.122', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2023-12-28 10:55:00', '192.168.1.123', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2023-12-27 15:15:00', '192.168.1.124', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||||
|
|
||||||
|
-- 3. 插入用戶按讚記錄 (8條) - 使用不同的用戶避免重複約束
|
||||||
|
INSERT INTO user_likes (id, app_id, user_id, liked_at) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), '2024-01-20 10:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), '2024-01-19 14:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), '2024-01-18 09:20:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), '2024-01-17 16:50:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), '2024-01-16 11:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), '2024-01-15 08:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), '2024-01-14 15:15:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), '2024-01-13 12:50:00');
|
||||||
|
|
||||||
|
-- 4. 插入用戶評分記錄 (10條) - 使用不同的用戶避免重複約束
|
||||||
|
INSERT INTO user_ratings (id, app_id, user_id, rating, rated_at) VALUES
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 0), 5, '2024-01-20 10:40:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 1), 4, '2024-01-19 14:30:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 2), 5, '2024-01-18 09:25:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 3), 3, '2024-01-17 16:55:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 4), 4, '2024-01-16 11:40:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 5), 5, '2024-01-15 08:30:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 6), 4, '2024-01-14 15:20:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 7), 5, '2024-01-13 12:55:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 8), 4, '2024-01-12 17:35:00'),
|
||||||
|
(UUID(), '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7', (SELECT id FROM users WHERE status = 'active' ORDER BY id LIMIT 1 OFFSET 9), 5, '2024-01-11 09:20:00');
|
||||||
|
|
||||||
|
-- 5. 更新應用的統計數據
|
||||||
|
UPDATE apps
|
||||||
|
SET
|
||||||
|
views_count = (SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'),
|
||||||
|
likes_count = (SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7'),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7';
|
||||||
|
|
||||||
|
-- 6. 顯示統計摘要
|
||||||
|
SELECT
|
||||||
|
'統計摘要' as 項目,
|
||||||
|
(SELECT COUNT(*) FROM user_views WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總瀏覽數,
|
||||||
|
(SELECT COUNT(*) FROM user_likes WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總讚數,
|
||||||
|
(SELECT COUNT(*) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 總評分數,
|
||||||
|
(SELECT AVG(rating) FROM user_ratings WHERE app_id = '7f7395f4-ad9f-4d14-9e2c-84962ecbcfd7') as 平均評分;
|
28
scripts/simple-check.js
Normal file
28
scripts/simple-check.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
async function simpleCheck() {
|
||||||
|
try {
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: 'localhost',
|
||||||
|
user: 'root',
|
||||||
|
password: '123456',
|
||||||
|
database: 'ai_showcase_platform'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 資料庫連接成功');
|
||||||
|
|
||||||
|
// 檢查 user_ratings 表
|
||||||
|
const [ratings] = await connection.execute('SELECT * FROM user_ratings LIMIT 5');
|
||||||
|
console.log('📊 user_ratings 數據:', ratings);
|
||||||
|
|
||||||
|
// 檢查 apps 表
|
||||||
|
const [apps] = await connection.execute('SELECT id, name FROM apps LIMIT 3');
|
||||||
|
console.log('📱 apps 數據:', apps);
|
||||||
|
|
||||||
|
await connection.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 錯誤:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleCheck();
|
Reference in New Issue
Block a user