- 编辑器:Tiptap 替换为 Vditor 3.11.2(wysiwyg 模式,本地托管 5.8MB,去 CDN) - 存储:Body 字段从 HTML 改为 Markdown,去除 BodyHTML,新增 Excerpt 列表摘要 - 安全:去除 bluemonday 依赖(MD 纯文本无 XSS 风险),go.mod 已清理 - Shortcode:新增 [zone:type:params] 扩展语法,支持活动/游戏/投票/资源卡片 - 图片上传:POST /api/posts/upload-image 直接返回 Vditor 原生响应格式 - 草稿:localStorage 键名改为 draft_post_body_md,与旧 HTML 草稿隔离 - 详情页:Vditor.method.min.js 客户端 MD → HTML 渲染,去 highlight.js CDN - 样式:去 Tiptap 样式 ~190 行,精简工具栏 CSS,新增卡片样式 - 文档:vditor-migration-plan.md 完整记录迁移决策、架构、用法
136 lines
5.4 KiB
JavaScript
136 lines
5.4 KiB
JavaScript
/**
|
||
* Shortcode 卡片渲染器 — 配合 [zone:type:params] Markdown 扩展语法
|
||
*
|
||
* 工作流程:
|
||
* 1. 后端 ShortcodeService 将 MD 中的 [zone:type:params] 替换为占位 <div class="zone-card">
|
||
* 2. Vditor.preview() 渲染 MD,占位 div 原样保留
|
||
* 3. 本脚本扫描所有 .zone-card,根据 data-zone-* 属性渲染对应卡片 UI
|
||
*
|
||
* 添加新卡片类型:
|
||
* 1. 在 cardRenderers 中注册类型标识和渲染函数
|
||
* 2. 渲染函数接收 id 参数,返回 HTML 字符串
|
||
* 3. 如果后端 API 已可用,在渲染函数中 fetch 数据;否则展示静态占位卡片
|
||
*
|
||
* 后端 shortcode 语法参考: /docs/vditor-migration-plan.md
|
||
*/
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
// =========================================================================
|
||
// 卡片渲染函数注册表
|
||
//
|
||
// 每个渲染函数签名为: function(id: string) → html: string
|
||
// id 来自 [zone:type:id] 中的 id 部分
|
||
// =========================================================================
|
||
|
||
var cardRenderers = {
|
||
|
||
/**
|
||
* 活动卡片 — [zone:event:活动ID]
|
||
* TODO: 接入活动 API 后改为 fetch 动态数据
|
||
*/
|
||
event: function (id) {
|
||
return '' +
|
||
'<div class="zone-card-event">' +
|
||
' <div class="zone-card-event-icon">🎪</div>' +
|
||
' <div class="zone-card-event-body">' +
|
||
' <div class="zone-card-event-title">官方活动</div>' +
|
||
' <div class="zone-card-event-id">活动 ID: ' + escapeHtml(id) + '</div>' +
|
||
' </div>' +
|
||
' <a class="zone-card-event-link" href="/events/' + escapeHtml(id) + '" target="_blank">查看详情 →</a>' +
|
||
'</div>';
|
||
},
|
||
|
||
/**
|
||
* 游戏卡片 — [zone:game:游戏slug]
|
||
* TODO: 接入游戏数据库 API 后改为 fetch 动态数据
|
||
*/
|
||
game: function (id) {
|
||
return '' +
|
||
'<div class="zone-card-game">' +
|
||
' <div class="zone-card-game-icon">🎮</div>' +
|
||
' <div class="zone-card-game-body">' +
|
||
' <div class="zone-card-game-title">相关游戏</div>' +
|
||
' <div class="zone-card-game-slug">' + escapeHtml(id) + '</div>' +
|
||
' </div>' +
|
||
' <a class="zone-card-game-link" href="/games/' + escapeHtml(id) + '" target="_blank">查看 →</a>' +
|
||
'</div>';
|
||
},
|
||
|
||
/**
|
||
* 投票组件 — [zone:poll:投票ID]
|
||
* ※预留:后端投票 API 未实现,当前展示占位卡片
|
||
*/
|
||
poll: function (id) {
|
||
return '' +
|
||
'<div class="zone-card-poll">' +
|
||
' <div class="zone-card-poll-icon">📊</div>' +
|
||
' <div class="zone-card-poll-body">' +
|
||
' <div class="zone-card-poll-title">社区投票</div>' +
|
||
' <div class="zone-card-poll-id">投票 ID: ' + escapeHtml(id) + '</div>' +
|
||
' </div>' +
|
||
' <span class="zone-card-poll-pending">即将开放</span>' +
|
||
'</div>';
|
||
},
|
||
|
||
/**
|
||
* 资源卡片 — [zone:resource:资源ID]
|
||
* ※预留:后端资源 API 未实现,当前展示占位卡片
|
||
*/
|
||
resource: function (id) {
|
||
return '' +
|
||
'<div class="zone-card-resource">' +
|
||
' <div class="zone-card-resource-icon">📦</div>' +
|
||
' <div class="zone-card-resource-body">' +
|
||
' <div class="zone-card-resource-title">推荐资源</div>' +
|
||
' <div class="zone-card-resource-id">资源 ID: ' + escapeHtml(id) + '</div>' +
|
||
' </div>' +
|
||
' <a class="zone-card-resource-link" href="/resources/' + escapeHtml(id) + '" target="_blank">查看 →</a>' +
|
||
'</div>';
|
||
}
|
||
};
|
||
|
||
// =========================================================================
|
||
// 工具函数
|
||
// =========================================================================
|
||
|
||
function escapeHtml(str) {
|
||
var div = document.createElement('div');
|
||
div.appendChild(document.createTextNode(str));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// =========================================================================
|
||
// 主渲染逻辑
|
||
// =========================================================================
|
||
|
||
/**
|
||
* renderShortcodes — 扫描容器内所有 .zone-card 占位并渲染
|
||
* @param {HTMLElement} container — 包含 shortcode 占位的 DOM 容器
|
||
*/
|
||
window.renderShortcodes = function (container) {
|
||
if (!container) return;
|
||
|
||
var placeholders = container.querySelectorAll('.zone-card');
|
||
for (var i = 0; i < placeholders.length; i++) {
|
||
var el = placeholders[i];
|
||
var type = el.getAttribute('data-zone-type');
|
||
var id = el.getAttribute('data-zone-id');
|
||
|
||
if (!type || !id) continue;
|
||
|
||
var renderer = cardRenderers[type];
|
||
if (!renderer) continue;
|
||
|
||
// 渲染卡片 HTML 并替换占位 div
|
||
try {
|
||
el.outerHTML = renderer(id);
|
||
} catch (e) {
|
||
// 渲染失败时保留占位文本
|
||
console.warn('[Shortcode] render failed for type=' + type + ' id=' + id, e);
|
||
}
|
||
}
|
||
};
|
||
})();
|