(() => { 'use strict'; const page = location.pathname.split('/').pop(); const tokenKey = 'youthome:web:accessToken'; const userKey = 'youthome:web:authUser'; const token = () => sessionStorage.getItem(tokenKey) || localStorage.getItem(tokenKey); const user = () => { try { return JSON.parse(sessionStorage.getItem(userKey) || localStorage.getItem(userKey) || 'null'); } catch (error) { return null; } }; const toast = (message, error = false) => { document.querySelector('.web-toast')?.remove(); const node = document.createElement('div'); node.className = `web-toast${error ? ' is-error' : ''}`; node.setAttribute('role', 'status'); node.textContent = message; document.body.appendChild(node); setTimeout(() => node.remove(), 3500); }; const safeNext = (value) => /^[\w-]+\.html(?:\?[\w=&%-]+)?$/.test(value || '') ? value : 'feed.html'; const loginUrl = (next = 'publish-post.html') => `login.html?next=${encodeURIComponent(safeNext(next))}`; // 数据来源由 web-api.js 控制,默认本地演示;需要联调时可切回真实后端。 const request = async (path, options = {}) => { if (window.YouthomeApi?.request) return window.YouthomeApi.request(path, options); if (!window.YouthomeLocal) throw new Error('数据层未加载,请刷新页面重试'); return window.YouthomeLocal.request(path, options); }; const saveAuth = (data, remember = true) => { const storage = remember ? localStorage : sessionStorage; localStorage.removeItem(tokenKey); localStorage.removeItem(userKey); sessionStorage.removeItem(tokenKey); sessionStorage.removeItem(userKey); storage.setItem(tokenKey, data.token); storage.setItem(userKey, JSON.stringify(data.user)); }; const makeLink = (node, href) => { node.tabIndex = 0; node.setAttribute('role', 'link'); node.addEventListener('click', (event) => { if (!event.target.closest('button, a, input')) location.href = href; }); node.addEventListener('keydown', (event) => { if (event.key === 'Enter') location.href = href; }); }; const summary = (content) => content.replace(/\s+/g, ' ').trim().slice(0, 72); const statusText = (status) => ({ pending: '审核中', rejected: '审核未通过', approved: '已发布' }[String(status || '').toLowerCase()] || '处理中'); if (page === 'login.html') { const next = new URLSearchParams(location.search).get('next'); if (next) { sessionStorage.setItem('youthome:web:afterAuth', safeNext(next)); document.querySelectorAll('a[href="register.html"]').forEach((link) => { link.href = `register.html?next=${encodeURIComponent(safeNext(next))}`; }); } window.goLogin = async () => { const panel = document.querySelector('.auth-panel.active'); const phone = panel?.querySelector('input[type="tel"]')?.value.trim(); const isCode = panel?.id === 'panel-code'; const credential = panel?.querySelector(isCode ? 'input[placeholder*="验证码"]' : 'input[type="password"]')?.value.trim(); if (!/^1\d{10}$/.test(phone || '')) { toast('请输入正确的手机号', true); return; } if (!credential) { toast(isCode ? '请输入验证码' : '请输入密码', true); return; } const button = panel.querySelector('.btn-primary'); button.disabled = true; try { const payload = { phone, deviceId: 'youthome-web', deviceName: '网页端', platform: 'WEB' }; payload[isCode ? 'captcha' : 'password'] = credential; const result = await request(isCode ? '/auth/login-captcha' : '/auth/login', { method: 'POST', body: JSON.stringify(payload) }); saveAuth(result, document.querySelector('#panel-password input[type="checkbox"]')?.checked !== false); location.href = safeNext(new URLSearchParams(location.search).get('next') || sessionStorage.getItem('youthome:web:afterAuth')); } catch (error) { toast(error.message, true); button.disabled = false; } }; window.sendCode = async (button) => { const phone = button.closest('.auth-panel')?.querySelector('input[type="tel"]')?.value.trim(); if (!/^1\d{10}$/.test(phone || '')) { toast('请输入正确的手机号', true); return; } button.disabled = true; try { await request(`/auth/captcha?phone=${encodeURIComponent(phone)}`); toast('验证码已发送'); let left = 60; button.textContent = `${left}s`; const timer = setInterval(() => { left -= 1; button.textContent = left ? `${left}s` : '获取验证码'; if (!left) { clearInterval(timer); button.disabled = false; } }, 1000); } catch (error) { toast(error.message, true); button.disabled = false; } }; } if (page === 'register.html') { window.doRegister = async () => { const school = localStorage.getItem('youthome:web:selectedSchool'); const phone = document.querySelector('input[type="tel"]')?.value.trim(); const captcha = document.querySelector('input[placeholder*="验证码"]')?.value.trim(); const password = document.querySelector('input[type="password"]')?.value || ''; if (!school) { toast('请先选择学校', true); return; } if (!/^1\d{10}$/.test(phone || '')) { toast('请输入正确的手机号', true); return; } if (!/^\d{6}$/.test(captcha || '')) { toast('请输入 6 位验证码', true); return; } if (!/^(?=.*[A-Za-z])(?=.*\d).{8,20}$/.test(password)) { toast('密码需为 8-20 位,包含字母和数字', true); return; } if (!document.querySelector('input[type="checkbox"]')?.checked) { toast('请先同意用户协议和隐私政策', true); return; } try { const result = await request('/auth/register', { method: 'POST', body: JSON.stringify({ phone, captcha, password, universityName: school, deviceId: 'youthome-web', platform: 'WEB' }) }); saveAuth(result); location.href = safeNext(new URLSearchParams(location.search).get('next') || sessionStorage.getItem('youthome:web:afterAuth')); } catch (error) { toast(error.message, true); } }; window.sendCode = async (button) => { const phone = document.querySelector('input[type="tel"]')?.value.trim(); if (!/^1\d{10}$/.test(phone || '')) { toast('请输入正确的手机号', true); return; } button.disabled = true; try { await request(`/auth/captcha?phone=${encodeURIComponent(phone)}`); toast('验证码已发送'); } catch (error) { toast(error.message, true); } finally { setTimeout(() => { button.disabled = false; }, 60000); } }; } const uploadImage = async (file) => { const form = new FormData(); form.append('file', file); const result = await request('/file/upload/image', { method: 'POST', body: form }); if (!result?.url) throw new Error('图片上传失败,请重试'); return result.url; }; const initEditor = async () => { if (!token()) { location.replace(loginUrl('publish-post.html' + location.search)); return; } const content = document.querySelector('.post-textarea'); const grid = document.querySelector('.image-upload-grid'); const fileInput = document.querySelector('.post-image-input') || document.createElement('input'); if (!fileInput.isConnected) { fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.multiple = true; fileInput.hidden = true; grid.after(fileInput); } const topicWrap = document.querySelector('.topic-tags-wrap'); const topicButton = topicWrap.querySelector('.add-topic-btn'); topicWrap.querySelectorAll('.topic-tag-item').forEach((item) => item.remove()); const previewContent = document.querySelector('.preview-post-content'); const previewImages = document.querySelector('.preview-images'); const publishButton = document.querySelector('.top-bar-right .btn-primary'); const draftButton = document.querySelector('.top-bar-right .btn-secondary'); const actionBar = document.createElement('div'); actionBar.className = 'web-publish-actions'; const actionTitle = document.createElement('strong'); actionTitle.textContent = '发布动态'; actionBar.append(actionTitle, draftButton, publishButton); document.querySelector('.publish-layout').before(actionBar); const topics = []; const images = []; let draftId = new URLSearchParams(location.search).get('draft'); let saving = false; let publishing = false; let lastSaved = ''; const name = user()?.nickname || '我'; document.querySelector('.preview-username').textContent = name; document.querySelector('.preview-school').textContent = `${user()?.universityName || '校园'} · 刚刚`; document.querySelector('.preview-avatar').textContent = name.slice(0, 1); const previewAvatarImage = document.querySelector('.preview-avatar img'); previewAvatarImage?.remove(); const visibilityOptions = Array.from(document.querySelectorAll('.visibility-option')); const labelNode = (element) => Array.from(element.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim()); labelNode(visibilityOptions[0]).textContent = ' 校园公开 '; labelNode(visibilityOptions[1]).textContent = ' 仅好友可见 '; visibilityOptions[2]?.remove(); labelNode(document.querySelector('.visibility-btn')).textContent = ' 校园公开 '; let scope = 'CAMPUS'; window.selectVisibility = (element, label) => { scope = element === visibilityOptions[1] ? 'PERSONAL' : 'CAMPUS'; visibilityOptions.forEach((option) => option.classList.toggle('active', option === element)); labelNode(document.querySelector('.visibility-btn')).textContent = scope === 'CAMPUS' ? ' 校园公开 ' : ' 仅好友可见 '; document.querySelector('.visibility-dropdown').style.display = 'none'; }; document.querySelector('.settings-card')?.remove(); document.querySelectorAll('.editor-toolbar .tool-btn').forEach((button) => button.remove()); document.querySelector('.toolbar-divider')?.remove(); const refreshPreview = () => { const text = content.value; document.querySelector('.word-count').textContent = `${text.length} / 500`; previewContent.replaceChildren(document.createTextNode(text || '动态内容将在这里预览')); topics.forEach((topic) => { const tag = document.createElement('span'); tag.className = 'topic-tag'; tag.textContent = ` #${topic}`; previewContent.appendChild(tag); }); previewImages.replaceChildren(...images.map((item) => { const img = document.createElement('img'); img.src = item.preview || item.url; img.alt = '动态图片预览'; return img; })); previewImages.hidden = images.length === 0; }; const renderTopics = () => { topicWrap.querySelectorAll('.topic-tag-item').forEach((item) => item.remove()); topics.forEach((topic) => { const tag = document.createElement('span'); tag.className = 'topic-tag-item'; tag.textContent = `#${topic} `; const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'web-topic-remove'; remove.textContent = '×'; remove.setAttribute('aria-label', `移除${topic}`); remove.addEventListener('click', () => { topics.splice(topics.indexOf(topic), 1); renderTopics(); refreshPreview(); }); tag.appendChild(remove); topicWrap.insertBefore(tag, topicButton); }); refreshPreview(); }; topicButton.addEventListener('click', () => { if (topics.length >= 5) { toast('最多添加 5 个标签', true); return; } if (topicWrap.querySelector('.web-topic-input')) return; const input = document.createElement('input'); input.className = 'web-topic-input'; input.placeholder = '输入标签后按回车'; input.maxLength = 20; topicWrap.insertBefore(input, topicButton); input.focus(); let committed = false; const commit = () => { if (committed) return; committed = true; const value = input.value.trim().replace(/^#/, ''); input.remove(); if (value && !topics.includes(value)) { topics.push(value); renderTopics(); } }; input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); commit(); } else if (event.key === 'Escape') input.remove(); }); input.addEventListener('blur', commit, { once: true }); }); const renderImages = () => { grid.replaceChildren(); images.forEach((item, index) => { const slot = document.createElement('div'); slot.className = 'upload-slot filled'; const img = document.createElement('img'); img.src = item.preview || item.url; img.alt = `已选择的第 ${index + 1} 张图片`; const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'remove-btn'; remove.textContent = '×'; remove.setAttribute('aria-label', `移除第 ${index + 1} 张图片`); remove.addEventListener('click', () => { if (item.preview) URL.revokeObjectURL(item.preview); images.splice(index, 1); renderImages(); }); slot.append(img, remove); grid.appendChild(slot); }); if (images.length < 9) { const add = document.createElement('button'); add.type = 'button'; add.className = 'upload-slot'; add.innerHTML = '添加图片'; add.addEventListener('click', () => fileInput.click()); grid.appendChild(add); } refreshPreview(); }; const addFiles = (files) => { for (const file of files) { if (images.length >= 9) { toast('最多上传 9 张图片', true); break; } if (!file.type.startsWith('image/')) { toast('只能选择图片文件', true); continue; } if (file.size > 10 * 1024 * 1024) { toast('单张图片不能超过 10MB', true); continue; } images.push({ file, preview: URL.createObjectURL(file) }); } renderImages(); fileInput.value = ''; }; fileInput.addEventListener('change', () => addFiles(fileInput.files)); grid.addEventListener('dragover', (event) => { event.preventDefault(); grid.classList.add('is-dragging'); }); grid.addEventListener('dragleave', () => grid.classList.remove('is-dragging')); grid.addEventListener('drop', (event) => { event.preventDefault(); grid.classList.remove('is-dragging'); addFiles(event.dataTransfer.files); }); content.addEventListener('input', refreshPreview); const uploadPending = async () => { for (const item of images) { if (!item.url) item.url = await uploadImage(item.file); } }; const payload = () => ({ content: content.value.trim(), tags: [...topics], mediaUrls: images.map((item) => item.url), category: '校园生活', scope }); const saveDraft = async (quiet = false) => { if (saving || publishing || (!content.value.trim() && !images.length)) return; saving = true; draftButton.disabled = true; try { await uploadPending(); const data = { ...payload(), contentType: 'MOMENT', ...(draftId ? { draftId } : {}) }; const result = await request('/draft/save', { method: 'POST', body: JSON.stringify(data) }); draftId = result.draftId; lastSaved = JSON.stringify({ content: data.content, tags: data.tags, mediaUrls: data.mediaUrls, category: data.category, scope: data.scope }); history.replaceState(null, '', `publish-post.html?draft=${encodeURIComponent(draftId)}`); if (!quiet) toast('草稿已保存'); } catch (error) { if (!quiet) toast(error.message, true); } finally { saving = false; draftButton.disabled = false; } }; window.saveDraft = () => saveDraft(); window.publishPost = async () => { if (saving || publishing) { toast('草稿正在保存,请稍后再发布', true); return; } if (!content.value.trim()) { toast('请输入动态内容', true); content.focus(); return; } if (content.value.length > 500) { toast('动态不能超过 500 字', true); return; } publishing = true; publishButton.disabled = true; draftButton.disabled = true; try { await uploadPending(); const result = await request('/campus/moments', { method: 'POST', body: JSON.stringify(payload()) }); if (!result?.id) throw new Error('发布结果缺少动态编号,请到个人中心核对后再重试'); if (draftId) { try { await request(`/draft/${encodeURIComponent(draftId)}`, { method: 'DELETE' }); } catch (error) { sessionStorage.setItem('youthome:web:draftCleanupWarning', '动态已提交,但原草稿未能删除'); } } toast(statusText(result.reviewStatus) === '已发布' ? '动态已发布' : `动态已提交,${statusText(result.reviewStatus)}`); location.href = `post-detail.html?moment=${encodeURIComponent(result.id)}`; } catch (error) { toast(error.message, true); publishButton.disabled = false; draftButton.disabled = false; } finally { publishing = false; } }; if (draftId) { try { const drafts = await request('/draft/list'); const draft = drafts.find((item) => item.draftId === draftId); if (!draft) throw new Error('草稿不存在'); content.value = draft.content || ''; topics.push(...(Array.isArray(draft.tags) ? draft.tags : [])); images.push(...(Array.isArray(draft.mediaUrls) ? draft.mediaUrls.map((url) => ({ url })) : [])); scope = draft.scope === 'PERSONAL' ? 'PERSONAL' : 'CAMPUS'; window.selectVisibility(visibilityOptions[scope === 'PERSONAL' ? 1 : 0]); renderTopics(); renderImages(); lastSaved = JSON.stringify(payload()); } catch (error) { toast(error.message, true); } } else { content.value = sessionStorage.getItem('youthome:web:quickPost') || ''; sessionStorage.removeItem('youthome:web:quickPost'); renderTopics(); renderImages(); } setInterval(() => { const autoSave = document.querySelector('.settings-card .setting-item .switch.active'); if (autoSave && JSON.stringify(payload()) !== lastSaved) saveDraft(true); }, 30000); }; const initApiFeed = async () => { const api = window.YouthomeApi; const list = document.querySelector('.feed-list'); const template = list?.querySelector('.feed-card')?.cloneNode(true); const tabs = [...document.querySelectorAll('.feed-tab')]; const topics = document.querySelector('.topic-list'); const users = document.querySelector('.user-list'); if (!list || !template) return; const cacheScope = api.currentUser()?.id || 'anonymous'; list.replaceChildren(); topics?.replaceChildren(); users?.replaceChildren(); let feedRequest = 0; let batchToken = ''; const cacheRead = (key) => { try { return JSON.parse(sessionStorage.getItem(key) || 'null'); } catch (error) { return null; } }; const cacheWrite = (key, value) => { try { sessionStorage.setItem(key, JSON.stringify(value)); } catch (error) { /* 缓存不可用不影响接口 */ } }; const empty = (container, message) => { const node = document.createElement('p'); node.className = 'web-live-empty'; node.textContent = message; container.appendChild(node); }; const cardFor = (moment) => { const card = template.cloneNode(true); card.classList.add('web-live-card'); card.dataset.momentId = moment.id; card.querySelector('.card-title').textContent = summary(moment.content || ''); card.querySelector('.card-author-name').textContent = moment.userName || '青核用户'; const avatar = card.querySelector('.card-avatar-sm'); avatar.textContent = (moment.userName || '青').slice(0, 1); const avatarUrl = api.avatarUrl(moment.userAvatar); if (avatarUrl) { const img = document.createElement('img'); img.src = avatarUrl; img.alt = ''; avatar.appendChild(img); } card.querySelector('.card-like span').textContent = String(moment.likeCount || 0); const cover = card.querySelector('.card-cover'); cover.className = 'card-cover web-live-cover'; const imageUrl = moment.mediaUrls?.[0]; if (imageUrl) { const img = document.createElement('img'); img.src = imageUrl; img.alt = ''; cover.appendChild(img); cover.classList.add('has-image'); } const firstTag = moment.topicName || moment.tags?.[0]; if (firstTag) { const chip = document.createElement('span'); chip.className = 'web-live-tag'; chip.textContent = '#' + String(firstTag).replace(/^#/, ''); card.querySelector('.card-content')?.prepend(chip); } makeLink(card, `post-detail.html?moment=${encodeURIComponent(moment.id)}`); const authorRow = card.querySelector('.card-author-row'); if (authorRow && moment.userId) { authorRow.tabIndex = 0; authorRow.setAttribute('role', 'link'); const openUser = (event) => { event.stopPropagation(); location.href = `user.html?id=${encodeURIComponent(moment.userId)}`; }; authorRow.addEventListener('click', openUser); authorRow.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openUser(event); } }); } return card; }; const renderFeedItems = (label, data) => { let items = (data?.items || []).filter((item) => String(item.reviewStatus || '').toLowerCase() === 'approved' && !item.isYouthVideo); if (label === '关注') items = items.filter((item) => item.isFollowed); list.replaceChildren(...items.map(cardFor)); if (!items.length) empty(list, label === '关注' ? '当前动态中还没有已关注用户的内容' : '暂无动态'); }; const loadFeed = async (label) => { const requestId = ++feedRequest; const cacheKey = `youthome:web:${cacheScope}:feed:${label}`; const cached = cacheRead(cacheKey); if (cached) renderFeedItems(label, cached); else { list.replaceChildren(); empty(list, '正在加载动态…'); } const url = new URL(location.href); if (label === '推荐') url.searchParams.delete('tab'); else url.searchParams.set('tab', label); history.replaceState(null, '', url); try { const path = label === '热门' ? '/campus/moments/hot?page=1&size=20' : '/campus/moments?scope=CAMPUS&page=1&size=20'; const data = await api.get(path); if (requestId !== feedRequest) return; cacheWrite(cacheKey, data); renderFeedItems(label, data); } catch (error) { if (requestId !== feedRequest) return; list.replaceChildren(); empty(list, error.message || '动态加载失败'); } }; tabs.forEach((tab) => tab.addEventListener('click', () => { tabs.forEach((item) => item.classList.toggle('active', item === tab)); loadFeed(tab.textContent.trim()); })); const selected = tabs.find((tab) => tab.textContent.trim() === new URLSearchParams(location.search).get('tab')) || tabs[0]; tabs.forEach((tab) => tab.classList.toggle('active', tab === selected)); loadFeed(selected?.textContent.trim() || '推荐'); const renderTopics = (data) => { const items = [...(data?.items || [])].sort((a, b) => (b.heatScore || 0) - (a.heatScore || 0)); topics?.replaceChildren(...items.map((topic, index) => { const item = document.createElement('div'); item.className = 'topic-item' + (index >= 8 ? ' topic-item-extra' : ''); item.hidden = index >= 8; item.tabIndex = 0; item.setAttribute('role', 'link'); const rank = document.createElement('span'); rank.className = 'topic-rank'; rank.textContent = String(index + 1); const info = document.createElement('div'); info.className = 'topic-info'; const name = document.createElement('div'); name.className = 'topic-name'; name.textContent = '#' + topic.name; const count = document.createElement('div'); count.className = 'topic-count'; count.textContent = `${topic.followerCount || 0} 人关注`; info.append(name, count); item.append(rank, info); const open = () => { location.href = `topic-detail.html?topic=${encodeURIComponent(topic.name)}`; }; item.addEventListener('click', open); item.addEventListener('keydown', (event) => { if (event.key === 'Enter') open(); }); return item; })); if (topics && !items.length) empty(topics, '暂无话题'); const more = document.querySelector('.topic-more-button'); if (more) { more.hidden = items.length <= 8; more.onclick = () => { const expanded = more.getAttribute('aria-expanded') !== 'true'; more.setAttribute('aria-expanded', String(expanded)); topics.querySelectorAll('.topic-item-extra').forEach((item) => { item.hidden = !expanded; }); more.firstChild.textContent = expanded ? '收起' : '更多'; }; } }; const cachedTopics = cacheRead(`youthome:web:${cacheScope}:topics`); if (cachedTopics) renderTopics(cachedTopics); api.get('/topics/catalog?page=1&size=12').then((data) => { cacheWrite(`youthome:web:${cacheScope}:topics`, data); renderTopics(data); }).catch((error) => { if (topics && !cachedTopics) { topics.replaceChildren(); empty(topics, error.message || '话题加载失败'); } }); const loadRecommendations = async () => { if (!users) return; const cacheKey = `youthome:web:${cacheScope}:recommendations`; const cached = cacheRead(cacheKey); if (cached) renderRecommendations(cached); else users.replaceChildren(); try { const suffix = batchToken ? `&batchToken=${encodeURIComponent(batchToken)}` : ''; const data = await api.get(`/friendships/recommend?limit=4${suffix}`); batchToken = data?.batchToken || ''; cacheWrite(cacheKey, data); renderRecommendations(data); } catch (error) { if (!cached) empty(users, error.message || '推荐用户加载失败'); } }; const renderRecommendations = (data) => { if (!users) return; users.replaceChildren(); const items = data?.list || []; for (const person of items) { const item = document.createElement('div'); item.className = 'user-item'; const avatar = document.createElement('div'); avatar.className = 'user-item-avatar'; const avatarUrl = api.avatarUrl(person.avatar); if (avatarUrl) { const img = document.createElement('img'); img.src = avatarUrl; img.alt = ''; avatar.appendChild(img); } else avatar.textContent = (person.nickname || '青').slice(0, 1); const info = document.createElement('div'); info.className = 'user-item-info'; const name = document.createElement('div'); name.className = 'user-item-name'; name.textContent = person.nickname || '青核用户'; const desc = document.createElement('div'); desc.className = 'user-item-desc'; desc.textContent = [person.universityName, person.majorName].filter(Boolean).join(' · ') || '校园用户'; info.append(name, desc); const button = document.createElement('button'); button.className = 'follow-btn'; button.type = 'button'; let followed = ['FOLLOWED', 'FOLLOWING', 'MUTUAL'].includes(String(person.relationStatus || '').toUpperCase()); const update = () => { button.textContent = followed ? '已关注' : '关注'; button.classList.toggle('followed', followed); }; update(); button.addEventListener('click', async (event) => { event.stopPropagation(); button.disabled = true; try { await api.toggleFollow(person.id); followed = !followed; update(); } catch (error) { toast(error.message, true); } finally { button.disabled = false; } }); item.append(avatar, info, button); item.addEventListener('click', () => { location.href = `user.html?id=${encodeURIComponent(person.id)}`; }); users.appendChild(item); } if (!items.length) empty(users, '暂无推荐用户'); }; document.querySelector('.recommendation-refresh')?.addEventListener('click', loadRecommendations); loadRecommendations(); const searchInput = document.querySelector('.search-bar-input'); const openSearch = () => { const query = searchInput?.value.trim(); location.href = query ? `search.html?q=${encodeURIComponent(query)}` : 'search.html'; }; document.querySelector('.search-bar-btn')?.addEventListener('click', openSearch); searchInput?.addEventListener('keydown', (event) => { if (event.key === 'Enter') openSearch(); }); }; const initFeed = async () => { if (window.YouthomeApi && !window.YouthomeApi.isLocal()) return initApiFeed(); try { const data = await request('/campus/moments?scope=CAMPUS&page=1&size=20'); const list = document.querySelector('.feed-list'); const template = list.querySelector('.feed-card'); for (const moment of [...(data.items || [])].reverse()) { if (moment.reviewStatus !== 'approved' || moment.isYouthVideo) continue; const card = template.cloneNode(true); card.classList.add('web-live-card'); card.dataset.momentId = moment.id; card.querySelector('.card-title').textContent = summary(moment.content); card.querySelector('.card-author-name').textContent = moment.userName || '青核用户'; card.querySelector('.card-avatar-sm').textContent = (moment.userName || '青').slice(0, 1); card.querySelector('.card-like span').textContent = String(moment.likeCount || 0); const cover = card.querySelector('.card-cover'); cover.className = 'card-cover web-live-cover'; cover.dataset.cover = String(window.YouthomeLocal?.coverIndex(moment.id) || 1); if (moment.mediaUrls?.[0]) { const img = document.createElement('img'); img.src = moment.mediaUrls[0]; img.alt = ''; cover.appendChild(img); cover.classList.add('has-image'); } const firstTag = (moment.tags || [])[0]; if (firstTag) { const chip = document.createElement('span'); chip.className = 'web-live-tag'; chip.textContent = '#' + String(firstTag).replace(/^#/, ''); card.querySelector('.card-content')?.prepend(chip); } makeLink(card, `post-detail.html?moment=${encodeURIComponent(moment.id)}`); const authorRow = card.querySelector('.card-author-row'); if (authorRow && moment.userId) { authorRow.tabIndex = 0; authorRow.setAttribute('role', 'link'); authorRow.setAttribute('aria-label', `查看${moment.userName || '用户'}的主页`); const openUser = (event) => { event.stopPropagation(); location.href = `user.html?id=${encodeURIComponent(moment.userId)}`; }; authorRow.addEventListener('click', openUser); authorRow.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openUser(event); } }); } list.prepend(card); } const render = () => { const active = document.querySelector('.feed-tab.active')?.textContent.trim(); list.querySelectorAll('.web-live-card').forEach((card) => { card.hidden = active === '关注'; }); }; document.querySelectorAll('.feed-tab').forEach((tab) => tab.addEventListener('click', render)); render(); } catch (error) { toast(error.message, true); } }; const initProfile = async () => { if (!token()) return; const grid = document.querySelector('.content-grid'); const template = grid?.querySelector('.post-card'); const videoTemplate = grid?.querySelector('.content-card[data-href*="video-detail"]')?.cloneNode(true) || null; if (!grid || !template) return; const account = user() || {}; const api = window.YouthomeApi; const setHeader = (data) => { const name = data.nickname || '我的主页'; document.querySelector('.profile-name').textContent = name; const avatar = document.querySelector('.profile-avatar'); if (avatar) { avatar.textContent = ''; const url = api?.avatarUrl ? api.avatarUrl(data.avatar) : ''; if (url) { const img = document.createElement('img'); img.src = url; img.alt = name; avatar.appendChild(img); } else { avatar.textContent = name.slice(0, 1); } } const school = document.querySelector('.profile-school'); if (school?.lastChild) school.lastChild.textContent = ` ${data.universityName || '校园用户'}`; document.querySelector('.profile-bio').textContent = data.bio || '还没有填写个人简介'; }; setHeader(account); const isLocalMode = window.YouthomeApi?.isLocal ? window.YouthomeApi.isLocal() : true; if (!isLocalMode) { grid.replaceChildren(); document.querySelectorAll('.profile-stats .stat-num').forEach((node) => { node.textContent = '—'; }); } const anchor = isLocalMode ? grid.firstElementChild : null; try { // 作品列表是个人中心的首屏内容,先单独请求它,不被资料、关注和积分接口拖慢。 const profileTask = api?.profile ? api.profile().catch(() => null) : Promise.resolve(null); const followTask = api?.followStats ? api.followStats().catch(() => null) : Promise.resolve(null); const pointsTask = api?.points ? api.points().catch(() => null) : Promise.resolve(null); const momentsTask = request('/campus/moments/my?page=1&size=50'); const data = await momentsTask; const mine = (data.items || []).filter((moment) => !moment.isYouthVideo); let videoTotal = isLocalMode ? 0 : null; const ordered = isLocalMode ? [...mine].reverse() : mine; for (const moment of ordered) { const card = template.cloneNode(true); card.classList.add('web-live-card'); card.dataset.href = `post-detail.html?moment=${encodeURIComponent(moment.id)}`; card.querySelector('.card-title').textContent = summary(moment.content); const stats = card.querySelectorAll('.card-stat'); stats[0].lastChild.textContent = ` ${moment.likeCount || 0}`; stats[1].lastChild.textContent = ` ${moment.commentCount || 0}`; const cells = card.querySelectorAll('.post-img-cell'); if (!isLocalMode) cells.forEach((cell) => { cell.replaceChildren(); cell.style.background = 'none'; cell.hidden = true; }); (moment.mediaUrls || []).slice(0, 4).forEach((url, index) => { cells[index].style.background = 'none'; cells[index].hidden = false; const img = document.createElement('img'); img.src = url; img.alt = ''; cells[index].appendChild(img); }); if (!moment.mediaUrls?.length) { card.classList.add('web-text-card'); card.dataset.cover = String(window.YouthomeLocal?.coverIndex(moment.id) || 1); const badge = card.querySelector('.card-badge'); if (badge) badge.lastChild.textContent = ' 文字'; } makeLink(card, card.dataset.href); if (anchor) grid.insertBefore(card, anchor); else grid.appendChild(card); } if (!isLocalMode && !mine.length) { const empty = document.createElement('p'); empty.className = 'web-live-empty'; empty.textContent = '还没有发布动态,去发布一条吧'; grid.appendChild(empty); } // 我的视频作品 if (!isLocalMode && videoTemplate) { try { const youth = await request('/campus/moments/my-youth?page=1&size=50'); const videos = youth?.items || []; videoTotal = Number(youth?.total ?? videos.length); for (const video of videos) { const card = videoTemplate.cloneNode(true); card.classList.add('web-live-card'); card.dataset.href = 'video-detail.html?id=' + encodeURIComponent(video.id); card.setAttribute('data-href', card.dataset.href); const thumb = card.querySelector('.card-thumbnail-gradient'); if (thumb) { if (video.videoCoverUrl) thumb.style.background = 'url("' + video.videoCoverUrl + '") center/cover'; else thumb.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; } const title = card.querySelector('.card-title'); if (title) title.textContent = video.content || '未命名视频'; const duration = card.querySelector('.card-duration'); if (duration) duration.hidden = !video.videoDuration; const stats = card.querySelectorAll('.card-stat'); if (stats[0]) stats[0].lastChild.textContent = ' ' + (video.likeCount || 0); if (stats[1]) stats[1].lastChild.textContent = ' ' + (video.commentCount || 0); card.addEventListener('click', () => { location.href = card.dataset.href; }); if (anchor) grid.insertBefore(card, anchor); else grid.appendChild(card); } } catch (error) { /* 视频加载失败不影响动态展示 */ } } // 资料、关注和积分属于辅助信息,作品渲染完成后再补齐。 const [profile, followStats, points] = await Promise.all([profileTask, followTask, pointsTask]); setHeader({ ...account, ...(profile || {}) }); const statNodes = document.querySelectorAll('.profile-stats .stat-num'); // 离线数据的获赞数按当前作品计算;真实账号缺少总获赞接口。 const likesShown = Array.from(grid.querySelectorAll('.card-stat')) .map((node) => Number((node.textContent || '').replace(/[^0-9]/g, '')) || 0) .filter((_, index) => index % 2 === 0) .reduce((total, value) => total + value, 0); if (statNodes[0]) statNodes[0].textContent = !isLocalMode && videoTotal === null ? '—' : String(isLocalMode ? grid.querySelectorAll('.content-card').length : Number(data?.total ?? mine.length) + videoTotal); if (statNodes[1] && followStats) statNodes[1].textContent = String(followStats.followingCount ?? 0); if (statNodes[2] && followStats) statNodes[2].textContent = String(followStats.followersCount ?? 0); if (statNodes[3]) statNodes[3].textContent = isLocalMode ? String(likesShown) : '—'; if (statNodes[4] && points !== null && points !== undefined) statNodes[4].textContent = String(points); } catch (error) { if (!isLocalMode) { grid.replaceChildren(); const empty = document.createElement('p'); empty.className = 'web-live-empty'; empty.textContent = error.message || '个人作品加载失败'; grid.appendChild(empty); } toast(error.message, true); } }; const initDetail = async () => { const id = new URLSearchParams(location.search).get('moment'); if (!id) return; const fail = (message) => { document.documentElement.classList.remove('notification-route-pending'); document.querySelector('.detail-layout').replaceChildren(); const node = document.createElement('p'); node.className = 'web-live-empty'; node.textContent = message; document.querySelector('.detail-layout').appendChild(node); }; try { const moment = await request(`/campus/moments/detail?id=${encodeURIComponent(id)}`); document.title = `${summary(moment.content)} - 青核 Youthome`; document.body.classList.add('notification-post-detail', 'web-live-detail'); const breadcrumbs = document.querySelectorAll('.breadcrumb a'); breadcrumbs[0].textContent = '动态广场'; breadcrumbs[0].onclick = () => { location.href = 'feed.html'; }; breadcrumbs[1].textContent = '动态详情'; breadcrumbs[1].removeAttribute('href'); breadcrumbs[1].onclick = null; const author = moment.userName || user()?.nickname || '青核用户'; const ownPost = moment.userId === user()?.id; const authorHref = ownPost ? 'me.html' : (moment.userId ? `user.html?id=${encodeURIComponent(moment.userId)}` : ''); document.querySelectorAll('.post-detail-name, .author-name').forEach((node) => { node.textContent = author; node.onclick = authorHref ? () => { location.href = authorHref; } : null; node.style.cursor = authorHref ? 'pointer' : ''; if (authorHref) { node.setAttribute('role', 'link'); node.tabIndex = 0; } }); document.querySelectorAll('.post-detail-avatar, .author-avatar').forEach((node) => { node.textContent = author.slice(0, 1); node.onclick = authorHref ? () => { location.href = authorHref; } : null; node.style.cursor = authorHref ? 'pointer' : ''; if (authorHref) { node.setAttribute('role', 'link'); node.tabIndex = 0; } }); document.querySelector('.author-desc').textContent = ownPost ? (user()?.universityName || '校园用户') : '校园用户'; const meta = document.querySelectorAll('.post-detail-meta > span:not(.dot)'); meta[0].textContent = moment.createdAt || '刚刚'; meta[1].textContent = ownPost ? (user()?.universityName || '校园') : '校园'; meta[2].textContent = `浏览 ${moment.viewCount || 0}`; const content = document.querySelector('.post-detail-content'); content.replaceChildren(); const paragraph = document.createElement('p'); paragraph.textContent = moment.content; content.appendChild(paragraph); if (moment.reviewStatus !== 'approved') { const status = document.createElement('p'); status.className = 'web-review-state'; status.textContent = statusText(moment.reviewStatus); content.prepend(status); } const images = document.querySelector('.post-detail-images'); images.replaceChildren(); for (const url of moment.mediaUrls || []) { const img = document.createElement('img'); img.src = url; img.alt = '动态图片'; images.appendChild(img); } images.hidden = !moment.mediaUrls?.length; const tags = document.querySelector('.post-detail-tags'); tags.replaceChildren(); for (const value of moment.tags || []) { const tag = document.createElement('span'); tag.className = 'tag tag-primary'; tag.textContent = `#${String(value).replace(/^#/, '')}`; tags.appendChild(tag); } document.querySelector('.related-section')?.remove(); document.querySelector('.author-stats')?.remove(); document.querySelectorAll('.btn-follow').forEach((button) => button.remove()); const counts = document.querySelectorAll('.post-detail-actions .count'); counts[0].textContent = String(moment.likeCount || 0); counts[1].textContent = String(moment.commentCount || 0); counts[2].textContent = String(moment.shareCount || 0); document.querySelector('.post-detail-actions .action-btn-big.more')?.remove(); const actions = document.querySelectorAll('.post-detail-actions .action-btn-big'); const like = actions[0]; const commentAction = actions[1]; const share = actions[2]; const favorite = actions[3]; const bindAction = (node, label, handler) => { node.removeAttribute('onclick'); node.setAttribute('role', 'button'); node.setAttribute('tabindex', '0'); node.setAttribute('aria-label', label); node.addEventListener('click', handler); node.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); node.click(); } }); }; like.classList.toggle('liked', Boolean(moment.isLiked)); favorite.classList.toggle('saved', Boolean(moment.isFavorited)); bindAction(like, '点赞', async () => { if (like.dataset.busy) return; like.dataset.busy = 'true'; try { const result = await request(`/campus/moments/like?id=${encodeURIComponent(id)}`, { method: 'POST' }); like.classList.toggle('liked', Boolean(result.liked)); counts[0].textContent = String(result.likeCount); } catch (error) { toast(error.message, true); } finally { delete like.dataset.busy; } }); bindAction(favorite, '收藏', async () => { if (favorite.dataset.busy) return; favorite.dataset.busy = 'true'; try { const result = await request(`/campus/moments/favorite?id=${encodeURIComponent(id)}`, { method: 'POST' }); favorite.classList.toggle('saved', Boolean(result.favorited)); toast(result.favorited ? '已收藏' : '已取消收藏'); } catch (error) { toast(error.message, true); } finally { delete favorite.dataset.busy; } }); bindAction(commentAction, '评论', () => document.querySelector('.comment-input')?.focus()); const closeSheet = () => document.querySelector('.web-sheet-overlay')?.remove(); const copyLinkAndCount = async () => { try { await navigator.clipboard.writeText(location.href); toast('链接已复制'); } catch (error) { toast('复制链接失败,请从地址栏复制', true); return; } try { await request(`/campus/moments/share?id=${encodeURIComponent(id)}`, { method: 'POST' }); counts[2].textContent = String(Number(counts[2].textContent) + 1); } catch (error) { /* 链接已复制,计数失败不影响使用 */ } }; const openPicker = (kind, confirmText, onSubmit) => { closeSheet(); const overlay = document.createElement('div'); overlay.className = 'web-sheet-overlay'; const panel = document.createElement('div'); panel.className = 'web-sheet-panel'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); const head = document.createElement('div'); head.className = 'web-sheet-head'; const title = document.createElement('div'); title.className = 'web-sheet-title'; title.textContent = kind === 'friend' ? '分享给好友' : '分享到群聊'; const closeBtn = document.createElement('button'); closeBtn.className = 'web-sheet-close'; closeBtn.type = 'button'; closeBtn.textContent = '×'; head.append(title, closeBtn); const body = document.createElement('div'); body.className = 'web-sheet-body'; const hint = document.createElement('p'); hint.className = 'web-sheet-hint'; hint.textContent = '正在加载…'; body.appendChild(hint); const foot = document.createElement('div'); foot.className = 'web-sheet-foot'; const cancel = document.createElement('button'); cancel.className = 'web-sheet-btn ghost'; cancel.type = 'button'; cancel.textContent = '取消'; const confirm = document.createElement('button'); confirm.className = 'web-sheet-btn primary'; confirm.type = 'button'; confirm.textContent = confirmText; confirm.disabled = true; foot.append(cancel, confirm); panel.append(head, body, foot); overlay.appendChild(panel); document.body.appendChild(overlay); const close = () => overlay.remove(); overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); }); closeBtn.addEventListener('click', close); cancel.addEventListener('click', close); const selected = new Set(); const renderItems = (items) => { body.replaceChildren(); if (!items.length) { hint.textContent = '暂时没有可分享的对象'; body.appendChild(hint); return; } items.forEach((item) => { const row = document.createElement('label'); row.className = 'web-sheet-row'; const avatar = document.createElement('span'); avatar.className = 'web-sheet-avatar'; avatar.textContent = (item.nickname || item.name || '友').slice(0, 1); const info = document.createElement('span'); info.className = 'web-sheet-row-info'; const name = document.createElement('b'); name.textContent = item.nickname || item.name || '用户'; const desc = document.createElement('span'); desc.textContent = item.universityName || item.description || ''; info.append(name, desc); const check = document.createElement('input'); check.type = 'checkbox'; check.addEventListener('change', () => { if (check.checked) selected.add(item.id); else selected.delete(item.id); confirm.disabled = selected.size === 0; }); row.append(avatar, info, check); body.appendChild(row); }); }; (async () => { try { const data = kind === 'friend' ? await request('/friendships/my') : await request('/groups/my'); renderItems(Array.isArray(data) ? data : (data?.items || [])); } catch (error) { hint.textContent = error.message || '加载失败'; } })(); confirm.addEventListener('click', async () => { confirm.disabled = true; confirm.textContent = '分享中…'; try { await onSubmit([...selected]); close(); toast('分享成功'); } catch (error) { confirm.disabled = false; confirm.textContent = confirmText; hint.textContent = error.message || '分享失败'; hint.classList.add('error'); } }); }; const openReport = () => { closeSheet(); const overlay = document.createElement('div'); overlay.className = 'web-sheet-overlay'; const panel = document.createElement('div'); panel.className = 'web-sheet-panel'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); const head = document.createElement('div'); head.className = 'web-sheet-head'; const title = document.createElement('div'); title.className = 'web-sheet-title'; title.textContent = '举报内容'; const closeBtn = document.createElement('button'); closeBtn.className = 'web-sheet-close'; closeBtn.type = 'button'; closeBtn.textContent = '×'; head.append(title, closeBtn); const body = document.createElement('div'); body.className = 'web-sheet-body'; const reasons = ['垃圾广告', '色情低俗', '违法违规', '人身攻击', '虚假信息', '其他']; let reason = ''; reasons.forEach((value) => { const label = document.createElement('label'); label.className = 'web-sheet-radio'; const input = document.createElement('input'); input.type = 'radio'; input.name = 'report-reason'; input.value = value; input.addEventListener('change', () => { reason = value; }); label.append(input, document.createTextNode(' ' + value)); body.appendChild(label); }); const desc = document.createElement('textarea'); desc.className = 'web-sheet-textarea'; desc.rows = 3; desc.placeholder = '补充说明(可选,500 字以内)'; body.appendChild(desc); const foot = document.createElement('div'); foot.className = 'web-sheet-foot'; const cancel = document.createElement('button'); cancel.className = 'web-sheet-btn ghost'; cancel.type = 'button'; cancel.textContent = '取消'; const submit = document.createElement('button'); submit.className = 'web-sheet-btn primary'; submit.type = 'button'; submit.textContent = '提交举报'; foot.append(cancel, submit); panel.append(head, body, foot); overlay.appendChild(panel); document.body.appendChild(overlay); const close = () => overlay.remove(); overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); }); closeBtn.addEventListener('click', close); cancel.addEventListener('click', close); submit.addEventListener('click', async () => { if (!reason) { toast('请选择举报原因', true); return; } submit.disabled = true; submit.textContent = '提交中…'; try { await request('/reports', { method: 'POST', body: JSON.stringify({ targetType: 'MOMENT', targetId: id, reason, description: desc.value.trim() }) }); close(); toast('举报已提交,我们会尽快处理'); } catch (error) { submit.disabled = false; submit.textContent = '提交举报'; toast(error.message || '举报失败', true); } }); }; const openPostShareSheet = () => { const overlay = document.createElement('div'); overlay.className = 'web-sheet-overlay'; const panel = document.createElement('div'); panel.className = 'web-sheet-panel'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); const head = document.createElement('div'); head.className = 'web-sheet-head'; const title = document.createElement('div'); title.className = 'web-sheet-title'; title.textContent = '分享到'; const closeBtn = document.createElement('button'); closeBtn.className = 'web-sheet-close'; closeBtn.type = 'button'; closeBtn.textContent = '×'; head.append(title, closeBtn); const body = document.createElement('div'); body.className = 'web-sheet-body'; const options = [ ['复制链接', '生成链接,可粘贴到任意地方', () => copyLinkAndCount()], ['分享给好友', '选择好友,卡片直达会话', () => openPicker('friend', '分享给好友', (ids) => request('/campus/shares/friends', { method: 'POST', body: JSON.stringify({ targetType: 'MOMENT', targetId: id, receiverIds: ids }) }))], ['分享到群聊', '选择一个群聊发送卡片', () => openPicker('group', '分享到群聊', (ids) => request('/campus/shares/group', { method: 'POST', body: JSON.stringify({ targetType: 'MOMENT', targetId: id, groupId: ids[0] }) }))], ['举报内容', '不符合社区规范的内容', openReport] ]; options.forEach(([label, descText, handler]) => { const row = document.createElement('button'); row.className = 'web-sheet-option'; row.type = 'button'; const main = document.createElement('span'); main.className = 'web-sheet-option-label'; main.textContent = label; const sub = document.createElement('span'); sub.className = 'web-sheet-option-desc'; sub.textContent = descText; row.append(main, sub); row.addEventListener('click', () => { overlay.remove(); handler(); }); body.appendChild(row); }); panel.append(head, body); overlay.appendChild(panel); document.body.appendChild(overlay); overlay.addEventListener('click', (event) => { if (event.target === overlay) overlay.remove(); }); closeBtn.addEventListener('click', () => overlay.remove()); }; bindAction(share, '分享', openPostShareSheet); const list = document.querySelector('.comment-list'); list.replaceChildren(); const commentInput = document.querySelector('.comment-input'); const avatarPalette = [ 'linear-gradient(135deg, #73D3E6, #17B0CF)', 'linear-gradient(135deg, #95E1B5, #52C41A)', 'linear-gradient(135deg, #FFD4A3, #FF9F45)', 'linear-gradient(135deg, #B5D8FF, #4A90FF)', 'linear-gradient(135deg, #FFB3D9, #EB2F96)', 'linear-gradient(135deg, #D4B5FF, #9254DE)' ]; let replyTarget = null; // 回复提示条:回复某条评论时显示在输入框上方 const replyChip = document.createElement('div'); replyChip.className = 'web-reply-chip'; replyChip.hidden = true; const replyChipText = document.createElement('span'); const replyChipClose = document.createElement('button'); replyChipClose.type = 'button'; replyChipClose.setAttribute('aria-label', '取消回复'); replyChipClose.textContent = '取消'; replyChip.append(replyChipText, replyChipClose); document.querySelector('.comment-input-wrap')?.prepend(replyChip); const renderReplyChip = () => { if (!replyTarget) { replyChip.hidden = true; return; } replyChip.hidden = false; replyChipText.textContent = '正在回复 ' + replyTarget.userName; }; replyChipClose.addEventListener('click', () => { replyTarget = null; renderReplyChip(); }); const COMMENT_HEART = 'M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'; const makeHeart = (liked) => { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('width', '14'); svg.setAttribute('height', '14'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', COMMENT_HEART); path.setAttribute('stroke-width', '2'); svg.appendChild(path); const paint = (state) => { path.setAttribute('fill', state ? 'currentColor' : 'none'); path.setAttribute('stroke', state ? 'none' : 'currentColor'); }; paint(liked); return { svg, paint }; }; const likeComment = async (commentId, onDone) => { try { const result = await request('/campus/moments/comments/like?commentId=' + encodeURIComponent(commentId), { method: 'POST' }); onDone(result); } catch (error) { toast(error.message, true); } }; const buildCommentMeta = (comment) => { const meta = document.createElement('div'); meta.className = 'comment-meta'; const time = document.createElement('span'); time.textContent = comment.createdAgo || comment.createdAt || '刚刚'; const likeButton = document.createElement('span'); likeButton.className = 'like-btn' + (comment.isLiked ? ' liked' : ''); likeButton.setAttribute('role', 'button'); likeButton.tabIndex = 0; likeButton.setAttribute('aria-label', '点赞这条评论'); const heart = makeHeart(Boolean(comment.isLiked)); const likeCount = document.createElement('span'); likeCount.textContent = String(comment.likeCount || 0); likeButton.append(heart.svg, likeCount); const toggle = () => { if (likeButton.dataset.busy) return; likeButton.dataset.busy = 'true'; likeComment(comment.id, (result) => { likeButton.classList.toggle('liked', Boolean(result.liked)); heart.paint(Boolean(result.liked)); likeCount.textContent = String(result.likeCount); delete likeButton.dataset.busy; }); }; likeButton.addEventListener('click', (event) => { event.stopPropagation(); toggle(); }); likeButton.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggle(); } }); const replyButton = document.createElement('span'); replyButton.textContent = '回复'; replyButton.setAttribute('role', 'button'); replyButton.tabIndex = 0; replyButton.setAttribute('aria-label', '回复 ' + (comment.userName || '这条评论')); replyButton.addEventListener('click', (event) => { event.stopPropagation(); startReply(comment); }); meta.append(time, likeButton, replyButton); const canDelete = (comment.userId && comment.userId === user()?.id) || moment.userId === user()?.id; if (canDelete) { const deleteButton = document.createElement('span'); deleteButton.className = 'comment-delete-btn'; deleteButton.textContent = '删除'; deleteButton.setAttribute('role', 'button'); deleteButton.tabIndex = 0; deleteButton.addEventListener('click', (event) => { event.stopPropagation(); deleteComment(comment.id); }); meta.appendChild(deleteButton); } return meta; }; const buildReplyRow = (reply, index) => { const row = document.createElement('div'); row.className = 'reply-item'; const avatar = document.createElement('div'); avatar.className = 'reply-avatar'; avatar.setAttribute('style', avatarPalette[index % avatarPalette.length]); avatar.textContent = (reply.userName || '青').slice(0, 1); const body = document.createElement('div'); body.className = 'reply-body'; const nameRow = document.createElement('div'); nameRow.className = 'reply-name-row'; const name = document.createElement('span'); name.className = 'reply-name'; name.textContent = reply.userName || '青核用户'; nameRow.appendChild(name); const text = document.createElement('div'); text.className = 'reply-text'; text.textContent = reply.content; const meta = document.createElement('div'); meta.className = 'reply-meta'; const time = document.createElement('span'); time.textContent = reply.createdAgo || '刚刚'; const like = document.createElement('span'); like.className = 'like-btn' + (reply.isLiked ? ' liked' : ''); like.setAttribute('role', 'button'); like.tabIndex = 0; like.setAttribute('aria-label', '点赞这条回复'); like.textContent = '点赞 ' + (reply.likeCount || 0); like.addEventListener('click', (event) => { event.stopPropagation(); likeComment(reply.id, (result) => { like.classList.toggle('liked', Boolean(result.liked)); like.textContent = '点赞 ' + result.likeCount; }); }); const replyAgain = document.createElement('span'); replyAgain.textContent = '回复'; replyAgain.setAttribute('role', 'button'); replyAgain.tabIndex = 0; replyAgain.addEventListener('click', (event) => { event.stopPropagation(); startReply(reply); }); meta.append(time, like, replyAgain); body.append(nameRow, text, meta); row.append(avatar, body); return row; }; const buildCommentRow = (comment, index) => { const row = document.createElement('div'); row.className = 'comment-item'; row.dataset.commentId = comment.id; const avatar = document.createElement('div'); avatar.className = 'comment-avatar'; avatar.setAttribute('style', avatarPalette[index % avatarPalette.length]); avatar.textContent = (comment.userName || '青').slice(0, 1); const body = document.createElement('div'); body.className = 'comment-body'; const nameRow = document.createElement('div'); nameRow.className = 'comment-name-row'; const name = document.createElement('span'); name.className = 'comment-name'; name.textContent = comment.userName || '青核用户'; nameRow.appendChild(name); if (comment.userId && comment.userId === moment.userId) { const tag = document.createElement('span'); tag.className = 'comment-author-tag'; tag.textContent = '作者'; nameRow.appendChild(tag); } const text = document.createElement('div'); text.className = 'comment-text'; text.textContent = comment.content; body.append(nameRow, text, buildCommentMeta(comment)); if (comment.replies && comment.replies.length) { const replies = document.createElement('div'); replies.className = 'reply-list'; comment.replies.forEach((reply, replyIndex) => replies.appendChild(buildReplyRow(reply, replyIndex + index + 1))); body.appendChild(replies); } row.append(avatar, body); return row; }; const deleteComment = async (commentId) => { if (!window.confirm('确定删除这条评论吗?')) return; try { await request(`/campus/moments/comments?momentId=${encodeURIComponent(id)}&commentId=${encodeURIComponent(commentId)}`, { method: 'DELETE' }); toast('评论已删除'); await renderComments(); } catch (error) { toast(error.message || '删除失败,请重试', true); } }; const renderComments = async () => { const result = await request('/campus/moments/comments?momentId=' + encodeURIComponent(id) + '&page=1&size=50'); list.replaceChildren(); (result.items || []).forEach((comment, index) => list.appendChild(buildCommentRow(comment, index))); if (!list.children.length) { const empty = document.createElement('p'); empty.className = 'web-live-empty'; empty.textContent = '还没有评论,来说点什么'; list.appendChild(empty); } const total = (result.items || []).reduce((sum, item) => sum + 1 + (item.replies?.length || 0), 0); const totalNode = document.querySelector('.comment-count'); if (totalNode) totalNode.textContent = '共 ' + total + ' 条'; if (counts[1]) counts[1].textContent = String(total); }; // 回复二级评论时挂到它的顶级评论下,提示里显示被回复的人 const startReply = (comment) => { replyTarget = { id: comment.parentId || comment.id, userName: comment.userName }; renderReplyChip(); commentInput?.focus(); }; try { await renderComments(); } catch (error) { list.textContent = '评论暂时无法加载'; } const cleanupWarning = sessionStorage.getItem('youthome:web:draftCleanupWarning'); if (cleanupWarning) { sessionStorage.removeItem('youthome:web:draftCleanupWarning'); toast(cleanupWarning, true); } document.querySelector('.btn-send')?.addEventListener('click', async () => { const text = commentInput?.value.trim(); if (!text) { toast('请输入评论内容', true); commentInput?.focus(); return; } const asReply = Boolean(replyTarget); try { await request('/campus/moments/comments', { method: 'POST', body: JSON.stringify({ momentId: id, content: text, parentId: replyTarget?.id || null }) }); if (commentInput) commentInput.value = ''; replyTarget = null; renderReplyChip(); await renderComments(); toast(asReply ? '回复已发布' : '评论已发布'); } catch (error) { toast(error.message, true); } }); document.documentElement.classList.remove('notification-route-pending', 'notification-route-failed'); } catch (error) { fail(error.message); } }; const initDrafts = async () => { if (!token()) { location.replace(loginUrl('drafts.html')); return; } const list = document.querySelector('#draftsList'); const template = list.querySelector('.draft-card'); list.replaceChildren(); try { const drafts = (await request('/draft/list')).filter((item) => item.contentType === 'MOMENT'); document.querySelector('.tab-count').textContent = String(drafts.length); for (const draft of drafts) { const card = template.cloneNode(true); card.dataset.draftId = draft.draftId; card.querySelector('.draft-title').textContent = summary(draft.content || '未命名动态'); card.querySelector('.draft-preview').textContent = draft.content || '尚未填写内容'; card.querySelectorAll('.draft-meta-item')[0].lastChild.textContent = ` ${draft.updatedAt || '刚刚'} 保存`; card.querySelectorAll('.draft-meta-item')[1].lastChild.textContent = ` ${(draft.mediaUrls || []).length} 张图片`; card.querySelector('.draft-thumb-count').textContent = String((draft.mediaUrls || []).length); card.querySelector('.draft-action-edit').onclick = null; card.querySelector('.draft-action-edit').addEventListener('click', () => { location.href = `publish-post.html?draft=${encodeURIComponent(draft.draftId)}`; }); card.querySelector('.draft-action-publish').addEventListener('click', async () => { if (!draft.content?.trim()) { toast('草稿内容为空,请先编辑', true); return; } try { const result = await request('/campus/moments', { method: 'POST', body: JSON.stringify({ content: draft.content, mediaUrls: draft.mediaUrls || [], tags: draft.tags || [], category: draft.category || '校园生活', scope: draft.scope || 'CAMPUS' }) }); try { await request(`/draft/${encodeURIComponent(draft.draftId)}`, { method: 'DELETE' }); } catch (error) { sessionStorage.setItem('youthome:web:draftCleanupWarning', '动态已提交,但原草稿未能删除'); } location.href = `post-detail.html?moment=${encodeURIComponent(result.id)}`; } catch (error) { toast(error.message, true); } }); card.querySelector('.draft-action-delete').addEventListener('click', async () => { if (!confirm('确定删除这条草稿吗?')) return; try { await request(`/draft/${encodeURIComponent(draft.draftId)}`, { method: 'DELETE' }); card.remove(); toast('草稿已删除'); } catch (error) { toast(error.message, true); } }); list.appendChild(card); } if (!list.children.length) { const empty = document.createElement('p'); empty.className = 'web-live-empty'; empty.textContent = '草稿箱为空'; list.appendChild(empty); } document.querySelector('.action-bar-right .btn-primary').disabled = true; document.querySelectorAll('.action-bar-right .btn').forEach((button) => { button.hidden = true; }); document.querySelectorAll('.tab-bar .tab-item')[1]?.setAttribute('hidden', ''); } catch (error) { toast(error.message, true); } }; if (page === 'publish-post.html') initEditor(); if (page === 'feed.html') initFeed(); if (page === 'me.html') initProfile(); if (page === 'post-detail.html') initDetail(); if (page === 'drafts.html') initDrafts(); })();