广东网站开发推荐,网站建设虚拟主机说明,自己设计app软件,建站之星安装模板失败转载请注明出处#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你#xff0c;欢迎[点赞、收藏、关注]哦~ 网页效果 https://supervisor.xfxuezhang.cn/ 创建Cloudflare数据库 1、创建D1 SQL数据库#xff0c;名称随便填#xff0c;比如mentors。 2、“控…转载请注明出处小锋学长生活大爆炸[xfxuezhagn.cn]如果本文帮助到了你欢迎[点赞、收藏、关注]哦~网页效果https://supervisor.xfxuezhang.cn/创建Cloudflare数据库1、创建D1 SQL数据库名称随便填比如mentors。2、“控制台”输入sql语句PRAGMA foreign_keys ON; -- 导师表 CREATE TABLE IF NOT EXISTS mentors ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, school TEXT NOT NULL, college TEXT, field TEXT, created_at TEXT NOT NULL DEFAULT (datetime(now)) ); -- 检索与排序索引 CREATE INDEX IF NOT EXISTS idx_mentors_name ON mentors(name); CREATE INDEX IF NOT EXISTS idx_mentors_school ON mentors(school); CREATE INDEX IF NOT EXISTS idx_mentors_college ON mentors(college); CREATE INDEX IF NOT EXISTS idx_mentors_field ON mentors(field); CREATE INDEX IF NOT EXISTS idx_mentors_created_at ON mentors(created_at); -- 点评表 CREATE TABLE IF NOT EXISTS reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, mentor_id INTEGER NOT NULL, nickname TEXT NOT NULL, rating INTEGER NOT NULL CHECK (rating 1 AND rating 5), content TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now)), ip_country TEXT, ip_region TEXT, ip_city TEXT, FOREIGN KEY (mentor_id) REFERENCES mentors(id) ON DELETE CASCADE ); -- 点评查询索引 CREATE INDEX IF NOT EXISTS idx_reviews_mentor_id ON reviews(mentor_id); CREATE INDEX IF NOT EXISTS idx_reviews_created_at ON reviews(created_at); CREATE INDEX IF NOT EXISTS idx_reviews_rating ON reviews(rating);3、示例数据INSERT INTO mentors (name, school, college, field) VALUES (张三, 某某大学, 计算机学院, 机器学习), (李四, 某某大学, 信息学院, 计算机视觉), (王五, 某研究所, NULL, 自然语言处理); INSERT INTO reviews (mentor_id, nickname, rating, content, ip_country, ip_region, ip_city) VALUES (1, 匿名, 5, 指导很细致组会频率合适。, CN, Beijing, Beijing), (1, A同学, 4, 对产出要求高适合自驱强的人。, CN, Shanghai, Shanghai), (2, 匿名, 3, 方向不错但需要更主动沟通。, NULL, NULL, NULL);创建Cloudflare Worker1、新建Worker名称随便填比如supervisor。2、点击“绑定”选择创建的D1数据库名称填“DB”。3、点击“编辑代码”填入// 删除评论的密码需要更改 const DELETE_REVIEW_PASSWORD delreview; // 删除导师的密码需要更改 const DELETE_MENTOR_PASSWORD delmentor; /******************************************************/ function json(data, status 200, headers {}) { return new Response(JSON.stringify(data, null, 2), { status, headers: { content-type: application/json; charsetutf-8, cache-control: no-store, ...headers, }, }); } function bad(msg, status 400) { return json({ ok: false, error: msg }, status); } async function readJson(request) { const ct request.headers.get(content-type) || ; if (!ct.includes(application/json)) return null; try { return await request.json(); } catch { return null; } } function getPath(url) { return new URL(url).pathname; } function htmlPage() { return !doctype html html langzh head meta charsetutf-8 / meta nameviewport contentwidthdevice-width,initial-scale1 / title导师评价网/title style :root{ --bg:#f6f7fb; --bg2:#eef2ff; --card:#ffffff; --line:rgba(15,23,42,.12); --text:#0f172a; --muted:rgba(15,23,42,.62); --accent:#2563eb; --accent2:rgba(37,99,235,.12); --danger:#e11d48; --danger2:rgba(225,29,72,.10); } *{box-sizing:border-box} body{ margin:0; font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,Helvetica Neue,Arial; color:var(--text); background: radial-gradient(900px 520px at 18% 0%, rgba(37,99,235,.12), transparent 62%), radial-gradient(900px 520px at 82% 0%, rgba(147,197,253,.20), transparent 62%), linear-gradient(180deg, var(--bg2), var(--bg)); min-height:100vh; } .wrap{max-width:920px;margin:0 auto;padding:18px 14px 40px} .top{ border:1px solid var(--line); background:linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,.70)); border-radius:18px; padding:14px 14px; box-shadow:0 18px 50px rgba(15,23,42,.08); } h1{margin:0;font-size:18px;letter-spacing:.3px} .sub{margin-top:6px;color:var(--muted);font-size:13px;line-height:1.6} .stack{display:flex;flex-direction:column;gap:12px;margin-top:12px} .card{ border:1px solid var(--line); background:var(--card); border-radius:18px; padding:14px; box-shadow:0 14px 40px rgba(15,23,42,.06); } .titleRow{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px} .title{font-weight:800;font-size:14px} .muted{color:var(--muted);font-size:13px} .row{display:flex;gap:10px;flex-wrap:wrap} .grow{flex:1;min-width:240px} .starPicker{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px solid var(--line);border-radius:14px;background:#fff} .starBtn{border:0;background:transparent;padding:4px 2px;cursor:pointer;font-size:18px;line-height:1;color:rgba(15,23,42,.25)} .starBtn.on{color:#f59e0b} .starHelp{margin-left:6px;color:var(--muted);font-size:12px} input,textarea,button{ border-radius:14px; border:1px solid var(--line); background:#fff; color:var(--text); padding:10px 12px; font-size:14px; outline:none; } input::placeholder,textarea::placeholder{color:rgba(15,23,42,.45)} textarea{width:100%;min-height:96px;resize:vertical} button{ cursor:pointer; background:var(--accent2); border-color:rgba(37,99,235,.28); color:var(--text); } button:hover{background:rgba(37,99,235,.18)} button.primary{ background:var(--accent); border-color:var(--accent); color:#fff; } button.primary:hover{filter:brightness(.96)} button.secondary{ background:#fff; border-color:var(--line); color:var(--text); } button.secondary:hover{background:rgba(15,23,42,.03)} button.danger{ background:var(--danger2); border-color:rgba(225,29,72,.25); color:var(--danger); } button.danger:hover{background:rgba(225,29,72,.14)} .pill{ display:inline-flex;align-items:center;gap:6px; padding:3px 10px;border-radius:999px; border:1px solid var(--line); background:rgba(15,23,42,.03); color:var(--muted); font-size:12px; margin-right:6px; margin-top:6px; } .list{display:flex;flex-direction:column;gap:10px} .mentor-item{ border:1px solid var(--line); background:#fff; border-radius:16px; padding:12px; cursor:pointer; transition:transform .08s ease, background .08s ease, border-color .08s ease; } .mentor-item:hover{transform:translateY(-1px);background:rgba(37,99,235,.04);border-color:rgba(37,99,235,.25)} .mentor-item.selected{background:rgba(37,99,235,.06);border-color:rgba(37,99,235,.32)} .mentorHead{display:flex;align-items:flex-start;justify-content:space-between;gap:10px} .mentorName{font-weight:900;font-size:15px} .smallRight{color:var(--muted);font-size:12px;text-align:right} .stars{letter-spacing:.8px} .divider{height:1px;background:rgba(15,23,42,.08);margin:12px 0} .review{ border:1px solid var(--line); background:#fff; border-radius:16px; padding:12px; } .reviewTop{display:flex;align-items:center;justify-content:space-between;gap:10px} .reviewBody{white-space:pre-wrap;margin-top:8px} .twoLine{display:flex;flex-direction:column;gap:6px;margin-top:8px} .badge{ display:inline-flex;align-items:center; padding:2px 8px;border-radius:999px; border:1px solid var(--line); background:rgba(15,23,42,.03); color:var(--muted); font-size:12px; } /style /head body div classwrap div classtop h1导师评价网/h1 div classsub 用于信息分享与经验交流。请在发布前尽量核实事实避免夸大或误导。br/ 请勿发布他人隐私信息、联系方式、身份证号、真实住址等敏感内容。br/ 本站内容由用户提交仅供参考不构成任何建议或承诺。若内容侵权或不实可联系管理员处理。 /div /div div classstack div classcard div classtitleRow div classtitle导师检索/div div classmuted支持模糊匹配/div /div div classrow input idq classgrow placeholder输入姓名 学校 学院 方向 / button idbtnSearch classprimary搜索/button /div /div div classcard div classtitleRow div classtitle新增导师/div div idaddMentorMsg classmuted/div /div div classrow input idnewName classgrow placeholder姓名 必填 / input idnewSchool classgrow placeholder学校 必填 / /div div classrow stylemargin-top:10px input idnewCollege classgrow placeholder学院 可选 / input idnewField classgrow placeholder方向 可选 / /div div classrow stylemargin-top:10px button idbtnAddMentor classprimary新增/button button idbtnClearMentor classsecondary清空/button /div /div div classcard div classtitleRow div classtitle导师列表/div div idmentorListHint classmuted请输入关键词搜索或直接浏览最近导师/div /div div idmentorList classlist/div /div div classcard idmentorPanel styledisplay:none div classtitleRow div classtitle当前导师/div button idbtnDeleteMentor classdanger删除导师/button /div div idcurrentMentor/div div classdivider/div div classtitleRow stylemargin-bottom:8px div classtitle写点评/div div idaddReviewMsg classmuted/div /div div classrow input idnick classgrow placeholder昵称 必填 / div classstarPicker div idratingStars/div span classstarHelp点击选择评分/span /div input idratingValue typehidden value5 / /div div stylemargin-top:10px textarea idcontent placeholder点评内容 必填/textarea /div div classrow stylemargin-top:10px button idbtnAddReview classprimary发布/button button idbtnClearReview classsecondary清空/button /div div classdivider/div div classtitleRow stylemargin-bottom:8px div classtitle点评列表/div div classrow stylegap:8px span idmentorAvg classbadge styledisplay:none/span span idmentorCount classbadge styledisplay:none/span /div /div div idreviewList/div /div /div /div script let currentMentorId null; async function api(url, options) { const res await fetch(url, options); const data await res.json().catch(() null); if (!res.ok) { const msg data data.error ? data.error : 请求失败; throw new Error(msg); } return data; } function escapeHtml(s) { return String(s).replace(/[]/g, c ({:amp;,:lt;,:gt;,:quot;,:#39;}[c])); } function stars(n) { const v Math.max(1, Math.min(5, Number(n) || 0)); let s ; for (let i 1; i 5; i) s (i v ? ★ : ☆); return s; } function starsFromAvg(avg) { if (avg null) return ☆☆☆☆☆; const v Math.max(0, Math.min(5, Number(avg))); const rounded Math.round(v); return stars(rounded); } function renderRatingStars(value) { const box document.getElementById(ratingStars); const v Math.max(1, Math.min(5, Number(value) || 5)); box.innerHTML ; for (let i 1; i 5; i) { const b document.createElement(button); b.type button; b.className starBtn (i v ? on : ); b.textContent ★; b.onclick () { document.getElementById(ratingValue).value String(i); renderRatingStars(i); }; box.appendChild(b); } } function initRatingStars() { const init Number(document.getElementById(ratingValue).value || 5); renderRatingStars(init); } function setMentorBadges(avg, count) { const a document.getElementById(mentorAvg); const c document.getElementById(mentorCount); if (avg null) { a.style.display none; } else { a.style.display inline-flex; a.innerHTML 均分 Number(avg).toFixed(2) starsFromAvg(avg); } c.style.display inline-flex; c.textContent 评论数 (count || 0); } function highlightSelectedMentor() { document.querySelectorAll(.mentor-item).forEach(el { el.classList.toggle(selected, Number(el.dataset.id) Number(currentMentorId)); }); } function mentorItem(m) { const div document.createElement(div); div.className mentor-item; div.dataset.id m.id; const avgText m.avg_rating null ? - : Number(m.avg_rating).toFixed(2); const star starsFromAvg(m.avg_rating); div.innerHTML \ div classmentorHead div div classmentorName\${escapeHtml(m.name)}/div div \${m.school ? span classpillescapeHtml(m.school)/span : } \${m.college ? span classpillescapeHtml(m.college)/span : } \${m.field ? span classpillescapeHtml(m.field)/span : } /div /div div classsmallRight div classstars\${star}/div div均分 \${avgText}/div div评论 \${m.review_count}/div /div /div \; div.onclick () selectMentor(m.id); return div; } function reviewCard(r) { const loc [r.ip_city, r.ip_region, r.ip_country].filter(Boolean).join( ); const div document.createElement(div); div.className review; div.innerHTML \ div classreviewTop div b\${escapeHtml(r.nickname || )}/b span classmuted 评分 /span span classstars\${stars(r.rating)}/span /div button classdanger删除/button /div div classreviewBody\${escapeHtml(r.content)}/div div classtwoLine div classmuted归属地 \${escapeHtml(loc || 未知)}/div div classmuted\${escapeHtml(r.created_at)}/div /div \; div.querySelector(button).onclick async () { const pwd prompt(请输入删除评论密码); if (pwd null) return; try { await api(/api/reviews/ r.id, { method: DELETE, headers: {content-type:application/json}, body: JSON.stringify({ password: pwd }) }); await loadReviews(currentMentorId); await searchMentors(); } catch (e) { alert(e.message); } }; return div; } async function searchMentors() { const q document.getElementById(q).value.trim(); const data await api(/api/mentors?query encodeURIComponent(q)); const list document.getElementById(mentorList); const hint document.getElementById(mentorListHint); list.innerHTML ; const n (data.items || []).length; if (n 0) { hint.textContent q ? 没有找到匹配导师可换关键词或先新增导师 : 目前还没有导师记录可先新增导师; list.innerHTML div classmuted stylepadding:10px暂无结果/div; return; } hint.textContent q ? 共找到 n 位导师 : 最近导师共 n 位; data.items.forEach(m list.appendChild(mentorItem(m))); highlightSelectedMentor(); } async function selectMentor(id) { currentMentorId id; document.getElementById(mentorPanel).style.display block; document.getElementById(btnDeleteMentor).onclick async () { const pwd prompt(请输入删除导师密码); if (pwd null) return; if (!currentMentorId) return; const ok confirm(确认删除该导师及其所有点评此操作不可恢复); if (!ok) return; try { await api(/api/mentors/ currentMentorId, { method: DELETE, headers: {content-type:application/json}, body: JSON.stringify({ password: pwd }) }); alert(删除成功); currentMentorId null; document.getElementById(mentorPanel).style.display none; document.getElementById(currentMentor).innerHTML ; document.getElementById(reviewList).innerHTML ; setMentorBadges(null, 0); await searchMentors(); } catch (e) { alert(e.message); } }; const m await api(/api/mentors/ id); document.getElementById(currentMentor).innerHTML \ div stylefont-weight:900;font-size:16px\${escapeHtml(m.name)}/div div stylemargin-top:6px \${m.school ? span classpillescapeHtml(m.school)/span : } \${m.college ? span classpillescapeHtml(m.college)/span : } \${m.field ? span classpillescapeHtml(m.field)/span : } /div div classmuted stylemargin-top:8px创建时间 \${escapeHtml(m.created_at)}/div \; highlightSelectedMentor(); await loadReviews(id); } async function loadReviews(mentorId) { const box document.getElementById(reviewList); box.innerHTML div classmuted加载中/div; const data await api(/api/mentors/ mentorId /reviews); box.innerHTML ; if (!data.items.length) { box.innerHTML div classmuted暂无点评/div; setMentorBadges(null, 0); return; } let sum 0; data.items.forEach(r sum Number(r.rating) || 0); const avg sum / data.items.length; setMentorBadges(avg, data.items.length); data.items.forEach(r box.appendChild(reviewCard(r))); } async function addMentor() { const name document.getElementById(newName).value.trim(); const school document.getElementById(newSchool).value.trim(); const college document.getElementById(newCollege).value.trim(); const field document.getElementById(newField).value.trim(); const msg document.getElementById(addMentorMsg); msg.textContent ; if (!name || !school) { msg.textContent 姓名和学校必填; return; } try { const data await api(/api/mentors, { method:POST, headers: {content-type:application/json}, body: JSON.stringify({ name, school, college, field }) }); msg.textContent 已新增 ID data.id; document.getElementById(newName).value ; document.getElementById(newSchool).value ; document.getElementById(newCollege).value ; document.getElementById(newField).value ; await searchMentors(); await selectMentor(data.id); } catch (e) { msg.textContent e.message; } } async function addReview() { const msg document.getElementById(addReviewMsg); msg.textContent ; if (!currentMentorId) { msg.textContent 请先选择导师; return; } const nickname document.getElementById(nick).value.trim(); const rating Number(document.getElementById(ratingValue).value); const content document.getElementById(content).value.trim(); if (!nickname) { msg.textContent 昵称必填; return; } if (!content) { msg.textContent 点评内容必填; return; } try { await api(/api/mentors/ currentMentorId /reviews, { method:POST, headers: {content-type:application/json}, body: JSON.stringify({ nickname, rating, content }) }); msg.textContent 已发布; document.getElementById(content).value ; await loadReviews(currentMentorId); await searchMentors(); } catch (e) { msg.textContent e.message; } } document.getElementById(btnSearch).onclick searchMentors; document.getElementById(q).addEventListener(keydown, e { if (e.key Enter) searchMentors(); }); document.getElementById(btnAddMentor).onclick addMentor; document.getElementById(btnClearMentor).onclick () { document.getElementById(newName).value ; document.getElementById(newSchool).value ; document.getElementById(newCollege).value ; document.getElementById(newField).value ; document.getElementById(addMentorMsg).textContent ; }; document.getElementById(btnAddReview).onclick addReview; document.getElementById(btnClearReview).onclick () { document.getElementById(nick).value ; document.getElementById(ratingValue).value 5; renderRatingStars(5); document.getElementById(content).value ; document.getElementById(addReviewMsg).textContent ; }; setMentorBadges(null, 0); initRatingStars(); searchMentors(); /script /body /html; } export default { async fetch(request, env) { const url new URL(request.url); const path getPath(request.url); if (request.method GET path /) { return new Response(htmlPage(), { headers: { content-type: text/html; charsetutf-8, cache-control: no-store, }, }); } if (path.startsWith(/api/)) { try { return await handleApi(request, env, url); } catch (e) { return bad(e e.message ? e.message : 服务器错误, 500); } } return new Response(Not Found, { status: 404 }); }, }; async function handleApi(request, env, url) { const { DB } env; const path url.pathname; if (!DB) return bad(未绑定 D1 数据库 DB, 500); if (request.method GET path /api/mentors) { const q (url.searchParams.get(query) || ).trim(); const like %${q}%; const sql SELECT m.id, m.name, m.school, m.college, m.field, m.created_at, COUNT(r.id) AS review_count, AVG(r.rating) AS avg_rating FROM mentors m LEFT JOIN reviews r ON r.mentor_id m.id WHERE (? OR m.name LIKE ? OR IFNULL(m.school,) LIKE ? OR IFNULL(m.college,) LIKE ? OR IFNULL(m.field,) LIKE ? ) GROUP BY m.id ORDER BY m.created_at DESC LIMIT 50 ; const rs await DB.prepare(sql).bind(q, like, like, like, like).all(); return json({ ok: true, items: rs.results || [] }); } if (request.method POST path /api/mentors) { const body await readJson(request); if (!body) return bad(请用 JSON 请求); const name (body.name || ).trim(); const school (body.school || ).trim(); const college (body.college || ).trim(); const field (body.field || ).trim(); if (!name || !school) return bad(姓名和学校必填); const insert await DB.prepare( INSERT INTO mentors (name, school, college, field) VALUES (?, ?, ?, ?) ).bind( name, school, college || null, field || null ).run(); return json({ ok: true, id: insert.meta.last_row_id }, 201); } const mentorIdMatch path.match(/^\/api\/mentors\/(\d)$/); if (request.method GET mentorIdMatch) { const mentorId Number(mentorIdMatch[1]); const rs await DB.prepare(SELECT * FROM mentors WHERE id ?).bind(mentorId).first(); if (!rs) return bad(导师不存在, 404); return json({ ok: true, ...rs }); } const mentorReviewsMatch path.match(/^\/api\/mentors\/(\d)\/reviews$/); if (request.method GET mentorReviewsMatch) { const mentorId Number(mentorReviewsMatch[1]); const exists await DB.prepare(SELECT 1 FROM mentors WHERE id ?).bind(mentorId).first(); if (!exists) return bad(导师不存在, 404); const rs await DB.prepare( SELECT id, mentor_id, nickname, rating, content, created_at, ip_country, ip_region, ip_city FROM reviews WHERE mentor_id ? ORDER BY created_at DESC LIMIT 100 ).bind(mentorId).all(); return json({ ok: true, items: rs.results || [] }); } if (request.method POST mentorReviewsMatch) { const mentorId Number(mentorReviewsMatch[1]); const exists await DB.prepare(SELECT 1 FROM mentors WHERE id ?).bind(mentorId).first(); if (!exists) return bad(导师不存在, 404); const body await readJson(request); if (!body) return bad(请用 JSON 请求); const nickname (body.nickname || ).trim(); const content (body.content || ).trim(); const rating Number(body.rating); if (!nickname) return bad(昵称必填); if (!content) return bad(点评内容必填); if (!Number.isFinite(rating) || rating 1 || rating 5) return bad(评分需在 1 到 5); const cf request.cf || {}; const ip_country (cf.country || ).trim(); const ip_region (cf.region || ).trim(); const ip_city (cf.city || ).trim(); const insert await DB.prepare( INSERT INTO reviews (mentor_id, nickname, rating, content, ip_country, ip_region, ip_city) VALUES (?, ?, ?, ?, ?, ?, ?) ).bind( mentorId, nickname, rating, content, ip_country || null, ip_region || null, ip_city || null ).run(); return json({ ok: true, id: insert.meta.last_row_id }, 201); } const reviewIdMatch path.match(/^\/api\/reviews\/(\d)$/); if (request.method DELETE reviewIdMatch) { const reviewId Number(reviewIdMatch[1]); const body await readJson(request); const pwd body typeof body.password string ? body.password : ; if (pwd ! DELETE_REVIEW_PASSWORD) return bad(密码错误, 403); const del await DB.prepare(DELETE FROM reviews WHERE id ?).bind(reviewId).run(); if (!del.meta.changes) return bad(评论不存在, 404); return json({ ok: true }); } const mentorIdDeleteMatch path.match(/^\/api\/mentors\/(\d)$/); if (request.method DELETE mentorIdDeleteMatch) { const mentorId Number(mentorIdDeleteMatch[1]); const body await readJson(request); const pwd body typeof body.password string ? body.password : ; if (pwd ! DELETE_MENTOR_PASSWORD) return bad(密码错误, 403); const exists await DB.prepare(SELECT 1 FROM mentors WHERE id ?).bind(mentorId).first(); if (!exists) return bad(导师不存在, 404); // 保险起见先删评论再删导师 await DB.prepare(DELETE FROM reviews WHERE mentor_id ?).bind(mentorId).run(); const del await DB.prepare(DELETE FROM mentors WHERE id ?).bind(mentorId).run(); if (!del.meta.changes) return bad(导师不存在, 404); return json({ ok: true }); } return bad(接口不存在, 404); }4、点击“部署”。完工