You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1324 lines
70 KiB
1324 lines
70 KiB
(() => {
|
|
'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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg><span>添加图片</span>';
|
|
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();
|
|
})();
|
|
|