- escapeHtml 8 处定义合并为 shared/utils.js 1 处:删除 shortcode.js、posts/show.html、settings/index.html、comments.js、energy.js、common.js 中的重复定义 - formatTime 6 处定义合并为 1 处:统一 ISO 时间格式化逻辑 - api() 5 处定义收敛:前台页面使用 shared/utils.js 统一 api(method,url,data),admin 使用 common.js 的 api 对象 - showToast 3 处定义合并为 1 处:自包含内联样式,前后台通用 - admin/posts.js api 调用迁移为 api.post()
130 lines
5.3 KiB
JavaScript
130 lines
5.3 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>';
|
||
}
|
||
};
|
||
|
||
// =========================================================================
|
||
// 工具函数 — escapeHtml 由 shared/utils.js 统一提供
|
||
// =========================================================================
|
||
|
||
// =========================================================================
|
||
// 主渲染逻辑
|
||
// =========================================================================
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
}
|
||
};
|
||
})();
|