From 0360ee52610709ffa33613a9b7de752b3cc47f5c Mon Sep 17 00:00:00 2001 From: whm <973418690@qq.com> Date: Thu, 19 Mar 2026 17:31:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9D=83=E9=99=90=E5=88=97=E8=A1=A8JSON?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=B8=8E=E8=A7=92=E8=89=B2=E5=8F=AF=E7=BC=96?= =?UTF-8?q?=E8=BE=91;=20=E5=89=8D=E5=8F=B0site=5Fid=E4=B8=8ESPA;=20?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E7=A7=AF=E6=9C=A8=E6=89=A9=E5=B1=95=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- admin/src/views/settings/RolePermissions.vue | 107 ++++++++++++++----- admin/src/views/sites/HomepageEdit.vue | 35 +++++- admin/src/views/sites/PageList.vue | 29 +++-- docs/PAGE_BUILDER.md | 5 + server/handlers/role_permission.go | 3 +- server/models/permission.go | 31 +++--- server/models/site.go | 2 + web/src/api/webPages.js | 24 ++++- web/src/views/Home.vue | 27 ++++- web/vite.config.js | 1 + 10 files changed, 210 insertions(+), 54 deletions(-) diff --git a/admin/src/views/settings/RolePermissions.vue b/admin/src/views/settings/RolePermissions.vue index 1876bc7..0556bac 100644 --- a/admin/src/views/settings/RolePermissions.vue +++ b/admin/src/views/settings/RolePermissions.vue @@ -10,27 +10,28 @@ -

超级管理员(9527)拥有全部权限且不可修改。为其他角色勾选其可用的后台权限;可创建自定义角色并赋权。

+

+ 超级管理员(9527)拥有全部权限且不可改权限勾选(防误操作)。超级用户(0)、普通用户(1)可修改权限与显示名称;自定义角色可删除。 +

- + - + @@ -43,16 +44,21 @@ - + -
- - {{ p.name }} - +

勾选该角色可访问的后台能力:

