Initial commit

This commit is contained in:
2025-07-18 13:07:28 +08:00
commit e3832acfa8
91 changed files with 10929 additions and 0 deletions

250
lib/background-music.ts Normal file
View File

@@ -0,0 +1,250 @@
// 背景音樂管理系統 - 修復重新播放問題
class BackgroundMusicManager {
private audio: HTMLAudioElement | null = null
private isPlaying = false
private enabled = false
private volume = 0.3
private fadeInterval: NodeJS.Timeout | null = null
constructor() {
this.initAudio()
this.loadState()
}
private loadState() {
try {
const savedState = localStorage.getItem("backgroundMusicState")
if (savedState) {
const state = JSON.parse(savedState)
this.volume = state.volume || 0.3
this.enabled = state.enabled || false
// 不要自動恢復播放狀態,讓用戶手動控制
this.isPlaying = false
}
} catch (error) {
// 忽略載入錯誤
}
}
private saveState() {
try {
const state = {
volume: this.volume,
enabled: this.enabled,
isPlaying: this.isPlaying,
}
localStorage.setItem("backgroundMusicState", JSON.stringify(state))
} catch (error) {
// 忽略保存錯誤
}
}
private initAudio() {
try {
this.audio = new Audio("https://hebbkx1anhila5yf.public.blob.vercel-storage.com/just-relax-11157-iAgp15dV2YGybAezUJFtKmKZPbteXd.mp3")
this.audio.loop = true
this.audio.volume = this.volume
this.audio.preload = "metadata"
// 明確禁用自動播放相關屬性
this.audio.autoplay = false
this.audio.muted = false
// 靜默處理載入完成
this.audio.addEventListener("canplaythrough", () => {
// 音樂載入完成
})
// 靜默錯誤處理
this.audio.addEventListener("error", (e) => {
// 重新初始化音頻對象
this.reinitAudio()
})
// 播放結束處理
this.audio.addEventListener("ended", () => {
if (this.enabled && this.isPlaying) {
// 重新播放
this.audio?.play().catch(() => {
// 如果播放失敗,重新初始化
this.reinitAudio()
})
}
})
} catch (error) {
// 靜默處理初始化錯誤
}
}
// 重新初始化音頻對象
private reinitAudio() {
try {
if (this.audio) {
this.audio.pause()
this.audio.src = ""
this.audio = null
}
// 重新創建音頻對象
setTimeout(() => {
this.initAudio()
}, 100)
} catch (error) {
// 靜默處理錯誤
}
}
// 淡入效果
private fadeIn(duration = 2000) {
if (!this.audio) return
this.audio.volume = 0
const targetVolume = this.volume
const steps = 50
const stepTime = duration / steps
const volumeStep = targetVolume / steps
let currentStep = 0
this.fadeInterval = setInterval(() => {
if (currentStep >= steps || !this.audio) {
if (this.fadeInterval) {
clearInterval(this.fadeInterval)
this.fadeInterval = null
}
if (this.audio) {
this.audio.volume = targetVolume
}
return
}
this.audio.volume = Math.min(volumeStep * currentStep, targetVolume)
currentStep++
}, stepTime)
}
// 淡出效果
private fadeOut(duration = 1000) {
if (!this.audio) return
const startVolume = this.audio.volume
const steps = 50
const stepTime = duration / steps
const volumeStep = startVolume / steps
let currentStep = 0
this.fadeInterval = setInterval(() => {
if (currentStep >= steps || !this.audio) {
if (this.fadeInterval) {
clearInterval(this.fadeInterval)
this.fadeInterval = null
}
if (this.audio) {
this.audio.pause()
this.audio.currentTime = 0 // 重置播放位置
this.audio.volume = this.volume // 恢復原始音量
}
return
}
this.audio.volume = Math.max(startVolume - volumeStep * currentStep, 0)
currentStep++
}, stepTime)
}
async start() {
// 清除任何進行中的淡出效果
if (this.fadeInterval) {
clearInterval(this.fadeInterval)
this.fadeInterval = null
}
// 如果音頻對象不存在,重新初始化
if (!this.audio) {
this.initAudio()
// 等待一下讓音頻對象初始化完成
await new Promise((resolve) => setTimeout(resolve, 100))
}
if (!this.audio) return
try {
this.enabled = true
this.isPlaying = true
this.saveState()
// 確保音頻對象處於正確狀態
this.audio.currentTime = 0
this.audio.volume = 0 // 從0開始準備淡入
// 開始播放並淡入
await this.audio.play()
this.fadeIn(2000)
} catch (error) {
// 播放失敗,重新初始化並重試
this.reinitAudio()
this.isPlaying = false
this.enabled = false
this.saveState()
}
}
stop() {
if (!this.audio) return
this.enabled = false
this.isPlaying = false
this.saveState()
// 清除淡入效果
if (this.fadeInterval) {
clearInterval(this.fadeInterval)
this.fadeInterval = null
}
// 淡出並停止
this.fadeOut(1000)
}
setVolume(volume: number) {
this.volume = Math.max(0, Math.min(1, volume))
if (this.audio && this.isPlaying) {
this.audio.volume = this.volume
}
this.saveState()
}
getVolume() {
return this.volume
}
isEnabled() {
return this.enabled
}
getIsPlaying() {
return this.isPlaying && this.audio && !this.audio.paused
}
// 獲取當前狀態
getState() {
return {
isPlaying: this.getIsPlaying(),
enabled: this.enabled,
volume: this.volume,
}
}
// 獲取音樂資訊
getMusicInfo() {
if (!this.audio) return null
return {
duration: this.audio.duration || 0,
currentTime: this.audio.currentTime || 0,
loaded: this.audio.readyState >= 3,
}
}
}
// 全局背景音樂管理器
export const backgroundMusicManager = new BackgroundMusicManager()

