migrate((db) => { const dao = new Dao(db) // 1. 创建分类集合 const categoriesCollection = new Collection({ name: 'categories', type: 'base', system: false, schema: [ { name: 'name', type: 'text', required: true, options: { max: 50 } }, { name: 'slug', type: 'text', required: true, unique: true, options: { max: 60, pattern: '^[a-z0-9-]+$' } }, { name: 'description', type: 'editor', options: { convertHtml: false } }, { name: 'icon', type: 'text', options: { max: 100 } }, { name: 'color', type: 'text', options: { max: 7, pattern: '^#[0-9A-Fa-f]{6}$' } }, { name: 'sort_order', type: 'number', required: true, default: '0' }, { name: 'parent', type: 'relation', options: { collectionId: '', cascadeDelete: false, minSelect: 0, maxSelect: 1, displayFields: ['name'] } }, { name: 'is_active', type: 'bool', required: true, default: 'true' }, { name: 'seo_title', type: 'text', options: { max: 60 } }, { name: 'seo_description', type: 'text', options: { max: 160 } }, { name: 'seo_keywords', type: 'text', options: { max: 200 } }, ], indexes: [ 'CREATE UNIQUE INDEX idx_categories_slug ON categories (slug)', 'CREATE INDEX idx_categories_parent ON categories (parent)', 'CREATE INDEX idx_categories_sort ON categories (sort_order)', ], listRule: 'is_active = true', viewRule: 'is_active = true', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) // 自引用修复 categoriesCollection.schema.find(f => f.name === 'parent')!.options.collectionId = categoriesCollection.id dao.saveCollection(categoriesCollection) // 2. 创建标签集合 const tagsCollection = new Collection({ name: 'tags', type: 'base', system: false, schema: [ { name: 'name', type: 'text', required: true, unique: true, options: { max: 30 } }, { name: 'slug', type: 'text', required: true, unique: true, options: { max: 40, pattern: '^[a-z0-9-]+$' } }, { name: 'color', type: 'text', required: true, default: '#0ea5e9', options: { max: 7, pattern: '^#[0-9A-Fa-f]{6}$' } }, { name: 'description', type: 'editor', options: { convertHtml: false } }, { name: 'sort_order', type: 'number', required: true, default: '0' }, { name: 'is_active', type: 'bool', required: true, default: 'true' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_tags_slug ON tags (slug)', 'CREATE INDEX idx_tags_sort ON tags (sort_order)', ], listRule: 'is_active = true', viewRule: 'is_active = true', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) dao.saveCollection(tagsCollection) // 3. 创建文章集合 const postsCollection = new Collection({ name: 'posts', type: 'base', system: false, schema: [ { name: 'title', type: 'text', required: true, options: { max: 200 } }, { name: 'slug', type: 'text', required: true, unique: true, options: { max: 220, pattern: '^[a-z0-9-]+$' } }, { name: 'excerpt', type: 'editor', options: { convertHtml: false } }, { name: 'content', type: 'json', required: true }, // TipTap JSON 格式 { name: 'content_html', type: 'text' }, // 渲染后的 HTML { name: 'cover', type: 'file', options: { maxSelect: 1, maxSize: 10485760, mimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] } }, { name: 'gallery', type: 'file', options: { maxSelect: 20, maxSize: 10485760, mimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] } }, { name: 'video', type: 'file', options: { maxSelect: 1, maxSize: 524288000, mimeTypes: ['video/mp4', 'video/webm', 'video/ogg'] } }, { name: 'audio', type: 'file', options: { maxSelect: 1, maxSize: 104857600, mimeTypes: ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/mp3', 'audio/aac'] } }, { name: 'category', type: 'relation', required: false, options: { collectionId: '', cascadeDelete: false, minSelect: 0, maxSelect: 1, displayFields: ['name', 'slug'] } }, { name: 'tags', type: 'relation', required: false, options: { collectionId: '', cascadeDelete: false, minSelect: 0, maxSelect: 20, displayFields: ['name', 'slug', 'color'] } }, { name: 'author', type: 'relation', required: true, options: { collectionId: '_pb_users_auth_', cascadeDelete: false, minSelect: 1, maxSelect: 1, displayFields: ['name', 'email'] } }, { name: 'published', type: 'bool', required: true, default: 'false' }, { name: 'published_at', type: 'date', required: false }, { name: 'featured', type: 'bool', required: true, default: 'false' }, { name: 'pinned', type: 'bool', required: true, default: 'false' }, { name: 'allow_comment', type: 'bool', required: true, default: 'true' }, { name: 'views', type: 'number', required: true, default: '0' }, { name: 'likes', type: 'number', required: true, default: '0' }, { name: 'reading_time', type: 'number', required: true, default: '0' }, // 分钟 { name: 'seo_title', type: 'text', options: { max: 60 } }, { name: 'seo_description', type: 'text', options: { max: 160 } }, { name: 'seo_keywords', type: 'text', options: { max: 200 } }, { name: 'canonical_url', type: 'url', options: { max: 500 } }, { name: 'template', type: 'text', options: { max: 50 } }, // 自定义模板 { name: 'meta', type: 'json' }, // 扩展字段 ], indexes: [ 'CREATE UNIQUE INDEX idx_posts_slug ON posts (slug)', 'CREATE INDEX idx_posts_category ON posts (category)', 'CREATE INDEX idx_posts_published ON posts (published, published_at)', 'CREATE INDEX idx_posts_featured ON posts (featured, published_at)', 'CREATE INDEX idx_posts_author ON posts (author)', 'CREATE INDEX idx_posts_created ON posts (created)', ], listRule: 'published = true', viewRule: 'published = true || @request.auth.role = "admin" || @request.auth.id = author', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin" || @request.auth.id = author', deleteRule: '@request.auth.role = "admin"', options: {}, }) // 设置关联 postsCollection.schema.find(f => f.name === 'category')!.options.collectionId = categoriesCollection.id postsCollection.schema.find(f => f.name === 'tags')!.options.collectionId = tagsCollection.id dao.saveCollection(postsCollection) // 4. 创建页面集合 (用于单页文档、关于我们、搭建教程等) const pagesCollection = new Collection({ name: 'pages', type: 'base', system: false, schema: [ { name: 'title', type: 'text', required: true, options: { max: 200 } }, { name: 'slug', type: 'text', required: true, unique: true, options: { max: 100, pattern: '^[a-z0-9-]+$' } }, { name: 'content', type: 'json', required: true }, // TipTap JSON { name: 'content_html', type: 'text' }, { name: 'cover', type: 'file', options: { maxSelect: 1, maxSize: 10485760, mimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'] } }, { name: 'template', type: 'text', required: true, default: 'default', options: { max: 50 } }, // default, fullwidth, sidebar, doc { name: 'published', type: 'bool', required: true, default: 'false' }, { name: 'sort_order', type: 'number', required: true, default: '0' }, { name: 'show_in_nav', type: 'bool', required: true, default: 'false' }, { name: 'nav_label', type: 'text', options: { max: 30 } }, { name: 'seo_title', type: 'text', options: { max: 60 } }, { name: 'seo_description', type: 'text', options: { max: 160 } }, { name: 'seo_keywords', type: 'text', options: { max: 200 } }, { name: 'meta', type: 'json' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_pages_slug ON pages (slug)', 'CREATE INDEX idx_pages_published ON pages (published, sort_order)', ], listRule: 'published = true', viewRule: 'published = true || @request.auth.role = "admin"', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) dao.saveCollection(pagesCollection) // 5. 创建站点设置集合 (单例) const settingsCollection = new Collection({ name: 'site_settings', type: 'base', system: false, schema: [ // 基础信息 { name: 'site_name', type: 'text', required: true, default: '云升数码', options: { max: 100 } }, { name: 'site_subtitle', type: 'text', options: { max: 200 } }, { name: 'site_description', type: 'editor', options: { convertHtml: false } }, { name: 'site_keywords', type: 'text', options: { max: 500 } }, { name: 'site_url', type: 'url', options: { max: 500 } }, // Logo & Favicon { name: 'logo', type: 'file', options: { maxSelect: 1, maxSize: 2097152, mimeTypes: ['image/svg+xml', 'image/png', 'image/webp'] } }, { name: 'logo_dark', type: 'file', options: { maxSelect: 1, maxSize: 2097152, mimeTypes: ['image/svg+xml', 'image/png', 'image/webp'] } }, { name: 'favicon', type: 'file', options: { maxSelect: 1, maxSize: 1048576, mimeTypes: ['image/svg+xml', 'image/png', 'image/x-icon'] } }, { name: 'apple_touch_icon', type: 'file', options: { maxSelect: 1, maxSize: 1048576, mimeTypes: ['image/png'] } }, // 背景音乐 { name: 'background_music_enabled', type: 'bool', required: true, default: 'true' }, { name: 'background_music_type', type: 'select', required: true, default: 'local', options: { maxSelect: 1, values: ['local', 'netease', 'tencent', 'xiami', 'kugou', 'kuwo'] } }, { name: 'background_music_file', type: 'file', options: { maxSelect: 1, maxSize: 52428800, mimeTypes: ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/mp3', 'audio/aac'] } }, { name: 'background_music_url', type: 'url', options: { max: 500 } }, { name: 'background_music_title', type: 'text', options: { max: 100 } }, { name: 'background_music_artist', type: 'text', options: { max: 100 } }, { name: 'background_music_cover', type: 'file', options: { maxSelect: 1, maxSize: 2097152, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } }, { name: 'background_music_volume', type: 'number', required: true, default: '0.5', options: { min: 0, max: 1, step: 0.1 } }, { name: 'background_music_autoplay', type: 'bool', required: true, default: 'false' }, { name: 'background_music_loop', type: 'bool', required: true, default: 'true' }, // Footer { name: 'footer_text', type: 'text', options: { max: 500 } }, { name: 'footer_copyright', type: 'text', options: { max: 200 } }, { name: 'icp_number', type: 'text', options: { max: 50 } }, { name: 'police_number', type: 'text', options: { max: 50 } }, // 社交链接 { name: 'social_links', type: 'json' }, // { github, twitter, bilibili, weibo, zhihu, email, rss } // SEO & Analytics { name: 'google_analytics_id', type: 'text', options: { max: 50 } }, { name: 'baidu_analytics_id', type: 'text', options: { max: 50 } }, { name: 'custom_head_code', type: 'editor', options: { convertHtml: false } }, { name: 'custom_body_code', type: 'editor', options: { convertHtml: false } }, // 评论系统 { name: 'comment_enabled', type: 'bool', required: true, default: 'false' }, { name: 'comment_system', type: 'select', required: true, default: 'giscus', options: { maxSelect: 1, values: ['giscus', 'waline', 'twikoo', 'cusdis', 'disqus'] } }, { name: 'comment_config', type: 'json' }, // 功能开关 { name: 'maintenance_mode', type: 'bool', required: true, default: 'false' }, { name: 'maintenance_message', type: 'editor', options: { convertHtml: false } }, { name: 'registration_enabled', type: 'bool', required: true, default: 'false' }, { name: 'rss_enabled', type: 'bool', required: true, default: 'true' }, { name: 'search_enabled', type: 'bool', required: true, default: 'true' }, // 缓存 & 性能 { name: 'cache_ttl', type: 'number', required: true, default: '3600' }, { name: 'image_optimization', type: 'bool', required: true, default: 'true' }, { name: 'cdn_url', type: 'url', options: { max: 500 } }, ], indexes: [], listRule: '@request.auth.role = "admin"', viewRule: '@request.auth.role = "admin" || true', // 公开读取部分字段 createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: { // 单例模式:只能有一条记录 }, }) dao.saveCollection(settingsCollection) // 6. 创建媒体集合 (扩展 PocketBase 默认文件管理) const mediaCollection = new Collection({ name: 'media', type: 'base', system: false, schema: [ { name: 'file', type: 'file', required: true, options: { maxSelect: 1, maxSize: 104857600 } }, { name: 'title', type: 'text', options: { max: 200 } }, { name: 'alt', type: 'text', options: { max: 200 } }, { name: 'description', type: 'editor', options: { convertHtml: false } }, { name: 'folder', type: 'text', options: { max: 100 } }, { name: 'tags', type: 'relation', options: { collectionId: '', cascadeDelete: false, minSelect: 0, maxSelect: 20, displayFields: ['name'] } }, { name: 'metadata', type: 'json' }, // width, height, duration, bitrate, exif 等 { name: 'uploaded_by', type: 'relation', required: true, options: { collectionId: '_pb_users_auth_', cascadeDelete: false, minSelect: 1, maxSelect: 1, displayFields: ['name', 'email'] } }, ], indexes: [ 'CREATE INDEX idx_media_folder ON media (folder)', 'CREATE INDEX idx_media_uploaded_by ON media (uploaded_by)', 'CREATE INDEX idx_media_created ON media (created)', ], listRule: '@request.auth.role = "admin"', viewRule: '@request.auth.role = "admin"', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) mediaCollection.schema.find(f => f.name === 'tags')!.options.collectionId = tagsCollection.id dao.saveCollection(mediaCollection) // 7. 创建菜单集合 const menusCollection = new Collection({ name: 'menus', type: 'base', system: false, schema: [ { name: 'name', type: 'text', required: true, options: { max: 50 } }, { name: 'location', type: 'select', required: true, default: 'header', options: { maxSelect: 1, values: ['header', 'footer', 'mobile', 'sidebar'] } }, { name: 'items', type: 'json', required: true }, // [{ label, url, target, children[], icon, badge, order }] { name: 'is_active', type: 'bool', required: true, default: 'true' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_menus_location ON menus (location)', ], listRule: 'is_active = true', viewRule: 'is_active = true', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) dao.saveCollection(menusCollection) // 8. 创建重定向集合 (SEO 友好) const redirectsCollection = new Collection({ name: 'redirects', type: 'base', system: false, schema: [ { name: 'from_path', type: 'text', required: true, unique: true, options: { max: 500 } }, { name: 'to_path', type: 'text', required: true, options: { max: 500 } }, { name: 'status_code', type: 'select', required: true, default: '301', options: { maxSelect: 1, values: ['301', '302', '307', '308'] } }, { name: 'is_active', type: 'bool', required: true, default: 'true' }, { name: 'hits', type: 'number', required: true, default: '0' }, ], indexes: [ 'CREATE UNIQUE INDEX idx_redirects_from ON redirects (from_path)', ], listRule: '@request.auth.role = "admin"', viewRule: '@request.auth.role = "admin"', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) dao.saveCollection(redirectsCollection) // 9. 创建通知/公告集合 const noticesCollection = new Collection({ name: 'notices', type: 'base', system: false, schema: [ { name: 'title', type: 'text', required: true, options: { max: 100 } }, { name: 'content', type: 'editor', required: true, options: { convertHtml: false } }, { name: 'type', type: 'select', required: true, default: 'info', options: { maxSelect: 1, values: ['info', 'success', 'warning', 'error', 'announcement'] } }, { name: 'position', type: 'select', required: true, default: 'top', options: { maxSelect: 1, values: ['top', 'bottom', 'center', 'top-right', 'top-left'] } }, { name: 'show_close', type: 'bool', required: true, default: 'true' }, { name: 'auto_close', type: 'number', required: true, default: '0' }, // 0 = 不自动关闭,单位毫秒 { name: 'link', type: 'url', options: { max: 500 } }, { name: 'link_text', type: 'text', options: { max: 30 } }, { name: 'start_at', type: 'date', required: false }, { name: 'end_at', type: 'date', required: false }, { name: 'is_active', type: 'bool', required: true, default: 'true' }, { name: 'target_roles', type: 'json' }, // ['admin', 'user', 'guest'] { name: 'dismissible', type: 'bool', required: true, default: 'true' }, ], indexes: [ 'CREATE INDEX idx_notices_active ON notices (is_active, start_at, end_at)', ], listRule: 'is_active = true && (start_at = "" || start_at <= @now) && (end_at = "" || end_at >= @now)', viewRule: 'is_active = true && (start_at = "" || start_at <= @now) && (end_at = "" || end_at >= @now)', createRule: '@request.auth.role = "admin"', updateRule: '@request.auth.role = "admin"', deleteRule: '@request.auth.role = "admin"', options: {}, }) dao.saveCollection(noticesCollection) })