调试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 前端导航数据
* @returns {Promise}
*/
export function createFrontMenu(formData, options = {}) {
export function createFrontMenu(formData) {
return request({
url: "/backend/createfrontmenu",
url: "/backend/frontmenus",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
@@ -35,7 +32,7 @@ export function createFrontMenu(formData, options = {}) {
*/
export function editFrontMenu(id, frontMenuData) {
return request({
url: `/backend/editfrontmenu/${id}`,
url: `/backend/frontmenus/${id}`,
method: "post",
data: frontMenuData,
});
@@ -48,7 +45,7 @@ export function editFrontMenu(id, frontMenuData) {
*/
export function deleteFrontMenu(id) {
return request({
url: `/backend/deletefrontmenu/${id}`,
url: `/backend/frontmenus/${id}`,
method: "delete",
});
}
+34 -7
View File
@@ -6,7 +6,7 @@
</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>
<div class="error-text">{{ errorMsg }}</div>
<el-button size="small" @click="fetchMenus">重新加载</el-button>
@@ -289,13 +289,40 @@ const processMenus = (menus) => {
}));
};
const list = computed(() => {
const menuData = menuStore.menus;
if (!menuData || menuData.length === 0) {
return [];
}
const fixedCmsMenu = {
id: -200,
path: "/apps/cms",
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) => {
menus.forEach((menu) => {
+15
View File
@@ -3,6 +3,19 @@ import { convertMenusToRoutes } from "./dynamicRoutes";
// 静态子路由:需要在 Main 框架内显示的页面
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",
name: "userProfile",
@@ -90,6 +103,8 @@ export async function loadAndAddDynamicRoutes() {
routesLoadingPromise = null;
return Promise.resolve();
} else {
// 即使权限菜单为空,也要注册内置文章中心路由。
addDynamicRoutes([]);
dynamicRoutesAdded = true;
routesLoadingPromise = null;
return Promise.resolve();
@@ -156,6 +156,11 @@ function toggleExpand(category) {
}
function handleDelete(category) {
if (Number(category.tid) === 0 || category.is_global === true) {
ElMessage.warning("全局分类不可删除");
return;
}
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
@@ -53,9 +53,18 @@
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
<el-icon><Plus /></el-icon>
</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-button>
<el-tag v-else type="info" size="small" title="全局分类不可删除">
全局
</el-tag>
</el-button-group>
</div>
</div>
+3 -3
View File
@@ -2,18 +2,18 @@ import service from '@/utils/request'
// 获取前端导航
export const getHeadMenu = async () => {
const response = await service.get('index/headmenu')
const response = await service.get('/api/index/headmenu')
return response.data
}
// 根据路径获取单页内容
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
}
// 获取前端底部数据
export const getFooterData = async () => {
const response = await service.get('index/footerdata')
const response = await service.get('/api/index/footerdata')
return response.data
}
+28 -39
View File
@@ -1,52 +1,41 @@
import { createRouter, createWebHistory } from 'vue-router';
import { constantRoute, registerDynamicRoutes } from './routes';
import { createRouter, createWebHistory } from 'vue-router'
import { getHeadMenu } from '@/api/index'
import { constantRoute, registerDynamicRoutes } from './routes'
// 先创建路由,但不立即匹配
const router = createRouter({
history: createWebHistory(),
routes: constantRoute,
// 延迟路由匹配,等待动态路由注册完成
strict: false,
});
})
// 在应用启动时加载菜单并注册路由
let routesLoaded = false;
let routesLoadingPromise: Promise<void> | null = null;
let routesLoaded = false
let routesLoadingPromise: Promise<void> | null = null
// 固定路由先可用,再尝试加载后台菜单补充自定义页面。
export async function loadAndRegisterRoutes() {
// 如果已经加载过,直接返回
if (routesLoaded) {
return;
}
if (routesLoaded) return
if (routesLoadingPromise) return routesLoadingPromise
// 如果正在加载,等待加载完成
if (routesLoadingPromise) {
return routesLoadingPromise;
}
routesLoadingPromise = getHeadMenu()
.then((response) => {
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) => {
// 如果路由还未加载,先加载路由
if (!routesLoaded) {
// 等待路由加载完成
await routesLoadingPromise;
// 路由加载完成后,如果当前路径不匹配,尝试重新匹配
const matched = router.resolve(to.path).matched;
if (matched.length === 0 && to.path !== '/404') {
// 路由已加载但当前路径不匹配,可能是通配符路由
// 继续导航,让通配符路由处理
next();
return;
}
}
router.beforeEach(async (_to, _from, next) => {
await loadAndRegisterRoutes()
next()
})
// 路由已加载,继续导航
next();
});
export default router;
export default router
+99 -104
View File
@@ -1,124 +1,119 @@
import { createComponentLoader } from '@/utils/pathResolver'
// 对外暴配置路由,常量路由
// 对外暴配置路由。核心业务路由必须固定存在,不依赖后台菜单配置。
export const constantRoute = [
{
path: '/',
component: () => import('@/views/home/index.vue'),
name: '首页',
meta: {
title: '首页',
hidden: false,
},
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',
hidden: true,
icon: 'DocumentDelete',
},
},
]
{
path: '/',
component: () => import('@/views/home/index.vue'),
name: '首页',
meta: { title: '首页', hidden: false },
},
{
path: '/solution',
component: () => import('@/views/solution/index.vue'),
name: '解决方案',
meta: { title: '解决方案', hidden: false },
},
{
path: '/solution/solution',
component: () => import('@/views/solutions/solution/index.vue'),
name: '行业解决方案',
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[]) {
if (!menus || menus.length === 0) return
// 统一处理菜单路径大小写
const normalizeMenuPaths = (menus: any[]) => {
return menus.map(menu => {
const normalizedMenu = { ...menu }
// 将路径中的 /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 normalizeMenuPaths = (items: any[]): any[] => items.map((menu) => ({
...menu,
path: menu.path?.replace(/\/newscenter\//g, '/newsCenter/'),
children: menu.children?.length ? normalizeMenuPaths(menu.children) : menu.children,
}))
// 先规范化菜单路径
const normalizedMenus = normalizeMenuPaths(menus)
// 收集所有需要注册的路由
const routesToAdd: any[] = []
// 递归处理菜单,收集路由
const processMenu = (menu: any) => {
// type 2: 页面 - 根据 component_path 加载组件
if (menu.type === 2 && menu.path && menu.component_path) {
const path = menu.path
// 检查路由是否已存在
const existingRoute = router.getRoutes().find((r: any) => r.path === path)
if (!existingRoute) {
try {
// 使用 pathResolver 来解析 component_path
// component_path 可能的格式:
// 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)
}
if (!router.getRoutes().some((route: any) => route.path === path)) {
routesToAdd.push({
path,
name: `menu_${menu.id}`,
component: createComponentLoader(menu.component_path),
meta: { title: menu.title, menuId: menu.id, menuType: menu.type },
})
}
}
// type 4: 单页 - 使用通配符路由处理,不需要单独注册
// 递归处理子菜单
if (menu.children && menu.children.length > 0) {
menu.children.forEach((child: any) => processMenu(child))
}
menu.children?.forEach(processMenu)
}
// 处理所有菜单
normalizedMenus.forEach((menu) => processMenu(menu))
normalizeMenuPaths(menus).forEach(processMenu)
// 先移除通配符路由(如果存在),以便重新添加
const catchAllRoute = router.getRoutes().find((r: any) => r.path === '/:pathMatch(.*)*')
if (catchAllRoute) {
router.removeRoute('OnePage')
}
// 通配符路由必须最后注册,否则会抢先匹配后台新增的自定义页面。
const catchAll = router.getRoutes().find((route: any) => route.name === 'OnePage')
if (catchAll) router.removeRoute('OnePage')
// 注册 type 2 的路由(必须在通配符路由之前)
routesToAdd.forEach((route) => {
router.addRoute(route)
routesToAdd.forEach((route) => router.addRoute(route))
router.addRoute({
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实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
baseURL: '/',
timeout: 10000,
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
const devTenantId = sessionStorage.getItem('dev_tenant_id')
if (devTenantId) {
config.headers['X-Tenant-ID'] = encodeURIComponent(devTenantId)
}
return config
},
(error) => {
@@ -16,10 +20,10 @@ service.interceptors.request.use(
}
)
// 响应拦截器 - 返回 response.data
// 响应拦截器 - 保留完整 axios 响应,由 api 层统一取 response.data(即 {code, data, msg}
service.interceptors.response.use(
(response) => {
return response.data
return response
},
(error) => {
console.error('API请求错误:', error)
+80 -16
View File
@@ -301,12 +301,36 @@
<div class="search-icon" @click="showSearch = true">
<i class="fa-solid fa-magnifying-glass"></i>
</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">
<span>中文</span>
<i class="fa-solid fa-caret-down"></i>
</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
class="modal"
@@ -390,33 +414,62 @@ interface MenuItem {
// 菜单数据
const menuItems = ref<MenuItem[]>([])
// 统一处理菜单路径大小写
// 统一处理菜单路径大小写,并递归处理子菜单。
const normalizeMenuPaths = (menus: MenuItem[]): MenuItem[] => {
return menus.map((menu: MenuItem) => {
const normalizedMenu = { ...menu }
// 将路径中的 /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
})
return menus.map((menu) => ({
...menu,
path: menu.path?.replace(/\/newscenter\//g, '/newsCenter/'),
children: menu.children?.length ? normalizeMenuPaths(menu.children) : menu.children,
}))
}
// 后台菜单可配置,但文章中心是官网核心功能,必须始终存在。
const fixedNewsMenu: MenuChild = {
id: -101,
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 () => {
try {
const response = await getHeadMenu()
if (response.code === 200) {
menuItems.value = normalizeMenuPaths(response.data)
if (response.code === 200 && Array.isArray(response.data)) {
menuItems.value = mergeMenuItems(response.data)
return
}
} catch (error) {
console.error('加载菜单失败:', error)
menuItems.value = []
}
menuItems.value = [fixedArticleMenu]
}
// 拼接接口地址(用于 logo 等静态资源)
@@ -436,9 +489,18 @@ const activeMenu = ref<number | null>(null)
const activeCategory = ref<number>(0)
const showSearch = 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 currentLanguage = ref('zh')
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)
let closeTimer: ReturnType<typeof setTimeout> | null = null
@@ -834,6 +896,8 @@ onUnmounted(() => {
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
color: #333; /* 显式设置字体颜色为深色 */
background-color: #fff; /* 显式设置背景为白色 */
transition: border-color 0.3s ease;
&:focus {
+1 -1
View File
@@ -188,7 +188,7 @@ const formatDate = (date: string) => {
}
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 服务器地址
changeOrigin: true,
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: []
}
}
})
})
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
import (
"fmt"
"net/url"
"strconv"
"strings"
@@ -37,8 +39,54 @@ func (c *IndexPortalController) getTidByHost() uint64 {
return 1 // 默认租户
}
// GetHeadMenu GET /index/headmenu
// 获取后端菜单数据(支持开发环境租户切换)
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{}{
{
@@ -103,6 +151,27 @@ func (c *IndexPortalController) GetHeadMenu() {
_ = 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
func (c *IndexPortalController) GetFooterData() {
tid := c.getTidByHost()
@@ -296,7 +365,7 @@ func (c *IndexPortalController) GetCompanyNewsDetail() {
c.getArticleDetail()
}
// GetKingdeeNewsDetail GET /index/kingdeenews/detail/:id
// GetKingdeeNewsDetail GET /index/kingdeeNews/detail/:id
func (c *IndexPortalController) GetKingdeeNewsDetail() {
c.getArticleDetail()
}
@@ -393,4 +462,4 @@ func (c *IndexPortalController) GetOnePageByPath() {
"data": nil,
}
_ = 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
import (
"sync"
"time"
"github.com/beego/beego/v2/client/orm"
)
// CmsArticleCategory CMS 文章分类 yz_cms_article_category
type CmsArticleCategory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Cid uint64 `orm:"column(cid);default(0)" json:"cid"`
Name string `orm:"column(name);size(100)" json:"name"`
Image string `orm:"column(image);size(500);default()" json:"image"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
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"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *CmsArticleCategory) TableName() string {
return "yz_cms_article_category"
}
// CmsArticle CMS 文章 yz_cms_article
type CmsArticle struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Title string `orm:"column(title);size(255)" json:"title"`
Author string `orm:"column(author);size(100);default()" json:"author"`
CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"`
Content string `orm:"column(content);type(mediumtext);null" json:"content"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Image string `orm:"column(image);size(500);default()" json:"image"`
IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"`
TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"`
Status int8 `orm:"column(status);default(0)" json:"status"`
Top int8 `orm:"column(top);default(0)" json:"top"`
Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"`
Views int `orm:"column(views);default(0)" json:"views"`
Likes int `orm:"column(likes);default(0)" json:"likes"`
PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"`
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"`
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"`
}
func (m *CmsArticle) TableName() string {
return "yz_cms_article"
}
var cmsArticleTablesOnce sync.Once
// EnsureCmsArticleTables 首次使用时自动建表(若不存在)。
func EnsureCmsArticleTables() error {
var err error
cmsArticleTablesOnce.Do(func() {
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article_category (
id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0,
cid bigint unsigned NOT NULL DEFAULT 0,
name varchar(100) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '',
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
sort int NOT NULL DEFAULT 0,
status tinyint NOT NULL DEFAULT 1,
create_time datetime NOT NULL,
update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_tid_cid (tid, cid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article (
id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0,
title varchar(255) NOT NULL DEFAULT '',
author varchar(100) NOT NULL DEFAULT '',
cate_id bigint unsigned NOT NULL DEFAULT 0,
content mediumtext,
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '',
is_trans tinyint NOT NULL DEFAULT 0,
transurl varchar(500) DEFAULT NULL,
status tinyint NOT NULL DEFAULT 0,
top tinyint NOT NULL DEFAULT 0,
recommend tinyint NOT NULL DEFAULT 0,
views int NOT NULL DEFAULT 0,
likes int NOT NULL DEFAULT 0,
publisher_id bigint unsigned DEFAULT NULL,
publish_time datetime DEFAULT NULL,
create_time datetime NOT NULL,
update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_tid_status (tid, status),
KEY idx_cate_id (cate_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
})
return err
}
func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string {
out := make(map[uint64]string)
if len(ids) == 0 {
return out
}
var rows []CmsArticleCategory
_, _ = Orm.QueryTable(new(CmsArticleCategory)).
Filter("tid", tid).
Filter("id__in", ids).
Filter("delete_time__isnull", true).
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
}
package models
import (
"sync"
"time"
"github.com/beego/beego/v2/client/orm"
)
// CmsArticleCategory CMS 文章分类 yz_cms_article_category
type CmsArticleCategory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Cid uint64 `orm:"column(cid);default(0)" json:"cid"`
Name string `orm:"column(name);size(100)" json:"name"`
Image string `orm:"column(image);size(500);default()" json:"image"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
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"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *CmsArticleCategory) TableName() string {
return "yz_cms_article_category"
}
// CmsArticle CMS 文章 yz_cms_article
type CmsArticle struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Title string `orm:"column(title);size(255)" json:"title"`
Author string `orm:"column(author);size(100);default()" json:"author"`
CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"`
Content string `orm:"column(content);type(mediumtext);null" json:"content"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Image string `orm:"column(image);size(500);default()" json:"image"`
IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"`
TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"`
Status int8 `orm:"column(status);default(0)" json:"status"`
Top int8 `orm:"column(top);default(0)" json:"top"`
Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"`
Views int `orm:"column(views);default(0)" json:"views"`
Likes int `orm:"column(likes);default(0)" json:"likes"`
PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"`
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"`
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"`
}
func (m *CmsArticle) TableName() string {
return "yz_cms_article"
}
var cmsArticleTablesOnce sync.Once
// EnsureCmsArticleTables 首次使用时自动建表(若不存在)。
func EnsureCmsArticleTables() error {
var err error
cmsArticleTablesOnce.Do(func() {
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article_category (
id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0,
cid bigint unsigned NOT NULL DEFAULT 0,
name varchar(100) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '',
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
sort int NOT NULL DEFAULT 0,
status tinyint NOT NULL DEFAULT 1,
create_time datetime NOT NULL,
update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_tid_cid (tid, cid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article (
id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0,
title varchar(255) NOT NULL DEFAULT '',
author varchar(100) NOT NULL DEFAULT '',
cate_id bigint unsigned NOT NULL DEFAULT 0,
content mediumtext,
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '',
is_trans tinyint NOT NULL DEFAULT 0,
transurl varchar(500) DEFAULT NULL,
status tinyint NOT NULL DEFAULT 0,
top tinyint NOT NULL DEFAULT 0,
recommend tinyint NOT NULL DEFAULT 0,
views int NOT NULL DEFAULT 0,
likes int NOT NULL DEFAULT 0,
publisher_id bigint unsigned DEFAULT NULL,
publish_time datetime DEFAULT NULL,
create_time datetime NOT NULL,
update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_tid_status (tid, status),
KEY idx_cate_id (cate_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
})
return err
}
func EnsureCmsArticleDefaultCategories() error {
// 只初始化两级全局分类:文章中心(顶级)和新闻中心(文章中心的子分类)。
// 不在启动时创建其它业务分类,后续分类由管理员按需新增。
defaults := []struct {
name string
cid uint64
sort int
}{
{name: "文章中心", cid: 0, sort: 1},
{name: "新闻中心", cid: 1, sort: 2},
}
for _, item := range defaults {
count, err := Orm.QueryTable(new(CmsArticleCategory)).
Filter("tid", 0).
Filter("name", item.name).
Filter("cid", item.cid).
Filter("delete_time__isnull", true).
Count()
if err != nil {
return err
}
if count > 0 {
continue
}
now := time.Now()
_, err = Orm.Insert(&CmsArticleCategory{
Tid: 0,
Cid: item.cid,
Name: item.name,
Sort: item.sort,
Status: 1,
CreateTime: now,
})
if err != nil {
return err
}
}
return nil
}
func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string {
out := make(map[uint64]string)
if len(ids) == 0 {
return out
}
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/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
beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List")