Files
mce/templates/MetaLab-2026/static/js/shortcode.js
Victor_Jay 6ec12010f3 feat: Tiptap替换为Vditor + MD存储重构 + Shortcode卡片扩展
- 编辑器: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 完整记录迁移决策、架构、用法
2026-05-30 15:56:22 +08:00

136 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
}
}
};
})();