const APP_BRAND_NAME = "S-lake高校雷达网";
const state = {
route: "home",
params: {},
user: JSON.parse(localStorage.getItem("slakeUser") || "null"),
profile: JSON.parse(localStorage.getItem("slakeProfile") || "{}"),
signedCourseIds: JSON.parse(localStorage.getItem("slakeSignedCourses") || "[1,2]"),
signedActivityIds: JSON.parse(localStorage.getItem("slakeSignedActivities") || "[1]"),
calendarMonth: "2026-05",
activityCalendarMonth: "2026-05",
selectedDate: "2026-05-15",
signupSessionId: null,
demands: [
{
id: 1,
type: "技术合作",
title: "寻找工业视觉缺陷检测场景",
status: "跟进中",
date: "2026-05-18",
description: "希望对接具备真实产线数据的企业,共同验证实验室算法。"
},
{
id: 2,
type: "产业资源",
title: "先进材料中试资源对接",
status: "已受理",
date: "2026-05-14",
description: "需要寻找小批量中试平台,验证材料稳定性和制备工艺。"
}
]
};
const data = {
banner: {
title: "连接高校科研与产业资源",
subtitle: "课程、活动、资讯与伙伴需求一站触达"
},
courses: [
{
id: 5,
title: "高校科技成果转化实战营",
category: "成果转化",
status: "进行中",
date: "2026-05-25",
time: "09:30-17:30",
location: "苏州工业园区 · 科创中心",
teacher: "林清源 老师",
university: "S-lake 先进技术发展中心",
seats: 80,
signed: 72,
signupStartDate: "2026-05-01",
signupEndDate: "2026-05-24",
summary: "围绕高校成果筛选、产业需求拆解和项目路演,进行全天实战训练。",
audience: ["高校科研团队负责人", "科技成果转化经理人", "产业投资与孵化服务人员"],
agenda: ["成果筛选方法", "产业需求访谈", "路演材料打磨"]
},
{
id: 1,
title: "AI 科研成果转化闭门课",
category: "AI",
status: "报名中",
date: "2026-06-12",
endDate: "2026-06-14",
time: "14:00-17:00",
location: "苏州工业园区 · 元禾会议中心",
teacher: "陈知远 教授",
university: "上海交通大学",
seats: 48,
signed: 31,
signupStartDate: "2026-05-01",
signupEndDate: "2026-06-11",
promoUrl: "assets/course-detail-banner.svg",
summary: "面向高校科研团队和产业伙伴,拆解 AI 技术从论文、样机到商业落地的关键路径。",
audience: ["AI 方向高校老师与博士后团队", "关注 AI 项目落地的企业研发负责人", "科技成果转化与投资机构代表"],
agenda: ["成果转化案例复盘", "产业需求匹配", "闭门交流"]
},
{
id: 2,
title: "先进材料项目投融资工作坊",
category: "材料",
status: "报名中",
date: "2026-06-20",
endDate: "2026-06-21",
time: "09:30-12:00",
location: "南京大学苏州校区",
teacher: "李明澈 教授",
university: "南京大学",
seats: 36,
signed: 18,
signupStartDate: "2026-05-10",
signupEndDate: "2026-06-19",
summary: "聚焦新材料项目早期验证、知识产权梳理和资本沟通材料准备。",
audience: ["先进材料科研团队", "材料项目创业者", "关注硬科技投资的产业方与基金代表"],
agenda: ["技术路线表达", "专利与样品验证", "融资路演点评"]
},
{
id: 3,
title: "机器人感知技术应用沙龙",
category: "机器人",
status: "未开始",
date: "2026-07-05",
time: "19:00-21:00",
location: "杭州未来科技城 · 智能制造中心",
teacher: "王亦然 博士",
university: "浙江大学",
seats: 100,
signed: 100,
signupStartDate: "2026-05-01",
signupEndDate: "2026-07-04",
summary: "邀请高校实验室与企业研发负责人交流多模态感知、边缘计算和场景验证。",
audience: ["机器人与智能制造方向科研人员", "企业算法、硬件与产品负责人", "关注机器人场景验证的产业伙伴"],
agenda: ["前沿论文导读", "企业应用分享", "自由问答"]
},
{
id: 4,
title: "高校成果转化基础训练营",
category: "成果转化",
status: "已结束",
date: "2026-05-15",
time: "10:00-12:00",
location: "苏州工业园区 · 路演厅",
teacher: "周行之 老师",
university: "S-lake 先进技术发展中心",
seats: 60,
signed: 56,
signupStartDate: "2026-04-01",
signupEndDate: "2026-05-14",
summary: "面向初次参与成果转化的科研团队,讲解需求识别、商业化路径和对接材料准备。",
audience: ["首次参与成果转化的科研团队", "高校院系科研秘书与项目经理", "早期科技创业团队"],
agenda: ["成果转化流程", "报名材料准备", "案例答疑"]
},
{
id: 6,
title: "生物医药转化专题课",
category: "生物医药",
status: "报名中",
date: "2026-06-24",
time: "14:00-16:30",
location: "上海张江科学会堂",
teacher: "赵闻笛 教授",
university: "复旦大学",
seats: 50,
signed: 22,
signupStartDate: "2026-05-01",
signupEndDate: "2026-06-23",
summary: "围绕生物医药项目临床前验证、产业合作和转化路径展开专题分享。",
audience: ["生物医药科研团队", "医疗器械与药企研发负责人", "关注生命科学项目的投资机构"],
agenda: ["临床前验证", "产业合作路径", "转化案例讨论"]
}
],
activities: [
{
id: 1,
title: "长三角高校科技成果对接日",
date: "2026-06-18",
time: "13:30-18:00",
location: "苏州国际科技园",
status: "报名中",
signupStartDate: "2026-05-01",
signupEndDate: "2026-06-17",
summary: "组织高校团队、投资机构、产业方集中对接,推动技术需求和科研成果精准匹配。",
sessions: [
{ id: 101, title: "上午对接专场", date: "2026-06-18", time: "13:30-16:00", venue: "苏州国际科技园 A厅", capacity: 80, signed: 72 },
{ id: 102, title: "下午路演专场", date: "2026-06-18", time: "16:00-18:00", venue: "苏州国际科技园 B厅", capacity: 60, signed: 60 }
]
},
{
id: 2,
title: "青年科学家伙伴交流晚宴",
date: "2026-06-28",
time: "18:30-20:30",
location: "苏州金鸡湖会议中心",
status: "邀请制",
summary: "面向正式伙伴开放的小规模交流活动,促进跨校、跨学科合作。"
},
{
id: 3,
title: "产业需求闭门沟通会",
date: "2026-05-12",
time: "14:00-16:00",
location: "元禾控股会议室",
status: "已结束",
summary: "围绕企业真实技术需求,与高校老师进行小范围闭门沟通。"
},
{
id: 4,
title: "高校青年 PI 技术路演",
date: "2026-06-24",
time: "15:00-17:30",
location: "苏州工业园区 · 路演中心",
status: "报名中",
signupStartDate: "2026-05-15",
signupEndDate: "2026-06-23",
summary: "邀请高校青年 PI 展示前沿技术方向,与产业方、投资机构进行现场交流。",
sessions: [
{ id: 401, title: "路演上半场", date: "2026-06-24", time: "15:00-16:15", venue: "路演中心 1号厅", capacity: 50, signed: 22 },
{ id: 402, title: "路演下半场", date: "2026-06-24", time: "16:15-17:30", venue: "路演中心 1号厅", capacity: 50, signed: 10 }
]
}
],
news: [
{
id: 1,
title: "高校雷达网完成首批 AI 方向老师画像更新",
tag: "平台动态",
date: "2026-05-20",
summary: "围绕论文、项目、专利和产业合作意向,平台完成长三角首批重点老师画像更新。",
content: "高校雷达网近期完成 AI 方向首批重点老师画像更新,覆盖多模态大模型、具身智能、智能制造等方向。后续平台将继续补充材料、生物医药和机器人方向数据。"
},
{
id: 2,
title: "元禾先进技术发展中心启动伙伴需求共创机制",
tag: "伙伴服务",
date: "2026-05-16",
summary: "正式伙伴可通过移动端发布技术、产业、资金、场地等类型需求。",
content: "为提升高校科研团队和产业资源的连接效率,平台面向正式伙伴开放需求发布入口。需求提交后,运营团队将在后台进行分派和跟进。"
}
]
};
const app = document.querySelector("#app");
const title = document.querySelector("#pageTitle");
const tabbar = document.querySelector("#tabbar");
const topAction = document.querySelector("#topAction");
const backBtn = document.querySelector("#backBtn");
const courseSignupModal = document.querySelector("#courseSignupModal");
const signupSuccessModal = document.querySelector("#signupSuccessModal");
const signupSuccessTitle = document.querySelector("#signupSuccessTitle");
const signupSuccessMessage = document.querySelector("#signupSuccessMessage");
const signupModalTitle = document.querySelector("#signupModalTitle");
const signupCourseTitle = document.querySelector("#signupCourseTitle");
const signupCourseId = document.querySelector("#signupCourseId");
const signupType = document.querySelector("#signupType");
const signupPhoneError = document.querySelector("#signupPhoneError");
const demoToday = "2026-05-25";
const minCalendarMonth = "2026-05";
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function go(route, params = {}) {
state.route = route;
state.params = params;
history.pushState({ route, params }, "", `#${route}${params.id ? `/${params.id}` : ""}`);
render();
}
function replaceRoute(route, params = {}) {
state.route = route;
state.params = params;
history.replaceState({ route, params }, "", `#${route}${params.id ? `/${params.id}` : ""}`);
render();
}
function findById(list, id) {
return list.find((item) => String(item.id) === String(id));
}
function toast(message) {
openSignupSuccess(message, "操作成功");
}
function profileAvatarSrc() {
return state.profile?.avatar || "assets/profile-avatar.jpg";
}
function openSignupSuccess(message = "已为你同步到报名日程", modalTitle = "报名成功") {
signupSuccessTitle.textContent = modalTitle;
signupSuccessMessage.textContent = message;
signupSuccessModal.classList.add("visible");
signupSuccessModal.setAttribute("aria-hidden", "false");
}
function closeSignupSuccess() {
signupSuccessModal.classList.remove("visible");
signupSuccessModal.setAttribute("aria-hidden", "true");
}
function parseDateOnly(dateText) {
return new Date(`${dateText}T00:00:00`);
}
function isWithinSignupWindow(startText, endText, todayText = demoToday) {
const today = parseDateOnly(todayText);
if (startText && today < parseDateOnly(startText)) return false;
if (endText && today > parseDateOnly(endText)) return false;
return true;
}
function hasCourseCapacity(course) {
const capacity = Number(course.seats) || 0;
if (capacity <= 0) return true;
return Number(course.signed) < capacity;
}
function isCourseSignupOpen(course) {
if (!isWithinSignupWindow(course.signupStartDate, course.signupEndDate)) return false;
if (!hasCourseCapacity(course)) return false;
if (getCourseStatus(course) === "已结束") return false;
return true;
}
function canShowCourseSignupButton(course) {
return isCourseSignupOpen(course) && !state.signedCourseIds.includes(course.id);
}
function getActivityDisplayStatus(activity) {
if (isWithinSignupWindow(activity.signupStartDate, activity.signupEndDate)) return "报名中";
return getActivityStatus(activity);
}
function isActivitySignupOpen(activity) {
if (!isWithinSignupWindow(activity.signupStartDate, activity.signupEndDate)) return false;
if (getActivityDisplayStatus(activity) !== "报名中") return false;
return Array.isArray(activity.sessions) && activity.sessions.some((session) => hasSessionCapacity(session));
}
function hasSessionCapacity(session) {
const capacity = Number(session.capacity) || 0;
if (capacity <= 0) return true;
return Number(session.signed) < capacity;
}
function canShowActivitySignupButton(activity) {
return isActivitySignupOpen(activity) && !state.signedActivityIds.includes(activity.id);
}
function openCourseSignup(id) {
const course = findById(data.courses, id);
if (!course || !canShowCourseSignupButton(course)) return;
signupCourseId.value = id;
signupType.value = "course";
state.signupSessionId = null;
signupModalTitle.textContent = "课程报名";
signupCourseTitle.textContent = course.title;
signupPhoneError.textContent = "";
hideSignupSessionBlock();
courseSignupModal.classList.add("visible");
courseSignupModal.setAttribute("aria-hidden", "false");
}
function openActivitySignup(id) {
const activity = findById(data.activities, id);
if (!activity || !canShowActivitySignupButton(activity)) return;
signupCourseId.value = id;
signupType.value = "activity";
signupModalTitle.textContent = "活动报名";
signupCourseTitle.textContent = activity.title;
signupPhoneError.textContent = "";
renderSignupSessionOptions(activity);
state.signupSessionId = null;
courseSignupModal.classList.add("visible");
courseSignupModal.setAttribute("aria-hidden", "false");
}
function hideSignupSessionBlock() {
const block = document.querySelector("#signupSessionBlock");
if (block) block.hidden = true;
const err = document.querySelector("#signupSessionError");
if (err) err.textContent = "";
}
function renderSignupSessionOptions(activity) {
const block = document.querySelector("#signupSessionBlock");
const list = document.querySelector("#signupSessionOptions");
if (!block || !list) return;
const sessions = activity.sessions || [];
list.innerHTML = sessions.map((session) => {
const full = !hasSessionCapacity(session);
const active = state.signupSessionId === session.id;
return `
`;
}).join("");
block.hidden = sessions.length === 0;
}
function closeCourseSignup() {
courseSignupModal.classList.remove("visible");
courseSignupModal.setAttribute("aria-hidden", "true");
signupPhoneError.textContent = "";
state.signupSessionId = null;
hideSignupSessionBlock();
document.querySelector("#courseSignupForm").reset();
}
function isValidMobile(phone) {
return /^1[3-9]\d{9}$/.test(phone);
}
function tag(text, type = "") {
return `${escapeHtml(text)}`;
}
function persistSignup(type) {
if (type === "course") {
localStorage.setItem("slakeSignedCourses", JSON.stringify(state.signedCourseIds));
} else {
localStorage.setItem("slakeSignedActivities", JSON.stringify(state.signedActivityIds));
}
}
function getStatusByDate(dateText) {
if (dateText === demoToday) return "进行中";
if (dateText < demoToday) return "已结束";
return "未开始";
}
function getCourseEndDate(course) {
return course.endDate || course.date;
}
function getActivityEndDate(activity) {
return activity.endDate || activity.date;
}
function isCourseOnDate(course, dateText) {
return course.date <= dateText && dateText <= getCourseEndDate(course);
}
function isActivityOnDate(activity, dateText) {
return activity.date <= dateText && dateText <= getActivityEndDate(activity);
}
function daysBetween(startText, endText) {
const start = new Date(`${startText}T00:00:00`);
const end = new Date(`${endText}T00:00:00`);
return Math.round((end - start) / 86400000);
}
function getCourseStatus(course) {
if (course.date <= demoToday && demoToday <= getCourseEndDate(course)) return "进行中";
if (getCourseEndDate(course) < demoToday) return "已结束";
return "未开始";
}
function getActivityStatus(activity) {
if (activity.date <= demoToday && demoToday <= getActivityEndDate(activity)) return "进行中";
if (getActivityEndDate(activity) < demoToday) return "已结束";
return "未开始";
}
function statusTag(status) {
const map = { "未开始": "blue", "进行中": "green", "已结束": "" };
return tag(status, map[status] || "");
}
function statusClass(status) {
const map = { "未开始": "pending", "进行中": "active", "已结束": "done" };
return map[status] || "pending";
}
function coverClass(category) {
if (category.includes("AI")) return "cover-ai";
if (category.includes("材料")) return "cover-material";
if (category.includes("机器人")) return "cover-robot";
return "cover-transfer";
}
function sortByCourseTime(courses) {
return [...courses].sort((a, b) => {
const aTime = new Date(`${a.date} ${a.time.split("-")[0]}`.replace(/-/g, "/")).getTime();
const bTime = new Date(`${b.date} ${b.time.split("-")[0]}`.replace(/-/g, "/")).getTime();
return aTime - bTime;
});
}
function sortByStatusThenTime(courses) {
const order = { "进行中": 0, "未开始": 1, "已结束": 2 };
return [...courses].sort((a, b) => {
const statusDelta = order[getCourseStatus(a)] - order[getCourseStatus(b)];
if (statusDelta) return statusDelta;
const aTime = new Date(`${a.date} ${a.time.split("-")[0]}`.replace(/-/g, "/")).getTime();
const bTime = new Date(`${b.date} ${b.time.split("-")[0]}`.replace(/-/g, "/")).getTime();
return aTime - bTime;
});
}
function renderHome() {
const availableCourses = data.courses.filter((course) => getCourseStatus(course) !== "已结束");
const hotCourses = sortByCourseTime(availableCourses).slice(0, 3).map(courseCard).join("");
const latestCourses = sortByCourseTime(availableCourses).slice(0, 3).map(courseCard).join("");
return `
${serviceItem("course", "课程列表", "", "courses")}
${serviceItem("activity", "活动列表", "", "activities")}
${serviceItem("calendar", "课程日历", "", "calendar")}
`;
}
function renderCalendarCells() {
const [year, month] = state.calendarMonth.split("-").map(Number);
const first = new Date(year, month - 1, 1);
const start = new Date(year, month - 1, 1 - first.getDay());
const cells = Array.from({ length: 35 }, (_, index) => {
const date = new Date(start);
date.setDate(start.getDate() + index);
const dateText = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
return {
day: String(date.getDate()),
dateText,
muted: date.getMonth() !== month - 1,
past: dateText < demoToday,
courses: data.courses.filter((item) => isCourseOnDate(item, dateText))
};
});
return cells.map((cell, index) => {
const weekStart = cells[Math.floor(index / 7) * 7].dateText;
const weekEnd = cells[Math.floor(index / 7) * 7 + 6].dateText;
const event = cell.courses.map((course) => {
const visibleStart = course.date > weekStart ? course.date : weekStart;
if (cell.dateText !== visibleStart) return "";
const visibleEnd = getCourseEndDate(course) < weekEnd ? getCourseEndDate(course) : weekEnd;
const spanDays = daysBetween(visibleStart, visibleEnd) + 1;
return `
${escapeHtml(course.title)}
`;
}).join("");
const hasCourse = cell.courses.length > 0;
const signedMark = cell.courses.some((course) => state.signedCourseIds.includes(course.id))
? ``
: "";
return `
`;
}).join("");
}
function renderCourseCalendarBlock() {
const isMinMonth = state.calendarMonth <= minCalendarMonth;
return `
${escapeHtml(state.calendarMonth.replace("-", "年"))}月
未开始
进行中
已结束
日一二三四五六
${renderCalendarCells()}
`;
}
function renderActivityCalendarCells() {
const [year, month] = state.activityCalendarMonth.split("-").map(Number);
const first = new Date(year, month - 1, 1);
const start = new Date(year, month - 1, 1 - first.getDay());
const cells = Array.from({ length: 35 }, (_, index) => {
const date = new Date(start);
date.setDate(start.getDate() + index);
const dateText = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
return {
day: String(date.getDate()),
dateText,
muted: date.getMonth() !== month - 1,
past: dateText < demoToday,
activities: data.activities.filter((item) => isActivityOnDate(item, dateText))
};
});
return cells.map((cell, index) => {
const weekStart = cells[Math.floor(index / 7) * 7].dateText;
const weekEnd = cells[Math.floor(index / 7) * 7 + 6].dateText;
const hasActivity = cell.activities.length > 0;
const event = cell.activities.map((activity) => {
const visibleStart = activity.date > weekStart ? activity.date : weekStart;
if (cell.dateText !== visibleStart) return "";
const visibleEnd = getActivityEndDate(activity) < weekEnd ? getActivityEndDate(activity) : weekEnd;
const spanDays = daysBetween(visibleStart, visibleEnd) + 1;
return `${escapeHtml(activity.title)}
`;
}).join("");
const signedMark = cell.activities.some((activity) => state.signedActivityIds.includes(activity.id))
? ``
: "";
return `
`;
}).join("");
}
function renderActivityCalendarBlock() {
const isMinMonth = state.activityCalendarMonth <= minCalendarMonth;
return `
${escapeHtml(state.activityCalendarMonth.replace("-", "年"))}月
未开始
进行中
已结束
日一二三四五六
${renderActivityCalendarCells()}
`;
}
function serviceItem(icon, titleText, desc, route) {
return `
`;
}
function serviceIcon(icon) {
const icons = {
course: `
`,
activity: `
`,
calendar: `
`
};
return icons[icon] || "";
}
function courseCard(item) {
const status = getCourseStatus(item);
const signed = state.signedCourseIds.includes(item.id);
return `
${escapeHtml(item.category)}
${escapeHtml(item.title)}
${statusTag(status)}
`;
}
function renderCourses() {
const categories = ["全部", ...new Set(data.courses.map((course) => course.category).filter((category) => category !== "机器人"))];
const active = state.params.category || "全部";
const activeStatus = state.params.courseStatus || "";
const keyword = state.params.keyword || "";
const courses = (active === "全部" ? data.courses : data.courses.filter((course) => course.category === active))
.filter((course) => {
const status = getCourseStatus(course);
const matchStatus = activeStatus ? status === activeStatus : status !== "已结束";
const matchKeyword = !keyword || `${course.title}${course.teacher}${course.location}`.includes(keyword);
return matchStatus && matchKeyword;
});
const sortedCourses = sortByStatusThenTime(courses);
const statuses = ["进行中", "未开始", "已结束"];
return `
${categories.map((item) => ``).join("")}
${statuses.map((item) => ``).join("")}
${sortedCourses.map(courseCard).join("")}
`;
}
function renderMyCourses() {
const signed = data.courses.filter((item) => state.signedCourseIds.includes(item.id));
const statuses = ["全部", "未开始", "进行中", "已结束"];
const activeStatus = statuses.includes(state.params.myCourseStatus) ? state.params.myCourseStatus : "全部";
const courses = activeStatus === "全部" ? signed : signed.filter((item) => getCourseStatus(item) === activeStatus);
return `
${statuses.map((item) => ``).join("")}
${courses.length ? courses.map((item) => `
${escapeHtml(item.category)}
${escapeHtml(item.title)}
${statusTag(getCourseStatus(item))}
`).join("") : `
暂无${activeStatus === "全部" ? "已报名" : activeStatus}课程
`}
`;
}
function renderCourseDetailSignupButton(item) {
if (state.signedCourseIds.includes(item.id)) {
return ``;
}
if (!isCourseSignupOpen(item)) {
return "";
}
return ``;
}
function renderCourseDetail() {
const item = findById(data.courses, state.params.id);
if (!item) return `课程不存在
`;
const signupButton = renderCourseDetailSignupButton(item);
if (item.promoUrl) {
return `
${escapeHtml(item.title)}
${signupButton}
`;
}
return `
${escapeHtml(item.title)}
${escapeHtml(item.summary)}
课程安排
${scheduleItem("时间", `${item.date} ${item.time}`)}
${scheduleItem("地点", item.location)}
${scheduleItem("名额", `${item.signed}/${item.seats} 人`)}
${item.signupEndDate ? scheduleItem("报名截止", item.signupEndDate) : ""}
主讲师资
${escapeHtml(item.teacher)}
${escapeHtml(item.university)}
长期关注${escapeHtml(item.category)}方向科研成果转化与产业应用。
招生对象
${(item.audience || []).map((audience) => `
${escapeHtml(audience)}
`).join("")}
${signupButton}
`;
}
function infoRow(label, value) {
return `${escapeHtml(label)}${escapeHtml(value)}
`;
}
function scheduleItem(label, value) {
return `${escapeHtml(label)}:${escapeHtml(value)}
`;
}
function renderActivities() {
const categories = ["全部", "路演对接", "伙伴交流", "产业需求"];
const active = state.params.activityCategory || "全部";
const activeStatus = state.params.activityStatus || "";
const keyword = state.params.activityKeyword || "";
const activities = data.activities.filter((item) => {
const matchCategory = active === "全部" || item.title.includes(active.replace("交流", ""));
const status = getActivityStatus(item);
const matchStatus = activeStatus ? status === activeStatus : true;
const matchKeyword = !keyword || `${item.title}${item.location}${item.summary}`.includes(keyword);
return matchCategory && matchStatus && matchKeyword;
});
const statuses = ["未开始", "进行中", "已结束"];
return `
${categories.map((item) => ``).join("")}
${statuses.map((item) => ``).join("")}
${activities.map(activityCard).join("")}
`;
}
function renderMyActivities() {
const signed = data.activities.filter((item) => state.signedActivityIds.includes(item.id));
const statuses = ["全部", "未开始", "进行中", "已结束"];
const activeStatus = statuses.includes(state.params.myActivityStatus) ? state.params.myActivityStatus : "全部";
const activities = activeStatus === "全部" ? signed : signed.filter((item) => getActivityStatus(item) === activeStatus);
return `
${statuses.map((item) => ``).join("")}
${activities.length ? activities.map((item) => `
${escapeHtml(item.title)}
${statusTag(getActivityStatus(item))}
${escapeHtml(item.summary)}
${escapeHtml(item.date)} ${escapeHtml(item.time)} · ${escapeHtml(item.location)}
`).join("") : `
暂无${activeStatus === "全部" ? "已报名" : activeStatus}活动
`}
`;
}
function activityCard(item) {
const status = getActivityDisplayStatus(item);
const statusTagHtml = status === "报名中" ? tag(status, "brand") : statusTag(status);
return `
${statusTagHtml}
${escapeHtml(item.title)}
${escapeHtml(item.summary)}
${escapeHtml(item.date)} ${escapeHtml(item.time)} · ${escapeHtml(item.location)}
${canShowActivitySignupButton(item) ? `` : ""}
`;
}
function renderActivityDetailSignupButton(item) {
if (state.signedActivityIds.includes(item.id)) {
return ``;
}
if (!isActivitySignupOpen(item)) {
return "";
}
return ``;
}
function renderActivityDetail() {
const item = findById(data.activities, state.params.id);
if (!item) return `活动不存在
`;
const status = getActivityDisplayStatus(item);
const signupButton = renderActivityDetailSignupButton(item);
const sessions = (item.sessions || []).map((session) => {
const full = !hasSessionCapacity(session);
return `
${escapeHtml(session.title)}
${escapeHtml(session.date)} ${escapeHtml(session.time)} · ${escapeHtml(session.venue)}
${full ? "名额已满" : `剩余 ${Math.max(Number(session.capacity) - Number(session.signed), 0)} / ${session.capacity} 人`}
`;
}).join("");
return `
${status === "报名中" ? tag(status, "brand") : statusTag(status)}
${escapeHtml(item.title)}
${escapeHtml(item.summary)}
${infoRow("日期", item.date)}
${infoRow("时间", item.time)}
${infoRow("地点", item.location)}
${item.signupEndDate ? infoRow("报名截止", item.signupEndDate) : ""}
${sessions ? `` : ""}
${signupButton}
`;
}
function newsCard(item) {
return `
${escapeHtml(item.tag)}
${escapeHtml(item.title)}
${escapeHtml(item.date)}
`;
}
function renderDiscover() {
const categories = ["全部", "行业动态", "政策信息", "平台动态"];
const active = state.params.newsCategory || "全部";
const news = data.news.filter((item) => active === "全部" || item.tag === active);
return `
${categories.map((item) => ``).join("")}
${news.map(newsCard).join("")}
`;
}
function renderNewsDetail() {
const item = findById(data.news, state.params.id);
if (!item) return `资讯不存在
`;
return `
${escapeHtml(item.title)}
${escapeHtml(item.date)}
${escapeHtml(item.content)}
`;
}
function renderCalendar() {
return renderCourseCalendarBlock();
}
function renderActivityCalendar() {
return renderActivityCalendarBlock();
}
function renderDemands() {
return `
伙伴需求
正式伙伴可提交技术、产业、资金等协作需求
${state.demands.length ? `${state.demands.map(demandCard).join("")}
` : `暂无需求
`}
`;
}
function demandCard(item) {
return `
${tag(item.type, "brand")}${tag(item.status)}
${escapeHtml(item.title)}
${escapeHtml(item.description)}
${escapeHtml(item.date)}
`;
}
function renderDemandCreate() {
return `
`;
}
function renderProfile() {
const user = state.user || {};
const profile = state.profile || {};
const isPartner = state.user ? Boolean(state.user.isPartner) : true;
const displayName = profile.name || user.nickName || "王清萍";
return `
${escapeHtml(displayName)}
`;
}
function renderProfileEdit() {
const profile = state.profile || {};
return `
`;
}
function renderAbout() {
return `
关于我们
高校雷达网用于连接高校科研人才、课程活动与产业资源,帮助政企与高校团队更高效地完成成果转化对接。
${infoRow("联系电话", "0512-0000 0000")}
${infoRow("联系邮箱", "service@example.com")}
${infoRow("用户协议", "演示版本")}
${infoRow("隐私政策", "演示版本")}
`;
}
function profileMenuIcon(type) {
const icons = {
profile: "M7 5.5h10v13H7z M9.4 9h5.2 M9.4 12h5.2 M9.4 15h3.2",
course: "M6 6.5c1.8-.9 3.7-.9 5.5 0v11c-1.8-.9-3.7-.9-5.5 0v-11z M12.5 6.5c1.8-.9 3.7-.9 5.5 0v11c-1.8-.9-3.7-.9-5.5 0v-11z",
activity: "M7 5.5h10v13H7z M9 4v3 M15 4v3 M7 9h10 M10 12h4 M10 15h3",
demand: "M12 5.5l1.8 3.7 4.1.6-3 2.9.7 4.1-3.6-1.9-3.6 1.9.7-4.1-3-2.9 4.1-.6L12 5.5z",
star: "M12 5.4l1.9 3.8 4.2.6-3 2.9.7 4.2-3.8-2-3.8 2 .7-4.2-3-2.9 4.2-.6L12 5.4z",
contact: "M7 15.5v-3.2a5 5 0 0 1 10 0v3.2 M7 15.5h2v2H7z M15 15.5h2v2h-2z M9 18.5h6"
};
return `
`;
}
function menuItem(label, route, icon = "profile") {
return `
`;
}
function renderLogin() {
return `
S
${APP_BRAND_NAME}
连接长三角高校科研人才、课程活动与产业伙伴需求
登录演示
纯 HTML 版本不调用微信授权,点击后写入本地演示身份。
`;
}
function formatDate(dateText) {
const date = new Date(dateText.replace(/-/g, "/"));
return `${String(date.getMonth() + 1).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}`;
}
function formatWeekday(dateText) {
return ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][new Date(dateText.replace(/-/g, "/")).getDay()];
}
const titles = {
home: APP_BRAND_NAME,
courses: "课程学习",
courseDetail: "课程详情",
myCourses: "我的课程",
activities: "活动参与",
activityDetail: "活动详情",
myActivities: "我的活动",
activityCalendar: "活动日历",
discover: "发现",
newsDetail: "资讯详情",
calendar: "课程日历",
demands: "伙伴需求",
demandCreate: "发布需求",
profile: "我的",
profileEdit: "个人资料",
about: "关于我们",
login: "登录"
};
const tabRoutes = ["home", "courses", "activities", "discover", "profile"];
const tabActiveRoute = {
courseDetail: "courses",
myCourses: "profile",
calendar: "courses",
activityDetail: "activities",
myActivities: "profile",
activityCalendar: "activities",
newsDetail: "discover",
demands: "profile",
demandCreate: "profile",
profileEdit: "profile",
about: "profile",
login: "profile"
};
function render() {
const route = state.route;
document.body.dataset.route = route;
document.body.classList.toggle("home-route", route === "home");
title.textContent = titles[route] || APP_BRAND_NAME;
backBtn.classList.toggle("visible", !tabRoutes.includes(route));
tabbar.style.display = "flex";
topAction.textContent = state.user ? "已登录" : "登录";
const activeTab = tabActiveRoute[route] || route;
document.querySelectorAll(".tab-item").forEach((item) => {
item.classList.toggle("active", item.dataset.route === activeTab);
});
const views = {
home: renderHome,
courses: renderCourses,
courseDetail: renderCourseDetail,
myCourses: renderMyCourses,
activities: renderActivities,
activityDetail: renderActivityDetail,
myActivities: renderMyActivities,
activityCalendar: renderActivityCalendar,
discover: renderDiscover,
newsDetail: renderNewsDetail,
calendar: renderCalendar,
demands: renderDemands,
demandCreate: renderDemandCreate,
profile: renderProfile,
profileEdit: renderProfileEdit,
about: renderAbout,
login: renderLogin
};
app.innerHTML = (views[route] || renderHome)();
app.scrollTop = 0;
window.scrollTo({ top: 0, behavior: "auto" });
}
function parseHash() {
const [route = "home", id] = location.hash.replace(/^#/, "").split("/");
replaceRoute(route || "home", id ? { id } : {});
}
document.addEventListener("click", (event) => {
const goEl = event.target.closest("[data-go]");
if (goEl) {
go(goEl.dataset.go);
return;
}
const signupCourseEl = event.target.closest("[data-signup-course]");
if (signupCourseEl) {
event.stopPropagation();
const id = Number(signupCourseEl.dataset.signupCourse);
openCourseSignup(id);
return;
}
const sessionEl = event.target.closest("[data-session-id]");
if (sessionEl && !sessionEl.disabled) {
state.signupSessionId = Number(sessionEl.dataset.sessionId);
const activity = findById(data.activities, Number(signupCourseId.value));
if (activity) renderSignupSessionOptions(activity);
const err = document.querySelector("#signupSessionError");
if (err) err.textContent = "";
return;
}
if (event.target.closest("[data-close-signup]") || event.target === courseSignupModal) {
closeCourseSignup();
return;
}
if (event.target.closest("[data-close-success]") || event.target === signupSuccessModal) {
closeSignupSuccess();
return;
}
const signupActivityEl = event.target.closest("[data-signup-activity]");
if (signupActivityEl) {
event.stopPropagation();
const id = Number(signupActivityEl.dataset.signupActivity);
openActivitySignup(id);
return;
}
const detailEl = event.target.closest("[data-detail]");
if (detailEl) {
const map = {
course: "courseDetail",
activity: "activityDetail",
news: "newsDetail"
};
go(map[detailEl.dataset.detail], { id: detailEl.dataset.id });
return;
}
const categoryEl = event.target.closest("[data-category]");
if (categoryEl) {
state.params.category = categoryEl.dataset.category;
render();
return;
}
const courseStatusEl = event.target.closest("[data-course-status]");
if (courseStatusEl) {
const status = courseStatusEl.dataset.courseStatus;
state.params.courseStatus = state.params.courseStatus === status ? "" : status;
render();
return;
}
const myCourseStatusEl = event.target.closest("[data-my-course-status]");
if (myCourseStatusEl) {
state.params.myCourseStatus = myCourseStatusEl.dataset.myCourseStatus;
render();
return;
}
const activityCategoryEl = event.target.closest("[data-activity-category]");
if (activityCategoryEl) {
state.params.activityCategory = activityCategoryEl.dataset.activityCategory;
render();
return;
}
const activityStatusEl = event.target.closest("[data-activity-status]");
if (activityStatusEl) {
const status = activityStatusEl.dataset.activityStatus;
state.params.activityStatus = state.params.activityStatus === status ? "" : status;
render();
return;
}
const myActivityStatusEl = event.target.closest("[data-my-activity-status]");
if (myActivityStatusEl) {
state.params.myActivityStatus = myActivityStatusEl.dataset.myActivityStatus;
render();
return;
}
const newsCategoryEl = event.target.closest("[data-news-category]");
if (newsCategoryEl) {
state.params.newsCategory = newsCategoryEl.dataset.newsCategory;
render();
return;
}
const monthEl = event.target.closest("[data-month]");
if (monthEl) {
const [year, month] = state.calendarMonth.split("-").map(Number);
const next = new Date(year, month - 1 + Number(monthEl.dataset.month), 1);
const nextMonth = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`;
if (nextMonth < minCalendarMonth) return;
state.calendarMonth = nextMonth;
state.selectedDate = `${state.calendarMonth}-01`;
render();
return;
}
const activityMonthEl = event.target.closest("[data-activity-month]");
if (activityMonthEl) {
const [year, month] = state.activityCalendarMonth.split("-").map(Number);
const next = new Date(year, month - 1 + Number(activityMonthEl.dataset.activityMonth), 1);
const nextMonth = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`;
if (nextMonth < minCalendarMonth) return;
state.activityCalendarMonth = nextMonth;
render();
return;
}
const dateEl = event.target.closest("[data-date]");
if (dateEl) {
state.selectedDate = dateEl.dataset.date;
const dayCourse = data.courses.find((item) => isCourseOnDate(item, state.selectedDate));
if (dayCourse) go("courseDetail", { id: dayCourse.id });
return;
}
const activityDateEl = event.target.closest("[data-activity-date]");
if (activityDateEl) {
const dayActivity = data.activities.find((item) => isActivityOnDate(item, activityDateEl.dataset.activityDate));
if (dayActivity) go("activityDetail", { id: dayActivity.id });
return;
}
const signupEl = event.target.closest("[data-signup]");
if (signupEl) {
openSignupSuccess(signupEl.dataset.signup === "activity" ? "活动已同步到你的活动日程" : "课程已同步到你的课程日程");
return;
}
const checkinEl = event.target.closest("[data-checkin]");
if (checkinEl) {
toast("签到成功");
return;
}
const authEl = event.target.closest("[data-auth]");
if (authEl) {
if (authEl.dataset.auth === "logout") {
state.user = null;
localStorage.removeItem("slakeUser");
toast("已退出登录");
render();
return;
}
state.user = {
nickName: "高校伙伴",
role: "正式伙伴",
isPartner: true,
openId: "mock-openid",
unionId: "mock-unionid",
organization: "S-lake 先进技术发展中心"
};
localStorage.setItem("slakeUser", JSON.stringify(state.user));
toast("登录成功");
go("profile");
}
});
document.addEventListener("input", (event) => {
if (event.target.matches("#courseSignupForm [name='phone']")) {
signupPhoneError.textContent = "";
}
});
document.addEventListener("change", (event) => {
if (!event.target.matches("#profileAvatarInput")) return;
const [file] = event.target.files || [];
if (!file || !file.type.startsWith("image/")) return;
const reader = new FileReader();
reader.addEventListener("load", () => {
state.profile = {
...(state.profile || {}),
avatar: String(reader.result || "")
};
const preview = document.querySelector("#profileAvatarPreview");
if (preview) preview.src = state.profile.avatar;
});
reader.readAsDataURL(file);
});
document.addEventListener("submit", (event) => {
event.preventDefault();
const form = new FormData(event.target);
if (event.target.id === "courseSearchForm") {
state.params.keyword = String(form.get("keyword") || "");
render();
return;
}
if (event.target.id === "activitySearchForm") {
state.params.activityKeyword = String(form.get("keyword") || "");
state.params.activityStatus = state.params.activityStatus || "";
render();
return;
}
if (event.target.id === "courseSignupForm") {
const id = Number(form.get("courseId"));
const type = String(form.get("signupType") || "course");
const phone = String(form.get("phone") || "").trim();
const sessionError = document.querySelector("#signupSessionError");
if (type === "activity") {
const activity = findById(data.activities, id);
if (!activity || !canShowActivitySignupButton(activity)) {
if (sessionError) sessionError.textContent = "当前不可报名";
return;
}
const session = (activity.sessions || []).find((row) => row.id === state.signupSessionId);
if (!session) {
if (sessionError) sessionError.textContent = "请选择场次";
return;
}
if (!hasSessionCapacity(session)) {
if (sessionError) sessionError.textContent = "该场次名额已满";
return;
}
if (sessionError) sessionError.textContent = "";
if (phone && !isValidMobile(phone)) {
signupPhoneError.textContent = "请输入正确的手机号";
event.target.elements.phone.focus();
return;
}
signupPhoneError.textContent = "";
session.signed = Number(session.signed) + 1;
if (!state.signedActivityIds.includes(id)) state.signedActivityIds.push(id);
persistSignup("activity");
closeCourseSignup();
render();
openSignupSuccess("活动已同步到你的活动日程");
return;
}
const course = findById(data.courses, id);
if (!course || !canShowCourseSignupButton(course)) return;
if (!isValidMobile(phone)) {
signupPhoneError.textContent = "请输入正确的手机号";
event.target.elements.phone.focus();
return;
}
signupPhoneError.textContent = "";
course.signed = Number(course.signed) + 1;
if (!state.signedCourseIds.includes(id)) state.signedCourseIds.push(id);
persistSignup("course");
closeCourseSignup();
render();
openSignupSuccess("课程已同步到你的课程日程");
return;
}
if (event.target.id === "profileForm") {
state.profile = {
...(state.profile || {}),
name: form.get("name"),
company: form.get("company"),
title: form.get("title"),
research: form.get("research")
};
localStorage.setItem("slakeProfile", JSON.stringify(state.profile));
toast("资料已保存");
go("profile");
return;
}
if (event.target.id !== "demandForm") return;
state.demands.unshift({
id: Date.now(),
type: form.get("type"),
title: form.get("title"),
description: form.get("description"),
status: "已提交",
date: "2026-05-25"
});
toast("需求已提交");
go("demands");
});
tabbar.addEventListener("click", (event) => {
const item = event.target.closest(".tab-item");
if (item) go(item.dataset.route);
});
topAction.addEventListener("click", () => {
if (!state.user) go("login");
});
backBtn.addEventListener("click", () => {
history.length > 1 ? history.back() : go("home");
});
window.addEventListener("popstate", (event) => {
if (event.state) {
state.route = event.state.route;
state.params = event.state.params || {};
render();
return;
}
parseHash();
});
parseHash();