// ============================================= // js/render.js — 所有 HTML 產生函式 // ============================================= /* ---- Helpers ---- */ function fmtNum(n) { if (!n) return '0'; return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); } function fmtDate(s) { if (!s) return ''; return new Date(s).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }); } function initials(name) { return name ? name.slice(0, 1) : '?'; } function avatarColor(str) { const colors = ['#0e4d8a','#007a6e','#8a5000','#6a1a1a','#4a1a8a']; let h = 0; for (let c of (str||'')) h = (h * 31 + c.charCodeAt(0)) & 0xffffffff; return colors[Math.abs(h) % colors.length]; } function esc(s) { return String(s||'').replace(//g,'>').replace(/"/g,'"'); } /* ---- Header ---- */ function renderHeader(state) { if (!state.user) return ''; const pending = (state.pendingCount || 0); return `
${state.logoSrc ? `NMST Logo` : ``}
國立海洋科技博物館
AI PROMPT PLATFORM
${initials(state.profile?.name)}
`; } /* ---- Login Page ---- */ function renderLogin(tab = 'login') { return `
${tab === 'login' ? `
` : `
`}
`; } /* ---- Post Card ---- */ function renderPostCard(post, opts = {}) { const name = post.profiles?.name || '未知用戶'; const pct = opts.maxViews ? Math.round((post.view_count / opts.maxViews) * 100) : 0; const liked = opts.likedSet?.has(post.id); const thumb = (post.attachments || []).find(a => a.is_webp); const showStatus = opts.showStatus; return `
${initials(name)}
${showStatus ? ` ${post.status==='approved'?'已上架':post.status==='pending'?'待審核':'已拒絕'} ` : ''} ${post.content_type==='image'?' 圖片':' 文字'}
${esc(post.title)}
${(post.tags||[]).map((t,i)=>`# ${esc(t)}`).join('')}
提示詞:${esc(post.prompt)}
${thumb ? `
生成圖片
` : ''}
${pct}%
`; } /* ---- Feed Page ---- */ function renderFeed(state) { const { posts = [], sortBy = 'view_count', filterTag, searchQ = '', stats = {}, topPosts = [], likedSet, profile } = state; const maxViews = Math.max(...posts.map(p => p.view_count || 0), 1); const allTags = [...new Set(posts.flatMap(p => p.tags || []))]; const typeFilters = [ { key: null, label: '全部', icon: 'fa-border-all' }, { key: 'image', label: '圖像', icon: 'fa-image' }, { key: 'text', label: '文字', icon: 'fa-align-left' }, ]; const toolFilters = ['ChatGPT','Midjourney','DALL-E','Claude','Stable Diffusion','Gemini']; return `

${filterTag ? `# ${esc(filterTag)}` : '探索提示詞'}

${state.loading ? `
載入中…
` : posts.length === 0 ? `

尚無符合的內容

試試其他關鍵字或標籤
` : posts.map(p => renderPostCard(p, { maxViews, likedSet, showStatus: false })).join('') }
`; } /* ---- My Posts Page ---- */ function renderMyPage(state) { const { myPosts = [], likedSet } = state; const maxViews = Math.max(...myPosts.map(p => p.view_count || 0), 1); return `

我的分享

共 ${myPosts.length} 篇

${state.loading ? `
載入中…
` : myPosts.length === 0 ? `

尚未分享任何提示詞

點擊「分享提示詞」開始
` : myPosts.map(p => renderPostCard(p, { maxViews, likedSet, showStatus: true })).join('') }
`; } /* ---- Admin Page ---- */ function renderAdmin(state) { const { adminPosts = [], adminTab = 'pending', adminStats = {} } = state; return `

審核管理後台

${[ { label:'待審核', val: adminStats.pending||0, cls:'val-gold' }, { label:'已上架', val: adminStats.approved||0, cls:'val-teal' }, { label:'已拒絕', val: adminStats.rejected||0, cls:'val-coral' }, { label:'會員人數', val: adminStats.members||0, cls:'val-blue' }, ].map(s=>`
${s.label}
${s.val}
`).join('')}
${state.loading ? `
載入中…
` : adminPosts.length === 0 ? `

此分類目前沒有內容

` : adminPosts.map(p => { const name = p.profiles?.name || '未知'; const flags = p.sensitive_flags || []; return `
${esc(p.title)}
${p.status==='approved'?'已上架':p.status==='pending'?'待審核':'已拒絕'}
提交者:${esc(name)} · ${fmtDate(p.created_at)} · ${p.content_type==='image'?'圖片':'文字'}
${flags.length > 0 ? `
偵測到敏感詞:${flags.map(esc).join('、')}
` : ''}
提示詞:${esc(p.prompt)}
${p.status === 'pending' ? `
` : ''}
`; }).join('') }
`; } /* ---- Upload Modal ---- */ function renderUploadModal() { return ` `; } /* ---- Detail Modal ---- */ function renderDetailModal(state) { const post = state.detailPost; if (!post) return ``; const name = post.profiles?.name || '未知'; const liked = state.likedSet?.has(post.id); const imgs = (post.attachments || []).filter(a => a.is_webp); const files = (post.attachments || []).filter(a => !a.is_webp); return ` `; } /* ---- Profile Modal ---- */ function renderProfileModal(state) { const { profile, myPosts = [] } = state; const totalLikes = myPosts.reduce((a, p) => a + (p.like_count || 0), 0); const totalViews = myPosts.reduce((a, p) => a + (p.view_count || 0), 0); return ` `; } /* ---- Toast ---- */ function renderToast(toast) { if (!toast) return ''; return `
${esc(toast.msg)}
`; } /* ---- Rich editor helpers (global) ---- */ function rc(cmd) { document.getElementById('up-prompt')?.focus(); document.execCommand(cmd, false, null); } function rr(cmd) { document.getElementById('up-result')?.focus(); document.execCommand(cmd, false, null); }