调试frontend

This commit is contained in:
2026-07-21 14:04:49 +08:00
parent dd1e55aea3
commit b45152599e
18 changed files with 1840 additions and 1382 deletions
+4 -7
View File
@@ -16,14 +16,11 @@ export function getFrontMenus() {
* @param {Object} frontMenuData 前端导航数据 * @param {Object} frontMenuData 前端导航数据
* @returns {Promise} * @returns {Promise}
*/ */
export function createFrontMenu(formData, options = {}) { export function createFrontMenu(formData) {
return request({ return request({
url: "/backend/createfrontmenu", url: "/backend/frontmenus",
method: "post", method: "post",
data: formData, data: formData,
headers: {
"Content-Type": "multipart/form-data"
}
}); });
} }
@@ -35,7 +32,7 @@ export function createFrontMenu(formData, options = {}) {
*/ */
export function editFrontMenu(id, frontMenuData) { export function editFrontMenu(id, frontMenuData) {
return request({ return request({
url: `/backend/editfrontmenu/${id}`, url: `/backend/frontmenus/${id}`,
method: "post", method: "post",
data: frontMenuData, data: frontMenuData,
}); });
@@ -48,7 +45,7 @@ export function editFrontMenu(id, frontMenuData) {
*/ */
export function deleteFrontMenu(id) { export function deleteFrontMenu(id) {
return request({ return request({
url: `/backend/deletefrontmenu/${id}`, url: `/backend/frontmenus/${id}`,
method: "delete", method: "delete",
}); });
} }
+34 -7
View File
@@ -6,7 +6,7 @@
</div> </div>
<!-- 错误提示 --> <!-- 错误提示 -->
<div v-else-if="hasError" class="error-container"> <div v-else-if="hasError && list.length === 0" class="error-container">
<el-icon class="error-icon"><Warning /></el-icon> <el-icon class="error-icon"><Warning /></el-icon>
<div class="error-text">{{ errorMsg }}</div> <div class="error-text">{{ errorMsg }}</div>
<el-button size="small" @click="fetchMenus">重新加载</el-button> <el-button size="small" @click="fetchMenus">重新加载</el-button>
@@ -289,13 +289,40 @@ const processMenus = (menus) => {
})); }));
}; };
const list = computed(() => { const fixedCmsMenu = {
const menuData = menuStore.menus; id: -200,
if (!menuData || menuData.length === 0) { path: "/apps/cms",
return []; title: "文章中心",
} icon: "Document",
order: -200,
children: [
{
id: -201,
path: "/apps/cms/articles",
title: "文章管理",
icon: "Document",
order: 1,
children: []
},
{
id: -202,
path: "/apps/cms/articles/category",
title: "文章分类",
icon: "Folder",
order: 2,
children: []
}
]
};
const allMenus = processMenus(menuData); const mergeFixedCmsMenu = (menus) => {
const hasCmsMenu = menus.some((menu) => menu.path === fixedCmsMenu.path);
return hasCmsMenu ? menus : [fixedCmsMenu, ...menus];
};
const list = computed(() => {
const menuData = menuStore.menus || [];
const allMenus = processMenus(mergeFixedCmsMenu(menuData));
const sortMenusRecursively = (menus) => { const sortMenusRecursively = (menus) => {
menus.forEach((menu) => { menus.forEach((menu) => {
+15
View File
@@ -3,6 +3,19 @@ import { convertMenusToRoutes } from "./dynamicRoutes";
// 静态子路由:需要在 Main 框架内显示的页面 // 静态子路由:需要在 Main 框架内显示的页面
const staticMainChildren = [ const staticMainChildren = [
// CMS 文章中心是系统内置功能,不依赖数据库菜单配置。
{
path: "/apps/cms/articles",
name: "CmsArticles",
component: () => import("@/views/apps/cms/articles/index.vue"),
meta: { requiresAuth: true, title: "文章管理", modulePath: "/apps/cms" }
},
{
path: "/apps/cms/articles/category",
name: "CmsArticleCategories",
component: () => import("@/views/apps/cms/articles/category.vue"),
meta: { requiresAuth: true, title: "文章分类", modulePath: "/apps/cms" }
},
{ {
path: "/user/userProfile", path: "/user/userProfile",
name: "userProfile", name: "userProfile",
@@ -90,6 +103,8 @@ export async function loadAndAddDynamicRoutes() {
routesLoadingPromise = null; routesLoadingPromise = null;
return Promise.resolve(); return Promise.resolve();
} else { } else {
// 即使权限菜单为空,也要注册内置文章中心路由。
addDynamicRoutes([]);
dynamicRoutesAdded = true; dynamicRoutesAdded = true;
routesLoadingPromise = null; routesLoadingPromise = null;
return Promise.resolve(); return Promise.resolve();
@@ -156,6 +156,11 @@ function toggleExpand(category) {
} }
function handleDelete(category) { function handleDelete(category) {
if (Number(category.tid) === 0 || category.is_global === true) {
ElMessage.warning("全局分类不可删除");
return;
}
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", { ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
@@ -53,9 +53,18 @@
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)"> <el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
</el-button> </el-button>
<el-button type="text" class="danger" title="删除" @click.stop="$emit('delete', item)"> <el-button
v-if="item.tid !== 0"
type="text"
class="danger"
title="删除"
@click.stop="$emit('delete', item)"
>
<el-icon><Delete /></el-icon> <el-icon><Delete /></el-icon>
</el-button> </el-button>
<el-tag v-else type="info" size="small" title="全局分类不可删除">
全局
</el-tag>
</el-button-group> </el-button-group>
</div> </div>
</div> </div>
+3 -3
View File
@@ -2,18 +2,18 @@ import service from '@/utils/request'
// 获取前端导航 // 获取前端导航
export const getHeadMenu = async () => { export const getHeadMenu = async () => {
const response = await service.get('index/headmenu') const response = await service.get('/api/index/headmenu')
return response.data return response.data
} }
// 根据路径获取单页内容 // 根据路径获取单页内容
export const getOnePageByPath = async (path: string) => { export const getOnePageByPath = async (path: string) => {
const response = await service.get(`index/onepage/${encodeURIComponent(path)}`) const response = await service.get(`/api/index/onepage/${encodeURIComponent(path)}`)
return response.data return response.data
} }
// 获取前端底部数据 // 获取前端底部数据
export const getFooterData = async () => { export const getFooterData = async () => {
const response = await service.get('index/footerdata') const response = await service.get('/api/index/footerdata')
return response.data return response.data
} }
+28 -39
View File
@@ -1,52 +1,41 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router'
import { constantRoute, registerDynamicRoutes } from './routes'; import { getHeadMenu } from '@/api/index'
import { constantRoute, registerDynamicRoutes } from './routes'
// 先创建路由,但不立即匹配
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: constantRoute, routes: constantRoute,
// 延迟路由匹配,等待动态路由注册完成
strict: false, strict: false,
}); })
// 在应用启动时加载菜单并注册路由 let routesLoaded = false
let routesLoaded = false; let routesLoadingPromise: Promise<void> | null = null
let routesLoadingPromise: Promise<void> | null = null;
// 固定路由先可用,再尝试加载后台菜单补充自定义页面。
export async function loadAndRegisterRoutes() { export async function loadAndRegisterRoutes() {
// 如果已经加载过,直接返回 if (routesLoaded) return
if (routesLoaded) { if (routesLoadingPromise) return routesLoadingPromise
return;
}
// 如果正在加载,等待加载完成 routesLoadingPromise = getHeadMenu()
if (routesLoadingPromise) { .then((response) => {
return routesLoadingPromise; if (response?.code === 200 && Array.isArray(response.data)) {
} registerDynamicRoutes(router, response.data)
}
})
.catch((error) => {
// 后台菜单不可用时不影响固定文章路由和官网页面。
console.error('加载前端菜单失败:', error)
})
.finally(() => {
routesLoaded = true
})
return routesLoadingPromise; return routesLoadingPromise
} }
// 路由守卫:确保路由已加载 router.beforeEach(async (_to, _from, next) => {
router.beforeEach(async (to, _from, next) => { await loadAndRegisterRoutes()
// 如果路由还未加载,先加载路由 next()
if (!routesLoaded) { })
// 等待路由加载完成
await routesLoadingPromise;
// 路由加载完成后,如果当前路径不匹配,尝试重新匹配
const matched = router.resolve(to.path).matched;
if (matched.length === 0 && to.path !== '/404') {
// 路由已加载但当前路径不匹配,可能是通配符路由
// 继续导航,让通配符路由处理
next();
return;
}
}
// 路由已加载,继续导航 export default router
next();
});
export default router;
+99 -104
View File
@@ -1,124 +1,119 @@
import { createComponentLoader } from '@/utils/pathResolver' import { createComponentLoader } from '@/utils/pathResolver'
// 对外暴配置路由,常量路由 // 对外暴配置路由。核心业务路由必须固定存在,不依赖后台菜单配置。
export const constantRoute = [ export const constantRoute = [
{ {
path: '/', path: '/',
component: () => import('@/views/home/index.vue'), component: () => import('@/views/home/index.vue'),
name: '首页', name: '首页',
meta: { meta: { title: '首页', hidden: false },
title: '首页', },
hidden: false, {
}, path: '/solution',
}, component: () => import('@/views/solution/index.vue'),
{ name: '解决方案',
path: '/404', meta: { title: '解决方案', hidden: false },
component: () => import('@/views/404/index.vue'), },
name: '404', {
meta: { path: '/solution/solution',
title: '404', component: () => import('@/views/solutions/solution/index.vue'),
hidden: true, name: '行业解决方案',
icon: 'DocumentDelete', meta: { title: '行业解决方案', hidden: true },
}, },
}, {
] path: '/solution/successCases',
component: () => import('@/views/solutions/successCases/index.vue'),
name: '成功案例',
meta: { title: '成功案例', hidden: true },
},
{
path: '/newsCenter',
component: () => import('@/views/newsCenter/news/index.vue'),
name: '新闻中心',
meta: { title: '新闻中心', hidden: false },
},
{
path: '/newsCenter/news',
component: () => import('@/views/newsCenter/news/index.vue'),
name: '站点新闻',
meta: { title: '站点新闻', hidden: true },
},
{
path: '/newsCenter/announcement',
component: () => import('@/views/newsCenter/announcement/index.vue'),
name: '站点公告',
meta: { title: '站点公告', hidden: true },
},
{
path: '/newsCenter/technologyCenter',
component: () => import('@/views/newsCenter/technologyCenter/index.vue'),
name: '技术中心',
meta: { title: '技术中心', hidden: true },
},
// 文章详情路由固定注册,文章发布后无需先在前台菜单中配置路由。
{
path: '/newsCenter/companyNews/detail/:id',
component: () => import('@/views/components/articleDetail.vue'),
name: '公司新闻详情',
meta: { title: '公司新闻详情', hidden: true },
},
{
path: '/newsCenter/kingdeeNews/detail/:id',
component: () => import('@/views/components/articleDetail.vue'),
name: '金蝶新闻详情',
meta: { title: '金蝶新闻详情', hidden: true },
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: { title: '404', hidden: true, icon: 'DocumentDelete' },
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/onepage/index.vue'),
name: 'OnePage',
meta: { title: '单页', hidden: true },
},
]
// 动态路由注册函数 // 动态路由注册函数:后台菜单只负责补充自定义页面,不覆盖固定业务路由。
export function registerDynamicRoutes(router: any, menus: any[]) { export function registerDynamicRoutes(router: any, menus: any[]) {
if (!menus || menus.length === 0) return if (!menus || menus.length === 0) return
// 统一处理菜单路径大小写 const normalizeMenuPaths = (items: any[]): any[] => items.map((menu) => ({
const normalizeMenuPaths = (menus: any[]) => { ...menu,
return menus.map(menu => { path: menu.path?.replace(/\/newscenter\//g, '/newsCenter/'),
const normalizedMenu = { ...menu } children: menu.children?.length ? normalizeMenuPaths(menu.children) : menu.children,
// 将路径中的 /newscenter/ 替换为 /newsCenter/ }))
if (normalizedMenu.path) {
normalizedMenu.path = normalizedMenu.path.replace(/\/newscenter\//g, '/newsCenter/')
}
// 递归处理子菜单
if (normalizedMenu.children && normalizedMenu.children.length > 0) {
normalizedMenu.children = normalizeMenuPaths(normalizedMenu.children)
}
return normalizedMenu
})
}
// 先规范化菜单路径
const normalizedMenus = normalizeMenuPaths(menus)
// 收集所有需要注册的路由
const routesToAdd: any[] = [] const routesToAdd: any[] = []
// 递归处理菜单,收集路由
const processMenu = (menu: any) => { const processMenu = (menu: any) => {
// type 2: 页面 - 根据 component_path 加载组件
if (menu.type === 2 && menu.path && menu.component_path) { if (menu.type === 2 && menu.path && menu.component_path) {
const path = menu.path const path = menu.path
// 检查路由是否已存在 if (!router.getRoutes().some((route: any) => route.path === path)) {
const existingRoute = router.getRoutes().find((r: any) => r.path === path) routesToAdd.push({
if (!existingRoute) { path,
try { name: `menu_${menu.id}`,
// 使用 pathResolver 来解析 component_path component: createComponentLoader(menu.component_path),
// component_path 可能的格式: meta: { title: menu.title, menuId: menu.id, menuType: menu.type },
// 1. /views/newsCenter/companyNews/index.vue })
// 2. views/newsCenter/companyNews/index.vue
// 3. newsCenter/companyNews/index.vue
const componentLoader = createComponentLoader(menu.component_path)
if (!componentLoader) {
console.error(`Failed to create component loader for: ${menu.component_path}`)
return
}
routesToAdd.push({
path: path,
name: `menu_${menu.id}`,
component: componentLoader,
meta: {
title: menu.title,
menuId: menu.id,
menuType: menu.type,
},
})
} catch (error) {
console.error(`Failed to prepare route for path ${path}, component_path: ${menu.component_path}`, error)
}
} }
} }
// type 4: 单页 - 使用通配符路由处理,不需要单独注册 menu.children?.forEach(processMenu)
// 递归处理子菜单
if (menu.children && menu.children.length > 0) {
menu.children.forEach((child: any) => processMenu(child))
}
} }
// 处理所有菜单 normalizeMenuPaths(menus).forEach(processMenu)
normalizedMenus.forEach((menu) => processMenu(menu))
// 先移除通配符路由(如果存在),以便重新添加 // 通配符路由必须最后注册,否则会抢先匹配后台新增的自定义页面。
const catchAllRoute = router.getRoutes().find((r: any) => r.path === '/:pathMatch(.*)*') const catchAll = router.getRoutes().find((route: any) => route.name === 'OnePage')
if (catchAllRoute) { if (catchAll) router.removeRoute('OnePage')
router.removeRoute('OnePage')
}
// 注册 type 2 的路由(必须在通配符路由之前) routesToAdd.forEach((route) => router.addRoute(route))
routesToAdd.forEach((route) => { router.addRoute({
router.addRoute(route) path: '/:pathMatch(.*)*',
component: () => import('@/views/onepage/index.vue'),
name: 'OnePage',
meta: { title: '单页', hidden: true },
}) })
// 最后添加通配符路由(用于单页和其他未匹配的路径)
const catchAllExists = router.getRoutes().find((r: any) => r.path === '/:pathMatch(.*)*')
if (!catchAllExists) {
router.addRoute({
path: '/:pathMatch(.*)*',
component: () => import('@/views/onepage/index.vue'),
name: 'OnePage',
meta: {
title: '单页',
hidden: true,
},
})
}
} }
+7 -3
View File
@@ -2,13 +2,17 @@ import axios from 'axios'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, baseURL: '/',
timeout: 10000, timeout: 10000,
}) })
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config) => { (config) => {
const devTenantId = sessionStorage.getItem('dev_tenant_id')
if (devTenantId) {
config.headers['X-Tenant-ID'] = encodeURIComponent(devTenantId)
}
return config return config
}, },
(error) => { (error) => {
@@ -16,10 +20,10 @@ service.interceptors.request.use(
} }
) )
// 响应拦截器 - 返回 response.data // 响应拦截器 - 保留完整 axios 响应,由 api 层统一取 response.data(即 {code, data, msg}
service.interceptors.response.use( service.interceptors.response.use(
(response) => { (response) => {
return response.data return response
}, },
(error) => { (error) => {
console.error('API请求错误:', error) console.error('API请求错误:', error)
+80 -16
View File
@@ -301,12 +301,36 @@
<div class="search-icon" @click="showSearch = true"> <div class="search-icon" @click="showSearch = true">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
</div> </div>
<!-- 开发环境租户切换按钮 -->
<div v-if="isDev" class="tenant-switcher" @click="showTenantModal = true">
<i class="fa-solid fa-gear"></i>
</div>
<div class="language-switcher" @click="showLanguage = true"> <div class="language-switcher" @click="showLanguage = true">
<span>中文</span> <span>中文</span>
<i class="fa-solid fa-caret-down"></i> <i class="fa-solid fa-caret-down"></i>
</div> </div>
</div> </div>
<!-- 租户切换弹窗 -->
<div
class="modal"
:class="{ active: showTenantModal }"
@click.self="showTenantModal = false"
>
<div class="modal-content">
<h3>切换测试租户</h3>
<div class="search-box">
<input
type="text"
placeholder="请输入租户ID..."
v-model="devTenantId"
/>
<button @click="saveTenant">保存</button>
</div>
<button class="close-btn" @click="showTenantModal = false">×</button>
</div>
</div>
<!-- 搜索弹窗 --> <!-- 搜索弹窗 -->
<div <div
class="modal" class="modal"
@@ -390,33 +414,62 @@ interface MenuItem {
// 菜单数据 // 菜单数据
const menuItems = ref<MenuItem[]>([]) const menuItems = ref<MenuItem[]>([])
// 统一处理菜单路径大小写 // 统一处理菜单路径大小写,并递归处理子菜单。
const normalizeMenuPaths = (menus: MenuItem[]): MenuItem[] => { const normalizeMenuPaths = (menus: MenuItem[]): MenuItem[] => {
return menus.map((menu: MenuItem) => { return menus.map((menu) => ({
const normalizedMenu = { ...menu } ...menu,
// 将路径中的 /newscenter/ 替换为 /newsCenter/ path: menu.path?.replace(/\/newscenter\//g, '/newsCenter/'),
if (normalizedMenu.path) { children: menu.children?.length ? normalizeMenuPaths(menu.children) : menu.children,
normalizedMenu.path = normalizedMenu.path.replace(/\/newscenter\//g, '/newsCenter/') }))
} }
// 递归处理子菜单
if (normalizedMenu.children && normalizedMenu.children.length > 0) { // 后台菜单可配置,但文章中心是官网核心功能,必须始终存在。
normalizedMenu.children = normalizeMenuPaths(normalizedMenu.children) const fixedNewsMenu: MenuChild = {
} id: -101,
return normalizedMenu title: '新闻中心',
}) path: '/newsCenter/news',
type: 2,
}
const fixedArticleMenu: MenuItem = {
id: -100,
title: '文章中心',
path: '/newsCenter',
type: 1,
children: [fixedNewsMenu],
}
const mergeMenuItems = (menus: MenuItem[]): MenuItem[] => {
const normalizedMenus = normalizeMenuPaths(menus)
const articleMenu = normalizedMenus.find(
(menu) => menu.path === fixedArticleMenu.path || menu.title === fixedArticleMenu.title,
)
if (!articleMenu) return [fixedArticleMenu, ...normalizedMenus]
const hasNewsCenter = articleMenu.children?.some(
(child) => child.path === '/newsCenter/news' || child.title === '新闻中心',
)
if (!hasNewsCenter) {
articleMenu.children = [
...(articleMenu.children || []),
fixedNewsMenu,
]
}
return normalizedMenus
} }
// 加载菜单数据 // 加载菜单数据
const loadMenu = async () => { const loadMenu = async () => {
try { try {
const response = await getHeadMenu() const response = await getHeadMenu()
if (response.code === 200) { if (response.code === 200 && Array.isArray(response.data)) {
menuItems.value = normalizeMenuPaths(response.data) menuItems.value = mergeMenuItems(response.data)
return
} }
} catch (error) { } catch (error) {
console.error('加载菜单失败:', error) console.error('加载菜单失败:', error)
menuItems.value = []
} }
menuItems.value = [fixedArticleMenu]
} }
// 拼接接口地址(用于 logo 等静态资源) // 拼接接口地址(用于 logo 等静态资源)
@@ -436,9 +489,18 @@ const activeMenu = ref<number | null>(null)
const activeCategory = ref<number>(0) const activeCategory = ref<number>(0)
const showSearch = ref(false) const showSearch = ref(false)
const showLanguage = ref(false) const showLanguage = ref(false)
const showTenantModal = ref(false)
const devTenantId = ref(sessionStorage.getItem('dev_tenant_id') || '')
const isDev = import.meta.env.DEV
const searchQuery = ref('') const searchQuery = ref('')
const currentLanguage = ref('zh') const currentLanguage = ref('zh')
const isScrolled = ref(false) const isScrolled = ref(false)
const saveTenant = () => {
sessionStorage.setItem('dev_tenant_id', devTenantId.value)
showTenantModal.value = false
window.location.reload()
}
const footerData = ref<any | null>(null) const footerData = ref<any | null>(null)
let closeTimer: ReturnType<typeof setTimeout> | null = null let closeTimer: ReturnType<typeof setTimeout> | null = null
@@ -834,6 +896,8 @@ onUnmounted(() => {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-size: 16px; font-size: 16px;
color: #333; /* 显式设置字体颜色为深色 */
background-color: #fff; /* 显式设置背景为白色 */
transition: border-color 0.3s ease; transition: border-color 0.3s ease;
&:focus { &:focus {
+1 -1
View File
@@ -188,7 +188,7 @@ const formatDate = (date: string) => {
} }
const goToDetail = (id: number) => { const goToDetail = (id: number) => {
router.push(`/newscenter/companyNews/detail/${id}`) router.push(`/newsCenter/companyNews/detail/${id}`)
} }
// 加载更多数据 // 加载更多数据
+5 -1
View File
@@ -61,6 +61,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
target: env.VITE_API_BASE_URL, // 后端 API 服务器地址 target: env.VITE_API_BASE_URL, // 后端 API 服务器地址
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
},
'/index': {
target: env.VITE_API_BASE_URL,
changeOrigin: true
} }
}, },
// 配置静态资源服务 // 配置静态资源服务
@@ -91,4 +95,4 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
exclude: [] exclude: []
} }
} }
}) })
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
package controllers
import (
"encoding/json"
"server/models"
"server/pkg/jwtutil"
"strconv"
"strings"
"time"
beego "github.com/beego/beego/v2/server/web"
)
type BackendMenuFrontController struct {
beego.Controller
}
func (c *BackendMenuFrontController) checkAuth() (uint64, bool) {
authHeader := c.Ctx.Request.Header.Get("Authorization")
if authHeader == "" {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未提供认证信息"}
_ = c.ServeJSON()
return 0, false
}
authParts := strings.SplitN(authHeader, " ", 2)
if len(authParts) != 2 || authParts[0] != "Bearer" {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "认证信息格式错误"}
_ = c.ServeJSON()
return 0, false
}
claims, err := jwtutil.ParseToken(authParts[1])
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "token无效"}
_ = c.ServeJSON()
return 0, false
}
return uint64(claims.TenantId), true
}
func (c *BackendMenuFrontController) List() {
tid, ok := c.checkAuth()
if !ok {
return
}
var menus []models.BackendMenuFront
_, err := models.Orm.QueryTable("yz_backend_menu_front").
Filter("tenant_id", tid).
Filter("delete_time__isnull", true).
OrderBy("sort").
All(&menus)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败"}
} else {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": menus}
}
_ = c.ServeJSON()
}
func (c *BackendMenuFrontController) Create() {
tid, ok := c.checkAuth()
if !ok {
return
}
var menu models.BackendMenuFront
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &menu); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
menu.TenantID = tid
menu.CreateTime = nil // 由orm自动处理
_, err := models.Orm.Insert(&menu)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败"}
} else {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功"}
}
_ = c.ServeJSON()
}
func (c *BackendMenuFrontController) Update() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
tid, ok := c.checkAuth()
if !ok {
return
}
var menu models.BackendMenuFront
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &menu); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
num, err := models.Orm.QueryTable("yz_backend_menu_front").
Filter("id", id).
Filter("tenant_id", tid).
Update(map[string]interface{}{
"pid": menu.Pid,
"title": menu.Title,
"path": menu.Path,
"component_path": menu.ComponentPath,
"icon": menu.Icon,
"sort": menu.Sort,
"status": menu.Status,
"is_visible": menu.IsVisible,
"type": menu.Type,
"permission": menu.Permission,
"update_time": time.Now(),
})
if err != nil || num == 0 {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败"}
} else {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
}
_ = c.ServeJSON()
}
func (c *BackendMenuFrontController) Delete() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
tid, ok := c.checkAuth()
if !ok {
return
}
num, err := models.Orm.QueryTable("yz_backend_menu_front").
Filter("id", id).
Filter("tenant_id", tid).
Update(map[string]interface{}{"delete_time": time.Now()})
if err != nil || num == 0 {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败"}
} else {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
}
_ = c.ServeJSON()
}
+72 -3
View File
@@ -1,6 +1,8 @@
package controllers package controllers
import ( import (
"fmt"
"net/url"
"strconv" "strconv"
"strings" "strings"
@@ -37,8 +39,54 @@ func (c *IndexPortalController) getTidByHost() uint64 {
return 1 // 默认租户 return 1 // 默认租户
} }
// GetHeadMenu GET /index/headmenu // 获取后端菜单数据(支持开发环境租户切换)
func (c *IndexPortalController) GetHeadMenu() { func (c *IndexPortalController) GetHeadMenu() {
// DEV 环境下支持通过 X-Tenant-ID 切换租户
runmode, _ := beego.AppConfig.String("runmode")
tenantName := c.Ctx.Request.Header.Get("X-Tenant-ID")
// 解码
if decoded, err := url.QueryUnescape(tenantName); err == nil {
tenantName = decoded
}
// 强制打印,确保你能从终端看到真实值
fmt.Printf("[DEBUG] runmode: %s, Decoded TenantName: %s\n", runmode, tenantName)
if runmode == "dev" && tenantName != "" {
// 查找租户 ID - 支持 name 或 short_name
var tenant models.SystemTenant
// 使用 OR 逻辑查询 name 或 short_name
cond := orm.NewCondition()
cond = cond.Or("tenant_name", tenantName).Or("tenant_short_name", tenantName)
err := models.Orm.QueryTable(new(models.SystemTenant)).
SetCond(cond).
One(&tenant)
if err != nil {
fmt.Printf("[DEBUG] QueryTenant Error: %v, Looking for: %s\n", err, tenantName)
} else {
fmt.Printf("[DEBUG] Found Tenant ID: %d\n", tenant.ID)
// 查询该租户的自定义菜单
var menus []models.BackendMenuFront
_, err = models.Orm.QueryTable("yz_backend_menu_front").
Filter("tenant_id", tenant.ID).
Filter("delete_time__isnull", true).
OrderBy("sort").
All(&menus)
if err == nil && len(menus) > 0 {
// 转换数据格式供前端使用
menuData := buildFrontMenuTree(menus, 0)
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": menuData}
_ = c.ServeJSON()
return
} else {
fmt.Printf("[DEBUG] No menus found for tenant: %d\n", tenant.ID)
}
}
}
// 前台的静态导航菜单结构 // 前台的静态导航菜单结构
menuData := []map[string]interface{}{ menuData := []map[string]interface{}{
{ {
@@ -103,6 +151,27 @@ func (c *IndexPortalController) GetHeadMenu() {
_ = c.ServeJSON() _ = c.ServeJSON()
} }
// 辅助构建树结构
func buildFrontMenuTree(menus []models.BackendMenuFront, pid int64) []map[string]interface{} {
var tree []map[string]interface{}
for _, m := range menus {
if int64(m.Pid) == pid {
node := map[string]interface{}{
"id": m.ID,
"title": m.Title,
"path": m.Path,
"type": m.Type,
}
children := buildFrontMenuTree(menus, int64(m.ID))
if len(children) > 0 {
node["children"] = children
}
tree = append(tree, node)
}
}
return tree
}
// GetFooterData GET /index/footerdata // GetFooterData GET /index/footerdata
func (c *IndexPortalController) GetFooterData() { func (c *IndexPortalController) GetFooterData() {
tid := c.getTidByHost() tid := c.getTidByHost()
@@ -296,7 +365,7 @@ func (c *IndexPortalController) GetCompanyNewsDetail() {
c.getArticleDetail() c.getArticleDetail()
} }
// GetKingdeeNewsDetail GET /index/kingdeenews/detail/:id // GetKingdeeNewsDetail GET /index/kingdeeNews/detail/:id
func (c *IndexPortalController) GetKingdeeNewsDetail() { func (c *IndexPortalController) GetKingdeeNewsDetail() {
c.getArticleDetail() c.getArticleDetail()
} }
@@ -393,4 +462,4 @@ func (c *IndexPortalController) GetOnePageByPath() {
"data": nil, "data": nil,
} }
_ = c.ServeJSON() _ = c.ServeJSON()
} }
+33
View File
@@ -0,0 +1,33 @@
package models
import (
"time"
"github.com/beego/beego/v2/client/orm"
)
type BackendMenuFront struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantID uint64 `orm:"column(tenant_id)" json:"tenant_id"`
Pid int64 `orm:"column(pid)" json:"pid"`
Title string `orm:"column(title)" json:"title"`
Path string `orm:"column(path)" json:"path"`
ComponentPath string `orm:"column(component_path)" json:"component_path"`
Icon string `orm:"column(icon)" json:"icon"`
Sort int `orm:"column(sort)" json:"sort"`
Status int8 `orm:"column(status)" json:"status"`
IsVisible int8 `orm:"column(is_visible)" json:"is_visible"`
Type int8 `orm:"column(type)" json:"type"`
Permission string `orm:"column(permission)" json:"permission"`
CreateTime *time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
}
func (m *BackendMenuFront) TableName() string {
return "yz_backend_menu_front"
}
func init() {
orm.RegisterModel(new(BackendMenuFront))
}
+200 -159
View File
@@ -1,159 +1,200 @@
package models package models
import ( import (
"sync" "sync"
"time" "time"
"github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/client/orm"
) )
// CmsArticleCategory CMS 文章分类 yz_cms_article_category // CmsArticleCategory CMS 文章分类 yz_cms_article_category
type CmsArticleCategory struct { type CmsArticleCategory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"` ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"` Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Cid uint64 `orm:"column(cid);default(0)" json:"cid"` Cid uint64 `orm:"column(cid);default(0)" json:"cid"`
Name string `orm:"column(name);size(100)" json:"name"` Name string `orm:"column(name);size(100)" json:"name"`
Image string `orm:"column(image);size(500);default()" json:"image"` Image string `orm:"column(image);size(500);default()" json:"image"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"` Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Sort int `orm:"column(sort);default(0)" json:"sort"` Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"` Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
} }
func (m *CmsArticleCategory) TableName() string { func (m *CmsArticleCategory) TableName() string {
return "yz_cms_article_category" return "yz_cms_article_category"
} }
// CmsArticle CMS 文章 yz_cms_article // CmsArticle CMS 文章 yz_cms_article
type CmsArticle struct { type CmsArticle struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"` ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"` Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Title string `orm:"column(title);size(255)" json:"title"` Title string `orm:"column(title);size(255)" json:"title"`
Author string `orm:"column(author);size(100);default()" json:"author"` Author string `orm:"column(author);size(100);default()" json:"author"`
CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"` CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"`
Content string `orm:"column(content);type(mediumtext);null" json:"content"` Content string `orm:"column(content);type(mediumtext);null" json:"content"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"` Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Image string `orm:"column(image);size(500);default()" json:"image"` Image string `orm:"column(image);size(500);default()" json:"image"`
IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"` IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"`
TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"` TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"`
Status int8 `orm:"column(status);default(0)" json:"status"` Status int8 `orm:"column(status);default(0)" json:"status"`
Top int8 `orm:"column(top);default(0)" json:"top"` Top int8 `orm:"column(top);default(0)" json:"top"`
Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"` Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"`
Views int `orm:"column(views);default(0)" json:"views"` Views int `orm:"column(views);default(0)" json:"views"`
Likes int `orm:"column(likes);default(0)" json:"likes"` Likes int `orm:"column(likes);default(0)" json:"likes"`
PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"` PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"`
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"` PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
} }
func (m *CmsArticle) TableName() string { func (m *CmsArticle) TableName() string {
return "yz_cms_article" return "yz_cms_article"
} }
var cmsArticleTablesOnce sync.Once var cmsArticleTablesOnce sync.Once
// EnsureCmsArticleTables 首次使用时自动建表(若不存在)。 // EnsureCmsArticleTables 首次使用时自动建表(若不存在)。
func EnsureCmsArticleTables() error { func EnsureCmsArticleTables() error {
var err error var err error
cmsArticleTablesOnce.Do(func() { cmsArticleTablesOnce.Do(func() {
_, err = Orm.Raw(` _, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article_category ( CREATE TABLE IF NOT EXISTS yz_cms_article_category (
id bigint unsigned NOT NULL AUTO_INCREMENT, id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0, tid bigint unsigned NOT NULL DEFAULT 0,
cid bigint unsigned NOT NULL DEFAULT 0, cid bigint unsigned NOT NULL DEFAULT 0,
name varchar(100) NOT NULL DEFAULT '', name varchar(100) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '', image varchar(500) NOT NULL DEFAULT '',
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '', ` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
sort int NOT NULL DEFAULT 0, sort int NOT NULL DEFAULT 0,
status tinyint NOT NULL DEFAULT 1, status tinyint NOT NULL DEFAULT 1,
create_time datetime NOT NULL, create_time datetime NOT NULL,
update_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL, delete_time datetime DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY idx_tid_cid (tid, cid) KEY idx_tid_cid (tid, cid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
if err != nil { if err != nil {
return return
} }
_, err = Orm.Raw(` _, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article ( CREATE TABLE IF NOT EXISTS yz_cms_article (
id bigint unsigned NOT NULL AUTO_INCREMENT, id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0, tid bigint unsigned NOT NULL DEFAULT 0,
title varchar(255) NOT NULL DEFAULT '', title varchar(255) NOT NULL DEFAULT '',
author varchar(100) NOT NULL DEFAULT '', author varchar(100) NOT NULL DEFAULT '',
cate_id bigint unsigned NOT NULL DEFAULT 0, cate_id bigint unsigned NOT NULL DEFAULT 0,
content mediumtext, content mediumtext,
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '', ` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '', image varchar(500) NOT NULL DEFAULT '',
is_trans tinyint NOT NULL DEFAULT 0, is_trans tinyint NOT NULL DEFAULT 0,
transurl varchar(500) DEFAULT NULL, transurl varchar(500) DEFAULT NULL,
status tinyint NOT NULL DEFAULT 0, status tinyint NOT NULL DEFAULT 0,
top tinyint NOT NULL DEFAULT 0, top tinyint NOT NULL DEFAULT 0,
recommend tinyint NOT NULL DEFAULT 0, recommend tinyint NOT NULL DEFAULT 0,
views int NOT NULL DEFAULT 0, views int NOT NULL DEFAULT 0,
likes int NOT NULL DEFAULT 0, likes int NOT NULL DEFAULT 0,
publisher_id bigint unsigned DEFAULT NULL, publisher_id bigint unsigned DEFAULT NULL,
publish_time datetime DEFAULT NULL, publish_time datetime DEFAULT NULL,
create_time datetime NOT NULL, create_time datetime NOT NULL,
update_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL, delete_time datetime DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY idx_tid_status (tid, status), KEY idx_tid_status (tid, status),
KEY idx_cate_id (cate_id) KEY idx_cate_id (cate_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
}) })
return err return err
} }
func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string { func EnsureCmsArticleDefaultCategories() error {
out := make(map[uint64]string) // 只初始化两级全局分类:文章中心(顶级)和新闻中心(文章中心的子分类)。
if len(ids) == 0 { // 不在启动时创建其它业务分类,后续分类由管理员按需新增。
return out defaults := []struct {
} name string
var rows []CmsArticleCategory cid uint64
_, _ = Orm.QueryTable(new(CmsArticleCategory)). sort int
Filter("tid", tid). }{
Filter("id__in", ids). {name: "文章中心", cid: 0, sort: 1},
Filter("delete_time__isnull", true). {name: "新闻中心", cid: 1, sort: 2},
All(&rows, "ID", "Name") }
for _, r := range rows {
out[r.ID] = r.Name for _, item := range defaults {
} count, err := Orm.QueryTable(new(CmsArticleCategory)).
return out Filter("tid", 0).
} Filter("name", item.name).
Filter("cid", item.cid).
func CmsFormatTime(t *time.Time) string { Filter("delete_time__isnull", true).
if t == nil { Count()
return "" if err != nil {
} return err
return t.Format("2006-01-02 15:04:05") }
} if count > 0 {
continue
func CmsSimilarArticles(tid uint64, title string, limit int) ([]orm.Params, error) { }
if limit <= 0 { now := time.Now()
limit = 5 _, err = Orm.Insert(&CmsArticleCategory{
} Tid: 0,
var rows []CmsArticle Cid: item.cid,
_, err := Orm.QueryTable(new(CmsArticle)). Name: item.name,
Filter("tid", tid). Sort: item.sort,
Filter("delete_time__isnull", true). Status: 1,
Filter("title__icontains", title). CreateTime: now,
Limit(limit). })
All(&rows, "ID", "Title") if err != nil {
if err != nil { return err
return nil, err }
} }
out := make([]orm.Params, 0, len(rows)) return nil
for _, r := range rows { }
out = append(out, orm.Params{
"id": r.ID, func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string {
"title": r.Title, out := make(map[uint64]string)
"similarity": 80, if len(ids) == 0 {
}) return out
} }
return out, nil var rows []CmsArticleCategory
} qs := Orm.QueryTable(new(CmsArticleCategory)).
Filter("id__in", ids).
Filter("delete_time__isnull", true)
cond := orm.NewCondition().Or("tid", 0).Or("tid", tid)
_, _ = qs.SetCond(cond).All(&rows, "ID", "Name")
for _, r := range rows {
out[r.ID] = r.Name
}
return out
}
func CmsFormatTime(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("2006-01-02 15:04:05")
}
func CmsSimilarArticles(tid uint64, title string, limit int) ([]orm.Params, error) {
if limit <= 0 {
limit = 5
}
var rows []CmsArticle
_, err := Orm.QueryTable(new(CmsArticle)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Filter("title__icontains", title).
Limit(limit).
All(&rows, "ID", "Title")
if err != nil {
return nil, err
}
out := make([]orm.Params, 0, len(rows))
for _, r := range rows {
out = append(out, orm.Params{
"id": r.ID,
"title": r.Title,
"similarity": 80,
})
}
return out, nil
}
+3 -1
View File
@@ -31,7 +31,9 @@ func RegisterAuthRoutes() {
// 菜单接口 // 菜单接口
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu") beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllBackendMenus") // 前端菜单接口
beego.Router("/backend/frontmenus", &controllers.BackendMenuFrontController{}, "get:List;post:Create")
beego.Router("/backend/frontmenus/:id", &controllers.BackendMenuFrontController{}, "post:Update;delete:Delete")
// 操作日志(yz_system_operation_log // 操作日志(yz_system_operation_log
beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List") beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List")