378
lib/categorization.ts Normal file
View File

@@ -0,0 +1,378 @@
// 工作痛點分類系統 - 精簡版 10 個分類
export const categories = [
{
name: "重複作業",
description: "每天遇到都在做的重工、報表、開票等例行事務",
keywords: [
"重複",
"重工",
"報表",
"開票",
"例行",
"每天",
"固定",
"重複性",
"例行公事",
"重複工作",
"日常作業",
"定期",
"週期性",
"重複操作",
"機械化",
"單調",
],
color: "#3B82F6",
bgColor: "from-blue-500/20 to-blue-600/20",
borderColor: "border-blue-400/30",
textColor: "text-blue-200",
icon: "🔄",
},
{
name: "數據混亂",
description: "資料統整困難、爆表、無法分析或視覺化",
keywords: [
"數據",
"資料",
"統整",
"爆表",
"分析",
"視覺化",
"整理",
"混亂",
"散亂",
"不一致",
"格式",
"匯入",
"匯出",
"Excel",
"CSV",
"報告",
"圖表",
],
color: "#EC4899",
bgColor: "from-pink-500/20 to-rose-600/20",
borderColor: "border-pink-400/30",
textColor: "text-pink-200",
icon: "📊",
},
{
name: "找不到資料",
description: "文件、SOP、規則等資訊難查找",
keywords: [
"找不到",
"文件",
"SOP",
"規則",
"資訊",
"查找",
"搜尋",
"文檔",
"手冊",
"說明",
"指引",
"標準",
"程序",
"知識",
"資料庫",
"檔案",
"位置",
],
color: "#F59E0B",
bgColor: "from-yellow-500/20 to-amber-600/20",
borderColor: "border-yellow-400/30",
textColor: "text-yellow-200",
icon: "🔍",
},
{
name: "流程卡關",
description: "請購、採購、維修等流程追蹤不到",
keywords: [
"流程",
"請購",
"採購",
"維修",
"追蹤",
"卡關",
"停滯",
"延遲",
"審核",
"簽核",
"申請",
"進度",
"狀態",
"追不到",
"不知道",
"等待",
],
color: "#EF4444",
bgColor: "from-red-500/20 to-rose-600/20",
borderColor: "border-red-400/30",
textColor: "text-red-200",
icon: "🚧",
},
{
name: "文件太多",
description: "表單量、會議紀錄等需人工輸入/比對",
keywords: [
"文件",
"表單",
"會議紀錄",
"人工輸入",
"比對",
"繁瑣",
"太多",
"大量",
"手動",
"輸入",
"填寫",
"紙本",
"列印",
"掃描",
"歸檔",
"整理",
],
color: "#8B5CF6",
bgColor: "from-purple-500/20 to-violet-600/20",
borderColor: "border-purple-400/30",
textColor: "text-purple-200",
icon: "📄",
},
{
name: "協作混亂",
description: "人工配置、斷線、跨部門難協調",
keywords: [
"協作",
"人工配置",
"斷線",
"跨部門",
"協調",
"溝通",
"配合",
"團隊",
"合作",
"聯繫",
"回應",
"討論",
"會議",
"同事",
"部門",
"協調",
],
color: "#10B981",
bgColor: "from-green-500/20 to-emerald-600/20",
borderColor: "border-green-400/30",
textColor: "text-green-200",
icon: "🤝",
},
{
name: "時間浪費",
description: "等資料、等簽核、等開會系統等無效率時間",
keywords: [
"時間",
"等資料",
"等簽核",
"等開會",
"無效率",
"浪費",
"等待",
"拖延",
"慢",
"效率",
"速度",
"加班",
"忙碌",
"趕工",
"deadline",
"截止",
],
color: "#F97316",
bgColor: "from-orange-500/20 to-red-600/20",
borderColor: "border-orange-400/30",
textColor: "text-orange-200",
icon: "⏰",
},
{
name: "諮詢煩重",
description: "客戶或內部常見問答人力吃緊",
keywords: [
"諮詢",
"客戶",
"內部",
"問答",
"人力吃緊",
"常見問題",
"重複問題",
"客服",
"支援",
"解答",
"回覆",
"詢問",
"疑問",
"FAQ",
"服務",
"處理",
],
color: "#06B6D4",
bgColor: "from-cyan-500/20 to-teal-600/20",
borderColor: "border-cyan-400/30",
textColor: "text-cyan-200",
icon: "💬",
},
{
name: "權限不清",
description: "權限管理混亂、責任不明",
keywords: [
"權限",
"管理",
"混亂",
"責任",
"不明",
"不清楚",
"授權",
"存取",
"帳號",
"密碼",
"登入",
"角色",
"職責",
"負責人",
"歸屬",
"分工",
],
color: "#84CC16",
bgColor: "from-lime-500/20 to-green-600/20",
borderColor: "border-lime-400/30",
textColor: "text-lime-200",
icon: "🔐",
},
{
name: "系統太多",
description: "多系統操作、切換成本高",
keywords: [
"系統",
"多系統",
"切換",
"成本高",
"太多",
"太雜",
"操作",
"介面",
"平台",
"工具",
"軟體",
"應用程式",
"登入",
"帳號",
"整合",
"統一",
],
color: "#6366F1",
bgColor: "from-indigo-500/20 to-purple-600/20",
borderColor: "border-indigo-400/30",
textColor: "text-indigo-200",
icon: "💻",
},
]
export interface Wish {
id: number
title: string
currentPain: string
expectedSolution: string
expectedEffect: string
createdAt: string
}
export interface Category {
name: string
description: string
keywords: string[]
color: string
bgColor: string
borderColor: string
textColor: string
icon: string
}
// 多標籤自動分類函數 - 最多返回3個
export function categorizeWishMultiple(wish: Wish): Category[] {
const fullText = `${wish.title} ${wish.currentPain} ${wish.expectedSolution} ${wish.expectedEffect}`.toLowerCase()
const matchedCategories: Category[] = []
// 檢查每個分類,收集所有匹配的分類
categories.forEach((category) => {
const matchedKeywords = category.keywords.filter((keyword) => fullText.includes(keyword.toLowerCase()))
if (matchedKeywords.length > 0) {
matchedCategories.push({
...category,
matchScore: matchedKeywords.length, // 添加匹配分數
} as Category & { matchScore: number })
}
})
// 如果有匹配的分類按匹配分數排序並返回最多3個
if (matchedCategories.length > 0) {
return matchedCategories.sort((a, b) => (b as any).matchScore - (a as any).matchScore).slice(0, 3) // 最多3個標籤
}
// 如果沒有匹配到任何分類,返回默認分類
return [
{
name: "其他問題",
description: "未能歸類的特殊工作困擾",
keywords: [],
color: "#6B7280",
bgColor: "from-gray-500/20 to-slate-600/20",
borderColor: "border-gray-400/30",
textColor: "text-gray-200",
icon: "❓",
},
]
}
// 保持向後兼容的單一分類函數
export function categorizeWish(wish: Wish): Category {
const categories = categorizeWishMultiple(wish)
return categories[0] // 返回最匹配的分類
}
// 獲取所有願望的分類統計(支持多標籤)
export function getCategoryStats(wishes: Wish[]) {
const stats: { [key: string]: number } = {}
// 初始化統計
categories.forEach((cat) => {
stats[cat.name] = 0
})
stats["其他問題"] = 0
// 統計每個分類的數量(多標籤計算)
wishes.forEach((wish) => {
const wishCategories = categorizeWishMultiple(wish)
wishCategories.forEach((category) => {
stats[category.name]++
})
})
return stats
}
// 獲取分類統計(用於雷達圖,避免重複計算)
export function getCategoryStatsForRadar(wishes: Wish[]) {
const stats: { [key: string]: number } = {}
// 初始化統計
categories.forEach((cat) => {
stats[cat.name] = 0
})
stats["其他問題"] = 0
// 統計每個分類的數量(每個願望只計算一次,取主要分類)
wishes.forEach((wish) => {
const primaryCategory = categorizeWish(wish)
stats[primaryCategory.name]++
})
return stats
}

