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.
5291 lines
277 KiB
5291 lines
277 KiB
(function () {
|
|
'use strict';
|
|
|
|
const page = location.pathname.split('/').pop() || 'feed.html';
|
|
const sidebar = document.querySelector('.sidebar');
|
|
let app = sidebar && sidebar.closest('.app');
|
|
|
|
if (sidebar && !app) {
|
|
const content = sidebar.nextElementSibling;
|
|
app = document.createElement('div');
|
|
app.className = 'app';
|
|
sidebar.parentNode.insertBefore(app, sidebar);
|
|
app.appendChild(sidebar);
|
|
if (content && content.tagName !== 'SCRIPT') app.appendChild(content);
|
|
}
|
|
|
|
if (!sidebar || !app) {
|
|
const showStandaloneToast = (message, type = 'info') => {
|
|
document.querySelector('.web-toast')?.remove();
|
|
const toast = document.createElement('div');
|
|
toast.className = `web-toast${type === 'error' ? ' is-error' : ''}`;
|
|
toast.setAttribute('role', 'status');
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
window.setTimeout(() => toast.remove(), 2400);
|
|
};
|
|
if (page === 'register.html') {
|
|
const selectedSchool = localStorage.getItem('youthome:web:selectedSchool');
|
|
const schoolLabel = document.querySelector('.school-select .text');
|
|
if (selectedSchool && schoolLabel) { schoolLabel.textContent = selectedSchool; schoolLabel.classList.remove('placeholder'); }
|
|
window.selectSchool = () => { location.href = 'select-school.html'; };
|
|
window.doRegister = () => {
|
|
const phone = document.querySelector('input[type="tel"]');
|
|
const code = document.querySelector('input[placeholder*="验证码"]');
|
|
const password = document.querySelector('input[type="password"]');
|
|
const agreement = document.querySelector('input[type="checkbox"]');
|
|
if (!selectedSchool) { showStandaloneToast('请先选择学校', 'error'); return; }
|
|
if (!/^1\d{10}$/.test(phone?.value.trim() || '')) { showStandaloneToast('请输入正确的 11 位手机号', 'error'); phone?.focus(); return; }
|
|
if (!/^\d{6}$/.test(code?.value.trim() || '')) { showStandaloneToast('请输入 6 位验证码', 'error'); code?.focus(); return; }
|
|
if (!/^(?=.*[A-Za-z])(?=.*\d).{8,20}$/.test(password?.value || '')) { showStandaloneToast('密码需为 8-20 位且包含字母和数字', 'error'); password?.focus(); return; }
|
|
if (!agreement?.checked) { showStandaloneToast('请阅读并同意用户协议和隐私政策', 'error'); return; }
|
|
location.href = 'register-success.html';
|
|
};
|
|
}
|
|
if (page === 'login.html') {
|
|
window.goLogin = () => {
|
|
const active = document.querySelector('.auth-panel.active, .login-form.active') || document;
|
|
const phone = active.querySelector('input[type="tel"]');
|
|
const credential = active.querySelector('input[type="password"], input[placeholder*="验证码"]');
|
|
if (!/^1\d{10}$/.test(phone?.value.trim() || '')) { showStandaloneToast('请输入正确的 11 位手机号', 'error'); phone?.focus(); return; }
|
|
if (!credential?.value.trim()) { showStandaloneToast('请输入密码或验证码', 'error'); credential?.focus(); return; }
|
|
location.href = 'feed.html';
|
|
};
|
|
}
|
|
if (page === 'forgot-password.html') {
|
|
const originalGoStep = window.goStep;
|
|
window.goStep = (step) => {
|
|
const phone = document.querySelector('input[type="tel"]');
|
|
const code = document.querySelector('input[placeholder*="验证码"]');
|
|
const passwords = Array.from(document.querySelectorAll('input[type="password"]'));
|
|
if (step === 2 && (!/^1\d{10}$/.test(phone?.value.trim() || '') || !/^\d{6}$/.test(code?.value.trim() || ''))) {
|
|
showStandaloneToast('请输入正确的手机号和 6 位验证码', 'error'); return;
|
|
}
|
|
if (step === 3 && (passwords[0]?.value !== passwords[1]?.value || !/^(?=.*[A-Za-z])(?=.*\d).{8,20}$/.test(passwords[0]?.value || ''))) {
|
|
showStandaloneToast('两次密码需一致,且包含字母和数字', 'error'); return;
|
|
}
|
|
originalGoStep?.(step);
|
|
};
|
|
}
|
|
return;
|
|
}
|
|
|
|
const liveApi = window.YouthomeApi;
|
|
if (page !== 'help.html' && liveApi && !liveApi.isLocal() && !liveApi.token()) {
|
|
const next = page + location.search;
|
|
location.replace(`login.html?next=${encodeURIComponent(next)}`);
|
|
return;
|
|
}
|
|
|
|
const icon = {
|
|
plus: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>',
|
|
chevronDown: '<svg class="web-publish-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg>',
|
|
feed: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h16M4 12h10M4 19h16"/><circle cx="18" cy="12" r="2"/></svg>',
|
|
video: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="14" height="14" rx="2"/><path d="m17 10 4-2v8l-4-2z"/></svg>',
|
|
calendar: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 10h18"/></svg>',
|
|
user: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
|
|
bell: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 8-3 8h18s-3-1-3-8"/><path d="M10 20h4"/></svg>',
|
|
settings: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1l2-1.5-2-3.4-2.5 1A8 8 0 0 0 14.7 6L14.4 3h-4.8l-.4 3a8 8 0 0 0-1.7 1.1l-2.5-1-2 3.4L5.1 11a7 7 0 0 0 0 2L3 14.5l2 3.4 2.5-1A8 8 0 0 0 9.2 18l.4 3h4.8l.4-3a8 8 0 0 0 1.7-1.1l2.5 1 2-3.4-2.1-1.5a7 7 0 0 0 .1-1z"/></svg>',
|
|
arrow: '<svg class="web-user-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>',
|
|
menu: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>',
|
|
search: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>'
|
|
};
|
|
|
|
const groups = {
|
|
feed: ['feed.html', 'post-detail.html', 'post-detail-1.html', 'topic-detail.html', 'search.html', 'user.html'],
|
|
video: ['video.html', 'video-detail.html', 'publish-video.html'],
|
|
activities: ['activities.html', 'activity-detail.html', 'activity-create.html', 'my-activity-apply.html'],
|
|
me: ['me.html', 'me-edit.html', 'favorites.html', 'wallet.html', 'drafts.html', 'authentication.html', 'follow.html', 'help.html'],
|
|
notification: ['notification.html', 'chat.html'],
|
|
settings: ['settings.html']
|
|
};
|
|
const notificationHref = 'notification.html?v=20261013-message-links-3';
|
|
|
|
const fromNotification = ['post-detail.html', 'user.html'].includes(page) && new URLSearchParams(location.search).has('notice');
|
|
const activeKey = fromNotification ? 'notification' : Object.keys(groups).find((key) => groups[key].includes(page)) || 'feed';
|
|
const navLink = (key, label, target, iconMarkup, badge) => `
|
|
<button class="web-nav-link${activeKey === key ? ' is-active' : ''}" data-href="${target}" ${activeKey === key ? 'aria-current="page"' : ''}>
|
|
${iconMarkup}<span>${label}</span>${badge ? `<span class="web-nav-badge">${badge}</span>` : ''}
|
|
</button>`;
|
|
|
|
sidebar.innerHTML = `
|
|
<div class="web-sidebar">
|
|
<div class="web-brand" data-href="feed.html" role="link" tabindex="0" aria-label="返回动态广场">
|
|
<span class="web-brand-mark">青</span>
|
|
<span class="web-brand-copy">
|
|
<span class="web-brand-name">青核 Youthome</span>
|
|
<span class="web-brand-subtitle">校园青年社区</span>
|
|
</span>
|
|
</div>
|
|
<div class="web-publish-wrap">
|
|
<button class="web-publish-button" type="button" aria-expanded="false">
|
|
${icon.plus}<span>发布</span>${icon.chevronDown}
|
|
</button>
|
|
<div class="web-publish-menu" role="menu">
|
|
<button class="web-publish-option" data-href="publish-post.html" role="menuitem">发布动态</button>
|
|
<button class="web-publish-option" data-href="publish-video.html" role="menuitem">发布视频</button>
|
|
<button class="web-publish-option" data-href="activity-create.html" role="menuitem">发起活动</button>
|
|
</div>
|
|
</div>
|
|
<nav class="web-nav" aria-label="主导航">
|
|
<div class="web-nav-label">探索</div>
|
|
${navLink('feed', '动态广场', 'feed.html', icon.feed)}
|
|
${navLink('video', '视频广场', 'video.html', icon.video)}
|
|
${navLink('activities', '活动组局', 'activities.html', icon.calendar)}
|
|
<div class="web-nav-label">我的</div>
|
|
${navLink('me', '个人中心', 'me.html', icon.user)}
|
|
${navLink('notification', '消息通知', notificationHref, icon.bell, '3')}
|
|
${navLink('settings', '设置', 'settings.html', icon.settings)}
|
|
</nav>
|
|
<div class="web-sidebar-footer">
|
|
<button class="web-user-button" data-href="me.html" type="button">
|
|
<span class="web-user-avatar">我</span>
|
|
<span class="web-user-copy"><span class="web-user-name">我的账号</span><span class="web-user-school">校园用户</span></span>
|
|
${icon.arrow}
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
const main = sidebar.nextElementSibling;
|
|
if (main) main.classList.add('web-shell-main');
|
|
|
|
const title = (document.title || '青核 Youthome').split(' - ')[0].trim();
|
|
const topbar = document.createElement('header');
|
|
topbar.className = 'web-topbar';
|
|
topbar.innerHTML = `
|
|
<div class="web-topbar-leading">
|
|
<button class="web-menu-button" type="button" aria-label="打开导航">${icon.menu}</button>
|
|
<span class="web-page-title">${title}</span>
|
|
</div>
|
|
<form class="web-global-search" role="search">
|
|
${icon.search}
|
|
<input type="search" aria-label="全站搜索" placeholder="搜索动态、用户、话题…" autocomplete="off">
|
|
</form>
|
|
<div class="web-topbar-actions">
|
|
<button class="web-icon-button" type="button" data-href="${notificationHref}" aria-label="消息通知">${icon.bell}<span class="web-notification-dot"></span></button>
|
|
<button class="web-topbar-publish" type="button" data-href="publish-post.html">${icon.plus}<span>发布</span></button>
|
|
<button class="web-topbar-avatar" type="button" data-href="me.html" aria-label="个人中心">我</button>
|
|
</div>`;
|
|
document.body.appendChild(topbar);
|
|
|
|
const backdrop = document.createElement('button');
|
|
backdrop.className = 'web-drawer-backdrop';
|
|
backdrop.type = 'button';
|
|
backdrop.setAttribute('aria-label', '关闭导航');
|
|
document.body.appendChild(backdrop);
|
|
|
|
document.body.classList.add('web-shell-ready');
|
|
|
|
const showWebToast = (message, type = 'info') => {
|
|
document.querySelector('.web-toast')?.remove();
|
|
const toast = document.createElement('div');
|
|
toast.className = `web-toast${type === 'error' ? ' is-error' : ''}`;
|
|
toast.setAttribute('role', 'status');
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
window.setTimeout(() => toast.remove(), 2400);
|
|
};
|
|
|
|
const unreadNotificationIds = ['like-canteen', 'comment-canteen', 'mention-milk-tea'];
|
|
const notificationReadKey = (id) => `youthome:web:notification-read:${id}`;
|
|
const updateNotificationIndicators = () => {
|
|
const unread = unreadNotificationIds.filter((id) => localStorage.getItem(notificationReadKey(id)) !== 'true').length;
|
|
document.querySelectorAll('.web-nav-badge').forEach((badge) => {
|
|
badge.textContent = String(unread);
|
|
badge.hidden = unread === 0;
|
|
});
|
|
const dot = document.querySelector('.web-notification-dot');
|
|
if (dot) dot.hidden = unread === 0;
|
|
};
|
|
const refreshRemoteUnread = () => {
|
|
const api = window.YouthomeApi;
|
|
if (!api || api.isLocal() || !api.token()) return;
|
|
api.get('/campus/notifications?isRead=false&page=1&size=1')
|
|
.then((data) => {
|
|
const unread = Number(data?.total || 0);
|
|
document.querySelectorAll('.web-nav-badge').forEach((badge) => {
|
|
badge.textContent = String(unread);
|
|
badge.hidden = unread === 0;
|
|
});
|
|
const dot = document.querySelector('.web-notification-dot');
|
|
if (dot) dot.hidden = unread === 0;
|
|
})
|
|
.catch(() => {});
|
|
};
|
|
window.webRefreshNotificationIndicators = () => { updateNotificationIndicators(); refreshRemoteUnread(); };
|
|
updateNotificationIndicators();
|
|
refreshRemoteUnread();
|
|
|
|
// 登录后,侧栏和顶栏应当显示当前账号,而不是页面里写死的示例人物
|
|
const applyAccountIdentity = () => {
|
|
// 真实后端登录时本地数据层里没有这个账号,必须优先用登录态
|
|
const account = window.YouthomeApi?.currentUser?.() || window.YouthomeLocal?.account?.() || null;
|
|
if (!account) return;
|
|
const setAvatar = (node) => {
|
|
if (!node) return;
|
|
node.textContent = '';
|
|
const avatarUrl = window.YouthomeApi?.avatarUrl ? window.YouthomeApi.avatarUrl(account.avatar) : account.avatar;
|
|
if (avatarUrl) {
|
|
const img = document.createElement('img');
|
|
img.src = avatarUrl;
|
|
img.alt = account.nickname;
|
|
node.appendChild(img);
|
|
} else {
|
|
node.textContent = (account.nickname || '我').slice(0, 1);
|
|
}
|
|
};
|
|
const button = document.querySelector('.web-user-button');
|
|
const nameNode = button?.querySelector('.web-user-name');
|
|
const schoolNode = button?.querySelector('.web-user-school');
|
|
if (nameNode) nameNode.textContent = account.nickname;
|
|
if (schoolNode) schoolNode.textContent = account.universityName || '校园用户';
|
|
setAvatar(button?.querySelector('.web-user-avatar'));
|
|
setAvatar(document.querySelector('.web-topbar-avatar'));
|
|
};
|
|
window.webApplyAccountIdentity = applyAccountIdentity;
|
|
applyAccountIdentity();
|
|
if (liveApi && !liveApi.isLocal() && liveApi.token()) {
|
|
liveApi.profile().then((profile) => {
|
|
if (profile) {
|
|
liveApi.updateSessionUser(profile);
|
|
applyAccountIdentity();
|
|
}
|
|
}).catch(() => {});
|
|
}
|
|
|
|
const makeKeyboardClickable = (element, handler) => {
|
|
if (!element) return;
|
|
element.tabIndex = 0;
|
|
if (!element.getAttribute('role')) element.setAttribute('role', 'button');
|
|
element.addEventListener('keydown', (event) => {
|
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
event.preventDefault();
|
|
handler(event);
|
|
});
|
|
};
|
|
|
|
const setPressed = (elements, active) => {
|
|
elements.forEach((element) => {
|
|
const selected = element === active;
|
|
element.classList.toggle('active', selected);
|
|
element.setAttribute('aria-pressed', String(selected));
|
|
});
|
|
};
|
|
|
|
const addEmptyState = (container, title, description) => {
|
|
container?.querySelector('.web-empty-state')?.remove();
|
|
if (!container) return;
|
|
const empty = document.createElement('div');
|
|
empty.className = 'web-empty-state';
|
|
empty.innerHTML = `<div><strong>${title}</strong><span>${description}</span></div>`;
|
|
container.appendChild(empty);
|
|
};
|
|
|
|
const makeLinkedCard = (card, href) => {
|
|
if (!card) return;
|
|
card.tabIndex = 0;
|
|
card.setAttribute('role', 'link');
|
|
card.addEventListener('click', (event) => {
|
|
// 注意:卡片内的自有点击控件(如“取消收藏”)必须放行,否则会被这里拦掉
|
|
if (event.target.closest('button, a, input, textarea, select, .post-action, .video-likes, .post-footer-collect, [data-card-action]')) return;
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
location.href = href;
|
|
}, true);
|
|
card.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') location.href = href;
|
|
});
|
|
};
|
|
|
|
const parseCompactNumber = (value) => {
|
|
const text = String(value || '').trim().toLowerCase();
|
|
const number = Number.parseFloat(text) || 0;
|
|
if (text.includes('万')) return number * 10000;
|
|
if (text.includes('k')) return number * 1000;
|
|
return number;
|
|
};
|
|
|
|
const notificationPosts = {
|
|
canteen: {
|
|
title: '今天食堂的新菜也太好吃了吧!',
|
|
time: '2分钟前',
|
|
views: '386',
|
|
likes: 38,
|
|
shares: 6,
|
|
body: [
|
|
'今天在食堂尝到了新上的番茄牛腩饭,牛腩炖得很软,配菜也很足。',
|
|
'在一食堂二楼靠窗的窗口,午饭时间排队不算长。想换口味的同学可以试试!'
|
|
],
|
|
tags: ['#校园美食', '#食堂新菜'],
|
|
comments: [
|
|
{ id: 'li-mengqi', name: '李梦琪', text: '好棒!我也想去~', time: '15分钟前' },
|
|
{ id: 'zhang-zixuan', name: '张子轩', text: '下次去食堂就点这个!', time: '刚刚' }
|
|
]
|
|
},
|
|
'milk-tea': {
|
|
title: '学校西门的奶茶店,最近常去的一家',
|
|
time: '昨天',
|
|
views: '542',
|
|
likes: 64,
|
|
shares: 9,
|
|
body: [
|
|
'放学路过西门,发现巷口新开了一家奶茶店。点了少糖的茉莉奶绿,茶味很清爽。',
|
|
'店在西门出来右转约两百米的位置,门口有一排绿色座椅。想一起去尝尝的同学可以在评论里约时间。'
|
|
],
|
|
tags: ['#校园生活', '#西门美食'],
|
|
comments: [
|
|
{ id: 'wang-haoran', name: '王浩然', text: '@林小夏 你说的那家奶茶店在哪呀?我也想去尝尝', time: '1小时前' },
|
|
{ id: 'lin-xiaoxia', name: '林小夏', text: '西门出来右转约两百米,绿色座椅那家~', time: '45分钟前' }
|
|
]
|
|
},
|
|
'study-week': {
|
|
title: '期末复习周开始了,今天在图书馆学习了8小时!高数太难了',
|
|
time: '2小时前', views: '1,248', likes: 48, shares: 0, commentCount: 12,
|
|
body: ['期末复习周开始了,今天在图书馆学习了8小时。高数太难了,继续加油!'],
|
|
tags: ['#期末复习', '#图书馆'], comments: []
|
|
},
|
|
'hotpot-weekend': {
|
|
title: '周末和室友去吃了火锅,太满足了!这家店的毛肚真的绝了',
|
|
time: '周末', views: '856', likes: 86, shares: 0, commentCount: 18,
|
|
body: ['周末和室友去吃了火锅,太满足了!这家店的毛肚真的绝了。'],
|
|
tags: ['#校园美食', '#周末生活'], comments: []
|
|
},
|
|
'vscode-extensions': {
|
|
title: '分享一个超好用的VS Code插件合集,提升开发效率必备!',
|
|
time: '3天前', views: '1,632', likes: 128, shares: 0, commentCount: 34,
|
|
body: ['分享一个超好用的 VS Code 插件合集,提升开发效率必备!'],
|
|
tags: ['#开发工具', '#学习分享'], comments: []
|
|
},
|
|
'campus-sunset': {
|
|
title: '今天的天空好美啊,随手拍了一张,校园的晚霞永远最治愈',
|
|
time: '4天前', views: '743', likes: 64, shares: 0, commentCount: 8,
|
|
body: ['今天的天空好美啊,随手拍了一张。校园的晚霞永远最治愈。'],
|
|
tags: ['#校园生活', '#校园晚霞'], comments: []
|
|
}
|
|
};
|
|
|
|
const profileVideos = {
|
|
'study-vlog': { title: '期末复习vlog | 图书馆打卡12小时高效学习日常', views: '4.3万', likes: '1567', duration: '03:24', tags: ['#期末复习', '#图书馆'] },
|
|
'dorm-makeover': { title: '宿舍改造vlog | 花500块变身ins风小窝', views: '8.2万', likes: '1823', duration: '01:47', tags: ['#宿舍生活', '#宿舍改造'] },
|
|
'volunteer-day': { title: '志愿服务vlog | 周末敬老院做义工的一天', views: '2.4万', likes: '678', duration: '02:15', tags: ['#志愿服务', '#周末生活'] },
|
|
'campus-food': { title: '校园美食探店 | 学校周边最好吃的10家店测评', views: '6.7万', likes: '2341', duration: '04:02', tags: ['#校园美食', '#探店'] },
|
|
'freshman-guide': { title: '大一新生必看 | 校园生存指南与社团选择建议', views: '12.5万', likes: '4521', duration: '05:30', tags: ['#新生指南', '#校园生活'] }
|
|
};
|
|
|
|
const ensureHls = () => new Promise((resolve) => {
|
|
if (window.Hls) { resolve(true); return; }
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js';
|
|
script.onload = () => resolve(Boolean(window.Hls));
|
|
script.onerror = () => resolve(false);
|
|
document.head.appendChild(script);
|
|
});
|
|
|
|
const initApiVideo = async (api) => {
|
|
const grid = document.querySelector('.video-grid');
|
|
const tabs = Array.from(document.querySelectorAll('.category-tab'));
|
|
const loadButton = document.querySelector('.load-more-btn');
|
|
if (!grid) return;
|
|
const template = grid.querySelector('.video-card')?.cloneNode(true);
|
|
if (!template) return;
|
|
grid.replaceChildren();
|
|
addEmptyState(grid, '正在加载视频…', '');
|
|
if (tabs[0]) tabs[0].textContent = '最新';
|
|
tabs.slice(1).forEach((tab) => { tab.hidden = true; });
|
|
let page = 1;
|
|
let total = 0;
|
|
let loading = false;
|
|
|
|
const buildCard = (item) => {
|
|
const card = template.cloneNode(true);
|
|
const thumbnail = card.querySelector('.video-thumbnail');
|
|
const cover = thumbnail?.querySelector('.video-thumbnail-gradient');
|
|
if (cover) cover.style.background = item.videoCoverUrl
|
|
? `url("${item.videoCoverUrl}") center / cover`
|
|
: '#e8edf0';
|
|
const duration = card.querySelector('.video-duration');
|
|
if (!item.playUrl) {
|
|
if (duration) duration.textContent = '转码中';
|
|
card.classList.add('is-transcoding');
|
|
} else if (duration) {
|
|
duration.remove();
|
|
}
|
|
const title = card.querySelector('.video-title');
|
|
if (title) title.textContent = item.content || '未命名视频';
|
|
const name = card.querySelector('.author-name');
|
|
if (name) name.textContent = item.userName || '青核用户';
|
|
const avatar = card.querySelector('.author-avatar');
|
|
if (avatar) avatar.textContent = (item.userName || '青').slice(0, 1);
|
|
const likes = card.querySelector('.video-likes span');
|
|
if (likes) likes.textContent = String(item.likeCount || 0);
|
|
const views = card.querySelector('.video-views');
|
|
if (views) views.textContent = `${item.viewCount || 0} 次播放`;
|
|
card.addEventListener('click', () => { location.href = `video-detail.html?id=${encodeURIComponent(item.id)}`; });
|
|
return card;
|
|
};
|
|
|
|
const load = async (append) => {
|
|
if (loading) return;
|
|
loading = true;
|
|
try {
|
|
const data = await api.get(`/campus/moments/youth?page=${page}&size=20`);
|
|
const items = data?.items || [];
|
|
total = Number(data?.total || 0);
|
|
if (!append) grid.replaceChildren();
|
|
grid.append(...items.map(buildCard));
|
|
if (!items.length && !append) addEmptyState(grid, '还没有视频', '发布第一条校园视频吧。');
|
|
if (loadButton) {
|
|
const shown = grid.querySelectorAll('.video-card').length;
|
|
loadButton.hidden = shown >= total;
|
|
loadButton.textContent = shown >= total ? '已加载全部' : '加载更多';
|
|
}
|
|
} catch (error) {
|
|
if (!append) { grid.replaceChildren(); addEmptyState(grid, '视频加载失败', error.message || '请稍后刷新重试'); }
|
|
showWebToast(error.message || '视频加载失败', 'error');
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
};
|
|
|
|
if (loadButton) {
|
|
loadButton.addEventListener('click', () => { page += 1; load(true); });
|
|
}
|
|
await load(false);
|
|
};
|
|
|
|
const initVideo = () => {
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiVideo(api);
|
|
const tabs = Array.from(document.querySelectorAll('.category-tab'));
|
|
const cards = Array.from(document.querySelectorAll('.video-card'));
|
|
const grid = document.querySelector('.video-grid');
|
|
const loadButton = document.querySelector('.load-more-btn');
|
|
if (!tabs.length || !cards.length || !grid) return;
|
|
const categories = [
|
|
['音乐', '校园'], ['生活', '校园'], ['校园', '生活'], ['美食', '生活'],
|
|
['生活'], ['运动', '校园'], ['舞蹈'], ['科技'], ['生活'], ['校园', '生活'],
|
|
['科技', '校园'], ['科技', '校园']
|
|
];
|
|
let activeCategory = new URLSearchParams(location.search).get('category') || '推荐';
|
|
let expanded = false;
|
|
|
|
cards.forEach((card, index) => {
|
|
card.dataset.categories = (categories[index] || ['生活']).join(',');
|
|
makeLinkedCard(card, 'video-detail.html');
|
|
const like = card.querySelector('.video-likes');
|
|
if (like) {
|
|
makeKeyboardClickable(like, () => like.click());
|
|
like.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const liked = like.classList.toggle('is-liked');
|
|
like.setAttribute('aria-pressed', String(liked));
|
|
showWebToast(liked ? '已喜欢该视频' : '已取消喜欢');
|
|
});
|
|
}
|
|
});
|
|
const localVideos = window.YouthomeLocal?.readDb().moments.filter((item) => item.isYouthVideo && item.reviewStatus === 'approved') || [];
|
|
localVideos.forEach((item) => {
|
|
const card = cards[0].cloneNode(true);
|
|
card.classList.add('web-live-video');
|
|
card.dataset.categories = '校园,生活';
|
|
card.querySelector('.video-thumbnail-gradient').style.background = item.videoCoverUrl ? `url("${item.videoCoverUrl}") center / cover` : '';
|
|
card.querySelector('.video-title').textContent = item.content.split('\n')[0];
|
|
card.querySelector('.author-name').textContent = item.userName || '我';
|
|
card.querySelector('.author-avatar').textContent = (item.userName || '我').slice(0, 1);
|
|
card.querySelector('.video-duration').textContent = `${String(Math.floor((item.videoDuration || 0) / 60)).padStart(2, '0')}:${String((item.videoDuration || 0) % 60).padStart(2, '0')}`;
|
|
card.querySelector('.video-views').lastChild.textContent = ' ' + (item.viewCount || 0);
|
|
makeLinkedCard(card, `video-detail.html?id=${encodeURIComponent(item.id)}`);
|
|
grid.prepend(card);
|
|
cards.unshift(card);
|
|
});
|
|
|
|
const render = () => {
|
|
const matched = cards.filter((card) => activeCategory === '推荐' || card.dataset.categories.split(',').includes(activeCategory));
|
|
cards.forEach((card) => { card.hidden = true; });
|
|
matched.slice(0, expanded ? matched.length : 8).forEach((card) => { card.hidden = false; });
|
|
grid.querySelector('.web-empty-state')?.remove();
|
|
if (!matched.length) addEmptyState(grid, `暂无${activeCategory}视频`, '换个分类看看校园里的其他内容。');
|
|
if (loadButton) {
|
|
loadButton.hidden = matched.length <= 8;
|
|
loadButton.disabled = expanded;
|
|
loadButton.textContent = expanded ? '已加载全部' : `加载更多(还有 ${Math.max(0, matched.length - 8)} 条)`;
|
|
}
|
|
const params = new URLSearchParams();
|
|
if (activeCategory !== '推荐') params.set('category', activeCategory);
|
|
history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}`);
|
|
};
|
|
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => {
|
|
activeCategory = tab.textContent.trim();
|
|
expanded = false;
|
|
setPressed(tabs, tab);
|
|
render();
|
|
});
|
|
});
|
|
const requestedTab = tabs.find((tab) => tab.textContent.trim() === activeCategory) || tabs[0];
|
|
activeCategory = requestedTab.textContent.trim();
|
|
setPressed(tabs, requestedTab);
|
|
loadButton?.addEventListener('click', () => { expanded = true; render(); });
|
|
render();
|
|
};
|
|
|
|
const initFeed = () => {
|
|
if (window.YouthomeApi && !window.YouthomeApi.isLocal()) return;
|
|
const tabs = Array.from(document.querySelectorAll('.feed-tab'));
|
|
const cards = Array.from(document.querySelectorAll('.feed-card'));
|
|
const feed = document.querySelector('.feed-grid, .feed-list, .feed-content') || cards[0]?.parentElement;
|
|
if (!tabs.length || !cards.length || !feed) return;
|
|
const following = new Set([0, 2, 4, 7, 9]);
|
|
const hot = new Set([1, 3, 5, 6, 10]);
|
|
|
|
cards.forEach((card) => makeLinkedCard(card, 'post-detail.html'));
|
|
const render = (label) => {
|
|
cards.forEach((card, index) => {
|
|
card.hidden = label === '关注' ? !following.has(index) : label === '热门' ? !hot.has(index) : false;
|
|
});
|
|
const params = new URLSearchParams();
|
|
if (label !== '推荐') params.set('tab', label);
|
|
history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}`);
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { setPressed(tabs, tab); render(tab.textContent.trim()); });
|
|
});
|
|
const requested = new URLSearchParams(location.search).get('tab');
|
|
const selected = tabs.find((tab) => tab.textContent.trim() === requested) || tabs[0];
|
|
setPressed(tabs, selected);
|
|
render(selected.textContent.trim());
|
|
|
|
document.querySelectorAll('.topic-item').forEach((item) => {
|
|
const topic = item.querySelector('.topic-name')?.textContent.trim().replace(/^#/, '') || '热门话题';
|
|
makeLinkedCard(item, `topic-detail.html?topic=${encodeURIComponent(topic)}`);
|
|
});
|
|
const topicMore = document.querySelector('.topic-more-button');
|
|
const extraTopics = Array.from(document.querySelectorAll('.topic-item-extra'));
|
|
topicMore?.addEventListener('click', () => {
|
|
const expanded = topicMore.getAttribute('aria-expanded') !== 'true';
|
|
topicMore.setAttribute('aria-expanded', String(expanded));
|
|
extraTopics.forEach((item) => { item.hidden = !expanded; });
|
|
topicMore.firstChild.textContent = expanded ? '收起' : '更多';
|
|
});
|
|
|
|
const recommendations = [
|
|
['苏梦瑶', '中国人民大学 · 摄影', 'avatar-05-female-ponytail-medium.jpg'],
|
|
['周浩然', '同济大学 · 建筑设计', 'avatar-08-male-messy-medium.jpg'],
|
|
['黄诗雅', '中山大学 · 音乐', 'avatar-09-female-bob-dark.jpg'],
|
|
['吴一凡', '南京大学 · 旅行博主', 'avatar-01-female-short-light.jpg'],
|
|
['陈念', '浙江大学 · 产品设计', 'avatar-02-male-short-medium.jpg'],
|
|
['许知夏', '武汉大学 · 新闻传播', 'avatar-07-female-twintails-light.jpg'],
|
|
['顾言', '北京大学 · 篮球', 'avatar-06-male-buzz-dark.jpg'],
|
|
['林乔', '厦门大学 · 旅行摄影', 'avatar-04-male-curly-light.jpg'],
|
|
['沈星澜', '四川大学 · 插画', 'avatar-03-female-long-dark.jpg'],
|
|
['陆川', '西安交通大学 · 编程', 'avatar-10-male-parted-light.jpg'],
|
|
['江予安', '华中科技大学 · 音乐', 'avatar-flat-female-01.jpg'],
|
|
['秦越', '山东大学 · 运动', 'avatar-flat-male-01.jpg']
|
|
];
|
|
const userList = document.querySelector('.user-list');
|
|
const refresh = document.querySelector('.recommendation-refresh');
|
|
let recommendationBatch = 0;
|
|
const renderRecommendations = () => {
|
|
if (!userList) return;
|
|
const start = (recommendationBatch * 4) % recommendations.length;
|
|
const batch = Array.from({ length: 4 }, (_, offset) => recommendations[(start + offset) % recommendations.length]);
|
|
userList.innerHTML = '';
|
|
batch.forEach(([name, description, avatar], index) => {
|
|
const followed = localStorage.getItem(`youthome:web:follow:${name}`) === 'true';
|
|
const item = document.createElement('div');
|
|
item.className = 'user-item';
|
|
item.tabIndex = 0;
|
|
item.setAttribute('role', 'link');
|
|
item.dataset.userName = name;
|
|
item.innerHTML = `<div class="user-item-avatar color-${index + 1}"><img src="../assets/${avatar}" alt="${name}"></div><div class="user-item-info"><div class="user-item-name">${name}</div><div class="user-item-desc">${description}</div></div><button class="follow-btn${followed ? ' followed' : ''}" type="button">${followed ? '已关注' : '关注'}</button>`;
|
|
const openUser = () => { location.href = `user.html?name=${encodeURIComponent(name)}`; };
|
|
item.addEventListener('click', (event) => { if (!event.target.closest('.follow-btn')) openUser(); });
|
|
item.addEventListener('keydown', (event) => { if ((event.key === 'Enter' || event.key === ' ') && !event.target.closest('.follow-btn')) { event.preventDefault(); openUser(); } });
|
|
item.querySelector('.follow-btn').addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
const button = event.currentTarget;
|
|
const active = button.classList.toggle('followed');
|
|
button.textContent = active ? '已关注' : '关注';
|
|
localStorage.setItem(`youthome:web:follow:${name}`, String(active));
|
|
showWebToast(active ? `已关注 ${name}` : `已取消关注 ${name}`);
|
|
});
|
|
userList.appendChild(item);
|
|
});
|
|
};
|
|
refresh?.addEventListener('click', () => {
|
|
recommendationBatch = (recommendationBatch + 1) % Math.ceil(recommendations.length / 4);
|
|
refresh.classList.remove('is-refreshing');
|
|
void refresh.offsetWidth;
|
|
refresh.classList.add('is-refreshing');
|
|
renderRecommendations();
|
|
showWebToast('已更新推荐关注');
|
|
});
|
|
renderRecommendations();
|
|
const searchInput = document.querySelector('.search-bar-input');
|
|
const searchButton = document.querySelector('.search-bar-btn');
|
|
const openSearch = () => {
|
|
const query = searchInput?.value.trim();
|
|
location.href = query ? `search.html?q=${encodeURIComponent(query)}` : 'search.html';
|
|
};
|
|
searchButton?.addEventListener('click', openSearch);
|
|
searchInput?.addEventListener('keydown', (event) => { if (event.key === 'Enter') openSearch(); });
|
|
};
|
|
|
|
const initApiSearch = async (api) => {
|
|
const input = document.getElementById('pageSearchInput');
|
|
const searchButton = document.querySelector('.page-search-btn');
|
|
const tabs = Array.from(document.querySelectorAll('.search-tab'));
|
|
const grid = document.querySelector('.result-grid');
|
|
const userRow = document.querySelector('.user-results-row');
|
|
const countNode = document.querySelector('.result-count');
|
|
if (!input || !grid) return;
|
|
const params = new URLSearchParams(location.search);
|
|
let type = params.get('type') || 'all';
|
|
if (params.get('q')) input.value = params.get('q');
|
|
|
|
const makeCard = (title, desc, onClick) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'result-card';
|
|
card.innerHTML = '<div class="result-cover post"></div><div class="result-info"><div class="result-title"></div><div class="result-bottom"><div class="result-author-row"><div class="result-avatar-sm"></div><span class="result-author-name"></span></div></div></div>';
|
|
card.querySelector('.result-title').textContent = title;
|
|
card.querySelector('.result-author-sm')?.remove();
|
|
card.querySelector('.result-author-name').textContent = desc || '';
|
|
card.querySelector('.result-avatar-sm').textContent = String(title || '·').slice(0, 1);
|
|
card.addEventListener('click', onClick);
|
|
return card;
|
|
};
|
|
|
|
const run = async () => {
|
|
const keyword = input.value.trim();
|
|
if (!keyword) { showWebToast('请输入搜索内容', 'error'); input.focus(); return; }
|
|
grid.replaceChildren();
|
|
userRow?.replaceChildren();
|
|
const [users, activities, groups, topics] = await Promise.all([
|
|
api.get('/friendships/search?keyword=' + encodeURIComponent(keyword) + '&page=1&size=10').catch(() => ({ items: [] })),
|
|
api.post('/campus/activities/search?page=1&size=10', { keyword }).catch(() => ({ items: [] })),
|
|
api.get('/groups/search?keyword=' + encodeURIComponent(keyword) + '&page=1&size=10').catch(() => ({ items: [] })),
|
|
api.get('/topics/catalog?keyword=' + encodeURIComponent(keyword) + '&page=1&size=10').catch(() => ({ items: [] }))
|
|
]);
|
|
const userItems = users?.items || [];
|
|
const activityItems = activities?.items || [];
|
|
const groupItems = groups?.items || [];
|
|
const topicItems = topics?.items || [];
|
|
const cards = [];
|
|
if (type === 'all' || type === 'user') {
|
|
userItems.forEach((user) => cards.push(makeCard(user.nickname || '用户', user.universityName || '校园用户', () => { location.href = 'user.html?id=' + encodeURIComponent(user.id); })));
|
|
}
|
|
if (type === 'all' || type === 'activity') {
|
|
activityItems.forEach((item) => cards.push(makeCard(item.title || '未命名活动', item.creatorName || '校园组织', () => { location.href = 'activity-detail.html?id=' + encodeURIComponent(item.id); })));
|
|
}
|
|
if (type === 'all' || type === 'group') {
|
|
groupItems.forEach((group) => cards.push(makeCard(group.name || group.groupName || '群组', (group.memberCount || 0) + ' 位成员', () => showWebToast('群聊请在 App 中查看'))));
|
|
}
|
|
if (type === 'all' || type === 'topic') {
|
|
topicItems.forEach((topic) => cards.push(makeCard('#' + (topic.name || '话题'), topic.description || '话题讨论', () => { location.href = 'topic-detail.html?topic=' + encodeURIComponent(topic.name); })));
|
|
}
|
|
if (type === 'post' || type === 'video') {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = '该类型暂未提供搜索接口,可在动态广场浏览';
|
|
grid.appendChild(empty);
|
|
} else if (!cards.length) {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = '没有找到相关内容';
|
|
grid.appendChild(empty);
|
|
} else {
|
|
grid.append(...cards);
|
|
}
|
|
if (countNode) countNode.textContent = '共 ' + cards.length + ' 条结果';
|
|
history.replaceState(null, '', location.pathname + '?q=' + encodeURIComponent(keyword) + (type !== 'all' ? '&type=' + encodeURIComponent(type) : ''));
|
|
};
|
|
|
|
tabs.forEach((tab) => {
|
|
tab.addEventListener('click', () => {
|
|
tabs.forEach((node) => node.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
type = tab.dataset.tab || 'all';
|
|
run();
|
|
});
|
|
});
|
|
searchButton?.addEventListener('click', run);
|
|
input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); run(); } });
|
|
if (input.value.trim()) run();
|
|
};
|
|
|
|
const initSearch = () => {
|
|
const input = document.getElementById('pageSearchInput');
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiSearch(api);
|
|
const searchButton = document.querySelector('.page-search-btn');
|
|
const tabs = Array.from(document.querySelectorAll('.search-tab'));
|
|
const chips = Array.from(document.querySelectorAll('.filter-chip'));
|
|
const periods = Array.from(document.querySelectorAll('.sort-option'));
|
|
const users = Array.from(document.querySelectorAll('.user-mini-card'));
|
|
const results = Array.from(document.querySelectorAll('.result-card'));
|
|
const userRow = document.querySelector('.user-results-row');
|
|
const resultGrid = document.querySelector('.result-grid');
|
|
const count = document.querySelector('.result-count');
|
|
if (!input || !tabs.length || !resultGrid) return;
|
|
const params = new URLSearchParams(location.search);
|
|
if (params.get('q')) input.value = params.get('q');
|
|
let activeType = params.get('type') || 'all';
|
|
let activeSort = params.get('sort') || chips[0]?.textContent.trim() || '综合排序';
|
|
let activePeriod = params.get('period') || '全部';
|
|
results.forEach((card, index) => {
|
|
card.dataset.type = card.querySelector('.result-cover.video') ? 'video' : 'post';
|
|
card.dataset.age = String((index % 10) + 1);
|
|
card.dataset.likes = String(parseCompactNumber(card.querySelector('.result-like span')?.textContent));
|
|
makeLinkedCard(card, card.dataset.type === 'video' ? 'video-detail.html' : 'post-detail.html');
|
|
});
|
|
users.forEach((card) => {
|
|
const name = card.querySelector('.user-mini-name, .user-name, .result-author-name')?.textContent.trim();
|
|
makeLinkedCard(card, name ? `user.html?name=${encodeURIComponent(name)}` : 'user.html');
|
|
});
|
|
|
|
const render = () => {
|
|
const query = input.value.trim().toLowerCase();
|
|
const maxAge = activePeriod === '一天内' ? 1 : activePeriod === '一周内' ? 7 : activePeriod === '一月内' ? 30 : Infinity;
|
|
let matched = results.filter((card) => (
|
|
(activeType === 'all' || card.dataset.type === activeType) &&
|
|
(!query || card.textContent.toLowerCase().includes(query)) &&
|
|
Number(card.dataset.age) <= maxAge
|
|
));
|
|
if (activeSort === '最多点赞') matched.sort((a, b) => Number(b.dataset.likes) - Number(a.dataset.likes));
|
|
if (activeSort === '最多评论') matched.reverse();
|
|
results.forEach((card) => { card.hidden = true; });
|
|
matched.forEach((card) => { card.hidden = false; resultGrid.appendChild(card); });
|
|
const showUsers = (activeType === 'all' || activeType === 'user') && users.some((card) => !query || card.textContent.toLowerCase().includes(query));
|
|
users.forEach((card) => { card.hidden = !showUsers || (!!query && !card.textContent.toLowerCase().includes(query)); });
|
|
if (userRow) userRow.parentElement.hidden = !showUsers;
|
|
resultGrid.querySelector('.web-empty-state')?.remove();
|
|
if (!matched.length && !showUsers) addEmptyState(resultGrid, '没有找到相关内容', '换个关键词或筛选条件再试一次。');
|
|
const total = matched.length + (showUsers ? users.filter((card) => !card.hidden).length : 0);
|
|
if (count) count.innerHTML = `找到 <strong>${total}</strong> 条与“${input.value.trim() || '全部'}”相关的结果`;
|
|
const next = new URLSearchParams();
|
|
if (input.value.trim()) next.set('q', input.value.trim());
|
|
if (activeType !== 'all') next.set('type', activeType);
|
|
if (activeSort !== (chips[0]?.textContent.trim() || '综合排序')) next.set('sort', activeSort);
|
|
if (activePeriod !== '全部') next.set('period', activePeriod);
|
|
history.replaceState(null, '', `${location.pathname}${next.size ? `?${next}` : ''}`);
|
|
};
|
|
const submitSearch = () => { activeType = 'all'; setPressed(tabs, tabs[0]); render(); };
|
|
searchButton?.addEventListener('click', submitSearch);
|
|
input.addEventListener('keydown', (event) => { if (event.key === 'Enter') submitSearch(); });
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { activeType = tab.dataset.tab; setPressed(tabs, tab); render(); });
|
|
});
|
|
chips.forEach((chip) => {
|
|
makeKeyboardClickable(chip, () => chip.click());
|
|
chip.addEventListener('click', () => { activeSort = chip.textContent.trim(); setPressed(chips, chip); render(); });
|
|
});
|
|
periods.forEach((period) => {
|
|
makeKeyboardClickable(period, () => period.click());
|
|
period.addEventListener('click', () => { activePeriod = period.textContent.trim(); setPressed(periods, period); render(); });
|
|
});
|
|
const selectedTab = tabs.find((tab) => tab.dataset.tab === activeType) || tabs[0];
|
|
activeType = selectedTab.dataset.tab;
|
|
setPressed(tabs, selectedTab);
|
|
setPressed(chips, chips.find((chip) => chip.textContent.trim() === activeSort) || chips[0]);
|
|
setPressed(periods, periods.find((period) => period.textContent.trim() === activePeriod) || periods[0]);
|
|
render();
|
|
};
|
|
|
|
const optionLabel = (element) => {
|
|
if (element.dataset.filterLabel) return element.dataset.filterLabel;
|
|
const clone = element.cloneNode(true);
|
|
clone.querySelectorAll('.filter-checkbox, .filter-option-count, svg').forEach((node) => node.remove());
|
|
return clone.textContent.replace(/\.\.\.$/, '').trim();
|
|
};
|
|
|
|
const initApiActivities = async (api) => {
|
|
const list = document.querySelector('.activity-list');
|
|
const search = document.querySelector('.search-input');
|
|
const sortOptions = Array.from(document.querySelectorAll('.sort-option'));
|
|
const sortMenu = document.querySelector('.sort-menu');
|
|
const sortLabel = document.querySelector('.sort-dropdown');
|
|
const pagination = document.querySelector('.pagination');
|
|
const countNode = document.querySelector('.list-header-left b');
|
|
const sections = Array.from(document.querySelectorAll('.filter-section'));
|
|
if (!list) return;
|
|
const template = list.querySelector('.activity-list-card')?.cloneNode(true);
|
|
if (!template) return;
|
|
list.replaceChildren();
|
|
addEmptyState(list, '正在加载活动…', '');
|
|
// 后端暂不支持学校/费用/人数筛选,真实模式下隐藏这些分组,避免出现“能点但不生效”的筛选
|
|
sections.slice(1).forEach((section) => { section.hidden = true; });
|
|
|
|
const state = { page: 1, size: 10, type: '', keyword: '', sortBy: 'latest', total: 0 };
|
|
|
|
const buildCard = (item) => {
|
|
const card = template.cloneNode(true);
|
|
card.setAttribute('onclick', `location.href='activity-detail.html?id=${encodeURIComponent(item.id)}'`);
|
|
const ended = item.endTime && Date.parse(String(item.endTime).replace(/-/g, '/')) < Date.now();
|
|
const tag = card.querySelector('.activity-cover-tag');
|
|
if (tag) {
|
|
tag.textContent = ended ? '已结束' : '报名中';
|
|
tag.className = 'activity-cover-tag' + (ended ? '' : ' signing');
|
|
}
|
|
card.querySelector('.activity-credit-tag')?.remove();
|
|
const title = card.querySelector('.activity-list-title');
|
|
if (title) title.textContent = item.title || '未命名活动';
|
|
const cover = card.querySelector('.activity-list-cover');
|
|
const coverImage = item.coverImage || item.coverUrl;
|
|
if (cover && coverImage) cover.style.background = `url("${coverImage}") center / cover`;
|
|
const typeTag = card.querySelector('.activity-type-tag');
|
|
if (typeTag) typeTag.textContent = item.typeLabel || item.categoryLabel || '活动';
|
|
const metas = card.querySelectorAll('.meta-value');
|
|
if (metas[0]) metas[0].textContent = [item.startTime, item.endTime].filter(Boolean).join(' - ') || '以主办方通知为准';
|
|
if (metas[1]) metas[1].textContent = item.location || '待公布';
|
|
if (metas[2]) metas[2].textContent = item.creatorName || '校园组织';
|
|
const people = card.querySelector('.people-info');
|
|
if (people) {
|
|
const max = Number(item.maxCount || 0);
|
|
const current = Number(item.currentCount || 0);
|
|
const percent = max ? Math.min(100, Math.round((current / max) * 100)) : 0;
|
|
people.innerHTML = `<b>${current}</b>/${max || '—'} 人已报名<div class="people-bar"><div class="people-bar-fill" style="width:${percent}%"></div></div>`;
|
|
}
|
|
card.querySelector('.fee-info')?.remove();
|
|
card.querySelector('.organizer-info')?.remove();
|
|
const signup = card.querySelector('.btn-signup');
|
|
if (signup) {
|
|
signup.textContent = '查看详情';
|
|
signup.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
location.href = `activity-detail.html?id=${encodeURIComponent(item.id)}`;
|
|
});
|
|
}
|
|
return card;
|
|
};
|
|
|
|
const renderPagination = () => {
|
|
if (!pagination) return;
|
|
const pages = Math.max(1, Math.ceil(state.total / state.size));
|
|
pagination.replaceChildren();
|
|
const prev = document.createElement('button');
|
|
prev.type = 'button';
|
|
prev.className = 'page-btn';
|
|
prev.textContent = '上一页';
|
|
prev.disabled = state.page <= 1;
|
|
prev.addEventListener('click', () => { if (state.page > 1) { state.page -= 1; load(); } });
|
|
const info = document.createElement('span');
|
|
info.textContent = `第 ${state.page} / ${pages} 页`;
|
|
info.style.cssText = 'padding: 0 10px; font-size: 13px; color: var(--text-tertiary);';
|
|
const next = document.createElement('button');
|
|
next.type = 'button';
|
|
next.className = 'page-btn';
|
|
next.textContent = '下一页';
|
|
next.disabled = state.page >= pages;
|
|
next.addEventListener('click', () => { if (state.page < pages) { state.page += 1; load(); } });
|
|
pagination.append(prev, info, next);
|
|
};
|
|
|
|
const load = async () => {
|
|
const params = new URLSearchParams({ page: String(state.page), size: String(state.size) });
|
|
if (state.type) params.set('type', state.type);
|
|
let data;
|
|
try {
|
|
data = state.keyword || state.sortBy
|
|
? await api.post('/campus/activities/search?' + params.toString(), {
|
|
type: state.type || undefined,
|
|
keyword: state.keyword || undefined,
|
|
sortBy: ({ latest: 'TIME', hot: 'HEAT', soon: 'EVENT_TIME', popular: 'PARTICIPATION' })[state.sortBy] || 'TIME'
|
|
})
|
|
: await api.get('/campus/activities?' + params.toString());
|
|
}
|
|
catch (error) {
|
|
list.replaceChildren();
|
|
addEmptyState(list, '活动加载失败', error.message || '请稍后刷新重试');
|
|
showWebToast(error.message || '活动加载失败', 'error');
|
|
return;
|
|
}
|
|
const items = data?.items || [];
|
|
state.total = Number(data?.total || 0);
|
|
list.replaceChildren(...items.map(buildCard));
|
|
if (!items.length) addEmptyState(list, '没有找到活动', '换个关键词或筛选条件试试。');
|
|
if (countNode) countNode.textContent = String(state.total);
|
|
renderPagination();
|
|
};
|
|
|
|
// 类型筛选用后端字典重建,保证标签与接口参数一致
|
|
api.get('/dicts/items?code=activity_type').then((dict) => {
|
|
const wrap = sections[0]?.querySelector('.filter-options');
|
|
const sample = wrap?.querySelector('.filter-option');
|
|
if (wrap && sample) {
|
|
const makeOption = (label, value) => {
|
|
const option = sample.cloneNode(true);
|
|
option.removeAttribute('onclick');
|
|
const labelNode = Array.from(option.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
|
|
if (labelNode) labelNode.textContent = ' ' + label + ' ';
|
|
option.querySelector('.filter-option-count')?.remove();
|
|
option.classList.toggle('selected', value === '');
|
|
option.dataset.value = value;
|
|
option.addEventListener('click', () => {
|
|
wrap.querySelectorAll('.filter-option').forEach((node) => node.classList.remove('selected'));
|
|
option.classList.add('selected');
|
|
state.type = value;
|
|
state.page = 1;
|
|
load();
|
|
});
|
|
return option;
|
|
};
|
|
wrap.replaceChildren();
|
|
wrap.appendChild(makeOption('全部', ''));
|
|
(dict || []).forEach((item) => wrap.appendChild(makeOption(item.label, item.value)));
|
|
}
|
|
}).catch(() => {});
|
|
|
|
sortOptions.forEach((option) => {
|
|
option.removeAttribute('onclick');
|
|
option.addEventListener('click', () => {
|
|
const label = option.textContent.trim();
|
|
sortOptions.forEach((node) => node.classList.toggle('active', node === option));
|
|
sortMenu?.classList.remove('show');
|
|
sortLabel?.classList.remove('active');
|
|
const labelNode = sortLabel?.childNodes[0];
|
|
if (labelNode && labelNode.nodeType === Node.TEXT_NODE) labelNode.textContent = label;
|
|
state.sortBy = ({ 最新发布: 'latest', 最热活动: 'hot', 即将开始: 'soon', 报名人数: 'popular' })[label] || 'latest';
|
|
state.page = 1;
|
|
load();
|
|
});
|
|
});
|
|
|
|
let timer = null;
|
|
search?.addEventListener('input', () => {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => {
|
|
state.keyword = search.value.trim();
|
|
state.page = 1;
|
|
load();
|
|
}, 350);
|
|
});
|
|
|
|
await load();
|
|
};
|
|
|
|
const initActivities = () => {
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiActivities(api);
|
|
const list = document.querySelector('.activity-list');
|
|
const cards = Array.from(document.querySelectorAll('.activity-list-card'));
|
|
const sections = Array.from(document.querySelectorAll('.filter-section .filter-options'));
|
|
const pagination = document.querySelector('.pagination');
|
|
const resultCount = document.querySelector('.list-header-left b');
|
|
const selectedTags = document.querySelector('.filter-selected-tags');
|
|
if (!list || !cards.length || sections.length < 5) return;
|
|
|
|
const metadata = [
|
|
{ type: '比赛竞赛', time: ['本周', '本周末', '下周'], dates: ['12-28'], school: '复旦大学', fee: '免费', people: 186, popularity: 93, soon: 4 },
|
|
{ type: '学习交流', time: ['本周'], dates: ['12-21'], school: '复旦大学', fee: '免费', people: 48, popularity: 80, soon: 2 },
|
|
{ type: '公益志愿', time: ['今天', '本周'], dates: ['12-15', '12-16', '12-17', '12-18', '12-19', '12-20', '12-21', '12-22'], school: '复旦大学', fee: '免费', people: 32, popularity: 64, soon: 1 },
|
|
{ type: '运动健身', time: ['本周', '本周末', '下周'], dates: ['12-28'], school: '复旦大学', fee: '付费', people: 24, popularity: 75, soon: 5 },
|
|
{ type: '兴趣社团', time: ['本周', '本周末', '下周'], dates: ['12-28'], school: '复旦大学', fee: '付费', people: 38, popularity: 95, soon: 6 },
|
|
{ type: '线下聚会', time: ['本周'], dates: ['12-25'], school: '复旦大学', fee: '免费', people: 200, popularity: 100, soon: 3 },
|
|
{ type: '线下聚会', time: ['本周末', '下周'], dates: ['12-29'], school: '上海交通大学', fee: '付费', people: 52, popularity: 87, soon: 7 }
|
|
];
|
|
const typeLabels = { offline: '线下聚会', sports: '运动健身', study: '学习交流', club: '兴趣社团', competition: '比赛竞赛', volunteer: '公益志愿' };
|
|
(window.YouthomeLocal?.readDb().activities || []).slice().reverse().forEach((item) => {
|
|
const card = cards[0].cloneNode(true);
|
|
card.classList.add('web-live-activity');
|
|
card.setAttribute('onclick', `location.href='activity-detail.html?id=${encodeURIComponent(item.id)}'`);
|
|
const cover = card.querySelector('.activity-list-cover');
|
|
if (cover && (item.coverImage || item.coverUrl)) cover.style.background = `url("${item.coverImage || item.coverUrl}") center / cover`;
|
|
card.querySelector('.activity-list-title').textContent = item.title;
|
|
card.querySelector('.activity-type-tag').textContent = typeLabels[item.type] || item.type || '活动';
|
|
const values = card.querySelectorAll('.meta-value');
|
|
if (values[0]) values[0].textContent = `${item.startTime} - ${item.endTime}`;
|
|
if (values[1]) values[1].textContent = item.location;
|
|
if (values[2]) values[2].textContent = item.creatorName || '我';
|
|
card.querySelector('.people-info b').textContent = String(item.currentCount || 0);
|
|
const button = card.querySelector('.btn-signup');
|
|
button.textContent = '查看详情';
|
|
button.setAttribute('onclick', `event.stopPropagation();location.href='activity-detail.html?id=${encodeURIComponent(item.id)}'`);
|
|
list.prepend(card);
|
|
cards.unshift(card);
|
|
const day = new Date(String(item.startTime).replace(' ', 'T'));
|
|
metadata.unshift({ type: typeLabels[item.type] || item.type, time: ['今天', '本周', '本周末', '下周'], dates: [`${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`], school: item.universityName || '复旦大学', fee: Number(item.feeAmount) > 0 ? '付费' : '免费', people: Number(item.maxCount) || 20, popularity: 0, soon: 0 });
|
|
});
|
|
const sectionNames = ['type', 'time', 'school', 'fee', 'people'];
|
|
let sortMode = '最新发布';
|
|
let currentPage = 1;
|
|
const pageSize = 4;
|
|
|
|
cards.forEach((card, index) => {
|
|
card.dataset.originalIndex = String(index);
|
|
card.tabIndex = 0;
|
|
card.setAttribute('role', 'link');
|
|
card.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') location.href = 'activity-detail.html';
|
|
});
|
|
});
|
|
|
|
sections.forEach((section) => {
|
|
const options = Array.from(section.querySelectorAll('.filter-option'));
|
|
options.forEach((option, index) => {
|
|
option.classList.toggle('selected', index === 0);
|
|
option.setAttribute('aria-pressed', String(index === 0));
|
|
makeKeyboardClickable(option, () => window.toggleFilter(option));
|
|
});
|
|
});
|
|
|
|
const selectedValues = (section) => Array.from(section.querySelectorAll('.filter-option.selected'))
|
|
.slice(0)
|
|
.filter((option) => option !== section.querySelector('.filter-option'))
|
|
.map(optionLabel);
|
|
|
|
const matchesPeople = (count, labels) => labels.some((label) => (
|
|
(label === '10人以下' && count < 10) ||
|
|
(label === '10-30人' && count >= 10 && count <= 30) ||
|
|
(label === '30人以上' && count > 30)
|
|
));
|
|
|
|
const syncUrl = () => {
|
|
const params = new URLSearchParams();
|
|
sections.forEach((section, index) => {
|
|
const values = selectedValues(section);
|
|
if (values.length) params.set(sectionNames[index], values.join(','));
|
|
});
|
|
if (sortMode !== '最新发布') params.set('sort', sortMode);
|
|
if (currentPage > 1) params.set('page', String(currentPage));
|
|
history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}`);
|
|
};
|
|
|
|
const renderTags = () => {
|
|
if (!selectedTags) return;
|
|
selectedTags.innerHTML = '';
|
|
sections.forEach((section) => {
|
|
selectedValues(section).forEach((label) => {
|
|
const tag = document.createElement('button');
|
|
tag.type = 'button';
|
|
tag.className = 'filter-selected-tag';
|
|
tag.innerHTML = `<span>${label}</span><span aria-hidden="true">×</span>`;
|
|
tag.setAttribute('aria-label', `移除筛选:${label}`);
|
|
tag.addEventListener('click', () => {
|
|
const option = Array.from(section.querySelectorAll('.filter-option')).find((item) => optionLabel(item) === label);
|
|
if (option) window.toggleFilter(option);
|
|
});
|
|
selectedTags.appendChild(tag);
|
|
});
|
|
});
|
|
const label = selectedTags.previousElementSibling;
|
|
if (label) label.hidden = selectedTags.childElementCount === 0;
|
|
};
|
|
|
|
const renderPagination = (total) => {
|
|
if (!pagination) return;
|
|
const pages = Math.max(1, Math.ceil(total / pageSize));
|
|
currentPage = Math.min(currentPage, pages);
|
|
pagination.innerHTML = '';
|
|
if (total <= pageSize) {
|
|
pagination.hidden = true;
|
|
return;
|
|
}
|
|
pagination.hidden = false;
|
|
const addButton = (label, target, active = false, disabled = false) => {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = `page-btn${active ? ' active' : ''}`;
|
|
button.textContent = label;
|
|
button.disabled = disabled;
|
|
button.setAttribute('aria-label', label === '上一页' || label === '下一页' ? label : `第 ${label} 页`);
|
|
button.addEventListener('click', () => {
|
|
currentPage = target;
|
|
updateActivityView(false);
|
|
list.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
pagination.appendChild(button);
|
|
};
|
|
addButton('上一页', Math.max(1, currentPage - 1), false, currentPage === 1);
|
|
for (let number = 1; number <= pages; number += 1) addButton(String(number), number, number === currentPage);
|
|
addButton('下一页', Math.min(pages, currentPage + 1), false, currentPage === pages);
|
|
const totalLabel = document.createElement('span');
|
|
totalLabel.className = 'page-total';
|
|
totalLabel.textContent = `共 ${pages} 页`;
|
|
pagination.appendChild(totalLabel);
|
|
};
|
|
|
|
const updateActivityView = (resetPage = true) => {
|
|
if (resetPage) currentPage = 1;
|
|
const values = sections.map(selectedValues);
|
|
let matched = cards.map((card, index) => ({ card, data: metadata[index] })).filter(({ data }) => (
|
|
(!values[0].length || values[0].includes(data.type)) &&
|
|
(!values[1].length || values[1].some((value) => value.startsWith('自定义日期 ') ? data.dates.includes(value.slice(-5)) : data.time.includes(value))) &&
|
|
(!values[2].length || values[2].includes(data.school)) &&
|
|
(!values[3].length || values[3].includes(data.fee)) &&
|
|
(!values[4].length || matchesPeople(data.people, values[4]))
|
|
));
|
|
|
|
matched = matched.sort((a, b) => {
|
|
if (sortMode === '最热活动') return b.data.popularity - a.data.popularity;
|
|
if (sortMode === '即将开始') return a.data.soon - b.data.soon;
|
|
if (sortMode === '报名人数') return b.data.people - a.data.people;
|
|
return Number(a.card.dataset.originalIndex) - Number(b.card.dataset.originalIndex);
|
|
});
|
|
|
|
cards.forEach((card) => { card.hidden = true; });
|
|
matched.forEach(({ card }) => list.appendChild(card));
|
|
renderPagination(matched.length);
|
|
const start = (currentPage - 1) * pageSize;
|
|
matched.slice(start, start + pageSize).forEach(({ card }) => { card.hidden = false; });
|
|
resultCount.textContent = String(matched.length);
|
|
list.querySelector('.web-empty-state')?.remove();
|
|
if (!matched.length) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'web-empty-state';
|
|
empty.innerHTML = '<div><strong>没有符合条件的活动</strong><span>换一组筛选条件,或重置后查看全部活动。</span></div>';
|
|
list.appendChild(empty);
|
|
}
|
|
renderTags();
|
|
syncUrl();
|
|
document.querySelector('.activity-filter-count').textContent = String(values.reduce((sum, value) => sum + value.length, 0));
|
|
document.querySelector('.activity-filter-apply').textContent = `查看 ${matched.length} 个活动`;
|
|
};
|
|
|
|
window.toggleFilter = (element) => {
|
|
const section = element.closest('.filter-options');
|
|
const options = Array.from(section.querySelectorAll('.filter-option'));
|
|
const first = options[0];
|
|
if (optionLabel(element).startsWith('自定义日期') && !element.dataset.filterLabel) {
|
|
const picker = document.createElement('input');
|
|
picker.type = 'date';
|
|
picker.style.cssText = 'position:fixed;left:-9999px;top:0;';
|
|
picker.addEventListener('change', () => {
|
|
if (picker.value) {
|
|
element.dataset.filterLabel = `自定义日期 ${picker.value.slice(5)}`;
|
|
element.classList.add('selected');
|
|
first.classList.remove('selected');
|
|
element.setAttribute('aria-pressed', 'true');
|
|
updateActivityView();
|
|
}
|
|
picker.remove();
|
|
});
|
|
document.body.appendChild(picker);
|
|
if (picker.showPicker) picker.showPicker(); else picker.click();
|
|
return;
|
|
}
|
|
if (element === first) {
|
|
options.forEach((option) => option.classList.remove('selected'));
|
|
first.classList.add('selected');
|
|
} else {
|
|
element.classList.toggle('selected');
|
|
first.classList.remove('selected');
|
|
if (!options.slice(1).some((option) => option.classList.contains('selected'))) first.classList.add('selected');
|
|
}
|
|
options.forEach((option) => option.setAttribute('aria-pressed', String(option.classList.contains('selected'))));
|
|
updateActivityView();
|
|
};
|
|
|
|
window.resetAllFilters = () => {
|
|
sections.forEach((section) => {
|
|
Array.from(section.querySelectorAll('.filter-option')).forEach((option, index) => {
|
|
option.classList.toggle('selected', index === 0);
|
|
option.setAttribute('aria-pressed', String(index === 0));
|
|
});
|
|
});
|
|
updateActivityView();
|
|
showWebToast('筛选条件已重置');
|
|
};
|
|
|
|
window.setSort = (label, event) => {
|
|
event?.stopPropagation();
|
|
sortMode = label;
|
|
document.getElementById('sortLabel').textContent = label;
|
|
document.querySelectorAll('.sort-option').forEach((option) => option.classList.toggle('active', option.textContent.trim() === label));
|
|
document.getElementById('sortMenu')?.classList.remove('show');
|
|
document.querySelector('.sort-dropdown')?.classList.remove('active');
|
|
updateActivityView();
|
|
};
|
|
|
|
const filterButton = document.createElement('button');
|
|
filterButton.type = 'button';
|
|
filterButton.className = 'activity-filter-mobile';
|
|
filterButton.innerHTML = `${icon.settings}<span>筛选</span><span class="activity-filter-count">0</span>`;
|
|
filterButton.addEventListener('click', () => document.body.classList.add('web-filter-open'));
|
|
document.querySelector('.list-header-right')?.prepend(filterButton);
|
|
|
|
const applyButton = document.createElement('button');
|
|
applyButton.type = 'button';
|
|
applyButton.className = 'activity-filter-apply';
|
|
applyButton.textContent = '查看活动';
|
|
applyButton.addEventListener('click', () => document.body.classList.remove('web-filter-open'));
|
|
document.querySelector('.filter-sidebar')?.appendChild(applyButton);
|
|
|
|
document.querySelectorAll('.btn-signup').forEach((button) => {
|
|
button.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
button.className = 'btn-signed';
|
|
button.textContent = '已报名';
|
|
showWebToast('报名成功,可在“我的活动”中查看');
|
|
}, true);
|
|
});
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
sections.forEach((section, index) => {
|
|
const requested = (params.get(sectionNames[index]) || '').split(',').filter(Boolean);
|
|
if (!requested.length) return;
|
|
const options = Array.from(section.querySelectorAll('.filter-option'));
|
|
options.forEach((option) => option.classList.remove('selected'));
|
|
requested.forEach((label) => {
|
|
const option = options.find((candidate) => optionLabel(candidate) === label || (label.startsWith('自定义日期 ') && optionLabel(candidate).startsWith('自定义日期')));
|
|
if (option && label.startsWith('自定义日期 ')) option.dataset.filterLabel = label;
|
|
option?.classList.add('selected');
|
|
});
|
|
if (!options.some((option) => option.classList.contains('selected'))) options[0].classList.add('selected');
|
|
options.forEach((option) => option.setAttribute('aria-pressed', String(option.classList.contains('selected'))));
|
|
});
|
|
if (params.get('sort')) {
|
|
sortMode = params.get('sort');
|
|
const sortLabel = document.getElementById('sortLabel');
|
|
if (sortLabel) sortLabel.textContent = sortMode;
|
|
document.querySelectorAll('.sort-option').forEach((option) => option.classList.toggle('active', option.textContent.trim() === sortMode));
|
|
}
|
|
if (Number(params.get('page')) > 0) currentPage = Number(params.get('page'));
|
|
|
|
updateActivityView(false);
|
|
};
|
|
|
|
const initApiNotifications = async (api) => {
|
|
const list = document.querySelector('.message-list');
|
|
const categories = Array.from(document.querySelectorAll('.category-item'));
|
|
if (!list || !categories.length) return;
|
|
const template = list.querySelector('.message-item')?.cloneNode(true);
|
|
if (!template) return;
|
|
const accountId = api.currentUser()?.id || 'anonymous';
|
|
const conversationCacheKey = `youthome:web:${accountId}:chat-conversations`;
|
|
const conversationsTask = api.get('/campus/chat/conversations').then((items) => {
|
|
try { sessionStorage.setItem(conversationCacheKey, JSON.stringify(items || [])); } catch (error) { /* ignore */ }
|
|
return items || [];
|
|
}).catch(() => null);
|
|
window.__youthomeConversationsTask = conversationsTask;
|
|
list.replaceChildren();
|
|
addEmptyState(list, '正在加载通知…', '');
|
|
let items = [];
|
|
try { items = (await api.get('/campus/notifications?page=1&size=50'))?.items || []; }
|
|
catch (error) {
|
|
addEmptyState(list, '通知加载失败', error.message || '请稍后刷新重试');
|
|
showWebToast(error.message || '通知加载失败', 'error');
|
|
return;
|
|
}
|
|
|
|
const categoryOf = (type) => {
|
|
const value = String(type || '').toUpperCase();
|
|
if (value.includes('LIKE')) return '点赞';
|
|
if (value.includes('COMMENT') || value.includes('REPLY')) return '评论回复';
|
|
if (value.includes('MENTION') || value.includes('AT_')) return '@我的';
|
|
if (value.includes('FOLLOW') || value.includes('FRIEND')) return '新增关注';
|
|
if (value.includes('ACTIVITY')) return '活动提醒';
|
|
return '系统公告';
|
|
};
|
|
|
|
list.replaceChildren(...items.map((item) => {
|
|
const node = template.cloneNode(true);
|
|
const unread = item.isRead === false;
|
|
node.classList.toggle('unread', unread);
|
|
if (!unread) node.querySelector('.message-dot')?.remove();
|
|
node.dataset.notificationId = item.id;
|
|
node.dataset.category = categoryOf(item.type);
|
|
const name = node.querySelector('.message-username');
|
|
if (name) name.textContent = item.title || '通知';
|
|
const extra = node.querySelector('.message-extra');
|
|
if (extra) extra.textContent = '';
|
|
const text = node.querySelector('.message-text');
|
|
if (text) text.textContent = item.content || '';
|
|
const time = node.querySelector('.message-time');
|
|
if (time) time.textContent = window.YouthomeLocal?.relative?.(item.createdAt) || item.createdAt || '';
|
|
const avatar = node.querySelector('.message-avatar');
|
|
if (avatar) {
|
|
avatar.textContent = (item.avatar || '青').slice(0, 1);
|
|
}
|
|
const link = String(item.link || '');
|
|
const target = link.startsWith('/moment/')
|
|
? 'post-detail.html?moment=' + encodeURIComponent(link.split('/').pop())
|
|
: link.startsWith('/activity/')
|
|
? 'activity-detail.html?id=' + encodeURIComponent(link.split('/').pop())
|
|
: '';
|
|
if (target) node.setAttribute('href', target); else node.removeAttribute('href');
|
|
return node;
|
|
}));
|
|
|
|
// “好友消息”放在左侧分类里,点击进入聊天页
|
|
const categoryHost = categories[0]?.parentElement;
|
|
if (categoryHost && !categoryHost.querySelector('[data-chat-category]')) {
|
|
const chatCategory = categories[0].cloneNode(true);
|
|
chatCategory.dataset.chatCategory = 'true';
|
|
chatCategory.classList.remove('active');
|
|
const nameNode = chatCategory.querySelector('.category-name');
|
|
if (nameNode) nameNode.textContent = '好友消息';
|
|
const badge = chatCategory.querySelector('.category-badge');
|
|
if (badge) { badge.textContent = '0'; badge.classList.add('zero'); }
|
|
chatCategory.addEventListener('click', () => {
|
|
const panel = document.querySelector('.message-panel');
|
|
const chatView = document.querySelector('.web-chat-view');
|
|
if (!chatView) { location.href = 'chat.html'; return; }
|
|
if (panel) panel.hidden = true;
|
|
chatView.hidden = false;
|
|
categories.forEach((node) => node.classList.remove('active'));
|
|
chatCategory.classList.add('active');
|
|
initInlineChat(window.YouthomeApi);
|
|
});
|
|
categories[0].after(chatCategory);
|
|
conversationsTask.then((list) => {
|
|
if (!list) return;
|
|
const unread = list.reduce((sum, item) => sum + Number(item.unreadCount || 0), 0);
|
|
if (badge) { badge.textContent = String(unread); badge.classList.toggle('zero', unread === 0); }
|
|
});
|
|
}
|
|
|
|
let active = '全部';
|
|
const renderFilter = () => {
|
|
const all = Array.from(list.querySelectorAll('.message-item'));
|
|
all.forEach((node) => { node.hidden = active !== '全部' && node.dataset.category !== active; });
|
|
categories.forEach((category) => {
|
|
const name = category.querySelector('.category-name')?.textContent.trim() || '';
|
|
const unread = all.filter((node) => node.classList.contains('unread') && (name === '全部' || node.dataset.category === name)).length;
|
|
const badge = category.querySelector('.category-badge');
|
|
if (badge) { badge.textContent = String(unread); badge.classList.toggle('zero', unread === 0); }
|
|
});
|
|
const total = all.filter((node) => node.classList.contains('unread')).length;
|
|
document.querySelectorAll('.web-nav-badge').forEach((badge) => { badge.textContent = String(total); badge.hidden = total === 0; });
|
|
};
|
|
|
|
categories.forEach((category) => {
|
|
category.addEventListener('click', () => {
|
|
const chatView = document.querySelector('.web-chat-view');
|
|
const panel = document.querySelector('.message-panel');
|
|
if (chatView) chatView.hidden = true;
|
|
if (panel) panel.hidden = false;
|
|
categories.forEach((node) => node.classList.remove('active'));
|
|
category.classList.add('active');
|
|
active = category.querySelector('.category-name')?.textContent.trim() || '全部';
|
|
renderFilter();
|
|
});
|
|
});
|
|
|
|
list.querySelectorAll('.message-item').forEach((node) => {
|
|
node.addEventListener('click', async (event) => {
|
|
const id = node.dataset.notificationId;
|
|
const href = node.getAttribute('href');
|
|
if (node.classList.contains('unread')) {
|
|
try {
|
|
await api.request('/campus/notifications/' + encodeURIComponent(id) + '/read', { method: 'PUT' });
|
|
node.classList.remove('unread');
|
|
node.querySelector('.message-dot')?.remove();
|
|
renderFilter();
|
|
} catch (error) {
|
|
// 后端未实现(当前返回 501)时保留未读状态,不能假装已读
|
|
console.warn('标记通知已读失败:', error.message);
|
|
}
|
|
}
|
|
if (!href) event.preventDefault();
|
|
});
|
|
});
|
|
|
|
window.markAllRead = async () => {
|
|
try {
|
|
await api.request('/campus/notifications/read-all', { method: 'PUT' });
|
|
list.querySelectorAll('.message-item').forEach((node) => {
|
|
node.classList.remove('unread');
|
|
node.querySelector('.message-dot')?.remove();
|
|
});
|
|
renderFilter();
|
|
showWebToast('已全部标为已读');
|
|
} catch (error) {
|
|
showWebToast(error.message || '操作失败,请重试', 'error');
|
|
}
|
|
};
|
|
|
|
renderFilter();
|
|
};
|
|
|
|
const initInlineChat = (api) => {
|
|
const view = document.querySelector('.web-chat-view');
|
|
if (!view || view.dataset.ready === 'true') return;
|
|
view.dataset.ready = 'true';
|
|
const convList = view.querySelector('.web-chat-conv-items');
|
|
const msgs = view.querySelector('.web-chat-msgs');
|
|
const paneTitle = view.querySelector('.web-chat-pane-title');
|
|
const textarea = view.querySelector('.web-chat-textarea');
|
|
const send = view.querySelector('.web-chat-send');
|
|
const me = api.currentUser() || {};
|
|
let conversations = [];
|
|
let active = null;
|
|
let historyRequestId = 0;
|
|
const profiles = {};
|
|
const conversationCacheKey = `youthome:web:${me.id || 'anonymous'}:chat-conversations`;
|
|
const readConversationCache = () => {
|
|
try { return JSON.parse(sessionStorage.getItem(conversationCacheKey) || 'null'); } catch (error) { return null; }
|
|
};
|
|
|
|
const nameOf = async (conv) => {
|
|
if (conv.groupName || conv.peerName) return conv.groupName || conv.peerName;
|
|
if (String(conv.conversationType || '').toUpperCase() === 'GROUP') return '群聊';
|
|
if (!conv.peerId) return '会话';
|
|
if (profiles[conv.peerId]) return profiles[conv.peerId].nickname || '用户';
|
|
try {
|
|
const profile = await api.get('/users/profile?userId=' + encodeURIComponent(conv.peerId));
|
|
profiles[conv.peerId] = profile;
|
|
return profile?.nickname || '用户';
|
|
} catch (error) { return '用户'; }
|
|
};
|
|
|
|
const renderConvs = async () => {
|
|
const nodes = [];
|
|
const names = await Promise.all(conversations.map((conv) => nameOf(conv)));
|
|
conversations.forEach((conv, index) => {
|
|
const name = names[index];
|
|
const isGroup = String(conv.conversationType || '').toUpperCase() === 'GROUP';
|
|
const row = document.createElement('div');
|
|
row.className = 'web-chat-conv' + (active && active.id === conv.id ? ' active' : '');
|
|
row.innerHTML = '<div class="web-chat-conv-avatar"></div><div class="web-chat-conv-body"><div class="web-chat-conv-name"></div><div class="web-chat-conv-preview"></div></div><span class="web-chat-conv-time"></span><span class="web-chat-conv-unread"></span>';
|
|
row.querySelector('.web-chat-conv-avatar').textContent = isGroup ? '群' : name.slice(0, 1);
|
|
row.querySelector('.web-chat-conv-name').textContent = name;
|
|
row.querySelector('.web-chat-conv-preview').textContent = conv.lastMessage || '';
|
|
row.querySelector('.web-chat-conv-time').textContent = String(conv.lastMessageTime || '').slice(5, 16);
|
|
const unread = row.querySelector('.web-chat-conv-unread');
|
|
const unreadCount = Number(conv.unreadCount || 0);
|
|
unread.textContent = String(unreadCount);
|
|
unread.classList.toggle('zero', unreadCount === 0);
|
|
row.addEventListener('click', () => {
|
|
active = conv;
|
|
const requestId = ++historyRequestId;
|
|
if (paneTitle) paneTitle.textContent = name;
|
|
renderConvs();
|
|
loadHistory(conv, requestId);
|
|
});
|
|
nodes.push(row);
|
|
});
|
|
convList.replaceChildren(...nodes);
|
|
if (!nodes.length) {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = '还没有会话';
|
|
convList.appendChild(empty);
|
|
}
|
|
};
|
|
|
|
const loadHistory = async (conversation = active, requestId = ++historyRequestId) => {
|
|
if (!conversation) return;
|
|
const isGroup = String(conversation.conversationType || '').toUpperCase() === 'GROUP';
|
|
const path = isGroup
|
|
? '/campus/chat/history/group/' + encodeURIComponent(conversation.peerId) + '?page=1&size=50'
|
|
: '/campus/chat/history/private/' + encodeURIComponent(conversation.peerId) + '?page=1&size=50';
|
|
let items = [];
|
|
try {
|
|
const data = await api.get(path);
|
|
items = data?.items || [];
|
|
} catch (error) {
|
|
if (requestId !== historyRequestId || active?.id !== conversation.id) return;
|
|
msgs.replaceChildren();
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = isGroup ? '无法加载聊天记录(可能已被禁言或群已解散)' : '无法加载聊天记录,请稍后重试';
|
|
msgs.appendChild(empty);
|
|
return;
|
|
}
|
|
if (requestId !== historyRequestId || active?.id !== conversation.id) return;
|
|
msgs.replaceChildren(...items.map((item) => {
|
|
const self = item.senderId === me.id;
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'web-chat-msg-row' + (self ? ' self' : '');
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'web-chat-msg-avatar';
|
|
avatar.textContent = self ? (me.nickname || '我').slice(0, 1) : (item.senderName || '对').slice(0, 1);
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'web-chat-msg';
|
|
bubble.textContent = item.content || '';
|
|
wrapper.append(avatar, bubble);
|
|
return wrapper;
|
|
}));
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
};
|
|
|
|
const sendMessage = async () => {
|
|
const content = textarea?.value.trim();
|
|
if (!content || !active) return;
|
|
const isGroup = String(active.conversationType || '').toUpperCase() === 'GROUP';
|
|
try {
|
|
if (isGroup) await api.post('/campus/chat/messages/group/' + encodeURIComponent(active.peerId), { content });
|
|
else await api.post('/campus/chat/messages/private', { peerId: active.peerId, content, messageType: 'TEXT' });
|
|
if (textarea) textarea.value = '';
|
|
await loadHistory();
|
|
showWebToast('消息已发送');
|
|
} catch (error) {
|
|
showWebToast(/禁言|mute|禁止|无权限/i.test(String(error?.message || '')) ? '你已被禁言,暂时不能发言' : (error.message || '发送失败'), 'error');
|
|
}
|
|
};
|
|
|
|
send?.addEventListener('click', sendMessage);
|
|
textarea?.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); } });
|
|
|
|
const cachedConversations = readConversationCache();
|
|
if (Array.isArray(cachedConversations)) {
|
|
conversations = cachedConversations;
|
|
renderConvs();
|
|
} else {
|
|
convList.replaceChildren();
|
|
const loading = document.createElement('p');
|
|
loading.className = 'web-live-empty';
|
|
loading.textContent = '正在加载会话…';
|
|
convList.appendChild(loading);
|
|
}
|
|
const conversationsRequest = window.__youthomeConversationsTask || api.get('/campus/chat/conversations');
|
|
conversationsRequest.then(async (list) => {
|
|
conversations = list || [];
|
|
try { sessionStorage.setItem(conversationCacheKey, JSON.stringify(conversations)); } catch (error) { /* ignore */ }
|
|
await renderConvs();
|
|
}).catch((error) => {
|
|
if (!cachedConversations) {
|
|
convList.replaceChildren();
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = error.message || '会话加载失败';
|
|
convList.appendChild(empty);
|
|
}
|
|
});
|
|
|
|
window.setInterval(() => { if (active && view && !view.hidden) loadHistory(); }, 3000);
|
|
};
|
|
|
|
|
|
const initNotifications = () => {
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiNotifications(api);
|
|
const items = Array.from(document.querySelectorAll('.message-item'));
|
|
const categories = Array.from(document.querySelectorAll('.category-item'));
|
|
const list = document.querySelector('.message-list');
|
|
if (!items.length || !categories.length || !list) return;
|
|
|
|
const updateBadges = () => {
|
|
categories.forEach((category) => {
|
|
const name = category.querySelector('.category-name').textContent.trim();
|
|
const count = items.filter((item) => item.classList.contains('unread') && (name === '全部' || item.dataset.category === name)).length;
|
|
const badge = category.querySelector('.category-badge');
|
|
badge.textContent = String(count);
|
|
badge.classList.toggle('zero', count === 0);
|
|
});
|
|
updateNotificationIndicators();
|
|
};
|
|
|
|
items.forEach((item) => {
|
|
if (localStorage.getItem(notificationReadKey(item.dataset.notificationId)) === 'true') {
|
|
item.classList.remove('unread');
|
|
item.querySelector('.message-dot')?.remove();
|
|
}
|
|
const open = (event) => {
|
|
event.preventDefault();
|
|
localStorage.setItem(notificationReadKey(item.dataset.notificationId), 'true');
|
|
item.classList.remove('unread');
|
|
item.querySelector('.message-dot')?.remove();
|
|
updateBadges();
|
|
location.href = item.getAttribute('href') || item.dataset.href;
|
|
};
|
|
item.addEventListener('click', open);
|
|
});
|
|
|
|
window.selectCategory = (element, updateUrl = true) => {
|
|
categories.forEach((category) => category.classList.remove('active'));
|
|
element.classList.add('active');
|
|
const name = element.querySelector('.category-name').textContent.trim();
|
|
if (updateUrl) {
|
|
const params = new URLSearchParams();
|
|
if (name !== '全部') params.set('category', name);
|
|
history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}`);
|
|
}
|
|
let visible = 0;
|
|
items.forEach((item) => {
|
|
item.hidden = name !== '全部' && item.dataset.category !== name;
|
|
if (!item.hidden) visible += 1;
|
|
});
|
|
list.querySelector('.web-empty-state')?.remove();
|
|
if (!visible) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'web-empty-state';
|
|
empty.innerHTML = `<div><strong>暂无${name}消息</strong><span>新消息会显示在这里。</span></div>`;
|
|
list.appendChild(empty);
|
|
}
|
|
};
|
|
|
|
window.markAllRead = () => {
|
|
items.forEach((item) => {
|
|
localStorage.setItem(notificationReadKey(item.dataset.notificationId), 'true');
|
|
item.classList.remove('unread');
|
|
item.querySelector('.message-dot')?.remove();
|
|
});
|
|
updateBadges();
|
|
showWebToast('消息已全部标为已读');
|
|
};
|
|
|
|
categories.forEach((category) => makeKeyboardClickable(category, () => window.selectCategory(category)));
|
|
updateBadges();
|
|
const requestedCategory = new URLSearchParams(location.search).get('category');
|
|
const selectedCategory = categories.find((category) => category.querySelector('.category-name').textContent.trim() === requestedCategory) || categories[0];
|
|
window.selectCategory(selectedCategory, false);
|
|
};
|
|
|
|
const initSettings = () => {
|
|
const api = window.YouthomeApi;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
const navItems = Array.from(document.querySelectorAll('.settings-nav-item'));
|
|
const panels = Array.from(document.querySelectorAll('[data-settings-panel]'));
|
|
if (!navItems.length || !panels.length) return;
|
|
const pageTitle = document.querySelector('.settings-content > .page-title');
|
|
const headingTitle = document.querySelector('.settings-page-heading .page-title') || pageTitle;
|
|
const headingDescription = document.querySelector('.settings-page-description');
|
|
const copy = {
|
|
account: ['账号安全', '管理登录方式、身份认证与账号访问安全。'],
|
|
privacy: ['隐私设置', '决定谁可以看到你,以及其他用户如何与你联系。'],
|
|
notifications: ['通知设置', '只保留真正重要的校园消息提醒。'],
|
|
general: ['通用设置', '管理网页端播放偏好与本地存储。'],
|
|
about: ['关于青核', '查看版本信息、帮助内容和服务协议。']
|
|
};
|
|
const renderPanel = (key, updateUrl = true) => {
|
|
const selected = navItems.find((item) => item.dataset.settingsSection === key) || navItems[0];
|
|
const selectedKey = selected.dataset.settingsSection;
|
|
navItems.forEach((item) => item.classList.toggle('active', item === selected));
|
|
panels.forEach((panel) => { panel.hidden = panel.dataset.settingsPanel !== selectedKey; });
|
|
if (headingTitle) headingTitle.textContent = copy[selectedKey][0];
|
|
if (headingDescription) headingDescription.textContent = copy[selectedKey][1];
|
|
localStorage.setItem('youthome:web:settings-section', selectedKey);
|
|
if (updateUrl) history.replaceState(null, '', `${location.pathname}?section=${selectedKey}`);
|
|
document.querySelector('.content-area')?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
navItems.forEach((item, index) => {
|
|
makeKeyboardClickable(item, () => item.click());
|
|
item.addEventListener('click', () => renderPanel(item.dataset.settingsSection));
|
|
});
|
|
const requestedSection = new URLSearchParams(location.search).get('section');
|
|
const storedSection = localStorage.getItem('youthome:web:settings-section');
|
|
renderPanel(requestedSection || storedSection || 'account', false);
|
|
|
|
const settingsRows = Array.from(document.querySelectorAll('.setting-item'));
|
|
const namedRow = (name) => settingsRows.find((row) => row.querySelector('.setting-name')?.textContent.trim() === name);
|
|
if (!isLocal) {
|
|
['谁可以查看我的动态', '个性化推荐', '评论通知', '新粉丝通知'].forEach((name) => {
|
|
const row = namedRow(name);
|
|
if (row) row.style.display = 'none';
|
|
});
|
|
const socialName = namedRow('点赞通知')?.querySelector('.setting-name');
|
|
if (socialName) socialName.textContent = '社交通知';
|
|
const socialDesc = namedRow('社交通知')?.querySelector('.setting-desc');
|
|
if (socialDesc) socialDesc.textContent = '收到点赞、评论和关注时通知我';
|
|
}
|
|
const remoteFields = {
|
|
'允许陌生人私信': ['privacy', 'allowStrangerMessage'],
|
|
'展示在线状态': ['privacy', 'onlineStatusVisible'],
|
|
'社交通知': ['notifications', 'socialEnabled'],
|
|
'活动提醒': ['notifications', 'activityEnabled']
|
|
};
|
|
const remoteState = {};
|
|
const remotePaths = { privacy: '/users/privacy', notifications: '/campus/notifications/preferences' };
|
|
document.querySelectorAll('.switch').forEach((toggle) => {
|
|
const name = toggle.closest('.setting-item')?.querySelector('.setting-name')?.textContent.trim() || '设置';
|
|
const key = `youthome:web:setting:${name}`;
|
|
const binding = !isLocal && remoteFields[name];
|
|
const stored = binding ? null : localStorage.getItem(key);
|
|
if (stored !== null) toggle.classList.toggle('on', stored === 'true');
|
|
toggle.setAttribute('role', 'switch');
|
|
toggle.tabIndex = 0;
|
|
toggle.setAttribute('aria-label', name);
|
|
toggle.setAttribute('aria-checked', String(toggle.classList.contains('on')));
|
|
const change = async () => {
|
|
if (toggle.dataset.busy === 'true') return;
|
|
const enabled = !toggle.classList.contains('on');
|
|
if (binding) {
|
|
const [kind, field] = binding;
|
|
if (!remoteState[kind]) { showWebToast('设置尚未加载,请稍后重试', 'error'); return; }
|
|
toggle.dataset.busy = 'true';
|
|
try {
|
|
const payload = kind === 'notifications'
|
|
? Object.fromEntries(['pushEnabled', 'systemEnabled', 'socialEnabled', 'activityEnabled', 'chatEnabled', 'quietStart', 'quietEnd']
|
|
.filter((key) => Object.prototype.hasOwnProperty.call(remoteState[kind], key))
|
|
.map((key) => [key, key === field ? enabled : remoteState[kind][key]]))
|
|
: { [field]: enabled };
|
|
remoteState[kind] = await api.request(remotePaths[kind], { method: 'PUT', body: JSON.stringify(payload) });
|
|
} catch (error) {
|
|
showWebToast(error.message || '设置保存失败', 'error');
|
|
delete toggle.dataset.busy;
|
|
return;
|
|
}
|
|
delete toggle.dataset.busy;
|
|
} else localStorage.setItem(key, String(enabled));
|
|
toggle.classList.toggle('on', enabled);
|
|
toggle.setAttribute('aria-checked', String(enabled));
|
|
showWebToast(`${name}已${enabled ? '开启' : '关闭'}`);
|
|
};
|
|
toggle.addEventListener('click', change);
|
|
toggle.addEventListener('keydown', (event) => {
|
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
event.preventDefault();
|
|
change();
|
|
});
|
|
});
|
|
if (!isLocal) {
|
|
Object.entries(remotePaths).forEach(async ([kind, path]) => {
|
|
try {
|
|
remoteState[kind] = await api.get(path);
|
|
Object.entries(remoteFields).forEach(([name, [fieldKind, field]]) => {
|
|
if (fieldKind !== kind || typeof remoteState[kind]?.[field] !== 'boolean') return;
|
|
const toggle = namedRow(name)?.querySelector('.switch');
|
|
if (!toggle) return;
|
|
toggle.classList.toggle('on', remoteState[kind][field]);
|
|
toggle.setAttribute('aria-checked', String(remoteState[kind][field]));
|
|
});
|
|
} catch (error) { showWebToast(error.message || '设置加载失败', 'error'); }
|
|
});
|
|
}
|
|
|
|
const rows = Array.from(document.querySelectorAll('.setting-item'));
|
|
const rowByName = (name) => rows.find((row) => row.querySelector('.setting-name')?.textContent.trim() === name);
|
|
const linkRow = (name, href) => {
|
|
const row = rowByName(name);
|
|
if (!row) return;
|
|
row.addEventListener('click', (event) => {
|
|
if (event.target.closest('button, .switch')) return;
|
|
location.href = href;
|
|
});
|
|
makeKeyboardClickable(row, () => { location.href = href; });
|
|
};
|
|
linkRow('登录密码', 'forgot-password.html');
|
|
linkRow('实名认证', 'authentication.html');
|
|
document.querySelector('[data-setting-action="devices"]')?.addEventListener('click', () => showWebToast('当前有 2 台设备保持登录'));
|
|
document.querySelector('[data-setting-action="help"]')?.addEventListener('click', () => { location.href = 'help.html'; });
|
|
document.querySelector('[data-setting-action="agreement"]')?.addEventListener('click', () => { location.href = 'help.html?section=agreement'; });
|
|
document.querySelector('[data-setting-action="privacy-policy"]')?.addEventListener('click', () => { location.href = 'help.html?section=privacy'; });
|
|
|
|
const visibility = rowByName('谁可以查看我的动态');
|
|
const visibilityValues = ['所有人', '仅关注我的人', '仅自己'];
|
|
if (isLocal) visibility?.addEventListener('click', () => {
|
|
const value = visibility.querySelector('.setting-value');
|
|
const next = (visibilityValues.indexOf(value.textContent.trim()) + 1) % visibilityValues.length;
|
|
value.textContent = visibilityValues[next];
|
|
localStorage.setItem('youthome:web:setting:visibility', visibilityValues[next]);
|
|
showWebToast(`动态可见范围已设为“${visibilityValues[next]}”`);
|
|
});
|
|
|
|
window.openChangePhoneModal = () => {
|
|
const phone = window.prompt('请输入新的手机号(演示环境仅保存在本机)');
|
|
if (phone === null) return;
|
|
if (!/^1\d{10}$/.test(phone.trim())) {
|
|
showWebToast('请输入正确的 11 位手机号', 'error');
|
|
return;
|
|
}
|
|
rowByName('手机号').querySelector('.setting-value').textContent = `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
|
localStorage.setItem('youthome:web:setting:phone', phone);
|
|
showWebToast('手机号已更新');
|
|
};
|
|
|
|
window.openClearCacheModal = () => {
|
|
if (!window.confirm('清理缓存只会重置已读状态和临时记录。你发布的动态、草稿、关注和登录状态都会保留,确定继续吗?')) return;
|
|
// 只清理临时数据:账号、动态、草稿、关注、个人设置都属于用户数据,不能一起删掉
|
|
const transientPrefixes = ['youthome:web:notification-read:'];
|
|
const transientKeys = ['youthome:web:checkedIn', 'youthome:web:walletCheckedIn'];
|
|
const targets = Object.keys(localStorage).filter((key) =>
|
|
transientKeys.includes(key) || transientPrefixes.some((prefix) => key.startsWith(prefix)));
|
|
targets.forEach((key) => localStorage.removeItem(key));
|
|
const value = rowByName('清理缓存')?.querySelector('.setting-value');
|
|
if (value) value.textContent = formatUsage(localUsage());
|
|
window.webRefreshNotificationIndicators?.();
|
|
showWebToast(targets.length ? `已清理 ${targets.length} 项临时缓存,动态与草稿已保留` : '没有可清理的临时缓存');
|
|
};
|
|
|
|
// 显示真实的本机存储占用,不再使用写死的数值
|
|
const localUsage = () => Object.keys(localStorage)
|
|
.filter((key) => key.startsWith('youthome:web:'))
|
|
.reduce((bytes, key) => bytes + (localStorage.getItem(key) || '').length * 2, 0);
|
|
const formatUsage = (bytes) => (bytes < 1024
|
|
? `${bytes} B`
|
|
: bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(2)} MB`);
|
|
const cacheValue = rowByName('清理缓存')?.querySelector('.setting-value');
|
|
if (cacheValue) cacheValue.textContent = formatUsage(localUsage());
|
|
|
|
const savedPhone = localStorage.getItem('youthome:web:setting:phone');
|
|
if (savedPhone) rowByName('手机号').querySelector('.setting-value').textContent = `${savedPhone.slice(0, 3)}****${savedPhone.slice(-4)}`;
|
|
const savedVisibility = localStorage.getItem('youthome:web:setting:visibility');
|
|
if (savedVisibility) visibility.querySelector('.setting-value').textContent = savedVisibility;
|
|
};
|
|
|
|
const appendFavoriteCards = (grid, groups, templateNode) => {
|
|
if (!grid) return;
|
|
const template = templateNode || grid.querySelector('.post-mini-card');
|
|
if (!template) return;
|
|
const relative = (window.YouthomeLocal?.relative) || (() => '刚刚');
|
|
for (const group of groups) {
|
|
for (const moment of group.items) {
|
|
const key = `${group.kind}:${moment.id}`;
|
|
if (grid.querySelector(`.post-mini-card[data-key="${key}"]`)) continue;
|
|
const card = template.cloneNode(true);
|
|
card.dataset.kind = group.kind;
|
|
card.dataset.moment = moment.id;
|
|
card.dataset.key = key;
|
|
card.classList.add('web-live-card');
|
|
const href = `post-detail.html?moment=${encodeURIComponent(moment.id)}`;
|
|
card.dataset.href = href;
|
|
card.setAttribute('onclick', `location.href='${href}'`);
|
|
const avatar = card.querySelector('.post-avatar');
|
|
if (avatar) avatar.textContent = (moment.userName || '青').slice(0, 1);
|
|
const name = card.querySelector('.post-user-name');
|
|
if (name) name.textContent = moment.userName || '青核用户';
|
|
const time = card.querySelector('.post-time');
|
|
if (time) time.textContent = `${group.label} ${relative(moment.createdAt)}`;
|
|
const content = card.querySelector('.post-mini-content');
|
|
if (content) content.textContent = moment.content;
|
|
const images = card.querySelector('.post-mini-images');
|
|
if (images) {
|
|
if (moment.mediaUrls?.length) {
|
|
images.replaceChildren(...moment.mediaUrls.slice(0, 3).map((url) => {
|
|
const img = document.createElement('img');
|
|
img.src = url;
|
|
img.alt = '';
|
|
return img;
|
|
}));
|
|
} else {
|
|
card.classList.add('web-text-card');
|
|
images.remove();
|
|
}
|
|
}
|
|
const stats = card.querySelectorAll('.post-footer-item span');
|
|
if (stats[0]) stats[0].textContent = String(moment.likeCount || 0);
|
|
if (stats[1]) stats[1].textContent = String(moment.commentCount || 0);
|
|
const collect = card.querySelector('.post-footer-collect');
|
|
if (collect && group.kind !== 'favorite') collect.remove();
|
|
grid.insertBefore(card, grid.firstElementChild);
|
|
}
|
|
}
|
|
};
|
|
|
|
const injectRealFavorites = (grid, store) => {
|
|
if (!grid || !store?.account?.()) return;
|
|
appendFavoriteCards(grid, [
|
|
{ kind: 'favorite', label: '收藏于', items: store.bucketMoments('favorites') },
|
|
{ kind: 'like', label: '点赞于', items: store.bucketMoments('likes') },
|
|
{ kind: 'view', label: '浏览于', items: store.viewedMoments() }
|
|
]);
|
|
};
|
|
|
|
const initFavorites = async () => {
|
|
const tabs = Array.from(document.querySelectorAll('.tab-bar .tab-item'));
|
|
const grid = document.querySelector('.favorites-grid');
|
|
if (!tabs.length || !grid) return;
|
|
const api = window.YouthomeApi;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
if (isLocal) {
|
|
injectRealFavorites(grid, window.YouthomeLocal);
|
|
} else {
|
|
const template = grid.querySelector('.post-mini-card')?.cloneNode(true);
|
|
grid.replaceChildren();
|
|
try {
|
|
const items = await api.favorites();
|
|
appendFavoriteCards(grid, [{ kind: 'favorite', label: '收藏于', items }], template);
|
|
} catch (error) { showWebToast(error.message || '收藏加载失败', 'error'); }
|
|
if (!grid.querySelector('.post-mini-card')) {
|
|
addEmptyState(grid, '还没有收藏内容', '在动态详情里点收藏,内容会出现在这里。');
|
|
}
|
|
}
|
|
const cards = Array.from(grid.querySelectorAll('.post-mini-card'));
|
|
const subsets = {
|
|
收藏: cards.filter((card) => card.dataset.kind !== 'like' && card.dataset.kind !== 'view'),
|
|
喜欢: cards.filter((card) => card.dataset.kind !== 'favorite' && card.dataset.kind !== 'view'),
|
|
浏览历史: cards.filter((card) => card.dataset.kind !== 'favorite' && card.dataset.kind !== 'like')
|
|
};
|
|
let activeLabel = tabs[0].textContent.trim();
|
|
const render = (label) => {
|
|
activeLabel = label;
|
|
const visible = new Set((subsets[label] || cards).filter((card) => card.dataset.removed !== 'true'));
|
|
cards.forEach((card) => { card.hidden = !visible.has(card); });
|
|
grid.querySelector('.web-empty-state')?.remove();
|
|
if (!cards.some((card) => !card.hidden)) addEmptyState(grid, `暂无${label}内容`, '浏览或收藏动态后,相关内容会出现在这里。');
|
|
const subtitle = document.querySelector('.page-subtitle');
|
|
if (subtitle) subtitle.textContent = `${label}内容,共 ${cards.filter((card) => !card.hidden).length} 条`;
|
|
const params = new URLSearchParams();
|
|
if (label !== '收藏') params.set('tab', label);
|
|
history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}`);
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { setPressed(tabs, tab); render(tab.textContent.trim()); });
|
|
});
|
|
cards.forEach((card) => makeLinkedCard(card, card.dataset.href || 'post-detail.html'));
|
|
grid.querySelectorAll('.post-footer-collect').forEach((button) => {
|
|
makeKeyboardClickable(button, () => button.click());
|
|
button.setAttribute('aria-label', '取消收藏');
|
|
button.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const card = button.closest('.post-mini-card');
|
|
if (card?.dataset.kind === 'favorite' && card.dataset.moment) {
|
|
try { window.YouthomeLocal.removeFromBucket('favorites', card.dataset.moment); }
|
|
catch (error) { showWebToast(error.message || '操作失败,请重试', 'error'); return; }
|
|
}
|
|
card.dataset.removed = 'true';
|
|
card.hidden = true;
|
|
showWebToast('已取消收藏');
|
|
render(activeLabel);
|
|
});
|
|
});
|
|
const requested = new URLSearchParams(location.search).get('tab');
|
|
const selected = tabs.find((tab) => tab.textContent.trim() === requested) || tabs[0];
|
|
setPressed(tabs, selected);
|
|
render(selected.textContent.trim());
|
|
};
|
|
|
|
const initDrafts = () => {
|
|
if (document.querySelector('script[src*="web-publishing.js"]')) return;
|
|
const cards = Array.from(document.querySelectorAll('.draft-card'));
|
|
if (!cards.length) return;
|
|
const removeCards = (targets) => {
|
|
targets.forEach((card) => card.remove());
|
|
showWebToast(`已删除 ${targets.length} 条草稿`);
|
|
if (!document.querySelector('.draft-card')) addEmptyState(document.querySelector('.draft-list') || cards[0].parentElement, '草稿箱为空', '未发布的内容会自动保存在这里。');
|
|
};
|
|
cards.forEach((card) => {
|
|
card.querySelector('.draft-action-publish')?.addEventListener('click', () => {
|
|
showWebToast('草稿已发布');
|
|
card.remove();
|
|
});
|
|
card.querySelector('.draft-action-delete')?.addEventListener('click', () => {
|
|
if (window.confirm('确定删除这条草稿吗?')) removeCards([card]);
|
|
});
|
|
});
|
|
const bulkButtons = document.querySelectorAll('.action-bar-right .btn');
|
|
bulkButtons[0]?.addEventListener('click', () => {
|
|
const selected = Array.from(document.querySelectorAll('.draft-card.selected'));
|
|
selected.forEach((card) => card.remove());
|
|
if (selected.length) showWebToast(`已发布 ${selected.length} 条草稿`);
|
|
});
|
|
bulkButtons[1]?.addEventListener('click', () => {
|
|
const selected = Array.from(document.querySelectorAll('.draft-card.selected'));
|
|
if (selected.length && window.confirm(`确定删除选中的 ${selected.length} 条草稿吗?`)) removeCards(selected);
|
|
});
|
|
};
|
|
|
|
const injectFollowedUsers = (grid, store) => {
|
|
if (!grid || !store?.account?.()) return;
|
|
const template = grid.querySelector('.user-card');
|
|
if (!template) return;
|
|
const listed = new Set(Array.from(grid.querySelectorAll('.user-name')).map((node) => node.textContent.trim()));
|
|
store.following().forEach((name, index) => {
|
|
if (!name || listed.has(name)) return;
|
|
const card = template.cloneNode(true);
|
|
card.dataset.kind = 'real-follow';
|
|
card.classList.add('web-live-card');
|
|
const avatar = card.querySelector('.user-avatar');
|
|
if (avatar) {
|
|
const file = String((index % 10) + 1).padStart(2, '0');
|
|
avatar.innerHTML = `<img src="../assets/avatar-${file}-female-short-light.jpg" alt="${name}">`;
|
|
}
|
|
const nameNode = card.querySelector('.user-name');
|
|
if (nameNode) nameNode.textContent = name;
|
|
const desc = card.querySelector('.user-desc');
|
|
if (desc) desc.textContent = '校园用户';
|
|
const button = card.querySelector('.follow-btn');
|
|
if (button) {
|
|
button.className = 'follow-btn followed';
|
|
button.innerHTML = '<span class="btn-text-default">已关注</span><span class="btn-text-hover">取消关注</span>';
|
|
}
|
|
grid.insertBefore(card, grid.firstElementChild);
|
|
});
|
|
};
|
|
|
|
const injectApiFollowing = async (grid, api, templateNode) => {
|
|
if (!grid) return;
|
|
const template = templateNode || grid.querySelector('.user-card');
|
|
if (!template) return;
|
|
let users = [];
|
|
try { users = await api.following(); }
|
|
catch (error) { showWebToast(error.message || '关注列表加载失败', 'error'); return; }
|
|
const listed = new Set(Array.from(grid.querySelectorAll('.user-name')).map((node) => node.textContent.trim()));
|
|
users.forEach((user) => {
|
|
const nickname = user.nickname || user.userName || '校园用户';
|
|
if (listed.has(nickname)) return;
|
|
const card = template.cloneNode(true);
|
|
card.dataset.kind = 'api-follow';
|
|
card.dataset.userId = user.userId || user.id || '';
|
|
const avatar = card.querySelector('.user-avatar');
|
|
if (avatar) {
|
|
const url = api.avatarUrl?.(user.avatar) || '';
|
|
avatar.textContent = '';
|
|
if (url) {
|
|
const img = document.createElement('img');
|
|
img.src = url;
|
|
img.alt = nickname;
|
|
avatar.appendChild(img);
|
|
} else {
|
|
avatar.textContent = nickname.slice(0, 1);
|
|
}
|
|
}
|
|
const nameNode = card.querySelector('.user-name');
|
|
if (nameNode) nameNode.textContent = nickname;
|
|
const desc = card.querySelector('.user-desc');
|
|
if (desc) desc.textContent = user.universityName || '校园用户';
|
|
const button = card.querySelector('.follow-btn');
|
|
if (button) {
|
|
button.className = 'follow-btn followed';
|
|
button.innerHTML = '<span class="btn-text-default">已关注</span><span class="btn-text-hover">取消关注</span>';
|
|
button.addEventListener('click', async (event) => {
|
|
event.stopPropagation();
|
|
try {
|
|
await api.toggleFollow(card.dataset.userId);
|
|
card.remove();
|
|
showWebToast('已取消关注');
|
|
} catch (error) { showWebToast(error.message || '取关失败', 'error'); }
|
|
});
|
|
}
|
|
grid.insertBefore(card, grid.firstElementChild);
|
|
});
|
|
};
|
|
|
|
const initFollow = async () => {
|
|
const grid = document.querySelector('.user-grid');
|
|
const tabs = Array.from(document.querySelectorAll('.follow-tab'));
|
|
const inputs = Array.from(document.querySelectorAll('.search-input, .filter-search input'));
|
|
const filterGroups = Array.from(document.querySelectorAll('.filter-group'));
|
|
if (!grid) return;
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) {
|
|
const template = grid.querySelector('.user-card')?.cloneNode(true);
|
|
grid.replaceChildren();
|
|
await injectApiFollowing(grid, api, template);
|
|
if (!grid.querySelector('.user-card')) {
|
|
addEmptyState(grid, '还没有关注任何人', '在动态广场的推荐里关注感兴趣的人。');
|
|
return;
|
|
}
|
|
} else {
|
|
injectFollowedUsers(grid, window.YouthomeLocal);
|
|
}
|
|
const cards = Array.from(grid.querySelectorAll('.user-card'));
|
|
if (!cards.length) return;
|
|
const groups = ['其他学校', '同学', '同学', '其他学校', '其他学校', '社团好友'];
|
|
cards.forEach((card, index) => {
|
|
card.dataset.group = card.dataset.group || groups[index] || '同学';
|
|
card.dataset.popularity = card.dataset.popularity || String([78, 92, 66, 83, 55, 72][index] || 50);
|
|
const name = card.querySelector('.user-name')?.textContent.trim();
|
|
const userId = card.dataset.userId;
|
|
if (userId) makeLinkedCard(card, `user.html?id=${encodeURIComponent(userId)}`);
|
|
else if (name) makeLinkedCard(card, `user.html?name=${encodeURIComponent(name)}`);
|
|
});
|
|
let activeTab = '关注';
|
|
let activeGroup = '全部关注';
|
|
let activeSort = '最近关注';
|
|
let query = '';
|
|
const render = () => {
|
|
let matched = cards.filter((card) => {
|
|
const button = card.querySelector('.follow-btn');
|
|
const stateMatch = activeTab === '全部' ||
|
|
(activeTab === '关注' && !button.classList.contains('follow')) ||
|
|
(activeTab === '粉丝' && (button.classList.contains('mutual') || Number(card.dataset.popularity) > 75)) ||
|
|
(activeTab === '互相关注' && button.classList.contains('mutual'));
|
|
return stateMatch && (activeGroup === '全部关注' || card.dataset.group === activeGroup) && (!query || card.textContent.toLowerCase().includes(query));
|
|
});
|
|
if (activeSort === '粉丝最多') matched.sort((a, b) => Number(b.dataset.popularity) - Number(a.dataset.popularity));
|
|
if (activeSort === '最活跃') matched.reverse();
|
|
cards.forEach((card) => { card.hidden = true; });
|
|
matched.forEach((card) => { card.hidden = false; grid.appendChild(card); });
|
|
grid.querySelector('.web-empty-state')?.remove();
|
|
if (!matched.length) addEmptyState(grid, '没有找到用户', '调整分组、关系类型或搜索关键词。');
|
|
const count = document.querySelector('.follow-count b');
|
|
if (count) count.textContent = String(matched.length);
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { activeTab = tab.childNodes[0].textContent.trim(); setPressed(tabs, tab); render(); });
|
|
});
|
|
const groupOptions = Array.from(filterGroups[0]?.querySelectorAll('.filter-option') || []);
|
|
groupOptions.forEach((option) => {
|
|
makeKeyboardClickable(option, () => option.click());
|
|
option.addEventListener('click', () => { activeGroup = option.querySelector('span')?.textContent.trim(); setPressed(groupOptions, option); render(); });
|
|
});
|
|
const sortOptions = Array.from(filterGroups[1]?.querySelectorAll('.filter-option') || []);
|
|
sortOptions.forEach((option) => {
|
|
makeKeyboardClickable(option, () => option.click());
|
|
option.addEventListener('click', () => { activeSort = option.textContent.trim(); setPressed(sortOptions, option); render(); });
|
|
});
|
|
inputs.forEach((input) => input.addEventListener('input', () => {
|
|
query = input.value.trim().toLowerCase();
|
|
inputs.forEach((other) => { if (other !== input) other.value = input.value; });
|
|
render();
|
|
}));
|
|
window.addFriend = () => { location.href = 'search.html?type=user'; };
|
|
const originalToggle = window.toggleFollow;
|
|
window.toggleFollow = (button) => {
|
|
originalToggle?.(button);
|
|
const card = button.closest('.user-card');
|
|
const name = card?.querySelector('.user-name')?.textContent.trim();
|
|
const following = button.classList.contains('followed') || button.classList.contains('mutual');
|
|
if (name && window.YouthomeLocal?.setFollowing) window.YouthomeLocal.setFollowing(name, following);
|
|
if (card?.dataset.kind === 'real-follow' && !following) {
|
|
const index = cards.indexOf(card);
|
|
if (index >= 0) cards.splice(index, 1);
|
|
card.remove();
|
|
showWebToast('已取消关注');
|
|
}
|
|
window.setTimeout(render, 0);
|
|
};
|
|
const params = new URLSearchParams(location.search);
|
|
const requestedTab = params.get('tab');
|
|
const selectedTab = tabs.find((tab) => tab.childNodes[0].textContent.trim() === requestedTab);
|
|
if (selectedTab) {
|
|
activeTab = requestedTab;
|
|
setPressed(tabs, selectedTab);
|
|
}
|
|
query = (params.get('q') || '').trim().toLowerCase();
|
|
inputs.forEach((input) => { input.value = params.get('q') || ''; });
|
|
render();
|
|
};
|
|
|
|
const injectApiActivities = async (list, api, templateNode) => {
|
|
const template = templateNode || list?.querySelector('.activity-card');
|
|
if (!list || !template) return;
|
|
const [joinedResult, registrationsResult] = await Promise.allSettled([api.joinedActivities(), api.registrations()]);
|
|
if (joinedResult.status === 'rejected' && registrationsResult.status === 'rejected') {
|
|
showWebToast('报名记录加载失败,请稍后重试', 'error');
|
|
return;
|
|
}
|
|
const joined = joinedResult.status === 'fulfilled' ? joinedResult.value : [];
|
|
const registrations = (registrationsResult.status === 'fulfilled' ? registrationsResult.value : [])
|
|
.filter((item) => !['CANCELLED', 'REMOVED'].includes(String(item.status || '').toUpperCase()));
|
|
const covered = new Set(registrations.map((item) => item.activityId));
|
|
const details = await Promise.all(registrations.map((item) => api.get('/campus/activities/detail?id=' + encodeURIComponent(item.activityId)).catch(() => null)));
|
|
const records = [
|
|
...registrations.map((item, index) => ({ kind: 'registration', activityId: item.activityId, registrationId: item.id, registration: item, activity: details[index] || {} })),
|
|
...joined.filter((item) => !covered.has(item.id)).map((item) => ({ kind: 'joined', activityId: item.id, activity: item })),
|
|
...api.applicationRecords().filter((item) => !covered.has(item.activityId) && !joined.some((activity) => activity.id === item.activityId)).map((item) => ({ kind: 'application', activityId: item.activityId, activity: item }))
|
|
];
|
|
joined.forEach((item) => api.removeApplication(item.id));
|
|
records.forEach((record) => {
|
|
const item = record.activity;
|
|
const card = template.cloneNode(true);
|
|
card.dataset.joinedId = record.activityId;
|
|
card.dataset.kind = record.kind;
|
|
if (record.registrationId) card.dataset.registrationId = record.registrationId;
|
|
const status = card.querySelector('.status-tag');
|
|
const ended = item.endTime && Date.parse(String(item.endTime).replace(/-/g, '/')) < Date.now();
|
|
const registrationStatus = String(record.registration?.status || '').toUpperCase();
|
|
const reviewStatus = String(record.registration?.reviewStatus || '').toUpperCase();
|
|
const pendingPayment = registrationStatus === 'PENDING_PAYMENT';
|
|
const pendingReview = record.kind === 'application' || reviewStatus === 'PENDING';
|
|
const rejected = reviewStatus === 'REJECTED' || registrationStatus === 'REJECTED';
|
|
card.dataset.statusGroup = rejected ? 'rejected' : ended ? 'ended' : pendingPayment || pendingReview ? 'pending' : 'approved';
|
|
if (status) {
|
|
status.className = 'status-tag ' + (card.dataset.statusGroup === 'pending' ? 'status-pending' : 'status-approved');
|
|
status.textContent = ended ? '已结束' : pendingPayment ? '待支付' : rejected ? '已拒绝' : pendingReview ? '待审核' : '已通过';
|
|
}
|
|
const title = card.querySelector('.activity-title');
|
|
if (title) title.textContent = item.title || '未命名活动';
|
|
const meta = card.querySelectorAll('.meta-item span');
|
|
if (meta[0]) meta[0].textContent = [item.startTime, item.endTime].filter(Boolean).join(' - ') || '以主办方通知为准';
|
|
if (meta[1]) meta[1].textContent = item.location || '待公布';
|
|
if (meta[2]) meta[2].textContent = (item.currentCount ?? 1) + '/' + (item.maxCount ?? '—') + '人';
|
|
const bottom = card.querySelector('.activity-info-bottom');
|
|
if (bottom) bottom.textContent = record.kind === 'application' ? '本机保存的申请回执,审核结果以通知为准' : record.registration?.createdAt ? '报名时间:' + record.registration.createdAt : item.creatorName ? '发起人:' + item.creatorName : '';
|
|
if (record.kind === 'application') card.querySelector('.activity-actions .btn')?.remove();
|
|
list.insertBefore(card, list.firstElementChild);
|
|
});
|
|
};
|
|
|
|
const initMyActivities = async () => {
|
|
const list = document.querySelector('.activity-list');
|
|
const tabs = Array.from(document.querySelectorAll('.apply-tab'));
|
|
const input = document.querySelector('.search-input');
|
|
const store = window.YouthomeLocal;
|
|
const api = window.YouthomeApi;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
if (!isLocal) {
|
|
const template = list?.querySelector('.activity-card')?.cloneNode(true);
|
|
list?.replaceChildren();
|
|
await injectApiActivities(list, api, template);
|
|
if (list && !list.querySelector('.activity-card')) {
|
|
addEmptyState(list, '还没有报名活动', '在活动页报名后,记录会出现在这里。');
|
|
return;
|
|
}
|
|
}
|
|
const template = list?.querySelector('.activity-card');
|
|
if (isLocal && list && template && store?.account?.()) {
|
|
store.joinedActivities().forEach((record) => {
|
|
const card = template.cloneNode(true);
|
|
card.dataset.joinedId = record.id;
|
|
card.classList.add('web-live-card');
|
|
const status = card.querySelector('.status-tag');
|
|
if (status) { status.className = 'status-tag status-pending'; status.textContent = '待审核'; }
|
|
const title = card.querySelector('.activity-title');
|
|
if (title) title.textContent = record.title;
|
|
const meta = card.querySelectorAll('.meta-item span');
|
|
if (meta[0]) meta[0].textContent = record.time || '以主办方通知为准';
|
|
if (meta[1]) meta[1].textContent = record.place || '待公布';
|
|
if (meta[2]) meta[2].textContent = '1/200人';
|
|
const bottom = card.querySelector('.activity-info-bottom');
|
|
if (bottom) bottom.textContent = `报名时间:${record.joinedAt}`;
|
|
list.insertBefore(card, list.firstElementChild);
|
|
});
|
|
}
|
|
const cards = Array.from(document.querySelectorAll('.activity-card'));
|
|
if (!tabs.length || !cards.length || !list) return;
|
|
const stateOf = (card) => card.dataset.statusGroup || (card.querySelector('.status-pending') ? 'pending' : 'approved');
|
|
let state = 'all';
|
|
let query = '';
|
|
const refreshCounts = () => {
|
|
tabs.forEach((tab) => {
|
|
const badge = tab.querySelector('.tab-count');
|
|
if (!badge) return;
|
|
const total = cards.filter((card) => card.isConnected && (tab.dataset.tab === 'all' || stateOf(card) === tab.dataset.tab)).length;
|
|
badge.textContent = `(${total})`;
|
|
});
|
|
};
|
|
const render = () => {
|
|
const matched = cards.filter((card) => card.isConnected && (state === 'all' || stateOf(card) === state) && (!query || card.textContent.toLowerCase().includes(query)));
|
|
cards.forEach((card) => { card.hidden = !matched.includes(card); });
|
|
list.querySelector('.web-empty-state')?.remove();
|
|
if (!matched.length) addEmptyState(list, '没有符合条件的报名记录', '切换状态或更换搜索关键词。');
|
|
refreshCounts();
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { state = tab.dataset.tab; setPressed(tabs, tab); render(); });
|
|
});
|
|
input?.addEventListener('input', () => { query = input.value.trim().toLowerCase(); render(); });
|
|
document.querySelector('.filter-btn')?.addEventListener('click', () => {
|
|
const order = ['all', 'pending', 'approved', 'ended'];
|
|
state = order[(order.indexOf(state) + 1) % order.length];
|
|
const tab = tabs.find((item) => item.dataset.tab === state);
|
|
if (tab) { setPressed(tabs, tab); render(); }
|
|
});
|
|
cards.forEach((card) => {
|
|
card.querySelectorAll('.btn, .text-link, .text-link-muted').forEach((control) => {
|
|
const label = control.textContent.trim();
|
|
makeKeyboardClickable(control, () => control.click());
|
|
control.addEventListener('click', async (event) => {
|
|
event.stopPropagation();
|
|
if (label.includes('查看详情') || label.includes('活动回顾')) location.href = card.dataset.joinedId ? 'activity-detail.html?id=' + encodeURIComponent(card.dataset.joinedId) : 'activity-detail.html';
|
|
else if (label.includes('入场码')) showWebToast('入场码已打开,请在活动现场出示');
|
|
else if (label.includes('取消报名') && window.confirm('确定取消该活动报名吗?')) {
|
|
if (card.dataset.joinedId) {
|
|
try {
|
|
if (!isLocal && window.YouthomeApi) {
|
|
if (card.dataset.registrationId) await window.YouthomeApi.cancelRegistration(card.dataset.registrationId);
|
|
else await window.YouthomeApi.quitActivity(card.dataset.joinedId);
|
|
}
|
|
else window.YouthomeLocal.leaveActivity(card.dataset.joinedId);
|
|
} catch (error) { showWebToast(error.message || '操作失败,请重试', 'error'); return; }
|
|
}
|
|
card.remove();
|
|
showWebToast('报名已取消');
|
|
render();
|
|
}
|
|
});
|
|
});
|
|
makeLinkedCard(card, card.dataset.joinedId ? 'activity-detail.html?id=' + encodeURIComponent(card.dataset.joinedId) : 'activity-detail.html');
|
|
});
|
|
render();
|
|
};
|
|
|
|
const initProfile = () => {
|
|
if (window.YouthomeApi?.isLocal && !window.YouthomeApi.isLocal()) return;
|
|
const grid = document.querySelector('.content-grid');
|
|
if (grid) {
|
|
const postTemplate = grid.querySelector('.post-card');
|
|
['milk-tea', 'canteen'].forEach((id) => {
|
|
const post = notificationPosts[id];
|
|
const card = postTemplate.cloneNode(true);
|
|
card.classList.add('notification-work-card');
|
|
card.dataset.post = id;
|
|
card.dataset.href = `post-detail.html?post=${encodeURIComponent(id)}`;
|
|
card.querySelector('.card-title').textContent = post.title;
|
|
const stats = card.querySelectorAll('.card-stat');
|
|
stats[0].lastChild.textContent = ` ${post.likes}`;
|
|
stats[1].lastChild.textContent = ` ${post.comments.length}`;
|
|
card.querySelectorAll('.post-img-cell').forEach((cell) => cell.style.removeProperty('background'));
|
|
grid.prepend(card);
|
|
});
|
|
}
|
|
document.querySelector('.btn-edit')?.addEventListener('click', () => { location.href = 'me-edit.html'; });
|
|
document.querySelector('[data-profile-action="cover"]')?.addEventListener('click', () => { location.href = 'me-edit.html'; });
|
|
const share = document.querySelector('.btn-share');
|
|
share?.addEventListener('click', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(location.href);
|
|
showWebToast('主页链接已复制');
|
|
} catch (error) {
|
|
showWebToast('可从浏览器地址栏复制主页链接');
|
|
}
|
|
});
|
|
const moreButton = document.querySelector('[data-profile-action="more"]');
|
|
const moreMenu = document.querySelector('.profile-more-menu');
|
|
moreButton?.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
const expanded = moreButton.getAttribute('aria-expanded') !== 'true';
|
|
moreButton.setAttribute('aria-expanded', String(expanded));
|
|
if (moreMenu) moreMenu.hidden = !expanded;
|
|
});
|
|
document.addEventListener('click', () => {
|
|
moreButton?.setAttribute('aria-expanded', 'false');
|
|
if (moreMenu) moreMenu.hidden = true;
|
|
});
|
|
moreMenu?.addEventListener('click', (event) => event.stopPropagation());
|
|
moreMenu?.querySelector('[data-profile-menu="copy"]')?.addEventListener('click', () => share?.click());
|
|
moreMenu?.querySelector('[data-profile-menu="settings"]')?.addEventListener('click', () => { location.href = 'settings.html'; });
|
|
|
|
const tabs = Array.from(document.querySelectorAll('.profile-tab'));
|
|
const destinations = ['me.html', 'favorites.html', 'favorites.html?tab=喜欢', 'favorites.html?tab=浏览历史'];
|
|
tabs.forEach((tab, index) => {
|
|
makeKeyboardClickable(tab, () => { location.href = destinations[index]; });
|
|
tab.addEventListener('click', () => { location.href = destinations[index]; });
|
|
});
|
|
const statTargets = { 关注: 'follow.html?tab=关注', 粉丝: 'follow.html?tab=粉丝', 积分: 'wallet.html' };
|
|
document.querySelectorAll('.stat-item').forEach((item) => {
|
|
const label = item.querySelector('.stat-label')?.textContent.trim();
|
|
if (statTargets[label]) makeLinkedCard(item, statTargets[label]);
|
|
else makeKeyboardClickable(item, () => showWebToast(`${label}数据会随内容互动实时更新`));
|
|
});
|
|
document.querySelectorAll('.content-card').forEach((card) => {
|
|
const isVideo = card.querySelector('.card-badge')?.textContent.includes('视频');
|
|
makeLinkedCard(card, card.dataset.href || (isVideo ? 'video-detail.html' : 'post-detail.html'));
|
|
});
|
|
document.querySelector('.level-display')?.addEventListener('click', () => { location.href = 'wallet.html'; });
|
|
const checkin = document.querySelector('.checkin-btn');
|
|
if (checkin) {
|
|
const checked = localStorage.getItem('youthome:web:checkedIn') === 'true';
|
|
if (checked) { checkin.disabled = true; checkin.textContent = '今日已签到'; }
|
|
checkin.addEventListener('click', () => {
|
|
localStorage.setItem('youthome:web:checkedIn', 'true');
|
|
checkin.disabled = true;
|
|
checkin.textContent = '今日已签到';
|
|
showWebToast('签到成功,积分 +5');
|
|
});
|
|
}
|
|
};
|
|
|
|
const initProfileEdit = () => {
|
|
const fields = Array.from(document.querySelectorAll('.edit-form input:not([disabled]), .edit-form select, .edit-form textarea'));
|
|
const nickname = fields.find((field) => field.previousElementSibling?.textContent.includes('昵称'));
|
|
const bio = document.querySelector('.form-textarea');
|
|
const previewName = document.querySelector('.preview-name');
|
|
const save = Array.from(document.querySelectorAll('.form-footer .btn')).find((button) => button.textContent.includes('保存'));
|
|
const updatePreview = () => {
|
|
if (previewName && nickname) previewName.textContent = nickname.value.trim() || '未设置昵称';
|
|
const hint = bio?.nextElementSibling;
|
|
if (hint && bio) hint.textContent = `${bio.value.length}/200`;
|
|
};
|
|
const account = window.YouthomeApi?.currentUser ? window.YouthomeApi.currentUser() : window.YouthomeLocal?.currentUser?.();
|
|
if (account) {
|
|
if (nickname && account.nickname) nickname.value = account.nickname;
|
|
if (bio && account.bio) bio.value = account.bio;
|
|
}
|
|
fields.forEach((field) => field.addEventListener('input', updatePreview));
|
|
save?.addEventListener('click', async () => {
|
|
if (!nickname || nickname.value.trim().length < 2) { showWebToast('昵称至少需要 2 个字符', 'error'); nickname?.focus(); return; }
|
|
const api = window.YouthomeApi;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
const patchData = { nickname: nickname.value.trim(), bio: bio?.value.trim() || '' };
|
|
if (isLocal) {
|
|
if (!window.YouthomeLocal?.token() || !window.YouthomeLocal?.currentUser()) {
|
|
showWebToast('请先登录后再修改资料', 'error');
|
|
window.setTimeout(() => { location.href = 'login.html?next=me-edit.html'; }, 700);
|
|
return;
|
|
}
|
|
try {
|
|
window.YouthomeLocal.updateProfile(patchData);
|
|
showWebToast('资料已保存');
|
|
window.setTimeout(() => { location.href = 'me.html'; }, 500);
|
|
} catch (error) { showWebToast(error.message || '保存失败,请重试', 'error'); }
|
|
return;
|
|
}
|
|
if (!api?.token?.()) {
|
|
showWebToast('请先登录后再修改资料', 'error');
|
|
window.setTimeout(() => { location.href = 'login.html?next=me-edit.html'; }, 700);
|
|
return;
|
|
}
|
|
try {
|
|
const me = api.currentUser() || {};
|
|
await api.request('/profile', { method: 'PUT', body: JSON.stringify({ ...me, ...patchData }) });
|
|
api.updateSessionUser(patchData);
|
|
showWebToast('资料已保存');
|
|
window.setTimeout(() => { location.href = 'me.html'; }, 500);
|
|
} catch (error) { showWebToast(error.message || '保存失败,请重试', 'error'); }
|
|
});
|
|
// 头像上传:本地压缩后存进账号,不再是一句“待接入”
|
|
const avatarPreview = document.querySelector('.avatar-preview');
|
|
const avatarEditIcon = avatarPreview?.querySelector('.avatar-edit-icon');
|
|
const avatarFile = document.createElement('input');
|
|
avatarFile.type = 'file';
|
|
avatarFile.accept = 'image/*';
|
|
avatarFile.hidden = true;
|
|
document.body.appendChild(avatarFile);
|
|
const setAvatarPreview = (url) => {
|
|
if (!avatarPreview) return;
|
|
avatarPreview.textContent = '';
|
|
if (url) {
|
|
const img = document.createElement('img');
|
|
img.src = url;
|
|
img.alt = '头像预览';
|
|
avatarPreview.appendChild(img);
|
|
}
|
|
if (avatarEditIcon) avatarPreview.appendChild(avatarEditIcon);
|
|
};
|
|
const pickAvatar = () => {
|
|
if (!window.YouthomeLocal?.token() || !window.YouthomeLocal?.currentUser()) {
|
|
showWebToast('请先登录后再上传头像', 'error');
|
|
return;
|
|
}
|
|
avatarFile.click();
|
|
};
|
|
document.querySelector('.avatar-info .btn')?.addEventListener('click', pickAvatar);
|
|
document.querySelector('.avatar-edit-icon')?.addEventListener('click', pickAvatar);
|
|
avatarFile.addEventListener('change', async () => {
|
|
const file = avatarFile.files?.[0];
|
|
avatarFile.value = '';
|
|
if (!file) return;
|
|
if (!file.type.startsWith('image/')) { showWebToast('请选择图片文件', 'error'); return; }
|
|
if (file.size > 5 * 1024 * 1024) { showWebToast('图片不能超过 5MB', 'error'); return; }
|
|
try {
|
|
const api = window.YouthomeApi;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
const form = new FormData();
|
|
form.append('file', file);
|
|
let url = '';
|
|
if (isLocal) {
|
|
const uploaded = await window.YouthomeLocal.request('/file/upload/image', { method: 'POST', body: form });
|
|
window.YouthomeLocal.updateProfile({ avatar: uploaded.url });
|
|
url = uploaded.url;
|
|
} else {
|
|
const result = await api.request('/profile/avatar', { method: 'POST', body: form });
|
|
url = result?.url || result?.avatar || result?.avatarUrl || '';
|
|
if (url) api.updateSessionUser({ avatar: url });
|
|
}
|
|
if (url) setAvatarPreview(url);
|
|
window.webApplyAccountIdentity?.();
|
|
showWebToast('头像已更新');
|
|
} catch (error) {
|
|
showWebToast(error.message || '头像上传失败,请重试', 'error');
|
|
}
|
|
});
|
|
const currentAvatar = window.YouthomeApi?.currentUser ? window.YouthomeApi.currentUser()?.avatar : window.YouthomeLocal?.currentUser?.()?.avatar;
|
|
if (window.YouthomeApi?.avatarUrl) {
|
|
const url = window.YouthomeApi.avatarUrl(currentAvatar);
|
|
if (url) setAvatarPreview(url);
|
|
} else if (currentAvatar) {
|
|
setAvatarPreview(currentAvatar);
|
|
}
|
|
updatePreview();
|
|
};
|
|
|
|
const initApiChat = async (api) => {
|
|
const convLists = [...document.querySelectorAll('.conv-items')];
|
|
const convList = convLists.at(-1);
|
|
const messages = document.querySelector('.chat-messages');
|
|
const textarea = document.querySelector('.chat-textarea');
|
|
const send = document.querySelector('.send-btn');
|
|
if (!convList || !messages) return;
|
|
const template = convList.querySelector('.conv-item')?.cloneNode(true);
|
|
if (!template) return;
|
|
convLists.slice(0, -1).forEach((node) => node.remove());
|
|
document.querySelectorAll('.conv-section').forEach((node) => node.remove());
|
|
document.querySelector('.info-panel')?.remove();
|
|
document.querySelector('.chat-toolbar')?.remove();
|
|
document.querySelector('.chat-header-right')?.replaceChildren();
|
|
document.querySelector('.chat-header-info .status')?.remove();
|
|
document.querySelector('.chat-header-avatar')?.replaceChildren();
|
|
document.querySelector('.chat-header-info .name').textContent = '选择会话';
|
|
convList.replaceChildren();
|
|
addEmptyState(convList, '正在加载会话…', '');
|
|
messages.replaceChildren();
|
|
if (textarea) textarea.disabled = true;
|
|
if (send) send.disabled = true;
|
|
const me = api.currentUser() || {};
|
|
const requestedPeer = new URLSearchParams(location.search).get('peer');
|
|
const profiles = {};
|
|
let conversations = [];
|
|
let active = null;
|
|
let historyRequestId = 0;
|
|
|
|
const nameOf = async (conv) => {
|
|
if (conv.groupName || conv.peerName) return conv.groupName || conv.peerName;
|
|
// 群会话的 peerId 不是用户 ID,不能拿去查用户资料(后端会 500)
|
|
if (String(conv.conversationType || '').toUpperCase() === 'GROUP') return '群聊';
|
|
if (!conv.peerId) return '会话';
|
|
if (profiles[conv.peerId]) return profiles[conv.peerId].nickname || '用户';
|
|
try {
|
|
const profile = await api.get('/users/profile?userId=' + encodeURIComponent(conv.peerId));
|
|
profiles[conv.peerId] = profile;
|
|
return profile?.nickname || '用户';
|
|
} catch (error) { return '用户'; }
|
|
};
|
|
|
|
const renderConversations = async () => {
|
|
const nodes = [];
|
|
for (const conv of conversations) {
|
|
const node = template.cloneNode(true);
|
|
const name = await nameOf(conv);
|
|
const nameNode = node.querySelector('.conv-name');
|
|
if (nameNode) nameNode.textContent = name;
|
|
const avatar = node.querySelector('.conv-avatar');
|
|
if (avatar) {
|
|
avatar.replaceChildren();
|
|
const url = api.avatarUrl(conv.peerAvatar);
|
|
if (url) {
|
|
const image = document.createElement('img');
|
|
image.src = url;
|
|
image.alt = '';
|
|
avatar.appendChild(image);
|
|
} else avatar.textContent = name.slice(0, 1);
|
|
}
|
|
node.querySelector('.conv-online-dot')?.remove();
|
|
const preview = node.querySelector('.conv-preview');
|
|
if (preview) preview.textContent = conv.lastMessage || '';
|
|
const time = node.querySelector('.conv-time');
|
|
if (time) time.textContent = String(conv.lastMessageTime || '').slice(5, 16);
|
|
const unread = node.querySelector('.conv-unread');
|
|
if (unread) {
|
|
unread.textContent = String(conv.unreadCount || 0);
|
|
unread.hidden = !conv.unreadCount;
|
|
}
|
|
node.classList.toggle('active', Boolean(active && active.id === conv.id));
|
|
node.addEventListener('click', async () => {
|
|
const selectedId = conv.id;
|
|
const requestId = ++historyRequestId;
|
|
active = conv;
|
|
await renderConversations();
|
|
await syncHeader(selectedId, requestId);
|
|
await loadHistory(conv, requestId);
|
|
});
|
|
nodes.push(node);
|
|
}
|
|
convList.replaceChildren(...nodes);
|
|
if (!nodes.length) addEmptyState(convList, '还没有会话', '和好友聊聊或加入群组后,会话会出现在这里。');
|
|
const search = document.querySelector('.conv-search .search-input');
|
|
if (search?.value.trim()) {
|
|
const keyword = search.value.trim().toLowerCase();
|
|
nodes.forEach((node) => { node.hidden = !node.textContent.toLowerCase().includes(keyword); });
|
|
}
|
|
};
|
|
|
|
const refreshConversations = async () => {
|
|
try {
|
|
const list = (await api.get('/campus/chat/conversations')) || [];
|
|
const previous = active;
|
|
const activeId = previous?.id;
|
|
conversations = list;
|
|
if (String(activeId || '').startsWith('new-private-') &&
|
|
!conversations.some((item) => item.peerId === previous.peerId && String(item.conversationType).toUpperCase() === 'PRIVATE')) {
|
|
conversations.unshift(previous);
|
|
}
|
|
if (activeId) active = conversations.find((item) => item.id === activeId) ||
|
|
conversations.find((item) => item.peerId === previous.peerId && item.conversationType === previous.conversationType) || null;
|
|
if (!active && conversations.length) active = conversations[0];
|
|
await renderConversations();
|
|
await syncHeader();
|
|
if (!active) messages.replaceChildren();
|
|
} catch (error) { /* 轮询失败不打扰用户 */ }
|
|
};
|
|
|
|
const loadHistory = async (conversation = active, requestId = ++historyRequestId) => {
|
|
if (!conversation) return;
|
|
const isGroup = String(conversation.conversationType || '').toUpperCase() === 'GROUP';
|
|
const path = isGroup
|
|
? `/campus/chat/history/group/${encodeURIComponent(conversation.peerId)}?page=1&size=50`
|
|
: `/campus/chat/history/private/${encodeURIComponent(conversation.peerId)}?page=1&size=50`;
|
|
let items = [];
|
|
try { const data = await api.get(path); items = data?.items || []; }
|
|
catch (error) { showWebToast(error.message || '聊天记录加载失败', 'error'); return; }
|
|
if (requestId !== historyRequestId || active?.id !== conversation.id) return;
|
|
messages.replaceChildren(...items.map((item) => {
|
|
const self = item.senderId === me.id;
|
|
const row = document.createElement('div');
|
|
row.className = 'msg-row' + (self ? ' self' : '');
|
|
const who = self ? (me.nickname || '我') : (item.senderGroupNickname || item.senderName || '对方');
|
|
row.innerHTML = '<div class="msg-avatar"><span></span></div><div class="msg-body"><div class="msg-meta"><span></span></div><div class="msg-bubble"></div></div>';
|
|
row.querySelector('.msg-avatar span').textContent = who.slice(0, 1);
|
|
row.querySelector('.msg-meta span').textContent = item.createdAt || '';
|
|
row.querySelector('.msg-bubble').textContent = item.content || '';
|
|
return row;
|
|
}));
|
|
messages.scrollTop = messages.scrollHeight;
|
|
};
|
|
|
|
const sendMessage = async () => {
|
|
const content = textarea?.value.trim();
|
|
if (!content || !active) return;
|
|
const isGroup = String(active.conversationType || '').toUpperCase() === 'GROUP';
|
|
try {
|
|
if (isGroup) await api.post('/campus/chat/messages/group/' + encodeURIComponent(active.peerId), { content });
|
|
else await api.post('/campus/chat/messages/private', { peerId: active.peerId, content, messageType: 'TEXT' });
|
|
if (textarea) textarea.value = '';
|
|
await loadHistory();
|
|
showWebToast('消息已发送');
|
|
} catch (error) { showWebToast(error.message || '发送失败', 'error'); }
|
|
};
|
|
|
|
send?.addEventListener('click', sendMessage);
|
|
textarea?.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }
|
|
});
|
|
|
|
// 环信 v4 实时消息:收到即刷新;连不上时保持 REST 轮询
|
|
window.__youthomeIm = { connected: false };
|
|
const ensureImSdk = () => new Promise((resolve) => {
|
|
if (window.WebIM) { resolve(true); return; }
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdn.jsdelivr.net/npm/easemob-websdk@4.24.3/Easemob-chat.js';
|
|
script.onload = () => resolve(Boolean(window.WebIM));
|
|
script.onerror = () => resolve(false);
|
|
document.head.appendChild(script);
|
|
});
|
|
const connectRealtime = async () => {
|
|
try {
|
|
if (!(await ensureImSdk())) return;
|
|
const config = await api.get('/campus/chat/config');
|
|
if (!config?.appKey || !config?.username || !config?.password) return;
|
|
const conn = new window.WebIM.connection({ appKey: config.appKey });
|
|
window.__youthomeIm.conn = conn;
|
|
conn.listen({
|
|
onOpened: () => { window.__youthomeIm.connected = true; },
|
|
onClosed: () => { window.__youthomeIm.connected = false; },
|
|
onError: () => { window.__youthomeIm.connected = false; },
|
|
onTextMessage: (message) => {
|
|
const from = String(message.from || '').replace(/^u/, '').replace(/-/g, '');
|
|
const activePeer = String(active?.peerId || '').replace(/-/g, '');
|
|
if (active && (!activePeer || from === activePeer)) loadHistory();
|
|
refreshConversations();
|
|
}
|
|
});
|
|
await conn.open({ user: config.username, pwd: config.password });
|
|
window.__youthomeIm.connected = true;
|
|
} catch (error) {
|
|
window.__youthomeIm.connected = false;
|
|
console.warn('环信实时连接失败,继续使用轮询:', error?.message || error);
|
|
}
|
|
};
|
|
|
|
try { conversations = (await api.get('/campus/chat/conversations')) || []; }
|
|
catch (error) {
|
|
addEmptyState(convList, '会话加载失败', error.message || '请稍后刷新重试');
|
|
showWebToast(error.message || '会话加载失败', 'error');
|
|
return;
|
|
}
|
|
if (requestedPeer && /^[0-9a-f-]{36}$/i.test(requestedPeer) && requestedPeer !== me.id) {
|
|
active = conversations.find((item) => item.peerId === requestedPeer && String(item.conversationType).toUpperCase() === 'PRIVATE');
|
|
if (!active) {
|
|
active = { id: `new-private-${requestedPeer}`, peerId: requestedPeer, conversationType: 'PRIVATE' };
|
|
conversations.unshift(active);
|
|
}
|
|
} else active = conversations[0] || null;
|
|
let inviteBtn = null;
|
|
const syncHeader = async (selectedId = active?.id, requestId = historyRequestId) => {
|
|
const nameNode = document.querySelector('.chat-header-info .name');
|
|
const avatarNode = document.querySelector('.chat-header-avatar img');
|
|
if (!active) return;
|
|
const conversation = active;
|
|
const name = await nameOf(conversation);
|
|
if (requestId !== historyRequestId || active?.id !== selectedId || active?.id !== conversation.id) return;
|
|
if (nameNode) nameNode.textContent = name;
|
|
if (avatarNode) { avatarNode.removeAttribute('src'); avatarNode.alt = name; }
|
|
const avatarBox = document.querySelector('.chat-header-avatar');
|
|
if (avatarBox) {
|
|
avatarBox.replaceChildren();
|
|
const url = api.avatarUrl(active.peerAvatar);
|
|
if (url) {
|
|
const image = document.createElement('img');
|
|
image.src = url;
|
|
image.alt = '';
|
|
avatarBox.appendChild(image);
|
|
} else avatarBox.textContent = name.slice(0, 1);
|
|
}
|
|
if (textarea) textarea.disabled = false;
|
|
if (send) send.disabled = false;
|
|
if (inviteBtn) inviteBtn.hidden = String(active.conversationType || '').toUpperCase() !== 'GROUP';
|
|
};
|
|
const openGroupInvite = () => {
|
|
if (!active) return;
|
|
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 message = document.createElement('textarea');
|
|
message.className = 'web-sheet-textarea';
|
|
message.rows = 2;
|
|
message.placeholder = '给好友留句话(可选,200 字以内)';
|
|
body.appendChild(message);
|
|
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 submit = document.createElement('button');
|
|
submit.className = 'web-sheet-btn primary';
|
|
submit.type = 'button';
|
|
submit.textContent = '发送邀请';
|
|
submit.disabled = true;
|
|
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);
|
|
const selected = new Set();
|
|
(async () => {
|
|
try {
|
|
const data = await api.get('/friendships/my');
|
|
const items = Array.isArray(data) ? data : (data?.items || []);
|
|
if (!items.length) { hint.textContent = '你还没有好友,先添加好友再来邀请'; return; }
|
|
body.replaceChild(document.createElement('div'), hint);
|
|
const listBox = body.querySelector('div');
|
|
listBox.className = 'web-sheet-list';
|
|
body.insertBefore(listBox, body.querySelector('.web-sheet-foot'));
|
|
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 || '友').slice(0, 1);
|
|
const info = document.createElement('span');
|
|
info.className = 'web-sheet-row-info';
|
|
const name = document.createElement('b');
|
|
name.textContent = item.nickname || '好友';
|
|
const desc = document.createElement('span');
|
|
desc.textContent = item.universityName || '';
|
|
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); submit.disabled = selected.size === 0; });
|
|
row.append(avatar, info, check);
|
|
listBox.appendChild(row);
|
|
});
|
|
} catch (error) { hint.textContent = error.message || '好友加载失败'; }
|
|
})();
|
|
submit.addEventListener('click', async () => {
|
|
submit.disabled = true;
|
|
submit.textContent = '发送中…';
|
|
try {
|
|
await api.post('/groups/' + encodeURIComponent(active.peerId) + '/invitations', { userIds: [...selected], message: message.value.trim() });
|
|
close();
|
|
showWebToast('邀请已发送');
|
|
} catch (error) {
|
|
submit.disabled = false;
|
|
submit.textContent = '发送邀请';
|
|
showWebToast(error.message || '邀请失败', 'error');
|
|
}
|
|
});
|
|
};
|
|
const addHeaderActions = () => {
|
|
const right = document.querySelector('.chat-header-right');
|
|
if (!right || right.querySelector('[data-chat-readall]')) return;
|
|
const readAll = document.createElement('button');
|
|
readAll.className = 'chat-header-btn';
|
|
readAll.dataset.chatReadall = 'true';
|
|
readAll.title = '全部已读';
|
|
readAll.textContent = '已读';
|
|
readAll.addEventListener('click', async () => {
|
|
try { await api.request('/campus/chat/read/all', { method: 'PUT' }); await refreshConversations(); showWebToast('已全部标为已读'); }
|
|
catch (error) { showWebToast(error.message || '操作失败', 'error'); }
|
|
});
|
|
inviteBtn = document.createElement('button');
|
|
inviteBtn.className = 'chat-header-btn';
|
|
inviteBtn.title = '邀请成员';
|
|
inviteBtn.textContent = '邀请';
|
|
inviteBtn.hidden = true;
|
|
inviteBtn.addEventListener('click', openGroupInvite);
|
|
right.insertBefore(readAll, right.firstElementChild);
|
|
right.appendChild(inviteBtn);
|
|
};
|
|
addHeaderActions();
|
|
await renderConversations();
|
|
await syncHeader();
|
|
if (active) await loadHistory();
|
|
document.querySelector('.conv-search .search-input')?.addEventListener('input', () => renderConversations());
|
|
// 轮询拉新(非实时推送):3 秒刷新当前会话,10 秒刷新会话列表
|
|
window.setInterval(() => { if (active && document.visibilityState === 'visible') loadHistory(); }, 3000);
|
|
connectRealtime();
|
|
window.setInterval(() => { if (document.visibilityState === 'visible') refreshConversations(); }, 10000);
|
|
};
|
|
|
|
const initChat = () => {
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiChat(api);
|
|
const textarea = document.querySelector('.chat-textarea');
|
|
const send = document.querySelector('.send-btn');
|
|
const messages = document.querySelector('.chat-messages');
|
|
if (!textarea || !send || !messages) return;
|
|
const sendMessage = () => {
|
|
const content = textarea.value.trim();
|
|
if (!content) return;
|
|
const row = document.createElement('div');
|
|
row.className = 'msg-row self';
|
|
row.innerHTML = `<div class="msg-avatar"><span>我</span></div><div class="msg-body"><div class="msg-meta"><span>刚刚</span></div><div class="msg-bubble"></div></div>`;
|
|
row.querySelector('.msg-bubble').textContent = content;
|
|
messages.appendChild(row);
|
|
textarea.value = '';
|
|
messages.scrollTop = messages.scrollHeight;
|
|
showWebToast('消息已发送');
|
|
};
|
|
send.addEventListener('click', sendMessage);
|
|
textarea.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }
|
|
});
|
|
document.querySelectorAll('.chat-header-btn, .chat-toolbar-btn, .info-action-btn').forEach((button) => {
|
|
button.addEventListener('click', () => showWebToast(`${button.title || button.textContent.trim()}功能将在实时通信接入后开放`));
|
|
});
|
|
};
|
|
|
|
const initHelp = () => {
|
|
const search = document.querySelector('.help-hero input, .help-search input, input[placeholder="搜索常见问题"]');
|
|
const faqs = Array.from(document.querySelectorAll('.faq-item'));
|
|
search?.addEventListener('input', () => {
|
|
const query = search.value.trim().toLowerCase();
|
|
faqs.forEach((item) => { item.hidden = !!query && !item.textContent.toLowerCase().includes(query); });
|
|
});
|
|
document.querySelectorAll('.category-card').forEach((card) => {
|
|
makeKeyboardClickable(card, () => card.click());
|
|
card.addEventListener('click', () => {
|
|
const keyword = card.querySelector('.category-title')?.textContent.trim() || '';
|
|
if (search) { search.value = keyword; search.dispatchEvent(new Event('input')); }
|
|
document.querySelector('.faq-list')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
});
|
|
document.querySelector('.submit-btn')?.addEventListener('click', () => {
|
|
const subject = document.querySelector('.feedback-form input');
|
|
const detail = document.querySelector('.feedback-form textarea');
|
|
if (!subject?.value.trim() || !detail?.value.trim()) { showWebToast('请填写问题标题和详细描述', 'error'); return; }
|
|
showWebToast('反馈已提交,我们会尽快处理');
|
|
subject.value = '';
|
|
detail.value = '';
|
|
});
|
|
document.querySelector('.contact-btn')?.addEventListener('click', () => { location.href = 'chat.html'; });
|
|
};
|
|
|
|
const initActivityDetail = () => {
|
|
const join = document.querySelector('.join-btn:not(.secondary)');
|
|
const store = window.YouthomeLocal;
|
|
const api = window.YouthomeApi;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
const title = (document.querySelector('.cover-title')?.textContent || document.title).trim();
|
|
const infoValue = (label) => {
|
|
const item = Array.from(document.querySelectorAll('.side-info-item'))
|
|
.find((node) => node.querySelector('.label')?.textContent.trim() === label);
|
|
return (item?.querySelector('.value')?.innerText || '').replace(/\s+/g, ' ').trim();
|
|
};
|
|
const hashOf = (text) => {
|
|
let value = 0;
|
|
for (let index = 0; index < text.length; index += 1) value = (value * 31 + text.charCodeAt(index)) % 1000000;
|
|
return value;
|
|
};
|
|
const requestedId = new URLSearchParams(location.search).get('id');
|
|
const activityId = requestedId || `local-${hashOf(title)}`;
|
|
|
|
const setSideValue = (label, value) => {
|
|
const item = Array.from(document.querySelectorAll('.side-info-item'))
|
|
.find((node) => node.querySelector('.label')?.textContent.trim() === label);
|
|
const target = item?.querySelector('.value');
|
|
if (target && value) target.textContent = value;
|
|
};
|
|
const applyActivity = (item) => {
|
|
if (!item) return;
|
|
const titleNode = document.querySelector('.cover-title');
|
|
if (titleNode) titleNode.textContent = item.title || '未命名活动';
|
|
const timeText = [item.startTime, item.endTime].filter(Boolean).join(' - ');
|
|
setSideValue('活动时间', timeText);
|
|
setSideValue('活动地点', item.location);
|
|
setSideValue('报名截止', item.registrationDeadline);
|
|
setSideValue('活动人数', (item.currentCount ?? 0) + '/' + (item.maxCount ?? '—') + ' 人');
|
|
const hostName = document.querySelector('.host-name');
|
|
if (hostName) hostName.textContent = item.creatorName || '校园组织';
|
|
const hostDesc = document.querySelector('.host-desc');
|
|
if (hostDesc) hostDesc.textContent = '活动发起人';
|
|
const hostAvatar = document.querySelector('.host-avatar');
|
|
if (hostAvatar) hostAvatar.textContent = (item.creatorName || '青').slice(0, 1);
|
|
const content = document.querySelector('.activity-content');
|
|
if (content && item.description) {
|
|
const paragraph = document.createElement('p');
|
|
paragraph.textContent = item.description;
|
|
content.replaceChildren(paragraph);
|
|
}
|
|
const meta = document.querySelectorAll('.activity-detail-meta .meta-tag');
|
|
if (meta[0]) meta[0].lastChild.textContent = item.startTime || '';
|
|
if (meta[1]) meta[1].lastChild.textContent = item.location || '';
|
|
if (meta[2]) meta[2].lastChild.textContent = (item.currentCount || 0) + '人已报名';
|
|
if (item.local) {
|
|
const tags = document.querySelectorAll('.cover-tags .cover-tag');
|
|
const typeLabels = { offline: '线下聚会', sports: '运动健身', study: '学习交流', club: '兴趣社团', competition: '比赛竞赛', volunteer: '公益志愿' };
|
|
if (tags[1]) tags[1].textContent = typeLabels[item.type] || '活动';
|
|
tags[2]?.remove();
|
|
const participants = document.querySelector('.participants-section');
|
|
if (participants) {
|
|
participants.querySelector('.participants-title').textContent = '已报名同学 (0)';
|
|
participants.querySelector('.participants-avatars')?.replaceChildren();
|
|
participants.querySelector('.participants-list')?.replaceChildren();
|
|
participants.querySelector('.participants-more')?.remove();
|
|
}
|
|
const detailTabs = document.querySelectorAll('.detail-tab');
|
|
if (detailTabs[1]) detailTabs[1].textContent = '参与者 (0)';
|
|
if (detailTabs[2]) detailTabs[2].textContent = '讨论 (0)';
|
|
}
|
|
const cover = document.querySelector('.activity-detail-cover');
|
|
if (cover) {
|
|
cover.dataset.activityId = item.id;
|
|
const coverImage = item.coverImage || item.coverUrl;
|
|
if (coverImage) cover.style.background = `url("${coverImage}") center / cover`;
|
|
}
|
|
document.title = (item.title || '活动详情') + ' - 青核 Youthome';
|
|
};
|
|
const showNotFound = () => {
|
|
const titleNode = document.querySelector('.cover-title');
|
|
if (titleNode) titleNode.textContent = '活动不存在或已下架';
|
|
document.querySelector('.join-btn:not(.secondary)')?.setAttribute('disabled', '');
|
|
document.querySelectorAll('.side-info-item .value').forEach((node) => { node.textContent = '—'; });
|
|
showWebToast('活动不存在或已下架', 'error');
|
|
};
|
|
if (isLocal && requestedId && store?.readDb().activities.some((item) => item.id === requestedId)) {
|
|
applyActivity(store.readDb().activities.find((item) => item.id === requestedId));
|
|
} else if (!isLocal && requestedId) {
|
|
// /activities/view 只记录浏览;详情走 /activities/detail?id=,失败时回退列表匹配兼容旧后端
|
|
api.post('/campus/activities/view?id=' + encodeURIComponent(requestedId)).catch(() => {});
|
|
api.get('/campus/activities/detail?id=' + encodeURIComponent(requestedId))
|
|
.then((item) => {
|
|
if (item) applyActivity(item);
|
|
else showNotFound();
|
|
})
|
|
.catch(async () => {
|
|
try {
|
|
const created = await api.get('/campus/activities/created?page=1&size=100');
|
|
const own = (created?.items || []).find((row) => row.id === requestedId);
|
|
if (own) { applyActivity(own); return; }
|
|
} catch (error) { /* 继续查询公开活动列表 */ }
|
|
try {
|
|
const data = await api.get('/campus/activities?page=1&size=100');
|
|
const item = (data?.items || []).find((row) => row.id === requestedId);
|
|
if (item) applyActivity(item);
|
|
else showNotFound();
|
|
} catch (error) { showWebToast(error.message || '活动详情加载失败', 'error'); }
|
|
});
|
|
}
|
|
const snapshot = {
|
|
id: activityId,
|
|
title: document.querySelector('.cover-title')?.textContent.trim() || title,
|
|
time: infoValue('活动时间'),
|
|
place: infoValue('活动地点').replace(/([^)]*)/g, '')
|
|
};
|
|
localStorage.removeItem('youthome:web:activityJoined');
|
|
const loggedIn = () => Boolean(store?.token?.() && store?.currentUser?.());
|
|
const syncJoinButton = async () => {
|
|
if (!join) return;
|
|
if (!loggedIn()) return;
|
|
if (isLocal) {
|
|
const joined = Boolean(store?.isJoined?.(activityId));
|
|
join.textContent = joined ? '已报名' : '立即报名';
|
|
join.disabled = joined;
|
|
return;
|
|
}
|
|
if (!requestedId) return;
|
|
try {
|
|
const [joined, registrations] = await Promise.all([api.joinedActivities(), api.registrations()]);
|
|
const registration = registrations.find((item) => item.activityId === requestedId && !['CANCELLED', 'REJECTED', 'REMOVED'].includes(String(item.status || '').toUpperCase()));
|
|
const confirmed = joined.some((item) => item.id === requestedId);
|
|
if (confirmed) api.removeApplication(requestedId);
|
|
const pending = api.applicationRecords().some((item) => item.activityId === requestedId);
|
|
join.textContent = registration
|
|
? (String(registration.paymentStatus || registration.status).toUpperCase().includes('PAY') ? '待支付' : '已报名')
|
|
: confirmed ? '已报名' : pending ? '待审核' : '立即报名';
|
|
join.disabled = Boolean(registration || confirmed || pending);
|
|
} catch (error) { showWebToast(error.message || '报名状态加载失败', 'error'); }
|
|
};
|
|
const closeSignupModal = () => {
|
|
document.querySelector('.web-signup-overlay')?.remove();
|
|
};
|
|
const submitSignup = async (overlay, submitBtn, errorBox) => {
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = '提交中…';
|
|
errorBox.style.display = 'none';
|
|
try {
|
|
if (isLocal) store.joinActivity(snapshot);
|
|
else {
|
|
const application = await api.joinActivity(requestedId, overlay.querySelector('.web-signup-remark')?.value.trim() || '');
|
|
api.saveApplication({ activityId: requestedId, participantId: application?.id, title: document.querySelector('.cover-title')?.textContent.trim() || title, status: application?.status || 'PENDING', createdAt: new Date().toISOString(), startTime: infoValue('活动时间'), location: infoValue('活动地点') });
|
|
join.textContent = String(application?.status || '').toUpperCase() === 'CONFIRMED' ? '已报名' : '待审核';
|
|
}
|
|
if (isLocal) join.textContent = '已报名';
|
|
join.disabled = true;
|
|
closeSignupModal();
|
|
showWebToast('报名申请已提交,可在“我的报名”中查看');
|
|
} catch (error) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '提交申请';
|
|
errorBox.textContent = error.message || '报名失败,请重试';
|
|
errorBox.style.display = 'block';
|
|
}
|
|
};
|
|
const buildActivityBox = (liveTitle) => {
|
|
const activityBox = document.createElement('div');
|
|
activityBox.className = 'web-signup-activity';
|
|
const rows = [
|
|
['web-signup-activity-name', liveTitle || '未命名活动'],
|
|
['web-signup-meta', infoValue('活动时间') || '时间待定'],
|
|
['web-signup-meta', infoValue('活动地点') || '地点待定'],
|
|
['web-signup-meta', '报名截止:' + (infoValue('报名截止') || '以主办方通知为准')]
|
|
];
|
|
rows.forEach(([className, text]) => {
|
|
const node = document.createElement('div');
|
|
node.className = className;
|
|
node.textContent = text;
|
|
activityBox.appendChild(node);
|
|
});
|
|
return activityBox;
|
|
};
|
|
const openApplicationModal = (user, liveTitle) => {
|
|
const displayName = user.nickname || user.name || user.phone || '我';
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'web-signup-overlay';
|
|
const modal = document.createElement('div');
|
|
modal.className = 'web-signup-modal';
|
|
modal.setAttribute('role', 'dialog');
|
|
modal.setAttribute('aria-modal', 'true');
|
|
modal.setAttribute('aria-label', '提交报名申请');
|
|
const head = document.createElement('div');
|
|
head.className = 'web-signup-head';
|
|
const headTitle = document.createElement('div');
|
|
headTitle.className = 'web-signup-title';
|
|
headTitle.textContent = '提交报名申请';
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.className = 'web-signup-close';
|
|
closeBtn.type = 'button';
|
|
closeBtn.setAttribute('aria-label', '关闭');
|
|
closeBtn.textContent = '×';
|
|
head.append(headTitle, closeBtn);
|
|
const body = document.createElement('div');
|
|
body.className = 'web-signup-body';
|
|
const userRow = document.createElement('div');
|
|
userRow.className = 'web-signup-user';
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'web-signup-avatar';
|
|
avatar.textContent = String(displayName).slice(0, 1) || '我';
|
|
const userMeta = document.createElement('div');
|
|
userMeta.className = 'web-signup-meta';
|
|
userMeta.textContent = '以「' + String(displayName) + '」的身份提交申请';
|
|
userRow.append(avatar, userMeta);
|
|
const remark = document.createElement('textarea');
|
|
remark.className = 'web-signup-remark';
|
|
remark.rows = 2;
|
|
remark.placeholder = '报名留言(可选)';
|
|
const errorBox = document.createElement('div');
|
|
errorBox.className = 'web-signup-error';
|
|
body.append(buildActivityBox(liveTitle), userRow, remark, errorBox);
|
|
const foot = document.createElement('div');
|
|
foot.className = 'web-signup-foot';
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.className = 'web-signup-btn ghost';
|
|
cancelBtn.type = 'button';
|
|
cancelBtn.textContent = '取消';
|
|
const submitBtn = document.createElement('button');
|
|
submitBtn.className = 'web-signup-btn primary web-signup-submit';
|
|
submitBtn.type = 'button';
|
|
submitBtn.textContent = '提交申请';
|
|
foot.append(cancelBtn, submitBtn);
|
|
modal.append(head, body, foot);
|
|
overlay.appendChild(modal);
|
|
document.body.appendChild(overlay);
|
|
overlay.addEventListener('click', (event) => { if (event.target === overlay) closeSignupModal(); });
|
|
closeBtn.addEventListener('click', closeSignupModal);
|
|
cancelBtn.addEventListener('click', closeSignupModal);
|
|
submitBtn.addEventListener('click', () => submitSignup(overlay, submitBtn, errorBox));
|
|
};
|
|
const openTicketModal = (user, liveTitle, tickets) => {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'web-signup-overlay';
|
|
const modal = document.createElement('div');
|
|
modal.className = 'web-signup-modal';
|
|
modal.setAttribute('role', 'dialog');
|
|
modal.setAttribute('aria-modal', 'true');
|
|
modal.setAttribute('aria-label', '报名信息');
|
|
const head = document.createElement('div');
|
|
head.className = 'web-signup-head';
|
|
const headTitle = document.createElement('div');
|
|
headTitle.className = 'web-signup-title';
|
|
headTitle.textContent = '填写报名信息';
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.className = 'web-signup-close';
|
|
closeBtn.type = 'button';
|
|
closeBtn.textContent = '×';
|
|
head.append(headTitle, closeBtn);
|
|
const body = document.createElement('div');
|
|
body.className = 'web-signup-body';
|
|
let ticketId = tickets[0]?.id || '';
|
|
const field = (label, input) => {
|
|
const wrap = document.createElement('label');
|
|
wrap.className = 'web-signup-field';
|
|
const text = document.createElement('span');
|
|
text.textContent = label;
|
|
wrap.append(text, input);
|
|
return wrap;
|
|
};
|
|
const nameInput = document.createElement('input');
|
|
nameInput.className = 'web-signup-input';
|
|
nameInput.placeholder = '请填写姓名';
|
|
nameInput.value = user.nickname || '';
|
|
const phoneInput = document.createElement('input');
|
|
phoneInput.className = 'web-signup-input';
|
|
phoneInput.placeholder = '请填写手机号';
|
|
phoneInput.value = user.phone || '';
|
|
const qtyInput = document.createElement('input');
|
|
qtyInput.className = 'web-signup-input';
|
|
qtyInput.type = 'number';
|
|
qtyInput.min = '1';
|
|
qtyInput.value = '1';
|
|
const emergencyName = document.createElement('input');
|
|
emergencyName.className = 'web-signup-input';
|
|
emergencyName.placeholder = '紧急联系人姓名';
|
|
const emergencyPhone = document.createElement('input');
|
|
emergencyPhone.className = 'web-signup-input';
|
|
emergencyPhone.type = 'tel';
|
|
emergencyPhone.placeholder = '紧急联系人手机号';
|
|
const ticketLabel = document.createElement('div');
|
|
ticketLabel.className = 'web-signup-field-label';
|
|
ticketLabel.textContent = '选择票档';
|
|
body.append(buildActivityBox(liveTitle), ticketLabel);
|
|
tickets.forEach((ticket, index) => {
|
|
const label = document.createElement('label');
|
|
label.className = 'web-signup-ticket' + (index === 0 ? ' active' : '');
|
|
const radio = document.createElement('input');
|
|
radio.type = 'radio';
|
|
radio.name = 'signup-ticket';
|
|
radio.checked = index === 0;
|
|
radio.addEventListener('change', () => { ticketId = ticket.id; });
|
|
const info = document.createElement('span');
|
|
info.className = 'web-signup-ticket-info';
|
|
const name = document.createElement('b');
|
|
name.textContent = ticket.name || '票档';
|
|
const meta = document.createElement('span');
|
|
meta.textContent = (Number(ticket.price) > 0 ? '¥' + Number(ticket.price) : '免费') + ' · 剩余 ' + (ticket.remainingCount ?? ticket.capacity ?? '—');
|
|
info.append(name, meta);
|
|
label.append(radio, info);
|
|
body.appendChild(label);
|
|
});
|
|
body.append(field('姓名', nameInput), field('手机号', phoneInput), field('数量', qtyInput), field('紧急联系人', emergencyName), field('紧急联系电话', emergencyPhone));
|
|
const errorBox = document.createElement('div');
|
|
errorBox.className = 'web-signup-error';
|
|
body.appendChild(errorBox);
|
|
const foot = document.createElement('div');
|
|
foot.className = 'web-signup-foot';
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.className = 'web-signup-btn ghost';
|
|
cancelBtn.type = 'button';
|
|
cancelBtn.textContent = '取消';
|
|
const submitBtn = document.createElement('button');
|
|
submitBtn.className = 'web-signup-btn primary';
|
|
submitBtn.type = 'button';
|
|
submitBtn.textContent = '提交报名';
|
|
foot.append(cancelBtn, submitBtn);
|
|
modal.append(head, body, foot);
|
|
overlay.appendChild(modal);
|
|
document.body.appendChild(overlay);
|
|
overlay.addEventListener('click', (event) => { if (event.target === overlay) closeSignupModal(); });
|
|
closeBtn.addEventListener('click', closeSignupModal);
|
|
cancelBtn.addEventListener('click', closeSignupModal);
|
|
submitBtn.addEventListener('click', async () => {
|
|
if (!ticketId) { errorBox.textContent = '请选择票档'; errorBox.style.display = 'block'; return; }
|
|
if (!nameInput.value.trim() || !/^1\d{10}$/.test(phoneInput.value.trim()) || !emergencyName.value.trim() || !/^1\d{10}$/.test(emergencyPhone.value.trim())) {
|
|
errorBox.textContent = '请填写姓名、手机号和紧急联系人信息'; errorBox.style.display = 'block'; return;
|
|
}
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = '提交中…';
|
|
errorBox.style.display = 'none';
|
|
try {
|
|
const registration = await api.request('/campus/activities/' + encodeURIComponent(requestedId) + '/registrations', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ticketId, quantity: Math.max(1, Number(qtyInput.value) || 1), attendeeName: nameInput.value.trim(), attendeePhone: phoneInput.value.trim(), universityName: user.universityName || '', emergencyContact: emergencyName.value.trim(), emergencyPhone: emergencyPhone.value.trim() })
|
|
});
|
|
const paymentPending = String(registration?.status || '').toUpperCase() === 'PENDING_PAYMENT';
|
|
join.textContent = paymentPending ? '待支付' : '已报名';
|
|
join.disabled = true;
|
|
closeSignupModal();
|
|
showWebToast(paymentPending ? '报名单已创建,请在“我的报名”中查看支付状态' : '报名成功,可在“我的报名”中查看');
|
|
} catch (error) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '提交报名';
|
|
errorBox.textContent = error.message || '报名失败,请重试';
|
|
errorBox.style.display = 'block';
|
|
}
|
|
});
|
|
};
|
|
window.openSignupModal = async () => {
|
|
if (!join || join.disabled) return;
|
|
if (!loggedIn()) {
|
|
showWebToast('请先登录后再报名', 'error');
|
|
window.setTimeout(() => {
|
|
location.href = `login.html?next=${encodeURIComponent(`activity-detail.html?id=${activityId}`)}`;
|
|
}, 700);
|
|
return;
|
|
}
|
|
if (!isLocal && !requestedId) {
|
|
showWebToast('这条活动来自页面示例,暂不支持报名', 'error');
|
|
return;
|
|
}
|
|
const liveTitle = (document.querySelector('.cover-title')?.textContent || title).trim();
|
|
const user = (api?.currentUser?.() || store?.currentUser?.()) || {};
|
|
if (!isLocal && requestedId) {
|
|
try {
|
|
const tickets = await api.get('/campus/activities/' + encodeURIComponent(requestedId) + '/tickets');
|
|
const list = Array.isArray(tickets) ? tickets : (tickets?.items || []);
|
|
const enabled = list.filter((ticket) => ticket.enabled !== false);
|
|
if (enabled.length) { openTicketModal(user, liveTitle, enabled); return; }
|
|
} catch (error) { /* 无票档时回退为报名申请 */ }
|
|
}
|
|
openApplicationModal(user, liveTitle);
|
|
};
|
|
syncJoinButton();
|
|
const favorite = document.querySelector('.join-btn.secondary');
|
|
favorite?.addEventListener('click', () => {
|
|
const active = favorite.classList.toggle('is-active');
|
|
favorite.textContent = active ? '已收藏' : '收藏活动';
|
|
showWebToast(active ? '活动已收藏' : '已取消收藏');
|
|
});
|
|
const follow = document.querySelector('.host-follow-btn');
|
|
follow?.addEventListener('click', () => {
|
|
const active = follow.classList.toggle('is-followed');
|
|
follow.textContent = active ? '已关注' : '关注';
|
|
const host = document.querySelector('.host-name')?.textContent.trim();
|
|
if (host && store?.setFollowing) store.setFollowing(host, active);
|
|
});
|
|
};
|
|
|
|
const initNotificationPostDetail = () => {
|
|
if (page !== 'post-detail.html') return null;
|
|
const params = new URLSearchParams(location.search);
|
|
const postId = params.get('post');
|
|
const post = notificationPosts[postId];
|
|
if (!post) return null;
|
|
|
|
document.body.classList.add('notification-post-detail');
|
|
document.title = `${post.title} - 青核 Youthome`;
|
|
const noticeCategory = {
|
|
'like-canteen': '点赞',
|
|
'comment-canteen': '评论回复',
|
|
'mention-milk-tea': '@我的'
|
|
}[params.get('notice')];
|
|
const breadcrumbs = document.querySelectorAll('.breadcrumb a');
|
|
if (breadcrumbs[0]) {
|
|
breadcrumbs[0].textContent = noticeCategory ? '消息通知' : '动态广场';
|
|
breadcrumbs[0].onclick = () => { location.href = noticeCategory ? `${notificationHref}&category=${encodeURIComponent(noticeCategory)}` : 'feed.html'; };
|
|
}
|
|
if (breadcrumbs[1]) {
|
|
breadcrumbs[1].textContent = '我的动态';
|
|
breadcrumbs[1].onclick = () => { location.href = 'me.html'; };
|
|
}
|
|
|
|
const meta = document.querySelectorAll('.post-detail-meta > span:not(.dot)');
|
|
if (meta[0]) meta[0].textContent = post.time;
|
|
if (meta[1]) meta[1].textContent = '复旦大学';
|
|
if (meta[2]) meta[2].textContent = `浏览 ${post.views}`;
|
|
const content = document.querySelector('.post-detail-content');
|
|
if (content) {
|
|
content.replaceChildren();
|
|
const heading = document.createElement('h1');
|
|
heading.className = 'notification-post-title';
|
|
heading.textContent = post.title;
|
|
content.appendChild(heading);
|
|
post.body.forEach((text) => {
|
|
const paragraph = document.createElement('p');
|
|
paragraph.textContent = text;
|
|
content.appendChild(paragraph);
|
|
});
|
|
}
|
|
const images = document.querySelector('.post-detail-images');
|
|
if (images) images.hidden = true;
|
|
const tags = document.querySelector('.post-detail-tags');
|
|
if (tags) {
|
|
tags.replaceChildren();
|
|
post.tags.forEach((text) => {
|
|
const tag = document.createElement('span');
|
|
tag.className = 'tag tag-primary';
|
|
tag.textContent = text;
|
|
tags.appendChild(tag);
|
|
});
|
|
}
|
|
const related = document.querySelector('.related-section');
|
|
if (related) related.hidden = true;
|
|
document.querySelectorAll('.post-detail-avatar, .post-detail-name, .author-avatar, .author-name').forEach((author) => {
|
|
author.onclick = () => { location.href = 'me.html'; };
|
|
});
|
|
const authorDescription = document.querySelector('.author-desc');
|
|
if (authorDescription) authorDescription.textContent = '复旦大学 · 校园创作者';
|
|
const authorStats = document.querySelectorAll('.author-stat-num');
|
|
['42', '1,256', '289'].forEach((value, index) => { if (authorStats[index]) authorStats[index].textContent = value; });
|
|
|
|
const actionCounts = document.querySelectorAll('.post-detail-actions .count');
|
|
if (actionCounts[0]) actionCounts[0].textContent = String(post.likes);
|
|
if (actionCounts[2]) actionCounts[2].textContent = String(post.shares);
|
|
const commentList = document.querySelector('.comment-list');
|
|
const commentSection = document.querySelector('.comment-section');
|
|
if (commentSection) commentSection.id = 'comments';
|
|
const commentInput = document.querySelector('.comment-input');
|
|
const storageKey = `youthome:web:post-comments:${postId}`;
|
|
let savedComments = [];
|
|
try {
|
|
const stored = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
|
if (Array.isArray(stored)) savedComments = stored.filter((entry) => typeof entry.text === 'string');
|
|
} catch (error) {
|
|
savedComments = [];
|
|
}
|
|
const allComments = [...post.comments, ...savedComments];
|
|
const makeComment = (comment) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'comment-item';
|
|
row.id = `comment-${comment.id}`;
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'comment-avatar';
|
|
avatar.textContent = comment.name.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.name;
|
|
nameRow.appendChild(name);
|
|
if (comment.name === '林小夏') {
|
|
const authorTag = document.createElement('span');
|
|
authorTag.className = 'comment-author-tag';
|
|
authorTag.textContent = '作者';
|
|
nameRow.appendChild(authorTag);
|
|
}
|
|
const content = document.createElement('div');
|
|
content.className = 'comment-text';
|
|
content.textContent = comment.text;
|
|
const meta = document.createElement('div');
|
|
meta.className = 'comment-meta';
|
|
const time = document.createElement('span');
|
|
time.textContent = comment.time;
|
|
const reply = document.createElement('button');
|
|
reply.type = 'button';
|
|
reply.className = 'notification-comment-reply';
|
|
reply.textContent = '回复';
|
|
reply.addEventListener('click', () => {
|
|
if (!commentInput) return;
|
|
commentInput.value = `@${comment.name} `;
|
|
commentInput.focus();
|
|
});
|
|
meta.append(time, reply);
|
|
body.append(nameRow, content, meta);
|
|
row.append(avatar, body);
|
|
return row;
|
|
};
|
|
const updateCommentCount = () => {
|
|
const countValue = (post.commentCount || post.comments.length) + savedComments.length;
|
|
if (actionCounts[1]) actionCounts[1].textContent = String(countValue);
|
|
const count = document.querySelector('.comment-count');
|
|
if (count) count.textContent = `共 ${countValue} 条`;
|
|
};
|
|
commentList?.replaceChildren(...allComments.map(makeComment));
|
|
if (!allComments.length && post.commentCount) {
|
|
const unavailable = document.createElement('p');
|
|
unavailable.className = 'profile-comments-unavailable';
|
|
unavailable.textContent = '原有评论内容尚未接入,暂时无法查看。';
|
|
commentList?.appendChild(unavailable);
|
|
}
|
|
updateCommentCount();
|
|
document.documentElement.classList.remove('notification-route-pending', 'notification-route-failed');
|
|
const target = params.get('comment');
|
|
if (target) {
|
|
const comment = document.getElementById(`comment-${target}`);
|
|
if (comment) {
|
|
comment.classList.add('is-targeted');
|
|
requestAnimationFrame(() => comment.scrollIntoView({ block: 'center' }));
|
|
}
|
|
}
|
|
document.querySelector('.post-detail-actions .action-btn-big.more')?.setAttribute('hidden', '');
|
|
const share = document.querySelectorAll('.post-detail-actions .action-btn-big')[2];
|
|
share?.addEventListener('click', async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(`${location.origin}${location.pathname}?post=${encodeURIComponent(postId)}`);
|
|
showWebToast('动态链接已复制');
|
|
} catch (error) {
|
|
showWebToast('可从浏览器地址栏复制动态链接');
|
|
}
|
|
});
|
|
return {
|
|
addComment(text) {
|
|
const comment = { id: `local-${Date.now()}`, name: '林小夏', text, time: '刚刚' };
|
|
savedComments.push(comment);
|
|
localStorage.setItem(storageKey, JSON.stringify(savedComments));
|
|
allComments.push(comment);
|
|
commentList?.querySelector('.profile-comments-unavailable')?.remove();
|
|
commentList?.appendChild(makeComment(comment));
|
|
updateCommentCount();
|
|
}
|
|
};
|
|
};
|
|
|
|
const initProfileVideoDetail = () => {
|
|
if (page !== 'video-detail.html') return;
|
|
const video = profileVideos[new URLSearchParams(location.search).get('video')];
|
|
if (!video) return;
|
|
document.title = `${video.title} - 青核 Youthome`;
|
|
document.body.classList.add('profile-video-detail');
|
|
const title = document.querySelector('.video-title');
|
|
if (title) title.textContent = video.title;
|
|
const meta = document.querySelectorAll('.video-meta-item');
|
|
if (meta[0]) meta[0].lastChild.textContent = ` ${video.views}次播放`;
|
|
if (meta[1]) meta[1].lastChild.textContent = ' 个人中心作品';
|
|
const tags = document.querySelector('.video-topics');
|
|
if (tags) {
|
|
tags.replaceChildren(...video.tags.map((label) => {
|
|
const tag = document.createElement('span');
|
|
tag.className = 'video-topic-tag';
|
|
tag.textContent = label;
|
|
return tag;
|
|
}));
|
|
}
|
|
const author = document.querySelector('.author-name');
|
|
if (author) author.textContent = '林小夏';
|
|
const school = document.querySelector('.author-school');
|
|
if (school) school.textContent = '复旦大学';
|
|
const avatar = document.querySelector('.author-avatar');
|
|
if (avatar) avatar.textContent = '夏';
|
|
const description = document.querySelector('.video-desc');
|
|
if (description) {
|
|
description.replaceChildren();
|
|
const heading = document.createElement('div');
|
|
heading.className = 'video-desc-title';
|
|
heading.textContent = '视频简介';
|
|
const paragraph = document.createElement('p');
|
|
paragraph.textContent = `${video.title}。时长 ${video.duration}。`;
|
|
description.append(heading, paragraph);
|
|
}
|
|
const player = document.querySelector('.video-player-wrap');
|
|
if (player) {
|
|
const notice = document.createElement('p');
|
|
notice.className = 'profile-video-unavailable';
|
|
notice.textContent = '视频文件尚未接入,暂时无法播放';
|
|
player.appendChild(notice);
|
|
}
|
|
document.documentElement.classList.remove('profile-video-route-pending');
|
|
};
|
|
|
|
const initDetailActions = () => {
|
|
if (page === 'post-detail.html' && new URLSearchParams(location.search).has('moment')) return;
|
|
const videoId = new URLSearchParams(location.search).get('id');
|
|
const localVideo = window.YouthomeApi?.isLocal() && window.YouthomeLocal?.readDb().moments.some((item) => item.id === videoId && item.isYouthVideo);
|
|
if (page === 'video-detail.html' && videoId && window.YouthomeApi && (!window.YouthomeApi.isLocal() || localVideo)) {
|
|
window.YouthomeApi.get('/campus/moments/detail?id=' + encodeURIComponent(videoId))
|
|
.then(async (item) => {
|
|
const player = document.querySelector('.video-player-wrap');
|
|
if (player) {
|
|
player.replaceChildren();
|
|
const showNotice = (text) => {
|
|
player.replaceChildren();
|
|
const notice = document.createElement('p');
|
|
notice.className = 'web-live-empty';
|
|
notice.textContent = text;
|
|
player.appendChild(notice);
|
|
};
|
|
const raw = item.playUrl || item.videoOriginalUrl || '';
|
|
const resolved = localVideo ? await window.YouthomeLocal.videoUrl(raw) : raw;
|
|
const url = resolved.replace(/^https?:\/\/video\.yoyoeco\.com\//i, '/media/');
|
|
if (!url) {
|
|
showNotice('视频仍在转码,暂不可播放,请稍后再试');
|
|
} else {
|
|
const video = document.createElement('video');
|
|
video.controls = true;
|
|
video.playsInline = true;
|
|
video.autoplay = true;
|
|
video.muted = true;
|
|
video.preload = 'auto';
|
|
if (item.videoCoverUrl) video.poster = item.videoCoverUrl;
|
|
video.style.cssText = 'width:100%;height:100%;object-fit:contain;background:#000;display:block;';
|
|
video.addEventListener('error', () => showNotice('视频加载失败,请稍后重试'));
|
|
const box = document.createElement('div');
|
|
box.className = 'web-player-box';
|
|
box.appendChild(video);
|
|
player.appendChild(box);
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'web-player-overlay';
|
|
overlay.innerHTML = '<button class="web-player-big-play" type="button" aria-label="播放"><svg viewBox="0 0 24 24" fill="currentColor"><polygon points="6 3 20 12 6 21 6 3"/></svg></button><div class="web-player-spinner" hidden></div>';
|
|
box.appendChild(overlay);
|
|
const bigPlay = overlay.querySelector('.web-player-big-play');
|
|
const spinner = overlay.querySelector('.web-player-spinner');
|
|
const syncPlayerUi = () => {
|
|
spinner.hidden = !(video.readyState < 3 && !video.paused);
|
|
bigPlay.hidden = !video.paused;
|
|
};
|
|
video.addEventListener('play', syncPlayerUi);
|
|
video.addEventListener('pause', syncPlayerUi);
|
|
video.addEventListener('playing', syncPlayerUi);
|
|
video.addEventListener('waiting', syncPlayerUi);
|
|
video.addEventListener('canplay', syncPlayerUi);
|
|
bigPlay.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
if (video.paused) video.play().catch(() => {}); else video.pause();
|
|
});
|
|
syncPlayerUi();
|
|
if (/\.m3u8(\?|$)/i.test(url)) {
|
|
ensureHls().then((ok) => {
|
|
if (ok && window.Hls && window.Hls.isSupported()) {
|
|
const hls = new window.Hls();
|
|
hls.on(window.Hls.Events.ERROR, (event, data) => { if (data.fatal) showNotice('视频加载失败,请稍后重试'); });
|
|
hls.loadSource(url);
|
|
hls.attachMedia(video);
|
|
hls.on(window.Hls.Events.MANIFEST_PARSED, () => { video.play().catch(() => {}); });
|
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
video.src = url;
|
|
} else {
|
|
showNotice('当前浏览器不支持该视频格式');
|
|
}
|
|
});
|
|
} else {
|
|
video.src = url;
|
|
video.play().catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
const [videoTitle, ...descriptionLines] = String(item.content || '').split(/\r?\n/);
|
|
const heading = videoTitle.trim() || '校园视频';
|
|
const description = descriptionLines.join('\n').trim();
|
|
const title = document.querySelector('.video-title');
|
|
if (title) title.textContent = heading;
|
|
document.title = heading + ' - 青核 Youthome';
|
|
const videoMeta = document.querySelectorAll('.video-meta-item');
|
|
if (videoMeta[0]) videoMeta[0].lastChild.textContent = ` ${item.viewCount || 0}次播放`;
|
|
if (videoMeta[1]) videoMeta[1].lastChild.textContent = ` 发布于 ${String(item.createdAt || '').slice(0, 10) || '刚刚'}`;
|
|
const topics = document.querySelector('.video-topics');
|
|
if (topics) topics.replaceChildren(...(item.tags || []).map((tag) => {
|
|
const node = document.createElement('span'); node.className = 'video-topic-tag'; node.textContent = tag.startsWith('#') ? tag : '#' + tag; return node;
|
|
}));
|
|
const actionButtons = document.querySelectorAll('.video-actions .action-btn');
|
|
const actionCounts = document.querySelectorAll('.video-actions .count');
|
|
if (actionCounts[0]) actionCounts[0].textContent = String(item.likeCount || 0);
|
|
if (actionCounts[1]) actionCounts[1].textContent = String(item.commentCount || 0);
|
|
if (actionCounts[2]) actionCounts[2].textContent = String(item.favoriteCount || 0);
|
|
actionButtons[0]?.classList.toggle('active', Boolean(item.isLiked));
|
|
actionButtons[2]?.classList.toggle('active', Boolean(item.isFavorited));
|
|
const bindVideoAction = (button, label, handler) => {
|
|
if (!button) return;
|
|
button.removeAttribute('onclick');
|
|
button.setAttribute('role', 'button'); button.tabIndex = 0; button.setAttribute('aria-label', label);
|
|
button.addEventListener('click', handler);
|
|
button.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); button.click(); } });
|
|
};
|
|
bindVideoAction(actionButtons[0], '点赞视频', async () => {
|
|
if (actionButtons[0].dataset.busy) return;
|
|
actionButtons[0].dataset.busy = 'true';
|
|
try {
|
|
const result = await window.YouthomeApi.request('/campus/moments/like?id=' + encodeURIComponent(videoId), { method: 'POST' });
|
|
actionButtons[0].classList.toggle('active', Boolean(result.liked));
|
|
actionCounts[0].textContent = String(result.likeCount);
|
|
} catch (error) { showWebToast(error.message || '点赞失败', 'error'); }
|
|
finally { delete actionButtons[0].dataset.busy; }
|
|
});
|
|
bindVideoAction(actionButtons[1], '查看评论', () => document.querySelector('.comments-section')?.scrollIntoView({ behavior: 'smooth' }));
|
|
bindVideoAction(actionButtons[2], '收藏视频', async () => {
|
|
if (actionButtons[2].dataset.busy) return;
|
|
actionButtons[2].dataset.busy = 'true';
|
|
try {
|
|
const result = await window.YouthomeApi.request('/campus/moments/favorite?id=' + encodeURIComponent(videoId), { method: 'POST' });
|
|
actionButtons[2].classList.toggle('active', Boolean(result.favorited));
|
|
actionCounts[2].textContent = String(result.favoriteCount);
|
|
showWebToast(result.favorited ? '已收藏' : '已取消收藏');
|
|
} catch (error) { showWebToast(error.message || '收藏失败', 'error'); }
|
|
finally { delete actionButtons[2].dataset.busy; }
|
|
});
|
|
bindVideoAction(actionButtons[3], '分享视频', async () => {
|
|
try { await navigator.clipboard.writeText(location.href); showWebToast('视频链接已复制'); }
|
|
catch (error) { showWebToast('可从浏览器地址栏复制视频链接'); }
|
|
});
|
|
actionButtons[4]?.remove();
|
|
document.querySelector('.author-stats')?.remove();
|
|
if (item.userId === window.YouthomeApi.currentUser()?.id) document.querySelector('.follow-btn')?.remove();
|
|
const descriptionBox = document.querySelector('.video-desc');
|
|
if (descriptionBox) {
|
|
descriptionBox.replaceChildren();
|
|
const label = document.createElement('div');
|
|
label.className = 'video-desc-title';
|
|
label.textContent = '视频简介';
|
|
const body = document.createElement('p');
|
|
body.textContent = description || '发布者暂未填写简介';
|
|
body.style.whiteSpace = 'pre-wrap';
|
|
descriptionBox.append(label, body);
|
|
}
|
|
const authorHref = item.userId ? `user.html?id=${encodeURIComponent(item.userId)}` : '';
|
|
const school = document.querySelector('.author-school');
|
|
if (school) school.lastChild.textContent = ' ' + (item.universityName || (item.userId === window.YouthomeApi.currentUser()?.id ? window.YouthomeApi.currentUser()?.universityName : '') || '校园创作者');
|
|
document.querySelectorAll('.author-name').forEach((node) => {
|
|
node.textContent = item.userName || '青核用户';
|
|
node.onclick = authorHref ? () => { location.href = authorHref; } : null;
|
|
node.style.cursor = authorHref ? 'pointer' : '';
|
|
if (authorHref) { node.setAttribute('role', 'link'); node.tabIndex = 0; }
|
|
});
|
|
document.querySelectorAll('.author-avatar').forEach((node) => {
|
|
node.textContent = (item.userName || '青').slice(0, 1);
|
|
node.onclick = authorHref ? () => { location.href = authorHref; } : null;
|
|
node.style.cursor = authorHref ? 'pointer' : '';
|
|
if (authorHref) { node.setAttribute('role', 'link'); node.tabIndex = 0; }
|
|
});
|
|
const list = document.querySelector('.comment-list');
|
|
const input = document.querySelector('.comment-textarea');
|
|
const submit = document.querySelector('.comment-submit-btn');
|
|
const count = document.querySelector('.comments-count');
|
|
const tabs = Array.from(document.querySelectorAll('.comment-tab'));
|
|
let comments = [];
|
|
let replyTarget = null;
|
|
let order = 'hot';
|
|
const replyChip = document.createElement('div');
|
|
replyChip.className = 'web-reply-chip';
|
|
replyChip.hidden = true;
|
|
const replyLabel = document.createElement('span');
|
|
const cancelReply = document.createElement('button');
|
|
cancelReply.type = 'button';
|
|
cancelReply.textContent = '取消';
|
|
cancelReply.addEventListener('click', () => { replyTarget = null; replyChip.hidden = true; });
|
|
replyChip.append(replyLabel, cancelReply);
|
|
document.querySelector('.comment-input-wrap')?.prepend(replyChip);
|
|
const renderComment = (comment, reply = false) => {
|
|
const row = document.createElement('div');
|
|
row.className = reply ? 'sub-reply' : 'comment-item';
|
|
row.dataset.commentId = comment.id;
|
|
const avatar = document.createElement('div');
|
|
avatar.className = reply ? 'sub-reply-avatar' : 'comment-avatar';
|
|
avatar.textContent = (comment.userName || '青').slice(0, 1);
|
|
const body = document.createElement('div');
|
|
body.className = reply ? 'sub-reply-body' : 'comment-body';
|
|
const head = document.createElement('div');
|
|
head.className = reply ? 'sub-reply-name' : 'comment-head';
|
|
const name = document.createElement('span');
|
|
name.className = 'comment-name';
|
|
name.textContent = comment.userName || '青核用户';
|
|
head.appendChild(name);
|
|
if (!reply) {
|
|
const time = document.createElement('span');
|
|
time.className = 'comment-time';
|
|
time.textContent = comment.createdAgo || String(comment.createdAt || '').slice(0, 16);
|
|
head.appendChild(time);
|
|
}
|
|
const content = document.createElement('div');
|
|
content.className = reply ? 'sub-reply-text' : 'comment-text';
|
|
content.textContent = comment.content || '';
|
|
const actions = document.createElement('div');
|
|
actions.className = reply ? 'sub-reply-footer' : 'comment-footer';
|
|
if (reply) {
|
|
const time = document.createElement('span');
|
|
time.textContent = comment.createdAgo || String(comment.createdAt || '').slice(0, 16);
|
|
actions.appendChild(time);
|
|
}
|
|
const like = document.createElement('button');
|
|
like.type = 'button';
|
|
like.className = 'comment-action' + (comment.isLiked ? ' liked' : '');
|
|
like.textContent = '赞 ' + (comment.likeCount || 0);
|
|
like.addEventListener('click', async () => {
|
|
if (like.disabled) return;
|
|
like.disabled = true;
|
|
try {
|
|
const result = await window.YouthomeApi.request('/campus/moments/comments/like?commentId=' + encodeURIComponent(comment.id), { method: 'POST' });
|
|
like.classList.toggle('liked', Boolean(result.liked));
|
|
like.textContent = '赞 ' + result.likeCount;
|
|
} catch (error) { showWebToast(error.message || '点赞失败', 'error'); }
|
|
finally { like.disabled = false; }
|
|
});
|
|
const replyButton = document.createElement('button');
|
|
replyButton.type = 'button';
|
|
replyButton.className = 'comment-action';
|
|
replyButton.textContent = '回复';
|
|
replyButton.addEventListener('click', () => {
|
|
replyTarget = { id: comment.parentId || comment.id, name: comment.userName || '青核用户' };
|
|
replyLabel.textContent = '正在回复 ' + replyTarget.name;
|
|
replyChip.hidden = false;
|
|
input?.focus();
|
|
});
|
|
actions.append(like, replyButton);
|
|
body.append(head, content, actions);
|
|
if (!reply && comment.replies?.length) {
|
|
const replies = document.createElement('div');
|
|
replies.className = 'sub-replies';
|
|
comment.replies.forEach((entry) => replies.appendChild(renderComment(entry, true)));
|
|
body.appendChild(replies);
|
|
}
|
|
row.append(avatar, body);
|
|
return row;
|
|
};
|
|
const showComments = () => {
|
|
const sorted = [...comments].sort((a, b) => order === 'latest'
|
|
? String(b.createdAt || '').localeCompare(String(a.createdAt || ''))
|
|
: (Number(b.likeCount || 0) - Number(a.likeCount || 0)) || String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
|
|
list?.replaceChildren(...sorted.map((comment) => renderComment(comment)));
|
|
if (list && !sorted.length) {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = '还没有评论,来说点什么';
|
|
list.appendChild(empty);
|
|
}
|
|
const total = comments.reduce((sum, comment) => sum + 1 + (comment.replies?.length || 0), 0);
|
|
if (count) count.textContent = String(total);
|
|
if (actionCounts[1]) actionCounts[1].textContent = String(total);
|
|
};
|
|
const loadComments = async () => {
|
|
const result = await window.YouthomeApi.get('/campus/moments/comments?momentId=' + encodeURIComponent(videoId) + '&page=1&size=50');
|
|
comments = result?.items || [];
|
|
showComments();
|
|
};
|
|
tabs.forEach((tab, index) => tab.addEventListener('click', () => {
|
|
order = index ? 'latest' : 'hot';
|
|
showComments();
|
|
}));
|
|
if (item.allowComment === false) {
|
|
document.querySelector('.comment-input-area')?.remove();
|
|
} else {
|
|
submit?.addEventListener('click', async () => {
|
|
const text = input?.value.trim();
|
|
if (!text) { showWebToast('请输入评论内容', 'error'); input?.focus(); return; }
|
|
submit.disabled = true;
|
|
try {
|
|
await window.YouthomeApi.post('/campus/moments/comments', { momentId: videoId, content: text, parentId: replyTarget?.id || null });
|
|
input.value = '';
|
|
replyTarget = null;
|
|
replyChip.hidden = true;
|
|
await loadComments();
|
|
showWebToast('评论已发布');
|
|
} catch (error) { showWebToast(error.message || '评论发布失败', 'error'); }
|
|
finally { submit.disabled = false; }
|
|
});
|
|
}
|
|
loadComments().catch((error) => {
|
|
if (list) list.textContent = error.message || '评论加载失败';
|
|
});
|
|
})
|
|
.catch((error) => showWebToast(error.message || '视频加载失败', 'error'));
|
|
return;
|
|
}
|
|
initProfileVideoDetail();
|
|
const notificationPost = initNotificationPostDetail();
|
|
const commentInput = document.querySelector('.comment-input, .comment-textarea, textarea[placeholder*="评论"]');
|
|
const send = document.querySelector('.btn-send, .comment-submit-btn');
|
|
|
|
// 回复提示条(静态评论区和动态评论共用同一套样式)
|
|
let replyTargetName = 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.textContent = '取消';
|
|
replyChip.append(replyChipText, replyChipClose);
|
|
const chipHost = document.querySelector('.comment-input-wrap') || commentInput?.parentElement;
|
|
chipHost?.prepend(replyChip);
|
|
const renderChip = () => {
|
|
if (!replyTargetName) { replyChip.hidden = true; return; }
|
|
replyChip.hidden = false;
|
|
replyChipText.textContent = '正在回复 ' + replyTargetName;
|
|
};
|
|
replyChipClose.addEventListener('click', () => { replyTargetName = null; renderChip(); });
|
|
|
|
const commentList = document.querySelector('.comment-list');
|
|
const bindCommentControls = (scope) => {
|
|
scope.querySelectorAll('.comment-meta, .reply-meta').forEach((meta) => {
|
|
const like = meta.querySelector('.like-btn');
|
|
if (like && like.dataset.bound !== 'true') {
|
|
like.dataset.bound = 'true';
|
|
like.setAttribute('role', 'button');
|
|
like.tabIndex = 0;
|
|
like.setAttribute('aria-label', '点赞这条评论');
|
|
like.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
const active = like.classList.toggle('liked');
|
|
const heart = like.querySelector('path');
|
|
if (heart) {
|
|
heart.setAttribute('fill', active ? 'currentColor' : 'none');
|
|
heart.setAttribute('stroke', active ? 'none' : 'currentColor');
|
|
}
|
|
const current = Number((like.textContent || '').replace(/[^0-9]/g, '')) || 0;
|
|
const next = Math.max(0, current + (active ? 1 : -1));
|
|
Array.from(like.childNodes).forEach((node) => {
|
|
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) node.textContent = ' ' + next;
|
|
});
|
|
showWebToast(active ? '已点赞这条评论' : '已取消点赞');
|
|
});
|
|
}
|
|
Array.from(meta.children).forEach((node) => {
|
|
if (node.textContent.trim() !== '回复' || node.dataset.bound === 'true') return;
|
|
node.dataset.bound = 'true';
|
|
node.setAttribute('role', 'button');
|
|
node.tabIndex = 0;
|
|
node.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
const owner = node.closest('.reply-item') || node.closest('.comment-item');
|
|
const who = owner?.querySelector('.reply-name, .comment-name')?.textContent.trim();
|
|
replyTargetName = who || null;
|
|
renderChip();
|
|
commentInput?.focus();
|
|
});
|
|
});
|
|
});
|
|
};
|
|
bindCommentControls(document.querySelector('.comment-list') || document);
|
|
|
|
const appendSampleComment = (text, targetName) => {
|
|
if (!commentList) return;
|
|
const row = document.createElement('div');
|
|
row.className = 'comment-item';
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'comment-avatar';
|
|
avatar.setAttribute('style', 'background: linear-gradient(135deg, #73D3E6, #17B0CF)');
|
|
avatar.textContent = '我';
|
|
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 = '我';
|
|
nameRow.appendChild(name);
|
|
const textNode = document.createElement('div');
|
|
textNode.className = 'comment-text';
|
|
textNode.textContent = targetName ? '@' + targetName + ' ' + text : text;
|
|
const meta = document.createElement('div');
|
|
meta.className = 'comment-meta';
|
|
['刚刚', '点赞 0', '回复'].forEach((label) => {
|
|
const span = document.createElement('span');
|
|
span.textContent = label;
|
|
meta.appendChild(span);
|
|
});
|
|
body.append(nameRow, textNode, meta);
|
|
row.append(avatar, body);
|
|
commentList.querySelector('.web-live-empty')?.remove();
|
|
commentList.appendChild(row);
|
|
bindCommentControls(row);
|
|
const countNode = document.querySelector('.comment-count');
|
|
const total = commentList.querySelectorAll('.comment-item').length;
|
|
if (countNode) countNode.textContent = '共 ' + total + ' 条';
|
|
};
|
|
|
|
send?.addEventListener('click', () => {
|
|
const text = commentInput?.value.trim();
|
|
if (!text) { showWebToast('请输入评论内容', 'error'); commentInput?.focus(); return; }
|
|
const target = replyTargetName;
|
|
if (notificationPost) notificationPost.addComment(text);
|
|
else appendSampleComment(text, target);
|
|
showWebToast(target ? '回复已发布' : '评论已发布');
|
|
commentInput.value = '';
|
|
replyTargetName = null;
|
|
renderChip();
|
|
});
|
|
document.querySelectorAll('.related-card').forEach((card) => makeLinkedCard(card, 'post-detail.html'));
|
|
|
|
// 示例详情页里“评论数 / 分享 / 更多”三个按钮此前点了没反应
|
|
const sampleActions = Array.from(document.querySelectorAll('.post-detail-actions .action-btn-big'));
|
|
const bindAction = (node, label, handler) => {
|
|
if (!node || node.dataset.bound === 'true') return;
|
|
node.dataset.bound = 'true';
|
|
node.setAttribute('role', 'button');
|
|
node.tabIndex = 0;
|
|
node.setAttribute('aria-label', label);
|
|
node.addEventListener('click', handler);
|
|
node.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handler(event); }
|
|
});
|
|
};
|
|
bindAction(sampleActions[1], '查看评论', () => {
|
|
document.querySelector('.comment-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
document.querySelector('.comment-input, .comment-textarea')?.focus();
|
|
});
|
|
bindAction(sampleActions[2], '分享', async () => {
|
|
try { await navigator.clipboard.writeText(location.href); showWebToast('链接已复制'); }
|
|
catch (error) { showWebToast('可从浏览器地址栏复制链接'); }
|
|
});
|
|
bindAction(sampleActions.find((node) => node.classList.contains('more')), '更多操作', (event) => {
|
|
event.stopPropagation();
|
|
const opened = document.getElementById('web-detail-more-menu');
|
|
if (opened) { opened.remove(); return; }
|
|
const menu = document.createElement('div');
|
|
menu.id = 'web-detail-more-menu';
|
|
menu.className = 'web-detail-more-menu';
|
|
menu.innerHTML = '<button type="button" data-action="copy">复制链接</button><button type="button" data-action="report">举报内容</button>';
|
|
sampleActions.find((node) => node.classList.contains('more')).appendChild(menu);
|
|
menu.addEventListener('click', async (event) => {
|
|
event.stopPropagation();
|
|
const action = event.target.closest('button')?.dataset.action;
|
|
if (!action) return;
|
|
menu.remove();
|
|
if (action === 'copy') {
|
|
try { await navigator.clipboard.writeText(location.href); showWebToast('链接已复制'); }
|
|
catch (error) { showWebToast('可从浏览器地址栏复制链接'); }
|
|
} else {
|
|
showWebToast('当前是演示环境,举报不会真的提交');
|
|
}
|
|
});
|
|
// 等这次点击的冒泡结束再挂“点外部关闭”,否则菜单会被同一次点击立刻关掉
|
|
window.setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 0);
|
|
});
|
|
};
|
|
|
|
const initApiUser = async (api) => {
|
|
const params = new URLSearchParams(location.search);
|
|
const userId = params.get('id');
|
|
const userName = params.get('name');
|
|
const grid = document.querySelector('.post-list');
|
|
const template = grid?.querySelector('.post-mini-card')?.cloneNode(true);
|
|
const videoGrid = document.querySelector('.video-grid-3');
|
|
const videoTemplate = videoGrid?.querySelector('.video-mini-card')?.cloneNode(true);
|
|
grid?.replaceChildren();
|
|
videoGrid?.replaceChildren();
|
|
document.querySelectorAll('.tab-state-label, .profile-badges').forEach((node) => node.remove());
|
|
const statNodes = [...document.querySelectorAll('.profile-stats .stat-num')];
|
|
statNodes.forEach((node) => { node.textContent = '—'; });
|
|
const tabs = [...document.querySelectorAll('.profile-tab')];
|
|
tabs.slice(2).forEach((tab) => { tab.hidden = true; });
|
|
const renderTab = (label) => {
|
|
if (grid) grid.hidden = label !== '动态';
|
|
if (videoGrid) videoGrid.hidden = label !== '视频';
|
|
};
|
|
tabs.slice(0, 2).forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => {
|
|
setPressed(tabs, tab);
|
|
renderTab(tab.textContent.trim());
|
|
});
|
|
});
|
|
renderTab('动态');
|
|
let profile = null;
|
|
if (userId) {
|
|
try { profile = await api.get('/users/profile?userId=' + encodeURIComponent(userId)); }
|
|
catch (error) { showWebToast(error.message || '用户资料加载失败', 'error'); }
|
|
} else if (userName) {
|
|
try {
|
|
const result = await api.get('/friendships/search?keyword=' + encodeURIComponent(userName) + '&page=1&size=20');
|
|
const list = result?.items || [];
|
|
const match = list.find((item) => (item.nickname || '') === userName) || list[0];
|
|
if (match?.id) profile = await api.get('/users/profile?userId=' + encodeURIComponent(match.id));
|
|
} catch (error) { /* 搜索失败走空状态 */ }
|
|
}
|
|
const nameNode = document.querySelector('.profile-name');
|
|
const avatarNode = document.querySelector('.profile-avatar');
|
|
const schoolNode = document.querySelector('.profile-school');
|
|
const bioNode = document.querySelector('.profile-bio');
|
|
if (!profile) {
|
|
if (nameNode) nameNode.textContent = userName || '用户主页';
|
|
if (avatarNode) avatarNode.textContent = (userName || '用').slice(0, 1);
|
|
if (bioNode) bioNode.textContent = '没有找到这位用户';
|
|
if (grid) addEmptyState(grid, '没有找到用户', '请返回后重新选择');
|
|
return;
|
|
}
|
|
const nickname = profile.nickname || userName || '校园用户';
|
|
if (nameNode) nameNode.textContent = nickname;
|
|
if (avatarNode) avatarNode.textContent = nickname.slice(0, 1);
|
|
if (schoolNode) {
|
|
const icon = schoolNode.querySelector('svg');
|
|
schoolNode.replaceChildren(...(icon ? [icon] : []), document.createTextNode(' ' + (profile.universityName || '校园用户')));
|
|
}
|
|
if (bioNode) bioNode.textContent = profile.bio || '这个人很神秘,还没有填写简介';
|
|
document.title = nickname + ' 的主页 - 青核 Youthome';
|
|
if (statNodes[1]) statNodes[1].textContent = String(profile.followersCount ?? 0);
|
|
if (statNodes[2]) statNodes[2].textContent = String(profile.followingCount ?? 0);
|
|
const messageButton = document.querySelector('.btn-message');
|
|
if (profile.id === api.currentUser()?.id) messageButton?.remove();
|
|
else messageButton?.addEventListener('click', () => {
|
|
location.href = 'chat.html?peer=' + encodeURIComponent(profile.id);
|
|
});
|
|
|
|
const follow = document.querySelector('.btn-follow-primary');
|
|
if (follow) {
|
|
try {
|
|
const status = await api.get('/users/follow-status?userId=' + encodeURIComponent(profile.id));
|
|
const following = Boolean(status?.isFollowed);
|
|
follow.classList.toggle('followed', following);
|
|
follow.textContent = following ? '已关注' : '+ 关注';
|
|
} catch (error) { /* 忽略 */ }
|
|
follow.addEventListener('click', async () => {
|
|
try {
|
|
const result = await api.request('/users/follow?userId=' + encodeURIComponent(profile.id), { method: 'POST' });
|
|
const following = Boolean(result?.following ?? result?.isFollowing ?? !follow.classList.contains('followed'));
|
|
follow.classList.toggle('followed', following);
|
|
follow.textContent = following ? '已关注' : '+ 关注';
|
|
showWebToast(following ? '已关注' : '已取消关注');
|
|
} catch (error) { showWebToast(error.message || '操作失败', 'error'); }
|
|
});
|
|
}
|
|
|
|
if (grid && template) {
|
|
try {
|
|
const data = await api.get('/campus/moments/user?userId=' + encodeURIComponent(profile.id) + '&page=1&size=20');
|
|
const items = (data?.items || []).filter((moment) => !moment.isYouthVideo);
|
|
if (statNodes[0]) statNodes[0].textContent = String(data?.total ?? items.length);
|
|
grid.replaceChildren(...items.map((moment) => {
|
|
const card = template.cloneNode(true);
|
|
card.removeAttribute('onclick');
|
|
const title = card.querySelector('.post-mini-content');
|
|
if (title) title.textContent = moment.content || '';
|
|
const cover = card.querySelector('.post-cover');
|
|
if (cover) {
|
|
cover.className = 'post-cover';
|
|
cover.replaceChildren();
|
|
cover.style.background = moment.mediaUrls?.[0]
|
|
? `url("${moment.mediaUrls[0]}") center / cover`
|
|
: '#e8edf0';
|
|
}
|
|
const author = card.querySelector('.post-author-name');
|
|
if (author) author.textContent = moment.userName || nickname;
|
|
const avatar = card.querySelector('.post-author-avatar');
|
|
if (avatar) avatar.textContent = (moment.userName || nickname).slice(0, 1);
|
|
const likes = card.querySelector('.post-stats span');
|
|
if (likes) likes.textContent = String(moment.likeCount || 0);
|
|
card.addEventListener('click', () => { location.href = 'post-detail.html?moment=' + encodeURIComponent(moment.id); });
|
|
return card;
|
|
}));
|
|
if (!items.length) addEmptyState(grid, '暂无公开动态', '这位用户的公开内容会显示在这里。');
|
|
} catch (error) {
|
|
addEmptyState(grid, '动态加载失败', error.message || '请稍后刷新重试');
|
|
showWebToast(error.message || '动态加载失败', 'error');
|
|
}
|
|
}
|
|
if (videoGrid && videoTemplate) {
|
|
try {
|
|
const data = await api.get('/campus/moments/youth?userId=' + encodeURIComponent(profile.id) + '&page=1&size=20');
|
|
const items = data?.items || [];
|
|
videoGrid.replaceChildren(...items.map((video) => {
|
|
const card = videoTemplate.cloneNode(true);
|
|
card.removeAttribute('onclick');
|
|
const cover = card.querySelector('.video-mini-cover');
|
|
if (cover) cover.style.background = video.videoCoverUrl
|
|
? `url("${video.videoCoverUrl}") center / cover`
|
|
: '#e8edf0';
|
|
const title = card.querySelector('.video-mini-title');
|
|
if (title) title.textContent = video.content || '未命名视频';
|
|
const duration = card.querySelector('.video-mini-duration');
|
|
if (duration) duration.textContent = video.videoDuration ? `${Math.floor(video.videoDuration / 60)}:${String(video.videoDuration % 60).padStart(2, '0')}` : '';
|
|
const meta = card.querySelectorAll('.video-mini-meta span');
|
|
if (meta[0]) meta[0].textContent = `${video.viewCount || 0} 播放`;
|
|
if (meta[1]) meta[1].textContent = `${video.likeCount || 0} 赞`;
|
|
card.addEventListener('click', () => { location.href = 'video-detail.html?id=' + encodeURIComponent(video.id); });
|
|
return card;
|
|
}));
|
|
if (!items.length) addEmptyState(videoGrid, '暂无公开视频', '这位用户的公开视频会显示在这里。');
|
|
} catch (error) {
|
|
addEmptyState(videoGrid, '视频加载失败', error.message || '请稍后刷新重试');
|
|
}
|
|
}
|
|
};
|
|
|
|
const initUser = () => {
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiUser(api);
|
|
const requestedName = new URLSearchParams(location.search).get('name');
|
|
if (requestedName && requestedName !== '林小夏') {
|
|
document.body.classList.add('external-user-page');
|
|
const knownUsers = {
|
|
'张子轩': '上海交通大学', '李梦琪': '同济大学', '王浩然': '复旦大学',
|
|
'陈诗雨': '华东师范大学', '刘一鸣': '上海大学', '赵小妍': '东华大学'
|
|
};
|
|
const name = requestedName.slice(0, 30);
|
|
document.title = `${name}的主页 - 青核 Youthome`;
|
|
const avatar = document.querySelector('.profile-avatar');
|
|
const nameNode = document.querySelector('.profile-name');
|
|
const school = document.querySelector('.profile-school');
|
|
const bio = document.querySelector('.profile-bio');
|
|
if (avatar) avatar.textContent = name.slice(0, 1);
|
|
if (nameNode) nameNode.textContent = name;
|
|
if (school) {
|
|
school.replaceChildren(school.querySelector('svg'), document.createTextNode(` ${knownUsers[name] || '校园用户'}`));
|
|
}
|
|
if (bio) bio.textContent = '暂未填写个人简介';
|
|
document.querySelector('.profile-badges')?.remove();
|
|
document.querySelectorAll('.tab-state-label').forEach((label) => label.remove());
|
|
document.querySelectorAll('.profile-stats .stat-num').forEach((number) => { number.textContent = '—'; });
|
|
document.querySelectorAll('.post-mini-card, .video-mini-card').forEach((card) => card.remove());
|
|
const postGrid = document.querySelector('.post-list');
|
|
const videoGrid = document.querySelector('.video-grid-3');
|
|
const tabs = Array.from(document.querySelectorAll('.profile-tab'));
|
|
const renderEmpty = (tab) => {
|
|
if (postGrid) postGrid.hidden = tab === '视频';
|
|
if (videoGrid) videoGrid.hidden = tab !== '视频';
|
|
const target = tab === '视频' ? videoGrid : postGrid;
|
|
postGrid?.querySelector('.web-empty-state')?.remove();
|
|
videoGrid?.querySelector('.web-empty-state')?.remove();
|
|
if (target) addEmptyState(target, `暂无公开${tab}`, '这位用户的公开内容会显示在这里。');
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { setPressed(tabs, tab); renderEmpty(tab.textContent.trim()); });
|
|
});
|
|
if (tabs[0]) renderEmpty(tabs[0].textContent.trim());
|
|
const followButton = document.querySelector('.btn-follow-primary');
|
|
if (name === '陈诗雨' && followButton) {
|
|
followButton.classList.add('followed');
|
|
followButton.textContent = '互相关注';
|
|
}
|
|
followButton?.addEventListener('click', () => {
|
|
const active = followButton.classList.toggle('followed');
|
|
followButton.textContent = active ? '已关注' : '+ 关注';
|
|
showWebToast(active ? '关注成功' : '已取消关注');
|
|
});
|
|
// The demo chat has a fixed recipient, so do not open it for another user's profile.
|
|
document.querySelector('.btn-message')?.remove();
|
|
return;
|
|
}
|
|
const follow = document.querySelector('.btn-follow-primary');
|
|
follow?.addEventListener('click', () => {
|
|
const active = follow.classList.toggle('followed');
|
|
follow.textContent = active ? '已关注' : '+ 关注';
|
|
showWebToast(active ? '关注成功' : '已取消关注');
|
|
});
|
|
document.querySelector('.btn-message')?.addEventListener('click', () => { location.href = 'chat.html'; });
|
|
document.querySelectorAll('.post-mini-card').forEach((card) => makeLinkedCard(card, 'post-detail.html'));
|
|
document.querySelectorAll('.video-mini-card').forEach((card) => makeLinkedCard(card, 'video-detail.html'));
|
|
const tabs = Array.from(document.querySelectorAll('.profile-tab'));
|
|
const posts = Array.from(document.querySelectorAll('.post-mini-card'));
|
|
const videos = Array.from(document.querySelectorAll('.video-mini-card'));
|
|
const videoGrid = document.querySelector('.video-grid-3');
|
|
const postGrid = posts[0]?.parentElement;
|
|
const labels = Array.from(document.querySelectorAll('.tab-state-label'));
|
|
const render = (label) => {
|
|
if (postGrid) postGrid.hidden = label !== '动态';
|
|
if (videoGrid) videoGrid.hidden = label !== '视频';
|
|
labels.forEach((item) => { item.hidden = true; });
|
|
if (label === '收藏') {
|
|
if (postGrid) postGrid.hidden = false;
|
|
posts.forEach((card, index) => { card.hidden = index > 3; });
|
|
} else posts.forEach((card) => { card.hidden = false; });
|
|
if (label === '活动') {
|
|
if (postGrid) { postGrid.hidden = false; addEmptyState(postGrid, '暂未公开参加的活动', '用户公开的活动记录会显示在这里。'); }
|
|
} else postGrid?.querySelector('.web-empty-state')?.remove();
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { setPressed(tabs, tab); render(tab.textContent.trim()); });
|
|
});
|
|
if (tabs.length) { setPressed(tabs, tabs[0]); render(tabs[0].textContent.trim()); }
|
|
};
|
|
|
|
const initPublishing = () => {
|
|
if (page === 'publish-post.html' && !document.querySelector('script[src*="web-publishing.js"]')) {
|
|
const content = document.querySelector('.post-textarea');
|
|
window.saveDraft = () => {
|
|
localStorage.setItem('youthome:web:postDraft', content?.value || '');
|
|
showWebToast('已保存到草稿箱');
|
|
};
|
|
window.publishPost = () => {
|
|
if (!content?.value.trim()) { showWebToast('请输入动态内容', 'error'); content?.focus(); return; }
|
|
localStorage.removeItem('youthome:web:postDraft');
|
|
showWebToast('动态发布成功');
|
|
window.setTimeout(() => { location.href = 'feed.html'; }, 600);
|
|
};
|
|
document.querySelector('.add-topic-btn')?.addEventListener('click', () => showWebToast('在正文中输入 #话题名称 即可添加话题'));
|
|
document.querySelectorAll('.tool-btn').forEach((button) => button.addEventListener('click', () => showWebToast(`${button.textContent.trim()}已准备好,可继续编辑`)));
|
|
const draft = localStorage.getItem('youthome:web:postDraft');
|
|
if (draft && content && !content.value.trim()) content.value = draft;
|
|
}
|
|
};
|
|
|
|
const initActivityCreate = () => {
|
|
document.body.classList.add('activity-create-page');
|
|
const titleInput = document.getElementById('activity-title');
|
|
const start = document.getElementById('start-time');
|
|
const end = document.getElementById('end-time');
|
|
const deadline = document.getElementById('registration-deadline');
|
|
const locationName = document.getElementById('location-name');
|
|
const locationAddress = document.getElementById('location-address');
|
|
const save = document.querySelector('.btn-draft');
|
|
const publishButtons = document.querySelectorAll('.btn-publish-main, .btn-next');
|
|
const previewButton = document.querySelector('.btn-preview');
|
|
const coverArea = document.getElementById('cover-upload');
|
|
let selectedCoordinates = null;
|
|
[locationName, locationAddress].forEach((field) => field?.addEventListener('input', () => { selectedCoordinates = null; }));
|
|
const coverInput = document.createElement('input');
|
|
coverInput.type = 'file';
|
|
coverInput.accept = 'image/png,image/jpeg,image/webp';
|
|
coverInput.hidden = true;
|
|
coverInput.className = 'web-activity-cover-input';
|
|
coverArea.after(coverInput);
|
|
let coverFile = null;
|
|
let coverUrl = '';
|
|
let submitting = false;
|
|
const showCover = (url) => {
|
|
coverArea.style.background = `url("${url}") center / cover`;
|
|
coverArea.dataset.previewUrl = url;
|
|
coverArea.classList.add('has-cover');
|
|
};
|
|
const selectCover = (file) => {
|
|
if (!file) return;
|
|
if (!file.type.startsWith('image/')) { showWebToast('请选择图片文件', 'error'); return; }
|
|
if (file.size > 10 * 1024 * 1024) { showWebToast('封面图片不能超过 10MB', 'error'); return; }
|
|
coverFile = file;
|
|
coverUrl = '';
|
|
const reader = new FileReader();
|
|
reader.onload = () => showCover(reader.result);
|
|
reader.readAsDataURL(file);
|
|
};
|
|
coverArea.addEventListener('click', () => coverInput.click());
|
|
coverInput.addEventListener('change', () => { selectCover(coverInput.files?.[0]); coverInput.value = ''; });
|
|
coverArea.addEventListener('dragover', (event) => { event.preventDefault(); coverArea.classList.add('dragover'); });
|
|
coverArea.addEventListener('dragleave', () => coverArea.classList.remove('dragover'));
|
|
coverArea.addEventListener('drop', (event) => { event.preventDefault(); coverArea.classList.remove('dragover'); selectCover(event.dataTransfer.files?.[0]); });
|
|
const padTime = (value) => String(value).padStart(2, '0');
|
|
const openCalendar = (input) => {
|
|
const dialog = document.createElement('dialog');
|
|
dialog.className = 'web-date-dialog';
|
|
const selected = input.value ? new Date(input.value.replace(' ', 'T')) : new Date(Date.now() + 86400000);
|
|
let year = selected.getFullYear();
|
|
let month = selected.getMonth();
|
|
let day = selected.getDate();
|
|
const head = document.createElement('div');
|
|
head.className = 'web-dialog-head';
|
|
const title = document.createElement('span');
|
|
title.textContent = input.getAttribute('aria-label');
|
|
const close = document.createElement('button');
|
|
close.type = 'button'; close.textContent = '×'; close.setAttribute('aria-label', '关闭');
|
|
close.addEventListener('click', () => dialog.close());
|
|
head.append(title, close);
|
|
const nav = document.createElement('div');
|
|
nav.className = 'web-calendar-nav';
|
|
const previous = document.createElement('button'); previous.type = 'button'; previous.textContent = '‹'; previous.setAttribute('aria-label', '上个月');
|
|
const monthLabel = document.createElement('span');
|
|
const next = document.createElement('button'); next.type = 'button'; next.textContent = '›'; next.setAttribute('aria-label', '下个月');
|
|
nav.append(previous, monthLabel, next);
|
|
const grid = document.createElement('div'); grid.className = 'web-calendar-grid';
|
|
const renderDays = () => {
|
|
monthLabel.textContent = `${year} 年 ${month + 1} 月`;
|
|
grid.replaceChildren();
|
|
['一', '二', '三', '四', '五', '六', '日'].forEach((label) => {
|
|
const weekday = document.createElement('span'); weekday.textContent = label; grid.appendChild(weekday);
|
|
});
|
|
const offset = (new Date(year, month, 1).getDay() + 6) % 7;
|
|
for (let index = 0; index < offset; index++) grid.appendChild(document.createElement('span'));
|
|
const last = new Date(year, month + 1, 0).getDate();
|
|
for (let number = 1; number <= last; number++) {
|
|
const button = document.createElement('button'); button.type = 'button'; button.textContent = String(number);
|
|
button.classList.toggle('selected', number === day);
|
|
button.addEventListener('click', () => { day = number; renderDays(); });
|
|
grid.appendChild(button);
|
|
}
|
|
};
|
|
previous.addEventListener('click', () => { const date = new Date(year, month - 1, 1); year = date.getFullYear(); month = date.getMonth(); day = Math.min(day, new Date(year, month + 1, 0).getDate()); renderDays(); });
|
|
next.addEventListener('click', () => { const date = new Date(year, month + 1, 1); year = date.getFullYear(); month = date.getMonth(); day = Math.min(day, new Date(year, month + 1, 0).getDate()); renderDays(); });
|
|
const timeRow = document.createElement('div'); timeRow.className = 'web-time-row';
|
|
const timeLabel = document.createElement('span'); timeLabel.textContent = '具体时间';
|
|
const hour = document.createElement('input'); hour.type = 'number'; hour.min = '0'; hour.max = '23'; hour.setAttribute('aria-label', '小时'); hour.value = padTime(input.value ? selected.getHours() : 18);
|
|
const colon = document.createElement('span'); colon.textContent = ':';
|
|
const minute = document.createElement('input'); minute.type = 'number'; minute.min = '0'; minute.max = '59'; minute.setAttribute('aria-label', '分钟'); minute.value = padTime(input.value ? selected.getMinutes() : 0);
|
|
timeRow.append(timeLabel, hour, colon, minute);
|
|
const foot = document.createElement('div'); foot.className = 'web-dialog-foot';
|
|
const cancel = document.createElement('button'); cancel.type = 'button'; cancel.textContent = '取消'; cancel.addEventListener('click', () => dialog.close());
|
|
const confirm = document.createElement('button'); confirm.type = 'button'; confirm.className = 'primary'; confirm.textContent = '确定';
|
|
confirm.addEventListener('click', () => {
|
|
const hours = Number(hour.value); const minutes = Number(minute.value);
|
|
if (!Number.isInteger(hours) || hours < 0 || hours > 23 || !Number.isInteger(minutes) || minutes < 0 || minutes > 59) {
|
|
showWebToast('请输入正确的时间', 'error'); return;
|
|
}
|
|
input.value = `${year}-${padTime(month + 1)}-${padTime(day)} ${padTime(hours)}:${padTime(minutes)}`;
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
dialog.close();
|
|
});
|
|
foot.append(cancel, confirm);
|
|
dialog.append(head, nav, grid, timeRow, foot);
|
|
document.body.appendChild(dialog);
|
|
dialog.addEventListener('close', () => dialog.remove(), { once: true });
|
|
renderDays(); dialog.showModal();
|
|
};
|
|
[start, end, deadline].forEach((input) => {
|
|
input?.addEventListener('click', () => openCalendar(input));
|
|
input?.parentElement.querySelector('.time-picker-btn')?.addEventListener('click', () => openCalendar(input));
|
|
});
|
|
start?.addEventListener('change', () => { if (end) end.min = start.value; });
|
|
document.getElementById('activity-desc')?.addEventListener('input', (event) => {
|
|
document.getElementById('desc-count').textContent = String(event.target.value.length);
|
|
});
|
|
const uploadCover = async () => {
|
|
if (!coverFile) return coverUrl;
|
|
const form = new FormData();
|
|
form.append('file', coverFile);
|
|
const result = await window.YouthomeApi.request('/file/upload/image', { method: 'POST', body: form });
|
|
if (!result?.url) throw new Error('封面上传失败,请重试');
|
|
coverUrl = result.url;
|
|
coverFile = null;
|
|
return coverUrl;
|
|
};
|
|
// 统一外壳隐藏了页面顶栏,把草稿/发布移到可见区域
|
|
const mainPublish = document.querySelector('.btn-publish-main');
|
|
if (save || mainPublish) {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'web-publish-actions';
|
|
const heading = document.createElement('strong');
|
|
heading.textContent = '发起活动';
|
|
bar.appendChild(heading);
|
|
if (previewButton) bar.appendChild(previewButton);
|
|
if (save) bar.appendChild(save);
|
|
if (mainPublish) bar.appendChild(mainPublish);
|
|
const anchor = document.querySelector('.stepper-wrapper, .edit-form, .content-wrapper, .main-content');
|
|
if (anchor) anchor.parentElement.insertBefore(bar, anchor);
|
|
}
|
|
const validate = () => {
|
|
if (!titleInput?.value.trim()) { showWebToast('请输入活动标题', 'error'); titleInput?.focus(); return false; }
|
|
const typeSelect = document.querySelector('#activity-type');
|
|
if (typeSelect && !typeSelect.value) { showWebToast('请选择活动类型', 'error'); typeSelect.focus(); return false; }
|
|
const descInput = document.querySelector('#activity-desc');
|
|
if (descInput && !descInput.value.trim()) { showWebToast('请填写活动介绍', 'error'); descInput.focus(); return false; }
|
|
if (!coverFile && !coverUrl) { showWebToast('请上传活动封面', 'error'); coverArea.focus(); return false; }
|
|
if (!start?.value || !end?.value || !deadline?.value) { showWebToast('请填写活动时间和报名截止时间', 'error'); start?.focus(); return false; }
|
|
const startAt = new Date(start.value.replace(' ', 'T')).getTime();
|
|
const endAt = new Date(end.value.replace(' ', 'T')).getTime();
|
|
const deadlineAt = new Date(deadline.value.replace(' ', 'T')).getTime();
|
|
if (startAt <= Date.now()) { showWebToast('活动开始时间必须晚于现在', 'error'); start.focus(); return false; }
|
|
if (endAt - startAt < 30 * 60000) { showWebToast('活动时长至少 30 分钟', 'error'); end.focus(); return false; }
|
|
if (deadlineAt <= Date.now() || deadlineAt > startAt) { showWebToast('报名截止时间应晚于现在且不晚于活动开始', 'error'); deadline.focus(); return false; }
|
|
if (!locationName?.value.trim()) { showWebToast('请填写活动地点', 'error'); locationName?.focus(); return false; }
|
|
if (document.querySelector('#fee-toggle .fee-option.active')?.dataset.fee === 'paid' && !(Number(document.getElementById('fee-amount')?.value) > 0)) { showWebToast('请填写活动费用', 'error'); return false; }
|
|
return true;
|
|
};
|
|
const formData = () => ({
|
|
title: titleInput?.value || '', description: document.getElementById('activity-desc')?.value || '', type: document.getElementById('activity-type')?.value || '',
|
|
start: start?.value || '', end: end?.value || '', deadline: deadline?.value || '',
|
|
location: locationName?.value || '', address: locationAddress?.value || '', coordinates: selectedCoordinates,
|
|
maxCount: document.getElementById('people-count')?.value || '20', fee: document.querySelector('#fee-toggle .fee-option.active')?.dataset.fee || 'free',
|
|
feeAmount: document.getElementById('fee-amount')?.value || '', feeDescription: document.getElementById('fee-desc')?.value || '', coverUrl
|
|
});
|
|
save?.addEventListener('click', async () => {
|
|
save.disabled = true;
|
|
try {
|
|
await uploadCover();
|
|
localStorage.setItem('youthome:web:activityDraft', JSON.stringify(formData()));
|
|
showWebToast('活动草稿已保存');
|
|
} catch (error) { showWebToast(error.message || '草稿保存失败', 'error'); }
|
|
finally { save.disabled = false; }
|
|
});
|
|
try {
|
|
const draft = JSON.parse(localStorage.getItem('youthome:web:activityDraft') || 'null');
|
|
if (draft) {
|
|
const fields = { 'activity-title': draft.title, 'activity-desc': draft.description, 'activity-type': draft.type, 'start-time': draft.start, 'end-time': draft.end, 'registration-deadline': draft.deadline, 'location-name': draft.location, 'location-address': draft.address, 'people-count': draft.maxCount, 'fee-amount': draft.feeAmount, 'fee-desc': draft.feeDescription };
|
|
Object.entries(fields).forEach(([id, value]) => { if (value != null) document.getElementById(id).value = value; });
|
|
selectedCoordinates = draft.coordinates || null;
|
|
if (draft.fee === 'paid') document.querySelector('#fee-toggle .fee-option[data-fee="paid"]')?.click();
|
|
if (draft.coverUrl) { coverUrl = draft.coverUrl; showCover(coverUrl); }
|
|
titleInput.dispatchEvent(new Event('input'));
|
|
document.getElementById('activity-desc').dispatchEvent(new Event('input'));
|
|
}
|
|
} catch (error) { localStorage.removeItem('youthome:web:activityDraft'); }
|
|
const normalizeTime = (value) => String(value || '').replace('T', ' ') + ':00';
|
|
publishButtons.forEach((button) => button.addEventListener('click', async () => {
|
|
if (submitting) return;
|
|
if (!validate()) return;
|
|
const api = window.YouthomeApi;
|
|
submitting = true;
|
|
publishButtons.forEach((node) => { node.disabled = true; });
|
|
button.disabled = true;
|
|
try {
|
|
const uploadedCover = await uploadCover();
|
|
const payload = {
|
|
title: titleInput.value.trim(), description: document.getElementById('activity-desc').value.trim(),
|
|
type: document.getElementById('activity-type').value, coverImage: uploadedCover,
|
|
location: [locationName.value.trim(), document.getElementById('location-address').value.trim()].filter(Boolean).join(' '),
|
|
latitude: selectedCoordinates?.latitude ?? null, longitude: selectedCoordinates?.longitude ?? null,
|
|
startTime: normalizeTime(start.value), endTime: normalizeTime(end.value), registrationDeadline: normalizeTime(deadline.value),
|
|
maxCount: document.getElementById('people-limit-switch').classList.contains('active') ? Number(document.getElementById('people-count').value) : 999,
|
|
feeAmount: document.querySelector('#fee-toggle .fee-option.active')?.dataset.fee === 'paid' ? Number(document.getElementById('fee-amount').value) : 0,
|
|
feeDescription: document.getElementById('fee-desc').value.trim(), scope: 'CAMPUS'
|
|
};
|
|
const result = await api.post('/campus/activities', payload);
|
|
if (!result?.id) throw new Error('发布结果缺少活动编号');
|
|
localStorage.removeItem('youthome:web:activityDraft');
|
|
location.href = 'activity-detail.html?id=' + encodeURIComponent(result.id);
|
|
} catch (error) {
|
|
showWebToast(error.message || '创建失败,请重试', 'error');
|
|
publishButtons.forEach((node) => { node.disabled = false; });
|
|
submitting = false;
|
|
}
|
|
}));
|
|
previewButton?.addEventListener('click', () => {
|
|
if (!validate()) return;
|
|
const summary = [titleInput.value.trim(), document.getElementById('activity-desc').value.trim(), `${start.value.replace('T', ' ')} 至 ${end.value.replace('T', ' ')}`, `报名截止:${deadline.value.replace('T', ' ')}`, `地点:${locationName.value.trim()}`];
|
|
const dialog = document.createElement('dialog');
|
|
dialog.className = 'web-activity-preview';
|
|
const heading = document.createElement('h3'); heading.textContent = '活动预览';
|
|
const image = document.createElement('img'); image.src = coverArea.dataset.previewUrl; image.alt = '活动封面';
|
|
const details = document.createElement('div');
|
|
summary.forEach((line) => { const row = document.createElement('p'); row.textContent = line; details.appendChild(row); });
|
|
const close = document.createElement('button'); close.type = 'button'; close.textContent = '关闭预览'; close.addEventListener('click', () => dialog.close());
|
|
dialog.append(heading, image, details, close);
|
|
document.body.appendChild(dialog);
|
|
dialog.addEventListener('close', () => dialog.remove(), { once: true });
|
|
dialog.showModal();
|
|
});
|
|
document.querySelector('.location-locate-btn')?.addEventListener('click', () => {
|
|
const addressInput = locationAddress;
|
|
const dialog = document.createElement('dialog'); dialog.className = 'web-map-dialog';
|
|
const head = document.createElement('div'); head.className = 'web-dialog-head';
|
|
const title = document.createElement('span'); title.textContent = '选择活动地点';
|
|
const close = document.createElement('button'); close.type = 'button'; close.textContent = '×'; close.setAttribute('aria-label', '关闭'); close.addEventListener('click', () => dialog.close());
|
|
head.append(title, close);
|
|
const search = document.createElement('div'); search.className = 'web-map-search';
|
|
const query = document.createElement('input'); query.type = 'search'; query.placeholder = '搜索地点或地址'; query.value = locationName.value.trim();
|
|
const searchButton = document.createElement('button'); searchButton.type = 'button'; searchButton.textContent = '搜索';
|
|
search.append(query, searchButton);
|
|
const canvas = document.createElement('div'); canvas.className = 'web-map-canvas';
|
|
const status = document.createElement('div'); status.className = 'web-map-status'; status.textContent = '点击地图选取位置,也可以搜索地点';
|
|
const foot = document.createElement('div'); foot.className = 'web-dialog-foot';
|
|
const cancel = document.createElement('button'); cancel.type = 'button'; cancel.textContent = '取消'; cancel.addEventListener('click', () => dialog.close());
|
|
const confirm = document.createElement('button'); confirm.type = 'button'; confirm.className = 'primary'; confirm.textContent = '使用此地点'; confirm.disabled = true;
|
|
foot.append(cancel, confirm);
|
|
dialog.append(head, search, canvas, status, foot);
|
|
document.body.appendChild(dialog);
|
|
dialog.addEventListener('close', () => dialog.remove(), { once: true });
|
|
dialog.showModal();
|
|
if (!window.L) { status.textContent = '地图未能加载,请手动填写地点'; return; }
|
|
const map = L.map(canvas).setView([31.299, 121.504], 15);
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map);
|
|
let marker = null;
|
|
let selection = null;
|
|
let selectionRevision = 0;
|
|
const pin = L.divIcon({ className: '', html: '<div class="web-map-pin"></div>', iconSize: [18, 18], iconAnchor: [9, 9] });
|
|
const selectPoint = (lat, lon, name, address) => {
|
|
selection = { name: name || '地图选取的位置', address: address || `${lat.toFixed(6)}, ${lon.toFixed(6)}`, latitude: lat, longitude: lon };
|
|
if (marker) marker.setLatLng([lat, lon]); else marker = L.marker([lat, lon], { icon: pin }).addTo(map);
|
|
map.panTo([lat, lon]);
|
|
status.textContent = selection.name + ' · ' + selection.address;
|
|
confirm.disabled = false;
|
|
};
|
|
const lookup = async (url) => {
|
|
const response = await fetch(url, { headers: { 'Accept-Language': 'zh-CN,zh' }, signal: AbortSignal.timeout(10000) });
|
|
if (!response.ok) throw new Error('地点搜索暂不可用');
|
|
return response.json();
|
|
};
|
|
map.on('click', async (event) => {
|
|
const { lat, lng } = event.latlng;
|
|
const revision = ++selectionRevision;
|
|
selectPoint(lat, lng);
|
|
try {
|
|
const place = await lookup(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`);
|
|
if (revision === selectionRevision && dialog.open) selectPoint(lat, lng, place.name || place.display_name?.split(',')[0], place.display_name);
|
|
} catch (error) { status.textContent = '已选取坐标,可直接使用或手动修改详细地址'; }
|
|
});
|
|
const searchPlace = async () => {
|
|
if (!query.value.trim()) return;
|
|
searchButton.disabled = true; status.textContent = '正在搜索地点…';
|
|
try {
|
|
const places = await lookup('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=' + encodeURIComponent(query.value.trim()));
|
|
if (!places.length) { status.textContent = '没有找到地点,请换个关键词'; return; }
|
|
const place = places[0]; const lat = Number(place.lat); const lon = Number(place.lon);
|
|
selectionRevision++;
|
|
map.setView([lat, lon], 17);
|
|
selectPoint(lat, lon, place.name || place.display_name.split(',')[0], place.display_name);
|
|
} catch (error) { status.textContent = error.message || '地点搜索暂不可用,请点击地图选点'; }
|
|
finally { searchButton.disabled = false; }
|
|
};
|
|
searchButton.addEventListener('click', searchPlace);
|
|
query.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); searchPlace(); } });
|
|
confirm.addEventListener('click', () => {
|
|
if (!selection) return;
|
|
locationName.value = selection.name;
|
|
addressInput.value = selection.address.startsWith(selection.name)
|
|
? selection.address.slice(selection.name.length).replace(/^[,,\s]+/, '')
|
|
: selection.address;
|
|
locationName.dispatchEvent(new Event('input', { bubbles: true }));
|
|
addressInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
selectedCoordinates = { latitude: selection.latitude, longitude: selection.longitude };
|
|
dialog.close();
|
|
});
|
|
dialog.addEventListener('close', () => map.remove(), { once: true });
|
|
setTimeout(() => map.invalidateSize(), 0);
|
|
});
|
|
};
|
|
|
|
const initApiTopic = async (api) => {
|
|
const params = new URLSearchParams(location.search);
|
|
let topicName = (params.get('topic') || params.get('name') || '').trim();
|
|
const titleNode = document.querySelector('.topic-title');
|
|
if (!topicName) topicName = (titleNode?.textContent || '').replace(/^#/, '').trim();
|
|
if (!topicName) return;
|
|
if (titleNode) titleNode.textContent = '#' + topicName;
|
|
document.title = topicName + ' - 青核 Youthome';
|
|
const grid = document.querySelector('.feed-grid');
|
|
const tabs = Array.from(document.querySelectorAll('.topic-tab'));
|
|
if (!grid) return;
|
|
let sort = 'latest';
|
|
|
|
const composer = document.createElement('div');
|
|
composer.className = 'web-discussion-composer';
|
|
const textarea = document.createElement('textarea');
|
|
textarea.placeholder = '分享你对这个话题的想法…';
|
|
textarea.rows = 2;
|
|
const publishBtn = document.createElement('button');
|
|
publishBtn.type = 'button';
|
|
publishBtn.className = 'web-discussion-publish';
|
|
publishBtn.textContent = '发布讨论';
|
|
composer.append(textarea, publishBtn);
|
|
const list = document.createElement('div');
|
|
list.className = 'web-discussion-list';
|
|
grid.replaceChildren(composer, list);
|
|
|
|
const buildCard = (item) => {
|
|
const card = document.createElement('article');
|
|
card.className = 'web-discussion-card';
|
|
const head = document.createElement('div');
|
|
head.className = 'web-discussion-head';
|
|
const avatar = document.createElement('span');
|
|
avatar.className = 'web-discussion-avatar';
|
|
avatar.textContent = (item.userName || '青').slice(0, 1);
|
|
const name = document.createElement('b');
|
|
name.textContent = item.anonymous ? '匿名' : (item.userName || '青核用户');
|
|
const time = document.createElement('span');
|
|
time.className = 'web-discussion-time';
|
|
time.textContent = String(item.createdAt || '').slice(0, 16);
|
|
head.append(avatar, name, time);
|
|
const content = document.createElement('p');
|
|
content.className = 'web-discussion-content';
|
|
content.textContent = item.content || '';
|
|
card.append(head, content);
|
|
if (item.mediaUrls?.length) {
|
|
const media = document.createElement('div');
|
|
media.className = 'web-discussion-media';
|
|
item.mediaUrls.slice(0, 9).forEach((url) => {
|
|
const img = document.createElement('img');
|
|
img.src = url;
|
|
img.alt = '';
|
|
media.appendChild(img);
|
|
});
|
|
card.appendChild(media);
|
|
}
|
|
return card;
|
|
};
|
|
|
|
const render = async () => {
|
|
list.replaceChildren();
|
|
const loading = document.createElement('p');
|
|
loading.className = 'web-live-empty';
|
|
loading.textContent = '正在加载讨论…';
|
|
list.appendChild(loading);
|
|
try {
|
|
const data = await api.get('/topics/' + encodeURIComponent(topicName) + '/discussions?sort=' + sort + '&page=1&size=30');
|
|
const items = data?.items || [];
|
|
list.replaceChildren(...items.map(buildCard));
|
|
if (!items.length) {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = '还没有讨论,来发第一条吧';
|
|
list.appendChild(empty);
|
|
}
|
|
} catch (error) {
|
|
list.replaceChildren();
|
|
const empty = document.createElement('p');
|
|
empty.className = 'web-live-empty';
|
|
empty.textContent = error.message || '讨论加载失败';
|
|
list.appendChild(empty);
|
|
}
|
|
};
|
|
|
|
publishBtn.addEventListener('click', async () => {
|
|
if (!api.token?.()) { showWebToast('请先登录后再发布', 'error'); location.href = 'login.html?next=' + encodeURIComponent('topic-detail.html?topic=' + topicName); return; }
|
|
const content = textarea.value.trim();
|
|
if (!content) { showWebToast('请输入讨论内容', 'error'); textarea.focus(); return; }
|
|
publishBtn.disabled = true;
|
|
publishBtn.textContent = '发布中…';
|
|
try {
|
|
await api.post('/topics/' + encodeURIComponent(topicName) + '/discussions', { content, mediaUrls: [], anonymous: false });
|
|
textarea.value = '';
|
|
showWebToast('讨论已发布');
|
|
sort = 'latest';
|
|
tabs.forEach((tab) => tab.classList.toggle('active', tab.textContent.trim() === '最新'));
|
|
await render();
|
|
} catch (error) {
|
|
showWebToast(error.message || '发布失败,请重试', 'error');
|
|
} finally {
|
|
publishBtn.disabled = false;
|
|
publishBtn.textContent = '发布讨论';
|
|
}
|
|
});
|
|
|
|
tabs.forEach((tab) => {
|
|
tab.addEventListener('click', () => {
|
|
tabs.forEach((node) => node.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
sort = tab.textContent.trim() === '热门' ? 'hot' : 'latest';
|
|
render();
|
|
});
|
|
});
|
|
|
|
api.get('/topics/' + encodeURIComponent(topicName)).then((detail) => {
|
|
if (!detail) return;
|
|
const desc = document.querySelector('.topic-desc');
|
|
if (desc && detail.description) desc.textContent = detail.description;
|
|
}).catch(() => {});
|
|
|
|
await render();
|
|
};
|
|
|
|
const initTopic = () => {
|
|
const api = window.YouthomeApi;
|
|
if (api?.isLocal && !api.isLocal()) return initApiTopic(api);
|
|
const tabs = Array.from(document.querySelectorAll('.topic-tab'));
|
|
const cards = Array.from(document.querySelectorAll('.post-card'));
|
|
const list = cards[0]?.parentElement;
|
|
const load = document.querySelector('.load-more-btn');
|
|
if (!tabs.length || !cards.length || !list) return;
|
|
let expanded = false;
|
|
const render = (label) => {
|
|
const matched = cards.filter((card, index) => label === '热门' || label === '最新' || (label === '图文' ? index % 3 !== 1 : index % 3 === 1));
|
|
cards.forEach((card) => { card.hidden = true; });
|
|
matched.slice(0, expanded ? matched.length : 5).forEach((card) => { card.hidden = false; });
|
|
if (load) { load.hidden = matched.length <= 5; load.textContent = expanded ? '已加载全部' : `加载更多(${matched.length - 5})`; }
|
|
};
|
|
cards.forEach((card) => makeLinkedCard(card, 'post-detail.html'));
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => { expanded = false; setPressed(tabs, tab); render(tab.textContent.trim()); });
|
|
});
|
|
load?.addEventListener('click', () => { expanded = true; render(document.querySelector('.topic-tab.active')?.textContent.trim() || '热门'); });
|
|
document.querySelector('.btn-share')?.addEventListener('click', () => showWebToast('话题链接已准备好,可从地址栏复制'));
|
|
document.querySelectorAll('.related-topic-item').forEach((item) => makeLinkedCard(item, 'topic-detail.html'));
|
|
render(tabs[0].textContent.trim());
|
|
};
|
|
|
|
const wireSideWidgets = () => {
|
|
const rotateList = (container, itemSelector) => {
|
|
const items = Array.from(container.querySelectorAll(itemSelector));
|
|
if (items.length < 2) return false;
|
|
container.appendChild(items[0]);
|
|
container.querySelectorAll('.hot-rank, .topic-rank, .rank-number').forEach((node, index) => {
|
|
node.textContent = String(index + 1);
|
|
});
|
|
return true;
|
|
};
|
|
document.querySelectorAll('.widget-more').forEach((button) => {
|
|
// 动态广场的这两个按钮由 initFeed 处理,避免重复绑定
|
|
if (button.classList.contains('topic-more-button') || button.classList.contains('recommendation-refresh')) return;
|
|
const widget = button.closest('.widget, .sidebar-widget');
|
|
if (!widget) return;
|
|
makeKeyboardClickable(button, () => button.click());
|
|
button.addEventListener('click', () => {
|
|
const label = button.textContent.trim();
|
|
const hotList = widget.querySelector('.hot-list');
|
|
const related = widget.querySelector('.related-topic-list');
|
|
const extras = Array.from(widget.querySelectorAll('.topic-item-extra'));
|
|
if (extras.length) {
|
|
const expanded = button.dataset.expanded !== 'true';
|
|
button.dataset.expanded = String(expanded);
|
|
extras.forEach((item) => { item.hidden = !expanded; });
|
|
button.textContent = expanded ? '收起' : '更多';
|
|
return;
|
|
}
|
|
if (/换一换|换一批/.test(label)) {
|
|
const rotated = (hotList && rotateList(hotList, '.hot-item')) || (related && rotateList(related, '.related-topic-item'));
|
|
showWebToast(rotated ? '已换一批' : '暂时没有更多内容');
|
|
return;
|
|
}
|
|
if (widget.querySelector('.related-topic-list') || hotList) { location.href = 'feed.html'; return; }
|
|
if (widget.querySelector('.active-user-list') || widget.querySelector('.user-list')) { location.href = 'follow.html'; return; }
|
|
showWebToast('完整列表请在对应页面查看');
|
|
});
|
|
});
|
|
};
|
|
|
|
const walletLevels = [
|
|
{ level: 1, name: '青核新芽', need: 0 },
|
|
{ level: 2, name: '校园达人', need: 500 },
|
|
{ level: 3, name: '活跃分子', need: 1500 },
|
|
{ level: 4, name: '校园之星', need: 3000 },
|
|
{ level: 5, name: '青核之光', need: 6000 }
|
|
];
|
|
const levelOfPoints = (points) => walletLevels.reduce((current, item) => (points >= item.need ? item : current), walletLevels[0]);
|
|
|
|
const initWallet = () => {
|
|
const api = window.YouthomeApi;
|
|
const store = window.YouthomeLocal;
|
|
const isLocal = api?.isLocal ? api.isLocal() : true;
|
|
const stamp2 = (value) => String(value).padStart(2, '0');
|
|
const dayKey = () => {
|
|
const date = new Date();
|
|
return `${date.getFullYear()}-${stamp2(date.getMonth() + 1)}-${stamp2(date.getDate())}`;
|
|
};
|
|
const sign = document.querySelector('.btn-signin');
|
|
const pointsNode = document.querySelector('.points-number');
|
|
const statValues = document.querySelectorAll('.points-stats .stat-value');
|
|
const statLabels = document.querySelectorAll('.points-stats .stat-label');
|
|
const todayEarned = () => Number(localStorage.getItem(`youthome:web:points-today:${dayKey()}`) || 0);
|
|
const checkedInToday = () => localStorage.getItem(`youthome:web:checkin:${dayKey()}`) === 'true';
|
|
let remote = null;
|
|
|
|
const renderLevels = (points) => {
|
|
const level = levelOfPoints(points);
|
|
const badge = statValues[2]?.querySelector('.level-badge');
|
|
if (badge) {
|
|
const icon = badge.querySelector('svg');
|
|
badge.replaceChildren(...(icon ? [icon] : []), document.createTextNode(` LV.${level.level}`));
|
|
}
|
|
const list = document.querySelector('.level-list');
|
|
if (!list) return;
|
|
list.replaceChildren(...walletLevels.map((item) => {
|
|
const row = document.createElement('div');
|
|
row.className = `level-item${item.level === level.level ? ' is-current' : ''}`;
|
|
const left = document.createElement('div');
|
|
const name = document.createElement('div');
|
|
name.className = 'level-name';
|
|
name.textContent = `LV.${item.level} ${item.name}`;
|
|
const need = document.createElement('div');
|
|
need.className = 'level-need';
|
|
need.textContent = item.need === 0 ? '注册即可获得' : `${item.need} 积分解锁`;
|
|
left.append(name, need);
|
|
const tag = document.createElement('span');
|
|
tag.className = 'level-tag';
|
|
tag.textContent = item.level === level.level ? '当前等级' : points >= item.need ? '已解锁' : '待解锁';
|
|
row.append(left, tag);
|
|
return row;
|
|
}));
|
|
};
|
|
|
|
const renderStats = async () => {
|
|
let points = 0;
|
|
if (isLocal) {
|
|
const account = store?.account?.();
|
|
points = account?.points || 0;
|
|
if (statValues[0]) statValues[0].textContent = `+${todayEarned()}`;
|
|
if (statLabels[0]) statLabels[0].textContent = '今日获得';
|
|
if (statValues[1]) statValues[1].textContent = String((account?.redemptions || []).length);
|
|
if (statLabels[1]) statLabels[1].textContent = '已兑换';
|
|
} else {
|
|
try { remote = await api.request('/campus/points/account'); }
|
|
catch (error) { showWebToast(error.message || '积分加载失败', 'error'); remote = null; }
|
|
points = Number(remote?.points || 0);
|
|
if (statValues[0]) statValues[0].textContent = '+' + Number(remote?.todayPoints || 0);
|
|
if (statLabels[0]) statLabels[0].textContent = '今日获得';
|
|
if (statValues[1]) statValues[1].textContent = '+' + Number(remote?.totalEarned || 0);
|
|
if (statLabels[1]) statLabels[1].textContent = '累计获得';
|
|
}
|
|
if (pointsNode) pointsNode.textContent = points.toLocaleString('zh-CN');
|
|
renderLevels(points);
|
|
if (sign) {
|
|
const done = isLocal ? checkedInToday() : Boolean(remote?.checkedInToday);
|
|
sign.disabled = done;
|
|
sign.textContent = done ? '今日已签到' : '签到';
|
|
}
|
|
};
|
|
|
|
// 标签页:任务中心 / 积分商城 / 等级特权
|
|
const panelNames = { 任务中心: 'task', 积分商城: 'mall', 等级特权: 'level' };
|
|
const panels = Array.from(document.querySelectorAll('[data-wallet-panel]'));
|
|
const tabs = Array.from(document.querySelectorAll('.tab-nav .tab-item'));
|
|
const showPanel = (name) => {
|
|
const key = panelNames[name] || 'task';
|
|
panels.forEach((panel) => { panel.hidden = panel.dataset.walletPanel !== key; });
|
|
tabs.forEach((tab) => tab.classList.toggle('active', tab.textContent.trim() === name));
|
|
const params = new URLSearchParams();
|
|
if (key !== 'task') params.set('tab', name);
|
|
history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}`);
|
|
};
|
|
tabs.forEach((tab) => {
|
|
makeKeyboardClickable(tab, () => tab.click());
|
|
tab.addEventListener('click', () => showPanel(tab.textContent.trim()));
|
|
});
|
|
|
|
// 签到
|
|
localStorage.removeItem('youthome:web:walletCheckedIn');
|
|
sign?.addEventListener('click', async () => {
|
|
if (isLocal) {
|
|
if (!store?.account?.()) { showWebToast('请先登录后再签到', 'error'); return; }
|
|
if (checkedInToday()) { showWebToast('今天已经签到过了,明天再来'); return; }
|
|
try {
|
|
store.addPoints(5);
|
|
localStorage.setItem(`youthome:web:checkin:${dayKey()}`, 'true');
|
|
localStorage.setItem(`youthome:web:points-today:${dayKey()}`, String(todayEarned() + 5));
|
|
} catch (error) { showWebToast(error.message || '签到失败,请重试', 'error'); return; }
|
|
showWebToast('签到成功,积分 +5');
|
|
} else {
|
|
try { await api.checkIn(); showWebToast('签到成功'); }
|
|
catch (error) { showWebToast(error.message || '签到失败,请重试', 'error'); return; }
|
|
}
|
|
await renderStats();
|
|
});
|
|
|
|
const openPointsDetail = async () => {
|
|
if (isLocal) {
|
|
const account = store?.account?.();
|
|
showWebToast(`当前 ${account ? (account.points || 0) : 0} 积分,今日获得 ${todayEarned()},已兑换 ${(account?.redemptions || []).length} 次`);
|
|
return;
|
|
}
|
|
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 hint = document.createElement('p');
|
|
hint.className = 'web-sheet-hint';
|
|
hint.textContent = '正在加载…';
|
|
body.appendChild(hint);
|
|
panel.append(head, body);
|
|
overlay.appendChild(panel);
|
|
document.body.appendChild(overlay);
|
|
const close = () => overlay.remove();
|
|
overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); });
|
|
closeBtn.addEventListener('click', close);
|
|
try {
|
|
const data = await api.get('/campus/points/transactions?page=1&size=50');
|
|
const items = data?.items || [];
|
|
body.replaceChildren();
|
|
if (!items.length) {
|
|
hint.textContent = '还没有积分记录';
|
|
body.appendChild(hint);
|
|
return;
|
|
}
|
|
items.forEach((item) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'web-points-row';
|
|
const left = document.createElement('div');
|
|
left.className = 'web-points-left';
|
|
const name = document.createElement('b');
|
|
name.textContent = item.ruleName || item.remark || '积分变动';
|
|
const time = document.createElement('span');
|
|
time.textContent = String(item.createdAt || '').slice(0, 16);
|
|
left.append(name, time);
|
|
const amount = document.createElement('span');
|
|
amount.className = 'web-points-amount' + (Number(item.points) > 0 ? ' in' : ' out');
|
|
amount.textContent = (Number(item.points) > 0 ? '+' : '') + Number(item.points || 0);
|
|
row.append(left, amount);
|
|
body.appendChild(row);
|
|
});
|
|
} catch (error) {
|
|
hint.textContent = error.message || '积分明细加载失败';
|
|
hint.classList.add('error');
|
|
}
|
|
};
|
|
document.querySelector('.btn-detail')?.addEventListener('click', openPointsDetail);
|
|
|
|
// 积分商城
|
|
const mallGrid = document.querySelector('.mall-grid');
|
|
const bindMallButton = (card) => {
|
|
const name = card.querySelector('.mall-name')?.textContent.trim() || '商品';
|
|
const cost = Number((card.querySelector('.mall-points')?.textContent || '').replace(/[^0-9]/g, ''));
|
|
const button = card.querySelector('.mall-btn');
|
|
if (!button || button.dataset.bound === 'true') return;
|
|
button.dataset.bound = 'true';
|
|
makeKeyboardClickable(button, () => button.click());
|
|
button.addEventListener('click', async () => {
|
|
if (isLocal) {
|
|
if (!store?.account?.()) { showWebToast('请先登录后再兑换', 'error'); return; }
|
|
try {
|
|
const result = store.redeem(cost, name);
|
|
showWebToast(`已兑换「${name}」,剩余 ${result.points} 积分`);
|
|
await renderStats();
|
|
} catch (error) { showWebToast(error.message || '兑换失败', 'error'); }
|
|
return;
|
|
}
|
|
if (!card.dataset.productId) { showWebToast('商品信息不完整,请稍后再试', 'error'); return; }
|
|
try {
|
|
await api.purchase(card.dataset.productId);
|
|
showWebToast(`已兑换「${name}」`);
|
|
await renderStats();
|
|
} catch (error) { showWebToast(error.message || '兑换失败', 'error'); }
|
|
});
|
|
};
|
|
|
|
const renderApiMall = async () => {
|
|
if (!mallGrid) return;
|
|
let products = [];
|
|
try { products = await api.mallProducts(); }
|
|
catch (error) { showWebToast(error.message || '商品加载失败', 'error'); return; }
|
|
const template = mallGrid.querySelector('.mall-card');
|
|
if (!template || !products.length) return;
|
|
const tints = [
|
|
'linear-gradient(135deg, #BCEAF3, #17B0CF)',
|
|
'linear-gradient(135deg, #FFE0CC, #FF9F45)',
|
|
'linear-gradient(135deg, #D4F7E7, #34C77B)',
|
|
'linear-gradient(135deg, #D4B5FF, #9254DE)'
|
|
];
|
|
mallGrid.replaceChildren(...products.map((product, index) => {
|
|
const card = template.cloneNode(true);
|
|
card.dataset.productId = product.id;
|
|
const cover = card.querySelector('.mall-cover');
|
|
if (cover) {
|
|
cover.style.background = tints[index % tints.length];
|
|
cover.textContent = String(product.name || '物').slice(0, 1);
|
|
}
|
|
const nameNode = card.querySelector('.mall-name');
|
|
if (nameNode) nameNode.textContent = product.name || '未命名商品';
|
|
const priceNode = card.querySelector('.mall-points');
|
|
if (priceNode && priceNode.lastChild) priceNode.lastChild.textContent = ' ' + (product.price ?? 0);
|
|
return card;
|
|
}));
|
|
mallGrid.querySelectorAll('.mall-card').forEach(bindMallButton);
|
|
};
|
|
|
|
document.querySelectorAll('.btn-task-done').forEach((button) => {
|
|
makeKeyboardClickable(button, () => button.click());
|
|
button.addEventListener('click', () => showWebToast('该任务今天已完成,明天可以再领'));
|
|
});
|
|
|
|
const taskTargets = { 去发布: 'publish-post.html', 继续浏览: 'feed.html', 继续点赞: 'feed.html', 去评论: 'feed.html', 去分享: 'feed.html', 去认证: 'authentication.html', 继续关注: 'follow.html', 去邀请: 'follow.html' };
|
|
document.querySelectorAll('.btn-task:not(.btn-task-done)').forEach((button) => {
|
|
button.addEventListener('click', () => { location.href = taskTargets[button.textContent.trim()] || 'feed.html'; });
|
|
});
|
|
|
|
const requestedTab = new URLSearchParams(location.search).get('tab');
|
|
showPanel(panelNames[requestedTab] ? requestedTab : tabs[0]?.textContent.trim() || '任务中心');
|
|
if (isLocal) mallGrid?.querySelectorAll('.mall-card').forEach(bindMallButton);
|
|
else renderApiMall();
|
|
renderStats();
|
|
};
|
|
|
|
const initAuthRouting = () => {
|
|
if (page === 'register.html') {
|
|
window.selectSchool = () => { location.href = 'select-school.html'; };
|
|
const school = localStorage.getItem('youthome:web:selectedSchool');
|
|
const schoolLabel = document.querySelector('.school-select .text');
|
|
if (school && schoolLabel) { schoolLabel.textContent = school; schoolLabel.classList.remove('placeholder'); }
|
|
window.doRegister = () => {
|
|
const phone = document.querySelector('input[type="tel"]');
|
|
const code = document.querySelector('input[placeholder*="验证码"]');
|
|
const password = document.querySelector('input[type="password"]');
|
|
const agreement = document.querySelector('input[type="checkbox"]');
|
|
if (!school) { showWebToast('请先选择学校', 'error'); return; }
|
|
if (!/^1\d{10}$/.test(phone?.value.trim() || '')) { showWebToast('请输入正确的 11 位手机号', 'error'); phone?.focus(); return; }
|
|
if (!/^\d{6}$/.test(code?.value.trim() || '')) { showWebToast('请输入 6 位验证码', 'error'); code?.focus(); return; }
|
|
if (!/^(?=.*[A-Za-z])(?=.*\d).{8,20}$/.test(password?.value || '')) { showWebToast('密码需为 8-20 位且包含字母和数字', 'error'); password?.focus(); return; }
|
|
if (!agreement?.checked) { showWebToast('请阅读并同意用户协议和隐私政策', 'error'); return; }
|
|
location.href = 'register-success.html';
|
|
};
|
|
}
|
|
if (page === 'select-school.html') {
|
|
document.querySelectorAll('.school-card').forEach((card) => card.addEventListener('click', () => {
|
|
localStorage.setItem('youthome:web:selectedSchool', card.dataset.school || card.textContent.trim());
|
|
}));
|
|
window.goNext = () => {
|
|
const selected = document.querySelector('.school-card.selected') || document.querySelector('.school-card');
|
|
if (!selected) { showWebToast('请选择你的学校', 'error'); return; }
|
|
localStorage.setItem('youthome:web:selectedSchool', selected.dataset.school || selected.textContent.trim());
|
|
location.href = 'register.html';
|
|
};
|
|
}
|
|
if (page === 'authentication.html') {
|
|
window.alert = (message) => showWebToast(message, 'error');
|
|
window.returnHome = () => { location.href = 'me.html'; };
|
|
}
|
|
};
|
|
|
|
const navigate = (target) => {
|
|
if (!target) return;
|
|
document.body.classList.remove('web-drawer-open');
|
|
location.href = target;
|
|
};
|
|
|
|
document.addEventListener('click', (event) => {
|
|
const trigger = event.target.closest('[data-href]');
|
|
if (trigger) {
|
|
event.preventDefault();
|
|
navigate(trigger.dataset.href);
|
|
return;
|
|
}
|
|
|
|
const publishButton = event.target.closest('.web-publish-button');
|
|
const publishWrap = document.querySelector('.web-publish-wrap');
|
|
if (publishButton && publishWrap) {
|
|
const open = publishWrap.classList.toggle('is-open');
|
|
publishButton.setAttribute('aria-expanded', String(open));
|
|
return;
|
|
}
|
|
if (publishWrap && !event.target.closest('.web-publish-wrap')) {
|
|
publishWrap.classList.remove('is-open');
|
|
publishWrap.querySelector('.web-publish-button')?.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
|
|
document.querySelector('.web-brand')?.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter' || event.key === ' ') navigate('feed.html');
|
|
});
|
|
|
|
topbar.querySelector('.web-global-search')?.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
const query = event.currentTarget.querySelector('input').value.trim();
|
|
location.href = query ? `search.html?q=${encodeURIComponent(query)}` : 'search.html';
|
|
});
|
|
|
|
if (page === 'activities.html') initActivities();
|
|
if (page === 'video.html') initVideo();
|
|
if (page === 'feed.html') initFeed();
|
|
if (page === 'search.html') initSearch();
|
|
if (page === 'notification.html') initNotifications();
|
|
if (page === 'settings.html') initSettings();
|
|
if (page === 'favorites.html') initFavorites();
|
|
if (page === 'drafts.html') initDrafts();
|
|
if (page === 'follow.html') initFollow();
|
|
if (page === 'my-activity-apply.html') initMyActivities();
|
|
if (page === 'me.html') initProfile();
|
|
if (page === 'me-edit.html') initProfileEdit();
|
|
if (page === 'chat.html') initChat();
|
|
if (page === 'help.html') initHelp();
|
|
if (page === 'activity-detail.html') initActivityDetail();
|
|
if (page === 'post-detail.html' || page === 'post-detail-1.html' || page === 'video-detail.html') initDetailActions();
|
|
if (page === 'user.html') initUser();
|
|
const initVideoPublish = () => {
|
|
const api = window.YouthomeApi;
|
|
if (!api) return;
|
|
const area = document.querySelector('.upload-area');
|
|
if (!area) return;
|
|
const videoInput = document.createElement('input');
|
|
videoInput.type = 'file';
|
|
videoInput.accept = 'video/mp4,video/quicktime,video/webm';
|
|
videoInput.hidden = true;
|
|
videoInput.className = 'web-video-input';
|
|
const coverInput = document.createElement('input');
|
|
coverInput.type = 'file';
|
|
coverInput.accept = 'image/png,image/jpeg,image/webp';
|
|
coverInput.hidden = true;
|
|
coverInput.className = 'web-cover-input';
|
|
area.after(videoInput, coverInput);
|
|
const preview = document.createElement('video');
|
|
preview.className = 'web-video-preview';
|
|
preview.controls = true;
|
|
preview.playsInline = true;
|
|
preview.preload = 'metadata';
|
|
preview.hidden = true;
|
|
preview.addEventListener('click', (event) => event.stopPropagation());
|
|
area.appendChild(preview);
|
|
const titleInput = document.querySelector('input[placeholder*="视频标题"]');
|
|
const description = document.querySelector('textarea[placeholder*="介绍一下"]');
|
|
const coverThumb = document.querySelector('.cover-thumb');
|
|
const draftKey = 'youthome:web:videoDraft';
|
|
const progress = document.querySelector('.upload-progress-wrap');
|
|
document.querySelectorAll('.info-item-value').forEach((node) => { node.textContent = '—'; });
|
|
progress.hidden = true;
|
|
progress.querySelector('.progress-bar')?.setAttribute('hidden', '');
|
|
progress.querySelector('.progress-percent')?.setAttribute('hidden', '');
|
|
let file = null;
|
|
let videoReference = '';
|
|
let videoObjectUrl = '';
|
|
let coverFile = null;
|
|
let coverReference = '';
|
|
let publishing = false;
|
|
const tags = ['校园日常'];
|
|
const formatDuration = (seconds) => `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(Math.floor(seconds % 60)).padStart(2, '0')}`;
|
|
const setCover = (url) => {
|
|
coverThumb.style.background = `url("${url}") center / cover`;
|
|
coverThumb.classList.add('has-cover');
|
|
};
|
|
const showVideo = async (url) => {
|
|
if (videoObjectUrl) URL.revokeObjectURL(videoObjectUrl);
|
|
videoObjectUrl = url;
|
|
preview.src = url;
|
|
preview.hidden = false;
|
|
area.classList.add('has-video');
|
|
preview.load();
|
|
};
|
|
preview.addEventListener('loadedmetadata', () => {
|
|
const values = document.querySelectorAll('.info-item-value');
|
|
if (values[0]) values[0].textContent = formatDuration(preview.duration || 0);
|
|
if (values[2]) values[2].textContent = `${preview.videoWidth} × ${preview.videoHeight}`;
|
|
});
|
|
preview.addEventListener('error', () => showWebToast('视频文件无法播放,请换用 MP4、MOV 或 WebM 文件', 'error'));
|
|
const selectVideo = (selected) => {
|
|
if (!selected) return;
|
|
if (!/^video\/(mp4|quicktime|webm)$/.test(selected.type)) { showWebToast('请选择 MP4、MOV 或 WebM 视频', 'error'); return; }
|
|
if (selected.size > 500 * 1024 * 1024) { showWebToast('视频不能超过 500MB', 'error'); return; }
|
|
file = selected;
|
|
videoReference = '';
|
|
coverFile = null;
|
|
coverReference = '';
|
|
coverThumb.style.background = '';
|
|
coverThumb.classList.remove('has-cover');
|
|
area.querySelector('.upload-title').textContent = selected.name;
|
|
area.querySelector('.upload-desc').textContent = '可拖动进度选择封面画面;点击空白处更换文件';
|
|
const values = document.querySelectorAll('.info-item-value');
|
|
if (values[1]) values[1].textContent = (selected.size / 1024 / 1024).toFixed(1) + ' MB';
|
|
if (values[3]) values[3].textContent = selected.name.split('.').pop().toUpperCase();
|
|
showVideo(URL.createObjectURL(selected));
|
|
};
|
|
window.triggerUpload = () => videoInput.click();
|
|
videoInput.addEventListener('change', () => { selectVideo(videoInput.files?.[0]); videoInput.value = ''; });
|
|
area.addEventListener('dragover', (event) => { event.preventDefault(); area.classList.add('is-dragging'); });
|
|
area.addEventListener('dragleave', () => area.classList.remove('is-dragging'));
|
|
area.addEventListener('drop', (event) => { event.preventDefault(); area.classList.remove('is-dragging'); selectVideo(event.dataTransfer.files?.[0]); });
|
|
window.uploadCover = () => coverInput.click();
|
|
coverInput.addEventListener('change', () => {
|
|
const selected = coverInput.files?.[0];
|
|
if (!selected) return;
|
|
if (!selected.type.startsWith('image/')) { showWebToast('请选择图片文件', 'error'); return; }
|
|
if (selected.size > 10 * 1024 * 1024) { showWebToast('封面图片不能超过 10MB', 'error'); return; }
|
|
coverFile = selected;
|
|
coverReference = '';
|
|
const reader = new FileReader();
|
|
reader.onload = () => setCover(reader.result);
|
|
reader.readAsDataURL(selected);
|
|
coverInput.value = '';
|
|
});
|
|
window.captureCover = async () => {
|
|
if (!preview.src) { showWebToast('请先选择视频', 'error'); return; }
|
|
if (preview.readyState < 2) {
|
|
await Promise.race([
|
|
new Promise((resolve) => preview.addEventListener('loadeddata', resolve, { once: true })),
|
|
new Promise((resolve) => setTimeout(resolve, 5000))
|
|
]);
|
|
}
|
|
if (!preview.videoWidth) { showWebToast('视频画面尚未加载,请稍后重试', 'error'); return; }
|
|
const canvas = document.createElement('canvas');
|
|
const scale = Math.min(1, 1280 / preview.videoWidth);
|
|
canvas.width = Math.round(preview.videoWidth * scale);
|
|
canvas.height = Math.round(preview.videoHeight * scale);
|
|
canvas.getContext('2d').drawImage(preview, 0, 0, canvas.width, canvas.height);
|
|
coverFile = await new Promise((resolve) => canvas.toBlob((blob) => resolve(blob && new File([blob], 'video-cover.jpg', { type: 'image/jpeg' })), 'image/jpeg', 0.86));
|
|
if (!coverFile) { showWebToast('截取封面失败,请改用上传封面', 'error'); return; }
|
|
coverReference = '';
|
|
setCover(canvas.toDataURL('image/jpeg', 0.86));
|
|
showWebToast('已截取当前画面作为封面');
|
|
};
|
|
const renderTags = () => {
|
|
const list = document.querySelector('.tags-list');
|
|
list.querySelectorAll('.tag').forEach((node) => node.remove());
|
|
tags.forEach((tag) => {
|
|
const chip = document.createElement('button');
|
|
chip.type = 'button';
|
|
chip.className = 'tag tag-primary';
|
|
chip.textContent = '#' + tag + ' ×';
|
|
chip.title = '移除话题';
|
|
chip.addEventListener('click', () => { tags.splice(tags.indexOf(tag), 1); renderTags(); });
|
|
list.insertBefore(chip, list.querySelector('.tag-add-btn'));
|
|
});
|
|
};
|
|
window.addTopic = () => {
|
|
if (tags.length >= 5) { showWebToast('最多添加 5 个话题', 'error'); return; }
|
|
const value = window.prompt('输入话题名称')?.trim().replace(/^#/, '');
|
|
if (value && !tags.includes(value)) { tags.push(value.slice(0, 20)); renderTags(); }
|
|
};
|
|
renderTags();
|
|
// 统一外壳会隐藏页面原来的顶栏操作区,把“保存草稿/发布”移到可见编辑区
|
|
const topBar = document.querySelector('.top-bar-right');
|
|
const draftButton = topBar?.querySelector('.btn-secondary');
|
|
const publishButton = topBar?.querySelector('.btn-primary');
|
|
if (draftButton && publishButton) {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'web-publish-actions';
|
|
const heading = document.createElement('strong');
|
|
heading.textContent = '发布视频';
|
|
bar.append(heading, draftButton, publishButton);
|
|
const anchor = document.querySelector('.publish-layout, .content-wrapper, .main-content');
|
|
if (anchor) anchor.parentElement.insertBefore(bar, anchor);
|
|
}
|
|
const upload = async (path, selected) => {
|
|
const form = new FormData();
|
|
form.append('file', selected);
|
|
const result = await api.request(path, { method: 'POST', body: form });
|
|
if (!result?.url) throw new Error('文件上传失败,请重试');
|
|
return result.url;
|
|
};
|
|
const ensureVideo = async () => {
|
|
if (!videoReference && file) videoReference = await upload('/file/upload/video', file);
|
|
return videoReference;
|
|
};
|
|
window.saveDraft = async () => {
|
|
try {
|
|
await ensureVideo();
|
|
if (coverFile) {
|
|
coverReference = await upload('/file/upload/image', coverFile);
|
|
coverFile = null;
|
|
}
|
|
localStorage.setItem(draftKey, JSON.stringify({ title: titleInput.value, description: description.value, videoReference, coverReference, tags }));
|
|
showWebToast('视频草稿已保存');
|
|
} catch (error) { showWebToast(error.message || '草稿保存失败', 'error'); }
|
|
};
|
|
try {
|
|
const draft = JSON.parse(localStorage.getItem(draftKey) || 'null');
|
|
if (draft) {
|
|
titleInput.value = draft.title || '';
|
|
description.value = draft.description || '';
|
|
videoReference = draft.videoReference || '';
|
|
coverReference = draft.coverReference || '';
|
|
if (Array.isArray(draft.tags)) { tags.splice(0, tags.length, ...draft.tags); renderTags(); }
|
|
if (coverReference) setCover(coverReference);
|
|
if (videoReference && api.isLocal()) window.YouthomeLocal.videoUrl(videoReference).then(showVideo).catch(() => {});
|
|
titleInput.dispatchEvent(new Event('input'));
|
|
description.dispatchEvent(new Event('input'));
|
|
}
|
|
} catch (error) { localStorage.removeItem(draftKey); }
|
|
window.publishVideo = async () => {
|
|
if (publishing) return;
|
|
const title = titleInput.value.trim();
|
|
const descriptionText = description.value.trim();
|
|
if (!title) { showWebToast('请输入视频标题', 'error'); return; }
|
|
if (!file && !videoReference) { showWebToast('请先选择视频文件', 'error'); return; }
|
|
const button = Array.from(document.querySelectorAll('.btn-primary')).find((node) => node.textContent.includes('发布'));
|
|
publishing = true;
|
|
if (button) button.disabled = true;
|
|
try {
|
|
progress.hidden = false;
|
|
progress.querySelector('.progress-info span').textContent = '正在保存视频与封面';
|
|
const url = await ensureVideo();
|
|
if (coverFile) coverReference = await upload('/file/upload/image', coverFile);
|
|
if (!coverReference) throw new Error('请上传封面或从视频截取一帧');
|
|
const visibility = document.querySelector('.radio-item.active')?.textContent.trim();
|
|
const switches = document.querySelectorAll('.settings-row .switch');
|
|
const moment = await api.post('/campus/moments', {
|
|
content: descriptionText ? title + '\n' + descriptionText : title,
|
|
isYouthVideo: true,
|
|
videoOriginalUrl: url,
|
|
videoCoverUrl: coverReference,
|
|
videoDuration: Math.round(preview.duration || 0),
|
|
tags: [...tags],
|
|
allowComment: switches[0]?.classList.contains('on') ?? true,
|
|
allowDownload: switches[1]?.classList.contains('on') ?? false,
|
|
category: '校园生活',
|
|
scope: visibility === '公开' ? 'CAMPUS' : 'PERSONAL'
|
|
});
|
|
if (!moment?.id) throw new Error('发布结果缺少视频编号');
|
|
localStorage.removeItem(draftKey);
|
|
location.href = 'video-detail.html?id=' + encodeURIComponent(moment.id);
|
|
} catch (error) {
|
|
showWebToast(error.message || '发布失败,请重试', 'error');
|
|
if (button) button.disabled = false;
|
|
} finally {
|
|
publishing = false;
|
|
progress.hidden = true;
|
|
}
|
|
};
|
|
};
|
|
|
|
if (page === 'publish-post.html' || page === 'publish-video.html') initPublishing();
|
|
if (page === 'publish-video.html') initVideoPublish();
|
|
if (page === 'activity-create.html') initActivityCreate();
|
|
if (page === 'topic-detail.html') initTopic();
|
|
if (page === 'wallet.html') initWallet();
|
|
if (page === 'search.html' || page === 'topic-detail.html') wireSideWidgets();
|
|
if (page === 'register.html' || page === 'select-school.html' || page === 'authentication.html') initAuthRouting();
|
|
|
|
const closeDrawer = () => document.body.classList.remove('web-drawer-open', 'web-filter-open');
|
|
topbar.querySelector('.web-menu-button')?.addEventListener('click', () => document.body.classList.toggle('web-drawer-open'));
|
|
backdrop.addEventListener('click', closeDrawer);
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key !== 'Escape') return;
|
|
closeDrawer();
|
|
const publishWrap = document.querySelector('.web-publish-wrap');
|
|
publishWrap?.classList.remove('is-open');
|
|
publishWrap?.querySelector('.web-publish-button')?.setAttribute('aria-expanded', 'false');
|
|
});
|
|
window.addEventListener('resize', () => {
|
|
if (window.innerWidth > 900) closeDrawer();
|
|
});
|
|
})();
|
|
|