+
+
@@ -77,10 +83,20 @@ const showCreate = ref(false) const creating = ref(false) const createForm = reactive({ role_name: '', _checked: {} }) +/** 兼容旧接口大写字段 Key/Name */ +function permKey(p) { + return p?.key || p?.Key || '' +} +function permLabel(p) { + const k = permKey(p) + return p?.name || p?.Name || k || '权限' +} + function buildChecked(permissions) { const o = {} allPermissions.value.forEach((p) => { - o[p.key] = permissions.includes(p.key) + const k = permKey(p) + if (k) o[k] = (permissions || []).includes(k) }) return o } @@ -106,9 +122,10 @@ const handleSave = async () => { try { for (const row of list.value) { if (row.role_id === 9527) continue - const permissions = allPermissions.value.filter((p) => row._checked[p.key]).map((p) => p.key) + const permissions = allPermissions.value.filter((p) => row._checked[permKey(p)]).map((p) => permKey(p)) const payload = { permissions } - if (row.is_custom && row.role_name) payload.role_name = row.role_name + const name = (row.role_name || '').trim() + if (name) payload.role_name = name await updateRolePermissions(row.role_id, payload) } ElMessage.success('保存成功') @@ -123,7 +140,8 @@ const resetCreateForm = () => { createForm.role_name = '' createForm._checked = {} allPermissions.value.forEach((p) => { - createForm._checked[p.key] = false + const k = permKey(p) + if (k) createForm._checked[k] = false }) } @@ -135,7 +153,7 @@ const handleCreate = async () => { } creating.value = true try { - const permissions = allPermissions.value.filter((p) => createForm._checked[p.key]).map((p) => p.key) + const permissions = allPermissions.value.filter((p) => createForm._checked[permKey(p)]).map((p) => permKey(p)) await createRole({ role_name: name, permissions }) ElMessage.success('创建成功') showCreate.value = false @@ -176,9 +194,50 @@ onMounted(fetchList) color: #666; font-size: 13px; margin-bottom: 16px; + line-height: 1.6; } -.perm-checkboxes { +.perm-all { + color: #909399; + font-size: 13px; +} +.perm-grid { display: flex; flex-wrap: wrap; + gap: 10px 20px; + align-items: flex-start; +} +.dialog-perm-grid { + max-height: 360px; + overflow-y: auto; + padding: 8px 0; +} +.perm-item { + display: flex; + align-items: flex-start; + gap: 8px; + cursor: pointer; + min-width: 200px; + max-width: 240px; +} +.perm-text { + display: flex; + flex-direction: column; + gap: 2px; + line-height: 1.35; +} +.perm-name { + font-size: 13px; + color: #303133; +} +.perm-key { + font-size: 11px; + color: #909399; + font-family: ui-monospace, monospace; + word-break: break-all; +} +.dialog-perm-hint { + margin: 0 0 8px; + font-size: 12px; + color: #909399; } diff --git a/admin/src/views/sites/HomepageEdit.vue b/admin/src/views/sites/HomepageEdit.vue index 3745e83..280199e 100644 --- a/admin/src/views/sites/HomepageEdit.vue +++ b/admin/src/views/sites/HomepageEdit.vue @@ -14,7 +14,7 @@
- + 导航与标题 @@ -110,6 +110,14 @@ + + 首页下方扩展区(可视化积木,可拖拽排序) +

+ 与「网页管理 → 积木」相同:从左侧手柄拖拽调整模块顺序。保存后内容显示在落地页主视觉与特性卡片之后、页脚之前。留空则不显示扩展区。 +

+ + +
@@ -129,6 +137,7 @@ import { ElMessage } from 'element-plus' import { getSites, getOfficialSite, getHomepage, updateHomepage } from '../../api/admin' import { useAuthStore } from '../../stores/auth' import LinkPickerDialog from '../../components/LinkPickerDialog.vue' +import PageBuilderEditor from '../../components/PageBuilderEditor.vue' const siteId = ref('') const sites = ref([]) @@ -162,7 +171,8 @@ const defaultForm = () => ({ { title: '量子同步', desc: '跨维度数据同步技术,您的数据在多宇宙中保持一致' }, { title: '星际防护', desc: '来自未来的安全加密协议,守护您的数字资产安全' } ], - footer_text: '© 2024 YUHENG ONE // STELLAR EXPLORATION INITIATIVE' + footer_text: '© 2024 YUHENG ONE // STELLAR EXPLORATION INITIATIVE', + body_builder: '' }) const form = reactive(defaultForm()) @@ -284,4 +294,25 @@ onMounted(() => { justify-content: space-between; align-items: center; } +.homepage-form { + max-width: 720px; +} +.builder-tip { + font-size: 13px; + color: #606266; + line-height: 1.6; + margin: 0 0 12px; + max-width: 900px; +} +.homepage-builder-wrap { + max-width: 1000px; +} +.homepage-builder-wrap :deep(.el-form-item__content) { + display: block; + margin-left: 0 !important; +} +.builder-form-item :deep(.el-form-item__content) { + display: block; + margin-left: 0 !important; +} diff --git a/admin/src/views/sites/PageList.vue b/admin/src/views/sites/PageList.vue index 5f3a309..30d9f40 100644 --- a/admin/src/views/sites/PageList.vue +++ b/admin/src/views/sites/PageList.vue @@ -53,10 +53,18 @@ + @@ -74,10 +82,10 @@ - - - - + + 积木(可视化拖拽) + HTML 源码 + 插入积木模板 @@ -86,7 +94,7 @@ - + @@ -202,7 +210,7 @@ const form = reactive({ title: '', type: 'page', content: '', - content_mode: 'html', + content_mode: 'builder', route_path: '', published: true }) @@ -231,7 +239,7 @@ const openDialog = (row) => { form.title = row ? row.title : '' form.type = row ? row.type || 'page' : 'page' form.content = row ? row.content || '' : '' - form.content_mode = row?.content_mode || 'html' + form.content_mode = row?.content_mode || 'builder' form.route_path = row?.route_path || '' form.published = row?.published !== false dialogVisible.value = true @@ -242,7 +250,7 @@ const resetForm = () => { form.title = '' form.type = 'page' form.content = '' - form.content_mode = 'html' + form.content_mode = 'builder' form.route_path = '' form.published = true editId.value = '' @@ -303,4 +311,7 @@ onMounted(() => { display: block; margin-left: 0 !important; } +.page-builder-wrap { + max-width: 100%; +} diff --git a/docs/PAGE_BUILDER.md b/docs/PAGE_BUILDER.md index 6a6a499..00ad2d6 100644 --- a/docs/PAGE_BUILDER.md +++ b/docs/PAGE_BUILDER.md @@ -52,6 +52,11 @@ 1. 在 `web/src/components/blocks/BlockRenderer.vue` 增加 `type` 分支与样式。 2. 后台仍通过 JSON 配置 `props`,无需改库表结构。 +## 首页编辑:下方扩展积木 + +- 除原有导航、主视觉、特性等表单项外,可增加 **「首页下方扩展区」**:与网页积木相同 JSON,保存后由前台 `Home.vue` 在特性区之后、页脚之前用 **同一套 BlockRenderer** 动态渲染。 +- 下载的静态 `index.html` 目前**不包含**该积木区(仅在线 SPA 展示)。 + ## 首页编辑(导航 / 下载 / 平台链接) - **管理后台 → 首页编辑与下载**:导航链接、下载按钮链接、各平台链接均可点 **「选择链接」**,与积木编辑器共用 `LinkPickerDialog`。 diff --git a/server/handlers/role_permission.go b/server/handlers/role_permission.go index e835716..420cd11 100644 --- a/server/handlers/role_permission.go +++ b/server/handlers/role_permission.go @@ -122,7 +122,8 @@ func UpdateRolePermissions(c *gin.Context) { coll := config.GetDB(config.DBName).Collection("role_permissions") filter := bson.M{"role_id": roleID} set := bson.M{"role_id": roleID, "permissions": input.Permissions} - if input.RoleName != "" && roleID >= customRoleIDStart { + // 超级管理员(9527)已拦截;其余预定义(0/1)与自定义角色均可更新显示名称 + if input.RoleName != "" { set["role_name"] = input.RoleName } update := bson.M{"$set": set} diff --git a/server/models/permission.go b/server/models/permission.go index fcb8e89..7f4a0f0 100644 --- a/server/models/permission.go +++ b/server/models/permission.go @@ -14,21 +14,24 @@ const ( PermRolePermission = "role:permission" // 角色权限管理 ) +// PermissionItem 单条权限定义(JSON 须用小写 key/name,供前端展示与勾选) +type PermissionItem struct { + Key string `json:"key"` + Name string `json:"name"` +} + // AllPermissions 所有可配置权限(用于角色权限管理页) -var AllPermissions = []struct { - Key string - Name string -}{ - {PermSiteManage, "站点管理"}, - {PermHomepageEdit, "首页编辑"}, - {PermPageManage, "网页管理"}, - {PermModuleUpload, "功能模块上传"}, - {PermUserManage, "用户管理"}, - {PermWorkspaceManage, "工作空间"}, - {PermConversationManage, "对话管理"}, - {PermSMSConfig, "短信配置"}, - {PermPaymentConfig, "支付配置"}, - {PermRolePermission, "角色权限管理"}, +var AllPermissions = []PermissionItem{ + {Key: PermSiteManage, Name: "站点管理"}, + {Key: PermHomepageEdit, Name: "首页编辑"}, + {Key: PermPageManage, Name: "网页管理"}, + {Key: PermModuleUpload, Name: "功能模块上传"}, + {Key: PermUserManage, Name: "用户管理"}, + {Key: PermWorkspaceManage, Name: "工作空间"}, + {Key: PermConversationManage, Name: "对话管理"}, + {Key: PermSMSConfig, Name: "短信配置"}, + {Key: PermPaymentConfig, Name: "支付配置"}, + {Key: PermRolePermission, Name: "角色权限管理"}, } // RolePermissionsDoc MongoDB 文档:角色 ID -> 名称与权限列表(支持自定义角色) diff --git a/server/models/site.go b/server/models/site.go index de91afb..c2ce99b 100644 --- a/server/models/site.go +++ b/server/models/site.go @@ -40,6 +40,8 @@ type HomepageData struct { BadgeText string `json:"badge_text"` // FREE ACCESS Features []FeatureItem `json:"features"` // 星际导航等 FooterText string `json:"footer_text"` // © 2024 YUHENG ONE + // BodyBuilder 首页下方扩展区:与网页积木相同 JSON 字符串 {"version":1,"blocks":[...]},空则仅展示上方模板 + BodyBuilder string `json:"body_builder,omitempty"` } type NavLink struct { diff --git a/web/src/api/webPages.js b/web/src/api/webPages.js index c796874..3b2abcb 100644 --- a/web/src/api/webPages.js +++ b/web/src/api/webPages.js @@ -2,14 +2,32 @@ import { apiBase } from '../config' const prefix = () => (apiBase ? `${apiBase}/api` : '/api') +/** 与 /web/routes 返回的 site_id 一致,拉取单页时附带,避免多站点下错站 */ +let cachedWebSiteId = '' + +export function getCachedWebSiteId() { + return cachedWebSiteId +} + export async function fetchWebRoutes() { - const res = await fetch(`${prefix()}/web/routes`) - if (!res.ok) return { site_id: '', routes: [] } - return res.json() + let url = `${prefix()}/web/routes` + const sid = new URLSearchParams(window.location.search).get('site_id') + if (sid) url += `?site_id=${encodeURIComponent(sid)}` + const res = await fetch(url) + if (!res.ok) { + cachedWebSiteId = '' + return { site_id: '', routes: [] } + } + const data = await res.json() + cachedWebSiteId = data.site_id || '' + return data } export async function fetchWebPageByPath(path) { const q = new URLSearchParams({ path: path || '/' }) + const fromQs = new URLSearchParams(window.location.search).get('site_id') + const sid = cachedWebSiteId || fromQs + if (sid) q.set('site_id', sid) const res = await fetch(`${prefix()}/web/page?${q}`) if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || '加载失败') return res.json() diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index ffd5184..2fe512f 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -48,6 +48,10 @@ +
+ +
+