View File

@@ -0,0 +1,259 @@
// 智能解決方案推薦系統
export interface SolutionCategory {
id: string
name: string
icon: string
description: string
color: string
bgColor: string
borderColor: string
textColor: string
keywords: string[]
examples: string[]
benefits: string[]
difficulty: "easy" | "medium" | "hard"
timeframe: string
techStack: string[]
}
export const solutionCategories: SolutionCategory[] = [
{
id: "chatbot",
name: "聊天機器人",
icon: "🤖",
description: "建立對話式助手,回答常見問題、流程說明",
color: "#3B82F6",
bgColor: "from-blue-500/20 to-blue-600/20",
borderColor: "border-blue-400/30",
textColor: "text-blue-200",
keywords: ["問答", "諮詢", "客服", "常見問題", "FAQ", "詢問", "回覆", "解答", "支援", "服務"],
examples: ["客戶、內部SOP詢問", "流程說明"],
benefits: ["24小時服務", "減少重複問答", "提升回應效率"],
difficulty: "medium",
timeframe: "2-4週",
techStack: ["ChatGPT", "Dialogflow", "LINE Bot"],
},
{
id: "document-generation",
name: "文件生成",
icon: "📄",
description: "自動產出報告、會議紀錄、表單填寫等",
color: "#10B981",
bgColor: "from-green-500/20 to-emerald-600/20",
borderColor: "border-green-400/30",
textColor: "text-green-200",
keywords: ["報表", "文件", "表單", "會議紀錄", "填寫", "產出", "生成", "製作", "撰寫", "輸出"],
examples: ["報告填寫", "表單填入", "合約產製"],
benefits: ["節省撰寫時間", "格式統一", "減少錯誤"],
difficulty: "easy",
timeframe: "1-2週",
techStack: ["Word模板", "Google Docs API", "PDF生成"],
},
{
id: "knowledge-management",
name: "知識整理",
icon: "📚",
description: "提取與重組非結構資料,打造企業知識庫",
color: "#8B5CF6",
bgColor: "from-purple-500/20 to-violet-600/20",
borderColor: "border-purple-400/30",
textColor: "text-purple-200",
keywords: ["知識", "整理", "歸檔", "分類", "搜尋", "文檔", "資料庫", "管理", "查找", "SOP"],
examples: ["FAQ建立", "SOP匯整", "文件歸檔"],
benefits: ["知識不流失", "快速查找", "經驗傳承"],
difficulty: "medium",
timeframe: "3-6週",
techStack: ["Notion", "Confluence", "向量資料庫"],
},
{
id: "data-visualization",
name: "數據視覺化",
icon: "📊",
description: "把複雜數據轉換成圖表、儀表板",
color: "#F59E0B",
bgColor: "from-yellow-500/20 to-amber-600/20",
borderColor: "border-yellow-400/30",
textColor: "text-yellow-200",
keywords: ["數據", "資料", "圖表", "分析", "統計", "視覺化", "儀表板", "報告", "趨勢", "爆表"],
examples: ["報表分析", "自動儀表板", "績效追蹤"],
benefits: ["一目了然", "決策支援", "趨勢洞察"],
difficulty: "medium",
timeframe: "2-4週",
techStack: ["Power BI", "Tableau", "Google Charts"],
},
{
id: "process-automation",
name: "流程自動化",
icon: "⚡",
description: "用RPA/n8n/Zapier等自動執行例行操作",
color: "#EF4444",
bgColor: "from-red-500/20 to-rose-600/20",
borderColor: "border-red-400/30",
textColor: "text-red-200",
keywords: ["自動化", "流程", "重複", "例行", "操作", "執行", "RPA", "機器人", "排程", "批次"],
examples: ["表單自動送簽", "報表排程產製", "系統跳轉操作"],
benefits: ["解放人力", "減少錯誤", "提升效率"],
difficulty: "hard",
timeframe: "4-8週",
techStack: ["UiPath", "n8n", "Zapier", "Power Automate"],
},
{
id: "data-cleaning",
name: "資料清洗",
icon: "🧹",
description: "格式轉換、資料對齊、電位補齊與比對",
color: "#06B6D4",
bgColor: "from-cyan-500/20 to-teal-600/20",
borderColor: "border-cyan-400/30",
textColor: "text-cyan-200",
keywords: ["資料", "清洗", "格式", "轉換", "對齊", "比對", "Excel", "CSV", "匯入", "匯出"],
examples: ["Excel合併", "去重", "比對鎖定", "轉格式"],
benefits: ["資料品質提升", "格式統一", "節省手工時間"],
difficulty: "easy",
timeframe: "1-3週",
techStack: ["Python", "Excel VBA", "Power Query"],
},
{
id: "ai-search",
name: "AI搜尋推薦",
icon: "🔍",
description: "讓使用者快速找到相關知識、紀錄或案例",
color: "#84CC16",
bgColor: "from-lime-500/20 to-green-600/20",
borderColor: "border-lime-400/30",
textColor: "text-lime-200",
keywords: ["搜尋", "查找", "推薦", "相關", "案例", "知識", "文件", "快速", "智能", "找到"],
examples: ["文件搜尋", "產品推薦", "知識引導"],
benefits: ["快速定位", "智能推薦", "提升查找效率"],
difficulty: "hard",
timeframe: "4-6週",
techStack: ["Elasticsearch", "AI向量搜尋", "RAG系統"],
},
{
id: "image-recognition",
name: "影像辨識",
icon: "👁️",
description: "分析票片、文件、物件、標籤等",
color: "#EC4899",
bgColor: "from-pink-500/20 to-rose-600/20",
borderColor: "border-pink-400/30",
textColor: "text-pink-200",
keywords: ["影像", "辨識", "掃描", "票據", "文件", "照片", "圖片", "識別", "分析", "OCR"],
examples: ["缺陷檢測", "清單金額讀取", "監控畫面分析"],
benefits: ["自動識別", "減少人工檢查", "提升準確度"],
difficulty: "hard",
timeframe: "6-10週",
techStack: ["OpenCV", "TensorFlow", "Azure Vision"],
},
{
id: "smart-summary",
name: "智能摘要",
icon: "📝",
description: "將會議/文件自動摘要成精華",
color: "#F97316",
bgColor: "from-orange-500/20 to-red-600/20",
borderColor: "border-orange-400/30",
textColor: "text-orange-200",
keywords: ["摘要", "會議", "文件", "精華", "重點", "總結", "整理", "濃縮", "提取", "歸納"],
examples: ["會議紀錄", "長文件摘要整理"],
benefits: ["快速掌握重點", "節省閱讀時間", "重點不遺漏"],
difficulty: "medium",
timeframe: "2-4週",
techStack: ["GPT-4", "Claude", "自然語言處理"],
},
{
id: "system-integration",
name: "系統整合",
icon: "🔗",
description: "多系統資料打通、統一操作",
color: "#6366F1",
bgColor: "from-indigo-500/20 to-purple-600/20",
borderColor: "border-indigo-400/30",
textColor: "text-indigo-200",
keywords: ["系統", "整合", "打通", "統一", "多系統", "切換", "介面", "平台", "連接", "同步"],
examples: ["ERP + CRM串接", "跨平台整合操作"],
benefits: ["減少重複輸入", "資料同步", "操作統一"],
difficulty: "hard",
timeframe: "8-12週",
techStack: ["API整合", "ETL工具", "中介軟體"],
},
]
// 智能推薦引擎
export function generateSolutionRecommendations(wish: any): {
recommendations: SolutionCategory[]
personalizedMessage: string
confidence: number
} {
const fullText = `${wish.title} ${wish.currentPain} ${wish.expectedSolution} ${wish.expectedEffect}`.toLowerCase()
// 計算每個解決方案的匹配分數
const scoredSolutions = solutionCategories
.map((solution) => {
const matchedKeywords = solution.keywords.filter((keyword) => fullText.includes(keyword.toLowerCase()))
const score = matchedKeywords.length
return { solution, score, matchedKeywords }
})
.filter((item) => item.score > 0)
// 按分數排序取前3個
const topSolutions = scoredSolutions
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map((item) => item.solution)
// 生成個人化訊息
const personalizedMessage = generatePersonalizedMessage(wish, topSolutions, scoredSolutions[0]?.matchedKeywords || [])
// 計算信心度
const confidence = Math.min(95, Math.max(60, scoredSolutions[0]?.score * 15 || 60))
return {
recommendations: topSolutions,
personalizedMessage,
confidence,
}
}
// 生成溫暖人性化的回覆訊息
function generatePersonalizedMessage(wish: any, solutions: SolutionCategory[], matchedKeywords: string[]): string {
const greetings = [
"看到你的困擾,我很能理解這種感受",
"這個問題確實很常見,你不是一個人在面對",
"感謝你願意分享這個困擾,讓我來幫你想想解決方案",
"我仔細看了你的描述,這確實是個需要解決的問題",
]
const empathyPhrases = [
"工作中遇到這樣的狀況真的很讓人頭疼",
"我完全理解這種重複性工作帶來的疲憊感",
"這種效率低下的情況確實會影響工作心情",
"面對這樣的挑戰,任何人都會感到困擾",
]
const solutionIntros = [
"根據你的描述,我認為以下幾個方向可能會對你有幫助:",
"結合你的具體情況,我建議可以考慮這些解決方案:",
"針對你提到的問題,我整理了幾個可行的改善方向:",
"基於我的經驗,這類問題通常可以透過以下方式來改善:",
]
const encouragements = [
"相信透過合適的工具和方法,這個問題是可以得到很好的改善的!",
"每個困擾都是改善的機會,你已經踏出了重要的第一步。",
"不要氣餒,很多看似複雜的問題其實都有相對簡單的解決方案。",
"你的這個想法很棒,讓我們一起找到最適合的解決方式吧!",
]
const greeting = greetings[Math.floor(Math.random() * greetings.length)]
const empathy = empathyPhrases[Math.floor(Math.random() * empathyPhrases.length)]
const solutionIntro = solutionIntros[Math.floor(Math.random() * solutionIntros.length)]
const encouragement = encouragements[Math.floor(Math.random() * encouragements.length)]
return `${greeting}${empathy}\n\n${solutionIntro}\n\n${encouragement}`
}
// 獲取解決方案的詳細建議
export function getSolutionDetails(solutionId: string): SolutionCategory | null {
return solutionCategories.find((solution) => solution.id === solutionId) || null
}

