幻影二级域名分发系统 V6 门户模板开发文档
文档版本:6.0.5
适用范围:系统内置 Vue 门户、可安装的 portal 静态模板、门户首页、文章详情页和开放 API 文档页
1. 门户模板运行方式
系统支持内置模板和外置模板,两者对访客使用相同的公开地址:
| 页面 | 公开地址 | 是否需要登录 |
|---|---|---|
| 门户首页 | / |
否 |
| 首页兼容地址 | /home |
否,外置模板会统一回到 / |
| 开放 API 文档 | /portal/api-docs |
否 |
| 文章详情 | /portal/article/{id} |
否 |
| 用户登录 | /user/login |
否 |
| 用户注册 | /user/register |
否 |
| 后台登录 | /admin/login |
否 |
外置模板文件虽然存放在:
但这个地址只是系统内部的静态资源目录,不是门户公开地址。模板代码、导航菜单、文章链接和对外文档中都不要展示或跳转到该目录。
1.1 模板选择流程
门户入口通过以下公开接口获取当前模板:
内置模板返回示例:
“code”: 200,
“message”: “获取门户模板成功”,
“data”: {
“code”: “builtin”,
“name”: “默认门户模板”,
“mode”: “builtin”,
“entry_url”: “”
},
“request_id”: “…”
}
外置模板返回示例:
“code”: 200,
“message”: “获取门户模板成功”,
“data”: {
“code”: “portal-bento”,
“name”: “portal-bento”,
“mode”: “external”,
“entry_url”: “/portal-templates/portal-bento/index.html”
},
“request_id”: “…”
}
字段说明:
| 字段 | 取值 | 说明 |
|---|---|---|
code |
builtin 或插件编号 |
当前模板唯一编号 |
name |
模板名称 | 用于加载提示和 iframe 标题 |
mode |
builtin / external |
builtin 加载系统 Vue 组件,external 加载插件入口 |
entry_url |
空或模板入口 | 系统内部使用,模板自身不要再次跳转到该地址 |
1.2 外置模板加载机制
当前系统按以下顺序加载外置模板:
- 后端读取
portal.active_template配置。 - 检查
plugins/portal/{plugin_code}/index.html是否存在。 - 访问
/、/portal/api-docs或/portal/article/{id}时,后端优先返回当前模板的index.html。 - 后端把入口 HTML 中的
./assets/...和assets/...自动映射到/portal-templates/{plugin_code}/assets/...。 - 浏览器地址保持公开页面地址,不再跳转到模板存储目录。
- 如果部署环境由 Nginx 直接提供主前端,主前端会在当前页面内兼容加载外置模板,浏览器顶层地址仍保持不变。
因此模板绝对不能执行下面的代码:
window.location.href = ‘/portal-templates/portal-bento/index.html’;
2. 模板目录和清单
最小模板结构:
├── manifest.json
├── index.html
└── assets/
├── app.js
├── app.css
└── images/
manifest.json 示例:
“plugin_code”: “portal-bento”,
“plugin_type”: “portal”,
“name”: “Bento 网格门户模板”,
“version”: “1.0.0”,
“description”: “Bento 网格风格企业门户模板”,
“author”: “幻影二级域名分发系统”,
“runtime”: “static”,
“entry”: “index.html”,
“minimum_system_version”: “6.0.5”,
“capabilities”: [
“site_info”,
“announcements”,
“article_categories”,
“articles”
]
}
清单字段说明:
| 字段 | 必填 | 固定值或格式 | 说明 |
|---|---|---|---|
plugin_code |
是 | 小写字母、数字、-、_ |
模板唯一编号,不能与其他 portal 模板重复 |
plugin_type |
是 | portal |
必须是门户插件类型 |
name |
是 | 字符串 | 后台展示名称 |
version |
是 | 如 1.0.0 |
每次发布升级版本号 |
description |
建议 | 字符串 | 模板说明 |
author |
建议 | 字符串 | 作者或厂商 |
runtime |
是 | static |
当前门户模板运行方式 |
entry |
是 | index.html |
模板入口文件 |
minimum_system_version |
建议 | 如 6.0.5 |
最低兼容系统版本 |
capabilities |
建议 | 字符串数组 | 声明模板使用的公开能力 |
压缩包根目录必须直接出现 manifest.json 和 index.html,不要在 ZIP 最外层再套一层同名文件夹。
3. 通用接口约定
门户只调用公开接口,统一使用相对地址:
禁止写死开发地址或线上域名:
fetch(‘http://127.0.0.1:8080/api/v1/site/home’);
fetch(‘https://某个固定域名/api/v1/site/home’);
所有接口使用统一响应结构:
“code”: 200,
“message”: “请求成功”,
“data”: {},
“request_id”: “20260916T011544.511616700Z”
}
判断成功时兼容 code === 0 和 code === 200:
const response = await fetch(`/api/v1${path}`, {
headers: { Accept: ‘application/json’ }
});
const result = await response.json();
if (result.code !== 0 && result.code !== 200) {
throw new Error(result.message || ‘请求失败’);
}
return result.data;
}
门户不得读取或保存用户 Token、管理员 Token、API 秘钥、数据库账号和插件私密配置。
4. 全站公共配置
三个门户页面都必须请求:
完整返回示例:
“code”: 200,
“message”: “获取站点信息成功”,
“data”: {
“name”: “幻影二级域名分发系统”,
“title”: “幻影DNS”,
“keywords”: “二级域名,域名解析”,
“description”: “稳定、清晰、可追踪的二级域名解析服务”,
“announcement”: “欢迎使用幻影二级域名分发系统”,
“logo”: “/uploads/site/logo.png”,
“ico”: “/uploads/site/favicon.ico”,
“footer”: “幻影科技版权所有”,
“icp_number”: “浙ICP备00000000号”,
“police_number”: “浙公网安备00000000000000号”
},
“request_id”: “…”
}
字段与页面位置:
| 后端字段 | 页面使用位置 | 空值处理 |
|---|---|---|
name |
顶部品牌名称、页脚品牌名称 | 回退“幻影二级域名分发系统” |
title |
浏览器标题基础文字 | 回退 name |
keywords |
<meta name="keywords"> |
空值时删除该 meta |
description |
Hero 描述、页脚说明、SEO 描述 | 空值时使用模板自己的简短默认文案 |
announcement |
首页短提示文字 | 空值时隐藏,不占位 |
logo |
顶部和页脚 Logo | 空值时显示文字 Logo 或模板默认图标 |
ico |
<link rel="icon"> |
空值时保留系统默认 favicon |
footer |
页脚版权主体 | 回退 name |
icp_number |
ICP 备案链接文字 | 空值时整项隐藏 |
police_number |
公安备案链接文字 | 空值时整项隐藏 |
建议封装:
const siteName = site.name || ‘幻影二级域名分发系统’;
const baseTitle = site.title || siteName;
document.title = baseTitle;
document.querySelector(‘[data-site-name]’).textContent = siteName;
5. 首页 /
5.1 首页数据对应关系
| 页面区域 | 数据来源 | 使用字段 |
|---|---|---|
| 顶部导航 | /site/home |
name、logo |
| Hero | /site/home |
name、description、announcement |
| 公告 | /announcements?position=home |
公告字段 |
| 文章分类 | /article-categories |
id、name、sort |
| 文章卡片 | /articles |
id、category_id、category_name、title、summary、时间字段 |
| 页脚 | /site/home |
footer、description、icp_number、police_number |
功能介绍、产品优势、FAQ 和开发流程属于模板展示内容,可以由模板提供;网站名称、Logo、文章、公告、版权和备案信息不能写死。
5.2 公告接口
返回列表字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
number | 公告编号 |
title |
string | 公告标题 |
summary |
string | 公告摘要 |
content |
string | 公告 HTML 内容 |
type |
string | 公告类型 |
positions |
string[] | 展示位置,首页应包含 home |
display_mode |
string | modal 表示弹窗,其他值按模板样式展示 |
sort_weight |
number | 排序权重,接口已经按权重排序 |
allow_close |
boolean | 是否允许关闭 |
display_once |
boolean | 是否只展示一次 |
repeat_interval |
number | 再次展示间隔,单位分钟 |
start_time |
string? | 生效时间 |
end_time |
string? | 结束时间 |
published_at |
string? | 发布时间 |
created_at |
string | 创建时间 |
接口只返回已发布且处于有效时间范围内的公告。模板不需要再次判断后台状态,但应按 display_mode、allow_close 和重复展示规则处理。
5.3 文章分类接口
返回示例:
“code”: 200,
“data”: {
“list”: [
{ “id”: 1, “name”: “官方通知”, “sort”: 0 }
]
}
}
分类选择器必须以 id 作为筛选值,以 name 作为显示文字。模板可以在前端额外增加“全部分类”,但不要把“全部分类”提交成真实分类编号。
5.4 文章列表接口
全部文章:
按分类筛选:
文章列表字段:
| 字段 | 类型 | 卡片用途 |
|---|---|---|
id |
number | 文章详情编号和列表 key |
category_id |
number? | 当前分类筛选 |
category_name |
string? | 分类标签 |
title |
string | 卡片标题 |
summary |
string | 卡片摘要 |
published_at |
string? | 优先显示的发布时间 |
created_at |
string | 没有发布时间时的回退时间 |
列表接口不返回正文 content。点击文章后必须再调用详情接口,不能把 summary 当作正文。
文章点击支持两种方式:
window.top.location.href = `/portal/article/${article.id}`;
// 首页弹窗
const detail = await request(`/articles/${article.id}`);
openArticleDialog(detail);
如果使用弹窗,必须提供关闭按钮、正文滚动区域、加载状态和请求失败提示。
5.5 首页固定链接
| 按钮 | 地址 |
|---|---|
| 登录 | /user/login |
| 免费注册 | /user/register |
| API 文档 | /portal/api-docs |
| 创建 API 秘钥 | /user/api-keys |
| 提交工单 | /user/workorder |
外置模板建议使用普通链接并指定顶层窗口:
<a href=”/user/register” target=”_top”>免费注册</a>
<a href=”/portal/api-docs” target=”_top”>API 文档</a>
这样在后端直出模式和前端兼容承载模式下都能正确离开模板页面。
6. 文章详情页 /portal/article/{id}
{id} 是文章列表返回的 id,不是分类编号。
请求接口:
详情字段:
| 字段 | 类型 | 页面位置 |
|---|---|---|
id |
number | 当前文章编号 |
category_id |
number? | 分类跳转或筛选 |
category_name |
string? | 标题上方分类标签 |
title |
string | 页面 H1 和浏览器标题 |
summary |
string | 标题下方摘要,可为空 |
content |
string | 文章 HTML 正文 |
published_at |
string? | 优先展示的发布时间 |
created_at |
string | 发布时间回退值 |
updated_at |
string | 最后更新时间 |
浏览器标题格式:
Vue Router 获取编号示例:
if (!Number.isInteger(articleId) || articleId <= 0) {
throw new Error(‘文章编号不正确’);
}
const article = await request(`/articles/${articleId}`);
原生 JS 获取编号示例:
const match = portalPath.match(/^\/portal\/article\/(\d+)$/);
const articleId = match ? Number(match[1]) : 0;
正文可以使用 innerHTML 或 Vue v-html 渲染,但模板自身不得拼接脚本,也不得执行正文中的任意 JavaScript。正文区域至少需要适配标题、段落、列表、引用、代码块、表格和响应式图片。
错误状态:
| 情况 | 页面处理 |
|---|---|
| ID 不是正整数 | 显示“文章编号不正确”并提供返回首页 |
接口返回 404 |
显示“文章不存在或已下线” |
| 接口返回其他错误 | 显示接口 message 和重试按钮 |
7. 开放 API 文档页 /portal/api-docs
API 文档页使用 /site/home 的以下字段:
| 字段 | 用途 |
|---|---|
name |
顶部品牌 |
title |
浏览器标题后缀 |
description |
文档页副标题和页脚说明 |
logo |
顶部 Logo |
footer |
页脚版权 |
icp_number |
ICP 备案 |
police_number |
公安备案 |
浏览器标题格式:
接口基础地址必须从当前访问站点生成:
页面应展示以下开放接口:
| 方法 | 地址 | 功能 |
|---|---|---|
| GET | /api/v1/open/user |
获取 API 秘钥所属用户、余额和会员信息 |
| GET | /api/v1/open/domains |
获取当前用户可使用的域名 |
| GET | /api/v1/open/domains/{domain_id}/lines |
获取指定域名的解析线路 |
| GET | /api/v1/open/records |
获取解析记录列表 |
| POST | /api/v1/open/records |
添加解析记录 |
| PUT | /api/v1/open/records/{record_id} |
修改解析记录 |
| DELETE | /api/v1/open/records/{record_id} |
删除解析记录 |
认证请求头示例:
Idempotency-Key: unique-request-id
文档示例只允许使用 phk_your_api_key,不能出现真实秘钥。API 文档页面只负责展示说明,不应自动请求需要秘钥的 /open/* 接口。
8. 路由兼容规范
外置模板必须能够识别三种公开路径:
/portal/api-docs
/portal/article/{id}
为兼容统一后端直出和旧部署方式,推荐在模板启动前规范化路径:
const hashPath = window.location.hash.startsWith(‘#/’)
? window.location.hash.slice(1)
: ”;
const storageEntry = /^\/portal-templates\/[^/]+\/(?:index\.html)?$/;
if (hashPath) {
window.history.replaceState(null, ”, hashPath);
return hashPath;
}
if (storageEntry.test(window.location.pathname)) {
window.history.replaceState(null, ”, ‘/’);
return ‘/’;
}
return window.location.pathname;
}
const initialPortalPath = normalizePortalLocation();
Vue 3 路由示例:
normalizePortalLocation();
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: ‘/’, component: HomePage },
{ path: ‘/portal/api-docs’, component: ApiDocsPage },
{ path: ‘/portal/article/:id’, component: ArticlePage }
]
});
不要使用模板目录作为 Router base,也不要把下面的地址注册成公开路由:
/portal-templates/portal-bento/#/
模板内部进入用户中心、后台、登录、注册和门户公开页面时,使用绝对路径和顶层跳转;页面内锚点滚动可以留在模板内部。
9. 静态资源与构建配置
HTML 中推荐使用相对资源:
<link rel=”stylesheet” href=”./assets/app.css”>
<img src=”./assets/images/hero.webp” alt=””>
系统从根路径提供模板入口时,会把这些地址映射到当前模板自己的资源目录。
Vite 模板必须设置相对资源基础路径:
import vue from ‘@vitejs/plugin-vue’;
export default defineConfig({
base: ‘./’,
plugins: [vue()],
build: {
outDir: ‘dist’,
emptyOutDir: true
}
});
禁止:
- 把资源写成开发机绝对路径。
- 写死
/assets/app.js,这可能与主程序资源重名。 - 引用
node_modules中未打包的文件。 - 依赖开发服务器才能加载的模块。
- 把 sourcemap、缓存目录、源码依赖和整个主系统前端打入模板包。
10. 标题、SEO 和 favicon
三个页面加载站点配置后分别设置标题:
// 首页
document.title = baseTitle;
// API 文档
document.title = `开放 API 文档 – ${baseTitle}`;
// 文章详情
document.title = `${article.title} – ${baseTitle}`;
更新 SEO:
let element = document.querySelector(`meta[name=”${name}”]`);
if (!content) {
element?.remove();
return;
}
if (!element) {
element = document.createElement(‘meta’);
element.setAttribute(‘name’, name);
document.head.appendChild(element);
}
element.setAttribute(‘content’, content);
}
updateMeta(‘keywords’, site.keywords);
updateMeta(‘description’, site.description);
favicon 使用 site.ico,Logo 使用 site.logo。图片地址可能是绝对 URL,也可能是 /uploads/... 相对站点地址,模板不要自行拼接固定域名。
11. 加载、空数据和错误状态
每个动态区域都必须分别处理:
| 状态 | 要求 |
|---|---|
| 加载中 | 显示稳定尺寸的骨架或加载提示,不让布局跳动 |
| 空数据 | 分类为空、文章为空、公告为空时正常隐藏或显示空状态 |
| 请求失败 | 显示简短错误信息和重试按钮,不让整个页面白屏 |
| 图片失败 | Logo 回退文字品牌,内容图片保持占位 |
| 文章不存在 | 显示返回首页按钮 |
站点配置接口失败时可以使用模板默认品牌文字继续显示;文章详情接口失败时不能伪造文章内容。
12. 安全限制
- 只调用
/api/v1/site/home、文章、分类、公告等公开接口。 - 不调用
/api/v1/admin/*。 - 不读取 localStorage 中的登录 Token。
- 不在模板中保存 API 秘钥、AppSecret、数据库信息和授权密钥。
- 不在浏览器中实现扣费、权限判断、实名认证或违规检测业务。
- 不执行文章和公告内容中的
<script>。 - 外部链接使用
rel="noopener noreferrer"。 - 所有用户输入和 URL 参数在展示前进行类型检查。
13. 本地开发和打包
开发阶段可以独立启动模板,也可以放入系统插件目录测试。发布前目录应整理为:
├── manifest.json
├── index.html
└── assets/
打包 ZIP 时压缩 dist 内部内容,而不是压缩 dist 文件夹本身。
安装后的文件位置:
plugins/portal/{plugin_code}/index.html
plugins/portal/{plugin_code}/assets/…
启用模板后依次验证:
/portal/api-docs
/portal/article/一个真实文章ID
/user/login
/user/register
浏览器地址不得出现:
/portal-templates/{plugin_code}/#/
14. 发布检查清单
- ☐ ZIP 根目录直接包含
manifest.json和index.html。 - ☐
plugin_type为portal,runtime为static。 - ☐ 首页、API 文档和文章详情三个公开地址都能直接刷新。
- ☐ 浏览器顶层地址不会跳转到
/portal-templates/。 - ☐ JS、CSS、图片均从当前模板目录加载,没有 404 和 MIME 错误。
- ☐ 登录、注册、用户中心链接使用绝对路径并跳转顶层窗口。
- ☐ 网站名称、标题、描述、Logo、favicon 来自
/site/home。 - ☐ 页脚版权、ICP 备案和公安备案来自
/site/home。 - ☐ 文章分类来自
/article-categories,没有写死后台分类。 - ☐ 文章列表不把摘要当正文,详情通过
/articles/{id}获取。 - ☐ 公告使用
position=home,并处理展示方式和关闭规则。 - ☐ API 文档没有真实秘钥和敏感信息。
- ☐ 请求失败、空数据和文章不存在时不会白屏。
- ☐ 桌面端、平板和移动端没有横向溢出或文字遮挡。
- ☐ 发布包不包含
node_modules、缓存、源码和主系统无关文件。
15. 系统内对应实现位置
| 功能 | 文件 |
|---|---|
| 门户入口和兼容承载 | ele-admin-plus-ts/src/views/portal/entry.vue |
| 门户公开路由 | ele-admin-plus-ts/src/router/routes.ts |
| 模板选择接口 | backend/cmd/server/portal_template.go |
| 根路径模板分发 | backend/cmd/server/install.go |
| 站点配置接口 | backend/cmd/server/site.go |
| 文章、分类和公告接口 | backend/cmd/server/content.go |
| 内置门户首页 | ele-admin-plus-ts/src/views/portal/index.vue |
| 内置文章详情 | ele-admin-plus-ts/src/views/portal/article/index.vue |
| 内置 API 文档 | ele-admin-plus-ts/src/views/portal/api-docs/index.vue |
| Bento 模板源码参考 | 首页门户模板/portal-bento/ |
版权:
转载请注明出处:https://www.52hyjs.com/317.html
微信扫一扫
支付宝扫一扫
还没有评论呢,快来抢沙发~