(() => {
'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();
})();