121
lib/sound-effects.ts Normal file
View File

@@ -0,0 +1,121 @@
// 更溫柔的音效管理系統
class SoundManager {
private audioContext: AudioContext | null = null
private enabled = true
constructor() {
this.initAudioContext()
}
private async initAudioContext() {
try {
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
} catch (error) {
console.log("Audio not supported")
}
}
// 創建溫柔的鈴聲效果
private async createGentleBell(frequency = 800, duration = 0.3) {
if (!this.audioContext) return
const oscillator = this.audioContext.createOscillator()
const gainNode = this.audioContext.createGain()
const filterNode = this.audioContext.createBiquadFilter()
// 使用正弦波創建溫柔的音調
oscillator.type = "sine"
oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime)
// 低通濾波器讓聲音更柔和
filterNode.type = "lowpass"
filterNode.frequency.setValueAtTime(2000, this.audioContext.currentTime)
filterNode.Q.setValueAtTime(1, this.audioContext.currentTime)
// 溫柔的音量包絡
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime)
gainNode.gain.linearRampToValueAtTime(0.1, this.audioContext.currentTime + 0.01)
gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + duration)
// 連接音頻節點
oscillator.connect(filterNode)
filterNode.connect(gainNode)
gainNode.connect(this.audioContext.destination)
// 播放音效
oscillator.start(this.audioContext.currentTime)
oscillator.stop(this.audioContext.currentTime + duration)
}
// 創建溫柔的和弦
private async createGentleChord(frequencies: number[], duration = 0.5) {
if (!this.audioContext) return
frequencies.forEach((freq, index) => {
setTimeout(() => {
this.createGentleBell(freq, duration)
}, index * 50) // 輕微延遲創造和弦效果
})
}
// 創建水滴聲效果
private async createWaterDrop() {
if (!this.audioContext) return
const oscillator = this.audioContext.createOscillator()
const gainNode = this.audioContext.createGain()
oscillator.type = "sine"
oscillator.frequency.setValueAtTime(1200, this.audioContext.currentTime)
oscillator.frequency.exponentialRampToValueAtTime(400, this.audioContext.currentTime + 0.1)
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime)
gainNode.gain.linearRampToValueAtTime(0.08, this.audioContext.currentTime + 0.01)
gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + 0.15)
oscillator.connect(gainNode)
gainNode.connect(this.audioContext.destination)
oscillator.start(this.audioContext.currentTime)
oscillator.stop(this.audioContext.currentTime + 0.15)
}
async play(soundName: string) {
if (!this.enabled || !this.audioContext) return
try {
// 確保音頻上下文已啟動
if (this.audioContext.state === "suspended") {
await this.audioContext.resume()
}
switch (soundName) {
case "click":
this.createWaterDrop()
break
case "submit":
this.createGentleChord([523, 659, 784], 0.6) // C大調和弦
break
case "success":
this.createGentleChord([523, 659, 784, 988], 0.8) // 更豐富的和弦
break
case "hover":
this.createGentleBell(1000, 0.1)
break
}
} catch (error) {
// 靜默處理錯誤
}
}
toggle() {
this.enabled = !this.enabled
}
isEnabled() {
return this.enabled
}
}
// 全局音效管理器
export const soundManager = new SoundManager()

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}