2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
8.6 KiB
JavaScript
208 lines
8.6 KiB
JavaScript
function plazaError(message, code) {
|
|
return Object.assign(new Error(message), { code });
|
|
}
|
|
|
|
export function escapePublicHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
export async function loadPlazaPublicPost(pool, postId, { devPreview = false } = {}) {
|
|
const [rows] = await pool.query(
|
|
`SELECT pp.id, pp.title, pp.summary, pp.status, pp.cover_url,
|
|
pr.public_url, pr.status AS publication_status
|
|
FROM plaza_posts pp
|
|
JOIN h5_publish_records pr ON pr.id = pp.publication_id
|
|
WHERE pp.id = ? AND pp.status != 'hidden'
|
|
LIMIT 1`,
|
|
[postId],
|
|
);
|
|
const row = rows[0];
|
|
if (!row) throw plazaError('帖子不存在或已隐藏', 'POST_NOT_FOUND');
|
|
if (row.publication_status !== 'online') {
|
|
throw plazaError('关联发布未上线', 'PUBLICATION_NOT_ONLINE');
|
|
}
|
|
if (row.status !== 'published') {
|
|
const allowPreview = devPreview && row.status === 'pending_review';
|
|
if (!allowPreview) throw plazaError('帖子不存在或已隐藏', 'POST_NOT_FOUND');
|
|
}
|
|
return row;
|
|
}
|
|
|
|
export function buildPublicationUrl(publicUrl, portalBase) {
|
|
const value = String(publicUrl ?? '').trim();
|
|
if (!value) return null;
|
|
if (/^https?:\/\//i.test(value)) return value;
|
|
const base = String(portalBase ?? '').replace(/\/$/, '');
|
|
if (value.startsWith('/')) return `${base}${value}`;
|
|
return `${base}/${value.replace(/^\/+/, '')}`;
|
|
}
|
|
|
|
export function renderPlazaPostRedirectHtml(post, targetUrl, { preview = false } = {}) {
|
|
const title = escapePublicHtml(post.title);
|
|
const summary = escapePublicHtml(post.summary);
|
|
const safeTarget = escapePublicHtml(targetUrl);
|
|
const previewNote = preview
|
|
? '<p class="preview">开发预览:该帖子尚未审核通过,仅本地可见。</p>'
|
|
: '';
|
|
|
|
return `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta http-equiv="refresh" content="0;url=${safeTarget}">
|
|
<title>${title}</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
display: grid;
|
|
place-items: center;
|
|
background: #f6f3eb;
|
|
color: #17221d;
|
|
font: 16px/1.7 ui-sans-serif, system-ui, sans-serif;
|
|
}
|
|
main {
|
|
width: min(560px, calc(100% - 32px));
|
|
padding: 28px;
|
|
border-radius: 24px;
|
|
background: rgba(255, 253, 247, 0.92);
|
|
border: 1px solid rgba(23, 34, 29, 0.08);
|
|
box-shadow: 0 20px 60px rgba(45, 53, 47, 0.08);
|
|
}
|
|
h1 { margin: 0 0 12px; font-size: 28px; line-height: 1.15; }
|
|
p { margin: 0 0 12px; color: #56615b; }
|
|
a { color: #8f6b2f; }
|
|
.preview {
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
background: rgba(143, 107, 47, 0.08);
|
|
color: #6f5528;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>${title}</h1>
|
|
${summary ? `<p>${summary}</p>` : ''}
|
|
${previewNote}
|
|
<p>正在跳转到内容页…</p>
|
|
<p><a href="${safeTarget}">若未自动跳转,请点击此处</a></p>
|
|
</main>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
export function attachPlazaPublicRoutes(app, { pool, portalBase, devPreview = false }) {
|
|
app.get('/plaza/p/:postId', async (req, res) => {
|
|
if (!pool) return res.status(503).send('Plaza 未启用');
|
|
try {
|
|
const post = await loadPlazaPublicPost(pool, req.params.postId, { devPreview });
|
|
const targetUrl = buildPublicationUrl(post.public_url, portalBase);
|
|
if (!targetUrl) return res.status(404).send('内容链接不存在');
|
|
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
res.set('Cache-Control', post.status === 'published' ? 'public, max-age=60' : 'private, no-store');
|
|
return res.send(
|
|
renderPlazaPostRedirectHtml(post, targetUrl, {
|
|
preview: devPreview && post.status === 'pending_review',
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
if (error?.code === 'POST_NOT_FOUND') return res.status(404).send('帖子不存在或已隐藏');
|
|
if (error?.code === 'PUBLICATION_NOT_ONLINE') return res.status(404).send('关联发布未上线');
|
|
return res.status(500).send('页面加载失败');
|
|
}
|
|
});
|
|
|
|
app.get(['/plaza', '/plaza/'], async (_req, res) => {
|
|
if (!pool) return res.status(503).send('Plaza 未启用');
|
|
try {
|
|
const [categoryRows] = await pool.query(
|
|
`SELECT slug, name, icon FROM plaza_categories WHERE is_active = 1 ORDER BY sort_order ASC`,
|
|
);
|
|
const [postRows] = await pool.query(
|
|
`SELECT pp.id, pp.title, pp.summary, pp.cover_url, pp.published_at,
|
|
c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon,
|
|
u.slug AS user_slug, u.display_name AS user_display_name
|
|
FROM plaza_posts pp
|
|
JOIN plaza_categories c ON c.id = pp.category_id
|
|
JOIN h5_users u ON u.id = pp.user_id
|
|
WHERE pp.status = 'published'
|
|
ORDER BY pp.hot_score DESC, pp.id DESC
|
|
LIMIT 24`,
|
|
);
|
|
|
|
const cards = postRows
|
|
.map((post) => {
|
|
const title = escapePublicHtml(post.title);
|
|
const summary = escapePublicHtml(post.summary);
|
|
const author = escapePublicHtml(post.user_display_name || post.user_slug);
|
|
const category = escapePublicHtml(`${post.category_icon ?? ''} ${post.category_name ?? ''}`.trim());
|
|
const href = `/plaza/p/${encodeURIComponent(post.id)}`;
|
|
return `<a class="card" href="${href}">
|
|
<div class="card-top"><span>${category}</span><span>${author}</span></div>
|
|
<h2>${title}</h2>
|
|
${summary ? `<p>${summary}</p>` : ''}
|
|
</a>`;
|
|
})
|
|
.join('');
|
|
|
|
const categoryLinks = categoryRows
|
|
.map(
|
|
(category) =>
|
|
`<a href="/plaza/?category=${encodeURIComponent(category.slug)}">${escapePublicHtml(`${category.icon ?? ''} ${category.name}`.trim())}</a>`,
|
|
)
|
|
.join('');
|
|
|
|
res.set('Content-Type', 'text/html; charset=utf-8');
|
|
res.set('Cache-Control', 'public, max-age=30');
|
|
return res.send(`<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>TKMind 发现广场</title>
|
|
<style>
|
|
body { margin: 0; background: #f6f3eb; color: #17221d; font: 16px/1.7 ui-sans-serif, system-ui, sans-serif; }
|
|
main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 40px 0 72px; }
|
|
.hero { padding-bottom: 28px; border-bottom: 1px solid rgba(23, 34, 29, .12); }
|
|
.eyebrow { margin: 0 0 12px; color: #8f6b2f; font: 700 12px/1.4 ui-sans-serif, sans-serif; letter-spacing: .18em; text-transform: uppercase; }
|
|
h1 { margin: 0; font-size: clamp(36px, 7vw, 72px); line-height: .98; letter-spacing: -.05em; }
|
|
.lead { max-width: 720px; margin: 16px 0 0; color: #56615b; }
|
|
.categories { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
|
|
.categories a { text-decoration: none; color: #405048; border: 1px solid rgba(23,34,29,.12); border-radius: 999px; padding: 8px 12px; background: rgba(255,255,255,.6); }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; margin-top: 32px; }
|
|
.card { display: flex; flex-direction: column; gap: 14px; min-height: 220px; padding: 22px; border-radius: 24px; text-decoration: none; color: inherit; background: rgba(255,253,247,.92); border: 1px solid rgba(23,34,29,.08); box-shadow: 0 20px 60px rgba(45,53,47,.08); }
|
|
.card:hover { transform: translateY(-2px); }
|
|
.card-top, .card-foot { display: flex; justify-content: space-between; gap: 12px; font: 600 12px/1.4 ui-sans-serif, sans-serif; color: #6a746f; text-transform: uppercase; letter-spacing: .08em; }
|
|
.card h2 { margin: 0; font-size: 24px; line-height: 1.1; letter-spacing: -.03em; }
|
|
.card p { margin: 0; color: #56615b; font-size: 15px; }
|
|
.empty { margin-top: 32px; padding: 28px; border-radius: 24px; background: rgba(255,253,247,.76); border: 1px dashed rgba(23,34,29,.18); color: #56615b; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<section class="hero">
|
|
<p class="eyebrow">TKMind Plaza</p>
|
|
<h1>发现广场</h1>
|
|
<p class="lead">浏览公开发布的 MindSpace 作品。点击卡片进入帖子详情页。</p>
|
|
${categoryLinks ? `<div class="categories">${categoryLinks}</div>` : ''}
|
|
</section>
|
|
${
|
|
cards
|
|
? `<section class="grid">${cards}</section>`
|
|
: '<section class="empty">广场还没有已发布的帖子。发布到 Plaza 并通过审核后会出现在这里。</section>'
|
|
}
|
|
</main>
|
|
</body>
|
|
</html>`);
|
|
} catch {
|
|
return res.status(500).send('广场首页加载失败');
|
|
}
|
|
});
|
|
}
|