新增資料庫架構

This commit is contained in:
2025-07-19 02:12:37 +08:00
parent e3832acfa8
commit 924f03c3d7
45 changed files with 12858 additions and 324 deletions

190
lib/image-utils.ts Normal file
View File

@@ -0,0 +1,190 @@
// 圖片處理工具
export interface ImageFile {
id: string
file?: File // 可選,因為從 localStorage 恢復時不會有原始 File
url: string // 改為 base64 URL
name: string
size: number
type: string
base64?: string // 新增 base64 字段
}
export interface ImageValidationResult {
isValid: boolean
error?: string
suggestion?: string
}
// 允許的圖片格式
export const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]
// 允許的檔案副檔名
export const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif"]
// 檔案大小限制 (5MB)
export const MAX_FILE_SIZE = 5 * 1024 * 1024
// 單次上傳數量限制
export const MAX_FILES_PER_UPLOAD = 10
// 總檔案數量限制
export const MAX_TOTAL_FILES = 20
export function validateImageFile(file: File): ImageValidationResult {
// 檢查檔案類型
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
return {
isValid: false,
error: `不支援的檔案格式: ${file.type}`,
suggestion: `請使用 JPG、PNG、WebP 或 GIF 格式`,
}
}
// 檢查檔案大小
if (file.size > MAX_FILE_SIZE) {
const sizeMB = (file.size / (1024 * 1024)).toFixed(1)
return {
isValid: false,
error: `檔案過大: ${sizeMB}MB`,
suggestion: `請壓縮圖片至 5MB 以下`,
}
}
// 檢查檔案名稱
if (file.name.length > 100) {
return {
isValid: false,
error: "檔案名稱過長",
suggestion: "請使用較短的檔案名稱",
}
}
return { isValid: true }
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
}
// 將 File 轉換為 base64
export function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
if (typeof reader.result === "string") {
resolve(reader.result)
} else {
reject(new Error("Failed to convert file to base64"))
}
}
reader.onerror = () => reject(reader.error)
reader.readAsDataURL(file)
})
}
// 創建圖片文件對象(使用 base64
export async function createImageFile(file: File): Promise<ImageFile> {
const base64 = await fileToBase64(file)
return {
id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
file,
url: base64, // 直接使用 base64 作為 URL
name: file.name,
size: file.size,
type: file.type,
base64,
}
}
// 從儲存的數據恢復圖片對象
export function restoreImageFile(data: any): ImageFile {
return {
id: data.id,
url: data.base64 || data.url, // 優先使用 base64向後兼容
name: data.name,
size: data.size,
type: data.type,
base64: data.base64,
}
}
// 不再需要 revokeImageUrl因為使用 base64
export function revokeImageUrl(imageFile: ImageFile): void {
// base64 不需要手動釋放
return
}
// 壓縮圖片並轉為 base64
export function compressImage(file: File, maxWidth = 1920, quality = 0.8): Promise<File> {
return new Promise((resolve) => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const img = new Image()
img.onload = () => {
// 計算新尺寸
let { width, height } = img
if (width > maxWidth) {
height = (height * maxWidth) / width
width = maxWidth
}
canvas.width = width
canvas.height = height
// 繪製壓縮後的圖片
ctx?.drawImage(img, 0, 0, width, height)
canvas.toBlob(
(blob) => {
if (blob) {
const compressedFile = new File([blob], file.name, {
type: file.type,
lastModified: Date.now(),
})
resolve(compressedFile)
} else {
resolve(file) // 如果壓縮失敗,返回原檔案
}
},
file.type,
quality,
)
}
img.src = URL.createObjectURL(file)
})
}
// 生成縮圖
export function generateThumbnail(file: File, size = 200): Promise<string> {
return new Promise((resolve) => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const img = new Image()
img.onload = () => {
canvas.width = size
canvas.height = size
// 計算裁切區域 (正方形縮圖)
const minDimension = Math.min(img.width, img.height)
const x = (img.width - minDimension) / 2
const y = (img.height - minDimension) / 2
ctx?.drawImage(img, x, y, minDimension, minDimension, 0, 0, size, size)
resolve(canvas.toDataURL(file.type, 0.7))
}
img.src = URL.createObjectURL(file)
})
}