优化backend产品和解决方案

This commit is contained in:
2026-08-20 17:47:47 +08:00
parent 735534c6e6
commit a4f7a97763
14 changed files with 201 additions and 67 deletions
+8
View File
@@ -296,6 +296,14 @@ const fixedCmsMenu = {
icon: "Document",
order: -200,
children: [
{
id: -2001,
path: "/apps/cms/analytics/content",
title: "内容统计",
icon: "DataAnalysis",
order: 0,
children: []
},
{
id: -201,
path: "/apps/cms/article",
+7 -5
View File
@@ -160,11 +160,13 @@ export function convertMenusToRoutes(menus, parentPath = '') {
} else if (hasChildren) {
route.component = () => import('@/views/layouts/EmptyLayout.vue');
route.children = convertMenusToRoutes(menu.children, fullPath);
const firstChild = menu.children[0];
if (firstChild && firstChild.path) {
const childFullPath = firstChild.path.startsWith('/')
? firstChild.path
: `${fullPath}/${firstChild.path}`;
const defaultChild = menu.children.find((child) =>
child.path === '/apps/cms/analytics/content' || child.path === 'analytics/content'
) || menu.children[0];
if (defaultChild && defaultChild.path) {
const childFullPath = defaultChild.path.startsWith('/')
? defaultChild.path
: `${fullPath}/${defaultChild.path}`;
route.redirect = childFullPath;
}
} else {
+6
View File
@@ -12,6 +12,12 @@ const staticMainChildren = [
},
// CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。
// 统一结构:index 为列表页,type 为分类页。
{
path: "/apps/cms/analytics/content",
name: "CmsContentAnalytics",
component: () => import("@/views/apps/cms/analytics/content/index.vue"),
meta: { requiresAuth: true, title: "内容统计", modulePath: "/apps/cms" }
},
{
path: "/apps/cms/article",
name: "CmsArticles",
@@ -98,6 +98,20 @@ const getCategoryName = (cate) => {
return category?.name || "无分类";
};
// 格式化日期
const formatDate = (date: string | number | Date) => {
if (!date) return "-";
const d = new Date(date);
if (isNaN(d.getTime())) return String(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
const seconds = String(d.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const emit = defineEmits(["update:modelValue"]);
const visible = ref(false);
@@ -213,8 +213,8 @@
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
import Edit from "./index/components/edit.vue";
import Preview from "./index/components/preview.vue";
import Edit from "./components/edit.vue";
import Preview from "./components/preview.vue";
import { useAuthStore } from "@/stores/auth";
import {
listArticles,
@@ -1,8 +1,9 @@
<template>
<el-dialog
<el-drawer
v-model="visible"
:title="title"
width="600px"
size="65%"
class="cms-product-drawer"
destroy-on-close
@close="handleClose"
>
@@ -11,6 +12,7 @@
:model="formData"
:rules="formRules"
label-width="100px"
class="product-form"
>
<el-form-item label="产品名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入产品名称" />
@@ -19,11 +21,11 @@
<el-input
v-model="formData.desc"
type="textarea"
:rows="5"
:rows="4"
placeholder="请输入产品描述"
/>
</el-form-item>
<el-form-item label="产品内容" prop="content">
<el-form-item label="产品内容" prop="content" class="content-form-item">
<div class="editor-container">
<UmoEditor v-model="formData.content" style="height: 450px;" />
</div>
@@ -87,7 +89,7 @@
</el-button>
</span>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup>
@@ -265,6 +267,30 @@ const handleSubmit = async () => {
</script>
<style scoped>
:deep(.el-drawer__body) {
overflow-x: hidden;
padding: 20px;
}
.product-form {
width: 100%;
}
:deep(.el-form-item__content) {
min-width: 0;
max-width: 100%;
}
.editor-container {
width: 100%;
min-width: 0;
max-width: 100%;
border: 1px solid var(--el-border-color, #dcdfe6);
border-radius: 4px;
overflow: hidden;
box-sizing: border-box;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
@@ -1,8 +1,9 @@
<template>
<el-dialog
<el-drawer
v-model="visible"
:title="title"
width="600px"
size="65%"
class="cms-solution-drawer"
destroy-on-close
@close="handleClose"
>
@@ -11,19 +12,25 @@
:model="formData"
:rules="formRules"
label-width="100px"
class="solution-form"
>
<el-form-item label="服务名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入服务名称" />
<el-form-item label="方案名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入解决方案名称" />
</el-form-item>
<el-form-item label="服务描述" prop="desc">
<el-form-item label="方案描述" prop="desc">
<el-input
v-model="formData.desc"
type="textarea"
:rows="5"
placeholder="请输入服务描述"
:rows="4"
placeholder="请输入解决方案描述"
/>
</el-form-item>
<el-form-item label="Logo" prop="thumb">
<el-form-item label="方案内容" prop="content" class="content-form-item">
<div class="editor-container">
<UmoEditor v-model="formData.content" style="height: 450px;" />
</div>
</el-form-item>
<el-form-item label="方案图片" prop="thumb">
<div class="flex-direction">
<el-upload
class="image-uploader"
@@ -42,7 +49,7 @@
/>
<div v-else class="upload-placeholder">
<el-icon class="image-uploader-icon"><Plus /></el-icon>
<div class="el-upload__text">点击上传服务图标</div>
<div class="el-upload__text">点击上传方案图片</div>
</div>
</el-upload>
<div
@@ -63,7 +70,7 @@
@click="handleRemoveImage"
style="margin-top: 8px"
>
删除图标
删除图片
</el-button>
</el-form-item>
<el-form-item label="排序" prop="sort">
@@ -82,7 +89,7 @@
</el-button>
</span>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup>
@@ -101,7 +108,7 @@ const props = defineProps({
},
title: {
type: String,
default: "添加服务",
default: "添加解决方案",
},
isEdit: {
type: Boolean,
@@ -144,7 +151,7 @@ const beforeImageUpload = (file) => {
// 图片上传成功
const handleImageSuccess = (response) => {
if (response.code === 200 || response.code === 201) {
formData.link_logo = response.data.url || response.data.path;
formData.thumb = response.data.url || response.data.path;
ElMessage.success(
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
);
@@ -160,7 +167,7 @@ const handleImageError = () => {
// 删除图片
const handleRemoveImage = () => {
formData.link_logo = "";
formData.thumb = "";
ElMessage.success("图片已删除");
};
@@ -174,6 +181,7 @@ const getImageUrl = (imagePath) => {
const formData = reactive({
title: "",
desc: "",
content: "",
thumb: "",
url: "",
sort: 0,
@@ -182,11 +190,11 @@ const formData = reactive({
const formRules = {
title: [
{ required: true, message: "请输入服务名称", trigger: "blur" },
{ required: true, message: "请输入解决方案名称", trigger: "blur" },
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
],
desc: [
{ required: true, message: "请输入服务描述", trigger: "blur" },
{ required: true, message: "请输入解决方案描述", trigger: "blur" },
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
],
};
@@ -214,6 +222,7 @@ watch(visible, (val) => {
const resetForm = () => {
formData.title = "";
formData.desc = "";
formData.content = "";
formData.thumb = "";
formData.url = "";
formData.sort = 0;
@@ -259,6 +268,30 @@ const handleSubmit = async () => {
</script>
<style scoped>
:deep(.el-drawer__body) {
overflow-x: hidden;
padding: 20px;
}
.solution-form {
width: 100%;
}
:deep(.el-form-item__content) {
min-width: 0;
max-width: 100%;
}
.editor-container {
width: 100%;
min-width: 0;
max-width: 100%;
border: 1px solid var(--el-border-color, #dcdfe6);
border-radius: 4px;
overflow: hidden;
box-sizing: border-box;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
@@ -1,7 +1,7 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>特色服务管理</h2>
<h2>解决方案管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleTypes">
<el-icon>
@@ -13,7 +13,7 @@
<el-icon>
<Plus />
</el-icon>
添加内容
添加解决方案
</el-button>
<el-button @click="refresh" :loading="loading">
<el-icon>
@@ -28,7 +28,7 @@
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="searchForm.keyword" placeholder="请输入内容搜索" clearable style="width: 200px; margin-right: 10px"
<el-input v-model="searchForm.keyword" placeholder="请输入解决方案名称搜索" clearable style="width: 200px; margin-right: 10px"
@keyup.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态筛选" clearable style="width: 120px; margin-right: 10px">
<el-option label="启用" :value="1" />
@@ -46,9 +46,23 @@
<!-- 数据表格 -->
<el-table :data="servicesList" style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="thumb" label="服务图片" min-width="150" align="center" />
<el-table-column prop="title" label="服务名称" min-width="150" align="center" />
<el-table-column prop="desc" label="服务描述" min-width="150" align="center" />
<el-table-column prop="thumb" label="方案图片" min-width="150" align="center">
<template #default="{ row }">
<el-image v-if="row.thumb" :src="getImageUrl(row.thumb)" :preview-src-list="[getImageUrl(row.thumb)]"
fit="cover" class="solution-thumb" :preview-teleported="true" lazy>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
<span v-else class="text-gray">暂无图片</span>
</template>
</el-table-column>
<el-table-column prop="title" label="方案名称" min-width="150" align="center" />
<el-table-column prop="desc" label="方案描述" min-width="150" align="center" />
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
@@ -148,8 +162,8 @@ const fetchList = async () => {
pagination.total = res.data.total
}
} catch (error) {
console.error('获取特色服务列表失败:', error)
ElMessage.error('获取特色服务列表失败')
console.error('获取解决方案列表失败:', error)
ElMessage.error('获取解决方案列表失败')
} finally {
loading.value = false
}
@@ -193,7 +207,7 @@ const handleSelectionChange = (selection) => {
// 添加
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '添加特色服务'
dialogTitle.value = '添加解决方案'
currentRow.value = {}
dialogVisible.value = true
}
@@ -201,7 +215,7 @@ const handleAdd = () => {
// 编辑
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑特色服务'
dialogTitle.value = '编辑解决方案'
currentRow.value = row
dialogVisible.value = true
}
@@ -209,7 +223,7 @@ const handleEdit = (row) => {
// 删除
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该特色服务吗?', '提示', {
await ElMessageBox.confirm('确定要删除该解决方案吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@@ -224,8 +238,8 @@ const handleDelete = async (row) => {
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除特色服务失败:', error)
ElMessage.error('删除特色服务失败')
console.error('删除解决方案失败:', error)
ElMessage.error('删除解决方案失败')
}
}
}
@@ -241,8 +255,8 @@ const handleStatusChange = async (row, val) => {
row.status = val === 1 ? 0 : 1
}
} catch (error) {
console.error('更新特色服务状态失败:', error)
ElMessage.error('更新特色服务状态失败')
console.error('更新解决方案状态失败:', error)
ElMessage.error('更新解决方案状态失败')
row.status = val === 1 ? 0 : 1
}
}
+26 -7
View File
@@ -1,11 +1,13 @@
<template>
<umo-editor
ref="editorRef"
v-bind="options"
@changed="onChanged"
@created="onCreated"
@file-upload="onFileUpload"
/>
<div class="umo-editor-wrapper">
<umo-editor
ref="editorRef"
v-bind="options"
@changed="onChanged"
@created="onCreated"
@file-upload="onFileUpload"
/>
</div>
</template>
<script setup>
@@ -64,3 +66,20 @@ watch(
}
)
</script>
<style scoped>
.umo-editor-wrapper {
width: 100%;
max-width: 100%;
min-width: 0;
height: 100%;
box-sizing: border-box;
overflow: hidden;
}
:deep(.umo-editor),
:deep(umo-editor) {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
</style>
+10 -5
View File
@@ -63,7 +63,7 @@
v-for="module in basicModules"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
@click="handleNavigate(module)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
@@ -85,7 +85,7 @@
v-for="module in systemModules"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
@click="handleNavigate(module)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
@@ -107,7 +107,7 @@
v-for="module in uncategorizedModules"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
@click="handleNavigate(module)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
@@ -222,8 +222,13 @@ const userName = computed(
const userAvatar = computed(() => authStore.user?.avatar || "");
// 处理导航跳转
function handleNavigate(path: string) {
if (path) router.push(path);
function handleNavigate(module: ModuleItem) {
const isWebsiteManagement = module.name === "网站管理" || module.path.startsWith("/apps/cms");
const targetPath = isWebsiteManagement
? "/apps/cms/analytics/content"
: module.path;
if (targetPath) router.push(targetPath);
}
// 刷新菜单
+10 -6
View File
@@ -60,6 +60,7 @@ func cmsSolutionToMap(row models.CmsSolution) map[string]interface{} {
"title": row.Title,
"thumb": row.Thumb,
"desc": row.Desc,
"content": row.Content,
"url": row.URL,
"sort": row.Sort,
"status": row.Status,
@@ -141,12 +142,13 @@ func (c *BackendSolutionController) List() {
}
type cmsSolutionPayload struct {
Title string `json:"title"`
Thumb string `json:"thumb"`
Desc string `json:"desc"`
URL string `json:"url"`
Sort int `json:"sort"`
Status int8 `json:"status"`
Title string `json:"title"`
Thumb string `json:"thumb"`
Desc string `json:"desc"`
Content string `json:"content"`
URL string `json:"url"`
Sort int `json:"sort"`
Status int8 `json:"status"`
}
// Create POST /backend/addServices
@@ -187,6 +189,7 @@ func (c *BackendSolutionController) Create() {
Title: title,
Thumb: strings.TrimSpace(p.Thumb),
Desc: strings.TrimSpace(p.Desc),
Content: p.Content,
URL: strings.TrimSpace(p.URL),
Sort: p.Sort,
Status: p.Status,
@@ -245,6 +248,7 @@ func (c *BackendSolutionController) Update() {
"title": title,
"thumb": strings.TrimSpace(p.Thumb),
"desc": strings.TrimSpace(p.Desc),
"content": p.Content,
"url": strings.TrimSpace(p.URL),
"sort": p.Sort,
"status": p.Status,
-6
View File
@@ -2,8 +2,6 @@ package models
import (
"time"
"github.com/beego/beego/v2/client/orm"
)
type BackendMenuFront struct {
@@ -27,7 +25,3 @@ type BackendMenuFront struct {
func (m *BackendMenuFront) TableName() string {
return "yz_backend_menu_front"
}
func init() {
orm.RegisterModel(new(BackendMenuFront))
}
+4
View File
@@ -12,6 +12,7 @@ type CmsSolution struct {
Title string `orm:"column(title);size(100)" json:"title"`
Thumb string `orm:"column(thumb);size(500);default()" json:"thumb"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Content string `orm:"column(content);type(mediumtext);null" json:"content"`
URL string `orm:"column(url);size(500);default()" json:"url"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
@@ -55,6 +56,7 @@ CREATE TABLE IF NOT EXISTS yz_cms_solution (
title varchar(100) NOT NULL DEFAULT '',
thumb varchar(500) NOT NULL DEFAULT '',
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
content mediumtext,
url varchar(500) NOT NULL DEFAULT '',
sort int NOT NULL DEFAULT 0,
status tinyint NOT NULL DEFAULT 1,
@@ -67,6 +69,8 @@ CREATE TABLE IF NOT EXISTS yz_cms_solution (
if err != nil {
return
}
// 确保已有表补齐 content 字段
_, _ = Orm.Raw("ALTER TABLE yz_cms_solution ADD COLUMN content mediumtext").Exec()
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_solution_category (
id bigint unsigned NOT NULL AUTO_INCREMENT,
+5
View File
@@ -69,11 +69,16 @@ func Init(_ string) {
new(CmsArticleCategory),
new(CmsArticle),
new(CmsProductCategory),
new(CmsProduct),
new(CmsSolutionCategory),
new(CmsSolution),
new(CmsFrontendTemplate),
new(CmsFrontendBanner),
new(CmsFrontendFriendlink),
new(CmsFrontendOnepage),
new(BackendMenuFront),
new(SystemReminderList),