优化企业网站功能
This commit is contained in:
@@ -51,3 +51,57 @@ export function deleteService(id) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
////////////////////////////分类相关////////////////////////////
|
||||
|
||||
/**
|
||||
* 获取解决方案分类列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getServicesTypesList(params) {
|
||||
return request({
|
||||
url: '/backend/servicesTypesList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加解决方案分类
|
||||
* @param {Object} data - 分类数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addServicesTypes(data) {
|
||||
return request({
|
||||
url: '/backend/addServicesTypes',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新解决方案分类
|
||||
* @param {number} id - 分类ID
|
||||
* @param {Object} data - 分类数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateServicesTypes(id, data) {
|
||||
return request({
|
||||
url: `/backend/editServicesTypes/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除解决方案分类
|
||||
* @param {number} id - 分类ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteServicesTypes(id) {
|
||||
return request({
|
||||
url: `/backend/deleteServicesTypes/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -292,13 +292,13 @@ const processMenus = (menus) => {
|
||||
const fixedCmsMenu = {
|
||||
id: -200,
|
||||
path: "/apps/cms",
|
||||
title: "文章中心",
|
||||
title: "内容管理",
|
||||
icon: "Document",
|
||||
order: -200,
|
||||
children: [
|
||||
{
|
||||
id: -201,
|
||||
path: "/apps/cms/articles",
|
||||
path: "/apps/cms/article",
|
||||
title: "文章管理",
|
||||
icon: "Document",
|
||||
order: 1,
|
||||
@@ -306,11 +306,43 @@ const fixedCmsMenu = {
|
||||
},
|
||||
{
|
||||
id: -202,
|
||||
path: "/apps/cms/articles/category",
|
||||
path: "/apps/cms/article/type",
|
||||
title: "文章分类",
|
||||
icon: "Folder",
|
||||
order: 2,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: -203,
|
||||
path: "/apps/cms/products",
|
||||
title: "产品管理",
|
||||
icon: "Goods",
|
||||
order: 3,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: -204,
|
||||
path: "/apps/cms/products/types",
|
||||
title: "产品分类",
|
||||
icon: "Folder",
|
||||
order: 4,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: -205,
|
||||
path: "/apps/cms/solutions",
|
||||
title: "解决方案管理",
|
||||
icon: "Promotion",
|
||||
order: 5,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: -206,
|
||||
path: "/apps/cms/solutions/types",
|
||||
title: "解决方案分类",
|
||||
icon: "Folder",
|
||||
order: 6,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -3,19 +3,60 @@ import { convertMenusToRoutes } from "./dynamicRoutes";
|
||||
|
||||
// 静态子路由:需要在 Main 框架内显示的页面
|
||||
const staticMainChildren = [
|
||||
// CMS 文章中心是系统内置功能,不依赖数据库菜单配置。
|
||||
// 默认首页:访问 / 时直接展示工作台,URL 保持为 /
|
||||
{
|
||||
path: "/apps/cms/articles",
|
||||
path: "",
|
||||
name: "Dashboard",
|
||||
component: () => import("@/views/dashboard/index.vue"),
|
||||
meta: { requiresAuth: true, title: "工作台" }
|
||||
},
|
||||
// CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。
|
||||
// 统一结构:index 为列表页,type 为分类页。
|
||||
{
|
||||
path: "/apps/cms/article",
|
||||
name: "CmsArticles",
|
||||
component: () => import("@/views/apps/cms/articles/index.vue"),
|
||||
component: () => import("@/views/apps/cms/article/index/index.vue"),
|
||||
meta: { requiresAuth: true, title: "文章管理", modulePath: "/apps/cms" }
|
||||
},
|
||||
{
|
||||
path: "/apps/cms/articles/category",
|
||||
path: "/apps/cms/article/type",
|
||||
name: "CmsArticleCategories",
|
||||
component: () => import("@/views/apps/cms/articles/category.vue"),
|
||||
component: () => import("@/views/apps/cms/article/type/index.vue"),
|
||||
meta: { requiresAuth: true, title: "文章分类", modulePath: "/apps/cms" }
|
||||
},
|
||||
{
|
||||
path: "/apps/cms/products",
|
||||
name: "CmsProducts",
|
||||
component: () => import("@/views/apps/cms/product/index/index.vue"),
|
||||
meta: { requiresAuth: true, title: "产品管理", modulePath: "/apps/cms" }
|
||||
},
|
||||
{
|
||||
path: "/apps/cms/products/types",
|
||||
name: "CmsProductCategories",
|
||||
component: () => import("@/views/apps/cms/product/type/index.vue"),
|
||||
meta: { requiresAuth: true, title: "产品分类", modulePath: "/apps/cms" }
|
||||
},
|
||||
{
|
||||
path: "/apps/cms/solutions",
|
||||
name: "CmsSolutions",
|
||||
component: () => import("@/views/apps/cms/solution/index/index.vue"),
|
||||
meta: { requiresAuth: true, title: "解决方案管理", modulePath: "/apps/cms" }
|
||||
},
|
||||
{
|
||||
path: "/apps/cms/solutions/types",
|
||||
name: "CmsSolutionCategories",
|
||||
component: () => import("@/views/apps/cms/solution/type/index.vue"),
|
||||
meta: { requiresAuth: true, title: "解决方案分类", modulePath: "/apps/cms" }
|
||||
},
|
||||
// 兼容旧路径:articles/* -> article/*
|
||||
{
|
||||
path: "/apps/cms/articles",
|
||||
redirect: "/apps/cms/article"
|
||||
},
|
||||
{
|
||||
path: "/apps/cms/articles/category",
|
||||
redirect: "/apps/cms/article/type"
|
||||
},
|
||||
{
|
||||
path: "/user/userProfile",
|
||||
name: "userProfile",
|
||||
@@ -138,7 +179,6 @@ function addDynamicRoutes(menus) {
|
||||
path: "/",
|
||||
name: "Main",
|
||||
component: () => import("@/views/Main.vue"),
|
||||
redirect: "/dashboard",
|
||||
meta: { requiresAuth: true },
|
||||
children: [...staticMainChildren, ...dynamicRoutes] // 合并静态和动态子路由
|
||||
});
|
||||
|
||||
+686
-686
File diff suppressed because it is too large
Load Diff
+349
-349
@@ -1,349 +1,349 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="文章预览" size="60%">
|
||||
<div class="article-preview">
|
||||
<div class="article-header">
|
||||
<h1 class="article-title">{{ model?.title || "无标题" }}</h1>
|
||||
<div class="article-meta">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-price-tag"></i>
|
||||
<el-tag type="primary">{{ getCategoryName(model?.cate) }}</el-tag>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-user"></i>
|
||||
作者:{{ model?.author }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
发布人:{{ model?.publisher || "暂未发布" }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
阅读量:{{ model?.views || 0 }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
收藏量:{{ model?.likes || 0 }}
|
||||
</span>
|
||||
<span class="meta-item" v-if="model?.publish_time">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ formatDate(model.publish_time) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
创建日期:{{ model?.create_time }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
<div
|
||||
v-if="model?.content"
|
||||
v-html="model.content"
|
||||
class="content-html"
|
||||
></div>
|
||||
<div v-else class="no-content">
|
||||
<el-empty description="暂无内容"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { listCategories } from "@/api/article";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const categoryOptions = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await listCategories({ page: 1, limit: 1000 });
|
||||
if (res.code === 200) {
|
||||
categoryOptions.value = Array.isArray(res.data)
|
||||
? res.data
|
||||
: Array.isArray(res.data?.list)
|
||||
? res.data.list
|
||||
: [];
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "获取分类列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取分类列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 根据分类ID获取分类名称
|
||||
const getCategoryName = (cate) => {
|
||||
if (!cate && cate !== 0) return "无分类";
|
||||
// 确保比较时类型一致
|
||||
const cateId = Number(cate);
|
||||
// 传入的不是数字(已经是分类名),直接展示
|
||||
if (Number.isNaN(cateId)) return String(cate);
|
||||
const category = categoryOptions.value.find(
|
||||
(item) => Number(item.id) === cateId,
|
||||
);
|
||||
return category?.name || "无分类";
|
||||
};
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-preview {
|
||||
padding: 20px;
|
||||
|
||||
.article-header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.article-title {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
/* 确保article-content直接子元素的样式 */
|
||||
& > div {
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 支持所有子元素的内联样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
ul,
|
||||
ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
blockquote {
|
||||
border-left: 4px solid #ebeef5;
|
||||
padding-left: 15px;
|
||||
margin: 15px 0;
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
:deep(code) {
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 13px;
|
||||
color: #f06b6b;
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
:deep(pre) {
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d3f 100%);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
margin: 20px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 图片样式 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保内联样式能够生效 */
|
||||
[style] {
|
||||
/* 允许内联样式正常工作 */
|
||||
all: unset;
|
||||
/* 重新应用基本样式 */
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
/* 允许特定内联样式覆盖 */
|
||||
&[style*="text-align"] {
|
||||
text-align: var(--text-align, inherit) !important;
|
||||
}
|
||||
&[style*="color"] {
|
||||
color: var(--color, inherit) !important;
|
||||
}
|
||||
&[style*="font-size"] {
|
||||
font-size: var(--font-size, inherit) !important;
|
||||
}
|
||||
&[style*="font-weight"] {
|
||||
font-weight: var(--font-weight, inherit) !important;
|
||||
}
|
||||
&[style*="font-style"] {
|
||||
font-style: var(--font-style, inherit) !important;
|
||||
}
|
||||
&[style*="text-decoration"] {
|
||||
text-decoration: var(--text-decoration, inherit) !important;
|
||||
}
|
||||
&[style*="margin"] {
|
||||
margin: var(--margin, inherit) !important;
|
||||
}
|
||||
&[style*="padding"] {
|
||||
padding: var(--padding, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 直接支持text-align属性 */
|
||||
[align] {
|
||||
text-align: attr(align) !important;
|
||||
}
|
||||
|
||||
/* 确保content-html类的样式 */
|
||||
.content-html {
|
||||
& > * {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
& > h1,
|
||||
& > h2,
|
||||
& > h3,
|
||||
& > h4,
|
||||
& > h5,
|
||||
& > h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 支持居中样式 */
|
||||
& > p[style*="text-align: center"],
|
||||
& > p[align="center"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* 图片居中 */
|
||||
& > p[style*="text-align: center"] img,
|
||||
& > p[align="center"] img {
|
||||
display: inline-block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.no-content {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="文章预览" size="60%">
|
||||
<div class="article-preview">
|
||||
<div class="article-header">
|
||||
<h1 class="article-title">{{ model?.title || "无标题" }}</h1>
|
||||
<div class="article-meta">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-price-tag"></i>
|
||||
<el-tag type="primary">{{ getCategoryName(model?.cate) }}</el-tag>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-user"></i>
|
||||
作者:{{ model?.author }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
发布人:{{ model?.publisher || "暂未发布" }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
阅读量:{{ model?.views || 0 }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
收藏量:{{ model?.likes || 0 }}
|
||||
</span>
|
||||
<span class="meta-item" v-if="model?.publish_time">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ formatDate(model.publish_time) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
创建日期:{{ model?.create_time }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
<div
|
||||
v-if="model?.content"
|
||||
v-html="model.content"
|
||||
class="content-html"
|
||||
></div>
|
||||
<div v-else class="no-content">
|
||||
<el-empty description="暂无内容"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { listCategories } from "@/api/article";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const categoryOptions = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await listCategories({ page: 1, limit: 1000 });
|
||||
if (res.code === 200) {
|
||||
categoryOptions.value = Array.isArray(res.data)
|
||||
? res.data
|
||||
: Array.isArray(res.data?.list)
|
||||
? res.data.list
|
||||
: [];
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "获取分类列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取分类列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 根据分类ID获取分类名称
|
||||
const getCategoryName = (cate) => {
|
||||
if (!cate && cate !== 0) return "无分类";
|
||||
// 确保比较时类型一致
|
||||
const cateId = Number(cate);
|
||||
// 传入的不是数字(已经是分类名),直接展示
|
||||
if (Number.isNaN(cateId)) return String(cate);
|
||||
const category = categoryOptions.value.find(
|
||||
(item) => Number(item.id) === cateId,
|
||||
);
|
||||
return category?.name || "无分类";
|
||||
};
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-preview {
|
||||
padding: 20px;
|
||||
|
||||
.article-header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.article-title {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
/* 确保article-content直接子元素的样式 */
|
||||
& > div {
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 支持所有子元素的内联样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
ul,
|
||||
ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
blockquote {
|
||||
border-left: 4px solid #ebeef5;
|
||||
padding-left: 15px;
|
||||
margin: 15px 0;
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
:deep(code) {
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 13px;
|
||||
color: #f06b6b;
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
:deep(pre) {
|
||||
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d3f 100%);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
margin: 20px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 图片样式 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保内联样式能够生效 */
|
||||
[style] {
|
||||
/* 允许内联样式正常工作 */
|
||||
all: unset;
|
||||
/* 重新应用基本样式 */
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
/* 允许特定内联样式覆盖 */
|
||||
&[style*="text-align"] {
|
||||
text-align: var(--text-align, inherit) !important;
|
||||
}
|
||||
&[style*="color"] {
|
||||
color: var(--color, inherit) !important;
|
||||
}
|
||||
&[style*="font-size"] {
|
||||
font-size: var(--font-size, inherit) !important;
|
||||
}
|
||||
&[style*="font-weight"] {
|
||||
font-weight: var(--font-weight, inherit) !important;
|
||||
}
|
||||
&[style*="font-style"] {
|
||||
font-style: var(--font-style, inherit) !important;
|
||||
}
|
||||
&[style*="text-decoration"] {
|
||||
text-decoration: var(--text-decoration, inherit) !important;
|
||||
}
|
||||
&[style*="margin"] {
|
||||
margin: var(--margin, inherit) !important;
|
||||
}
|
||||
&[style*="padding"] {
|
||||
padding: var(--padding, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 直接支持text-align属性 */
|
||||
[align] {
|
||||
text-align: attr(align) !important;
|
||||
}
|
||||
|
||||
/* 确保content-html类的样式 */
|
||||
.content-html {
|
||||
& > * {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
& > h1,
|
||||
& > h2,
|
||||
& > h3,
|
||||
& > h4,
|
||||
& > h5,
|
||||
& > h6 {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 支持居中样式 */
|
||||
& > p[style*="text-align: center"],
|
||||
& > p[align="center"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* 图片居中 */
|
||||
& > p[style*="text-align: center"] img,
|
||||
& > p[align="center"] img {
|
||||
display: inline-block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.no-content {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+677
-677
File diff suppressed because it is too large
Load Diff
+277
-277
@@ -1,278 +1,278 @@
|
||||
<template>
|
||||
<div class="category-node-wrapper">
|
||||
<div
|
||||
class="category-item"
|
||||
:class="{ 'child-item': level > 0, disabled: !item.status }"
|
||||
>
|
||||
<div
|
||||
class="category-main"
|
||||
:style="{ paddingLeft: (level * 24 + 20) + 'px' }"
|
||||
@click="handleRowClick"
|
||||
>
|
||||
<div class="expand-btn" v-if="item.children && item.children.length > 0">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="toggleExpand"
|
||||
class="expand-button"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.expanded ? 'ArrowDown' : 'ArrowRight'" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="expand-spacer" v-else></div>
|
||||
|
||||
<div class="category-info">
|
||||
<div class="color-dot" :style="{ backgroundColor: item.color }"></div>
|
||||
<div class="category-icon">
|
||||
<el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
|
||||
<el-icon v-else class="default-icon"><Document /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="category-text">
|
||||
<div class="category-title">
|
||||
<span class="name">{{ item.label }}</span>
|
||||
<el-tag
|
||||
:type="item.status ? 'success' : 'danger'"
|
||||
size="small"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ item.status ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="category-desc" v-if="item.remark">{{ item.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-actions">
|
||||
<el-button-group size="small">
|
||||
<el-button type="text" title="编辑" @click.stop="$emit('edit', item)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.children && item.children.length > 0 && item.expanded"
|
||||
class="category-children"
|
||||
>
|
||||
<category-node
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
@edit="$emit('edit', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown, ArrowRight, Edit, Plus, Delete, Document } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
level: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'add-child', 'delete']);
|
||||
|
||||
const toggleExpand = () => {
|
||||
props.item.expanded = !props.item.expanded;
|
||||
};
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (props.item.children && props.item.children.length > 0) {
|
||||
toggleExpand();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-children {
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
.category-main {
|
||||
padding: 12px !important;
|
||||
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<template>
|
||||
<div class="category-node-wrapper">
|
||||
<div
|
||||
class="category-item"
|
||||
:class="{ 'child-item': level > 0, disabled: !item.status }"
|
||||
>
|
||||
<div
|
||||
class="category-main"
|
||||
:style="{ paddingLeft: (level * 24 + 20) + 'px' }"
|
||||
@click="handleRowClick"
|
||||
>
|
||||
<div class="expand-btn" v-if="item.children && item.children.length > 0">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="toggleExpand"
|
||||
class="expand-button"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.expanded ? 'ArrowDown' : 'ArrowRight'" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="expand-spacer" v-else></div>
|
||||
|
||||
<div class="category-info">
|
||||
<div class="color-dot" :style="{ backgroundColor: item.color }"></div>
|
||||
<div class="category-icon">
|
||||
<el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
|
||||
<el-icon v-else class="default-icon"><Document /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="category-text">
|
||||
<div class="category-title">
|
||||
<span class="name">{{ item.label }}</span>
|
||||
<el-tag
|
||||
:type="item.status ? 'success' : 'danger'"
|
||||
size="small"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ item.status ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="category-desc" v-if="item.remark">{{ item.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-actions">
|
||||
<el-button-group size="small">
|
||||
<el-button type="text" title="编辑" @click.stop="$emit('edit', item)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="text" title="添加子分类" @click.stop="$emit('add-child', item)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.children && item.children.length > 0 && item.expanded"
|
||||
class="category-children"
|
||||
>
|
||||
<category-node
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
@edit="$emit('edit', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown, ArrowRight, Edit, Plus, Delete, Document } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
level: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'add-child', 'delete']);
|
||||
|
||||
const toggleExpand = () => {
|
||||
props.item.expanded = !props.item.expanded;
|
||||
};
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (props.item.children && props.item.children.length > 0) {
|
||||
toggleExpand();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-children {
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
.category-main {
|
||||
padding: 12px !important;
|
||||
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+324
-324
@@ -1,324 +1,324 @@
|
||||
<template>
|
||||
<el-dialog v-model="visibleInternal" :title="dialogTitle" width="600px" :close-on-click-modal="false"
|
||||
@close="closeDialog">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="分类名称" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入分类名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="父级分类" prop="parentId">
|
||||
<el-tree-select v-model="formData.parentId" :data="treeData"
|
||||
:props="{ label: 'label', value: 'id', children: 'children' }" placeholder="选择父级分类(可选)" clearable
|
||||
filterable check-strictly style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right"
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="默认图片" prop="image">
|
||||
<div class="uploads">
|
||||
<el-upload v-model:file-list="fileList" :auto-upload="false" :before-upload="beforeImgUpload" list-type="picture-card" :limit="1" :on-change="handleUploadChange">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<el-icon>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<div class="upload-tip">
|
||||
<span>建议尺寸:250px × 140px</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="4" placeholder="请输入分类描述..." maxlength="200"
|
||||
show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
{{ isEdit ? '更新' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Plus, ZoomIn, Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
import { createCategory, editCategory, listCategories } from '@/api/article'
|
||||
import { uploadFile } from '@/api/file.js'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
category: { type: Object as () => any | null, default: null }, // 传入的分类对象,null 表示新增
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
// 可见性同步
|
||||
const visibleInternal = ref(props.modelValue)
|
||||
watch(() => props.modelValue, v => (visibleInternal.value = v))
|
||||
watch(visibleInternal, v => {
|
||||
emit('update:modelValue', v)
|
||||
if (v) loadTreeData()
|
||||
})
|
||||
|
||||
// 数据加载:父级分类树
|
||||
const treeData = ref<any[]>([])
|
||||
watch(treeData, () => {
|
||||
// 触发tree-select重新渲染已选 label
|
||||
formData.parentId = formData.parentId ?? 0
|
||||
})
|
||||
|
||||
async function loadTreeData() {
|
||||
const res = await listCategories({ page: 1, pageSize: 1000 })
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.records || res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建树形
|
||||
function buildTree(data: any[]) {
|
||||
const map = new Map()
|
||||
const roots: any[] = []
|
||||
data.forEach((item) => {
|
||||
map.set(item.id, { ...item, label: item.name ?? item.label, children: [] })
|
||||
})
|
||||
map.forEach((item: any) => {
|
||||
if (item.cid && map.has(item.cid)) {
|
||||
map.get(item.cid).children.push(item)
|
||||
} else {
|
||||
roots.push(item)
|
||||
}
|
||||
})
|
||||
return [{ id: 0, label: '顶级', children: roots }]
|
||||
}
|
||||
|
||||
// 表单
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
id: null as number | null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0 as number,
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
if (formRef.value) formRef.value.clearValidate()
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// 当传入 category 变化时同步
|
||||
watch(
|
||||
() => props.category,
|
||||
(val) => {
|
||||
if (val) {
|
||||
Object.assign(formData, {
|
||||
id: val.id,
|
||||
label: val.label,
|
||||
image: val.image,
|
||||
remark: val.remark,
|
||||
sort: val.sort,
|
||||
status: val.status,
|
||||
parentId: val.parentId ?? val.cid ?? 0,
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 上传相关
|
||||
const fileList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
function beforeImgUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isImage) ElMessage.error('仅支持图片格式')
|
||||
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
|
||||
return isImage && isLt10M
|
||||
}
|
||||
|
||||
function handleRemove(file: any) {
|
||||
fileList.value = []
|
||||
formData.image = ''
|
||||
}
|
||||
|
||||
async function handleUploadChange(file: any) {
|
||||
if (file.raw) {
|
||||
const isImage = file.raw.type.startsWith('image/')
|
||||
const isLt10M = file.raw.size / 1024 / 1024 < 10
|
||||
if (!isImage || !isLt10M) {
|
||||
fileList.value = []
|
||||
ElMessage.error(isImage ? '图片大小不能超过10MB' : '仅支持图片格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePictureCardPreview(file: any) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 校验
|
||||
const formRules = {
|
||||
label: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [{ max: 200, message: '描述不能超过200个字符', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const isEdit = computed(() => !!formData.id)
|
||||
// 预览对话框
|
||||
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑分类' : '新增分类'))
|
||||
const submitLoading = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
submitLoading.value = true
|
||||
|
||||
let imageUrl = formData.image
|
||||
|
||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', fileList.value[0].raw)
|
||||
uploadFormData.append('cate', 'category')
|
||||
|
||||
const uploadRes = await uploadFile(uploadFormData)
|
||||
if (uploadRes?.data?.url) {
|
||||
imageUrl = uploadRes.data.url
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: formData.label,
|
||||
image: imageUrl,
|
||||
desc: formData.remark,
|
||||
sort: formData.sort,
|
||||
status: formData.status,
|
||||
cid: formData.parentId,
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await editCategory(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createCategory(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
emit('saved')
|
||||
closeDialog()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visibleInternal.value = false
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 如果需要额外样式,可在此处编写 */
|
||||
.avatar-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 32px;
|
||||
color: #8c939d;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.uploads{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<el-dialog v-model="visibleInternal" :title="dialogTitle" width="600px" :close-on-click-modal="false"
|
||||
@close="closeDialog">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="分类名称" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入分类名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="父级分类" prop="parentId">
|
||||
<el-tree-select v-model="formData.parentId" :data="treeData"
|
||||
:props="{ label: 'label', value: 'id', children: 'children' }" placeholder="选择父级分类(可选)" clearable
|
||||
filterable check-strictly style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right"
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="默认图片" prop="image">
|
||||
<div class="uploads">
|
||||
<el-upload v-model:file-list="fileList" :auto-upload="false" :before-upload="beforeImgUpload" list-type="picture-card" :limit="1" :on-change="handleUploadChange">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<el-icon>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<div class="upload-tip">
|
||||
<span>建议尺寸:250px × 140px</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="4" placeholder="请输入分类描述..." maxlength="200"
|
||||
show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
{{ isEdit ? '更新' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Plus, ZoomIn, Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
import { createCategory, editCategory, listCategories } from '@/api/article'
|
||||
import { uploadFile } from '@/api/file.js'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
category: { type: Object as () => any | null, default: null }, // 传入的分类对象,null 表示新增
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
// 可见性同步
|
||||
const visibleInternal = ref(props.modelValue)
|
||||
watch(() => props.modelValue, v => (visibleInternal.value = v))
|
||||
watch(visibleInternal, v => {
|
||||
emit('update:modelValue', v)
|
||||
if (v) loadTreeData()
|
||||
})
|
||||
|
||||
// 数据加载:父级分类树
|
||||
const treeData = ref<any[]>([])
|
||||
watch(treeData, () => {
|
||||
// 触发tree-select重新渲染已选 label
|
||||
formData.parentId = formData.parentId ?? 0
|
||||
})
|
||||
|
||||
async function loadTreeData() {
|
||||
const res = await listCategories({ page: 1, pageSize: 1000 })
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.records || res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建树形
|
||||
function buildTree(data: any[]) {
|
||||
const map = new Map()
|
||||
const roots: any[] = []
|
||||
data.forEach((item) => {
|
||||
map.set(item.id, { ...item, label: item.name ?? item.label, children: [] })
|
||||
})
|
||||
map.forEach((item: any) => {
|
||||
if (item.cid && map.has(item.cid)) {
|
||||
map.get(item.cid).children.push(item)
|
||||
} else {
|
||||
roots.push(item)
|
||||
}
|
||||
})
|
||||
return [{ id: 0, label: '顶级', children: roots }]
|
||||
}
|
||||
|
||||
// 表单
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
id: null as number | null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0 as number,
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
if (formRef.value) formRef.value.clearValidate()
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
label: '',
|
||||
image: '',
|
||||
remark: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
parentId: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// 当传入 category 变化时同步
|
||||
watch(
|
||||
() => props.category,
|
||||
(val) => {
|
||||
if (val) {
|
||||
Object.assign(formData, {
|
||||
id: val.id,
|
||||
label: val.label,
|
||||
image: val.image,
|
||||
remark: val.remark,
|
||||
sort: val.sort,
|
||||
status: val.status,
|
||||
parentId: val.parentId ?? val.cid ?? 0,
|
||||
})
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 上传相关
|
||||
const fileList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
function beforeImgUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isImage) ElMessage.error('仅支持图片格式')
|
||||
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
|
||||
return isImage && isLt10M
|
||||
}
|
||||
|
||||
function handleRemove(file: any) {
|
||||
fileList.value = []
|
||||
formData.image = ''
|
||||
}
|
||||
|
||||
async function handleUploadChange(file: any) {
|
||||
if (file.raw) {
|
||||
const isImage = file.raw.type.startsWith('image/')
|
||||
const isLt10M = file.raw.size / 1024 / 1024 < 10
|
||||
if (!isImage || !isLt10M) {
|
||||
fileList.value = []
|
||||
ElMessage.error(isImage ? '图片大小不能超过10MB' : '仅支持图片格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePictureCardPreview(file: any) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 校验
|
||||
const formRules = {
|
||||
label: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [{ max: 200, message: '描述不能超过200个字符', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const isEdit = computed(() => !!formData.id)
|
||||
// 预览对话框
|
||||
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑分类' : '新增分类'))
|
||||
const submitLoading = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
formRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
submitLoading.value = true
|
||||
|
||||
let imageUrl = formData.image
|
||||
|
||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', fileList.value[0].raw)
|
||||
uploadFormData.append('cate', 'category')
|
||||
|
||||
const uploadRes = await uploadFile(uploadFormData)
|
||||
if (uploadRes?.data?.url) {
|
||||
imageUrl = uploadRes.data.url
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: formData.label,
|
||||
image: imageUrl,
|
||||
desc: formData.remark,
|
||||
sort: formData.sort,
|
||||
status: formData.status,
|
||||
cid: formData.parentId,
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await editCategory(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createCategory(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
emit('saved')
|
||||
closeDialog()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visibleInternal.value = false
|
||||
resetForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 如果需要额外样式,可在此处编写 */
|
||||
.avatar-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 32px;
|
||||
color: #8c939d;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.uploads{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
+498
-498
@@ -1,498 +1,498 @@
|
||||
<template>
|
||||
<div class="category-manager">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h3>文章分类</h3>
|
||||
<p>共 {{ totalCount }} 个分类</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">
|
||||
新增分类
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-list" v-loading="loading">
|
||||
<div v-if="filteredCategories.length === 0" class="empty-state">
|
||||
<el-empty description="暂无文章分类" />
|
||||
</div>
|
||||
|
||||
<div v-else class="category-tree">
|
||||
<category-node
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:item="category"
|
||||
:level="0"
|
||||
@edit="handleEdit"
|
||||
@add-child="handleAddChild"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-cate
|
||||
v-model="dialogVisible"
|
||||
:category="currentEdit"
|
||||
@saved="fetchCategories"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Check,
|
||||
Close,
|
||||
Search,
|
||||
Refresh,
|
||||
Document,
|
||||
Picture,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { allCategories, deleteCategory } from "@/api/article";
|
||||
import EditCate from "@/views/apps/cms/articles/components/edit-cate.vue";
|
||||
import CategoryNode from "./components/CategoryNode.vue";
|
||||
|
||||
// 颜色预设
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const currentEdit = ref(null);
|
||||
const searchText = ref("");
|
||||
const categories = ref([]);
|
||||
|
||||
// 新增分类
|
||||
const addCategory = ref(null);
|
||||
const handleAdd = () => {
|
||||
addCategory.value = { parentId: 0 };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => categories.value.length);
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (!searchText.value) {
|
||||
return categories.value;
|
||||
}
|
||||
|
||||
const search = searchText.value.toLowerCase();
|
||||
return categories.value.filter(
|
||||
(category) =>
|
||||
category.label.toLowerCase().includes(search) ||
|
||||
(category.remark && category.remark.toLowerCase().includes(search)),
|
||||
);
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
function buildTree(data) {
|
||||
// 1. 标准化数据字段
|
||||
const transformed = data.map((item) => ({
|
||||
...item,
|
||||
label: item.name ?? item.label ?? "",
|
||||
remark: item.desc ?? item.remark ?? "",
|
||||
parentId: item.parentId ?? item.cid ?? 0,
|
||||
children: [],
|
||||
expanded: true, // 默认展开所有级
|
||||
}));
|
||||
|
||||
const map = new Map();
|
||||
transformed.forEach((item) => map.set(item.id, item));
|
||||
|
||||
const roots = [];
|
||||
transformed.forEach((item) => {
|
||||
const parent = map.get(item.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
// 如果没有父节点,或者是顶级节点 (parentId 为 0)
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function handleCreate() {
|
||||
currentEdit.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(category) {
|
||||
currentEdit.value = category;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleAddChild(parent) {
|
||||
currentEdit.value = { parentId: parent.id };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
await fetchCategories();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
function toggleExpand(category) {
|
||||
category.expanded = !category.expanded;
|
||||
}
|
||||
|
||||
function handleDelete(category) {
|
||||
if (Number(category.tid) === 0 || category.is_global === true) {
|
||||
ElMessage.warning("全局分类不可删除");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
deleteCategory(category.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
fetchCategories();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("删除分类失败:", error);
|
||||
ElMessage.error("删除失败");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// API调用
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await allCategories({ keyword: searchText.value });
|
||||
if (response.code === 200) {
|
||||
const categoryList = response.data || [];
|
||||
// 这里会递归生成三级、四级等
|
||||
categories.value = buildTree(categoryList);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-manager {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.page-title {
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏样式
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.el-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.search-stats {
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 分类列表容器
|
||||
.category-list {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.empty-state {
|
||||
padding: 80px 40px;
|
||||
.empty-icon {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归树结构核心样式
|
||||
.category-tree {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
// 层级背景区分 (可选:让子级背景稍微深一点点)
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px; // 左右 padding 保持,左侧缩进通过内联 style 控制
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
// 展开收起按钮占位
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分类具体内容
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮组
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4; // 默认低透明度,鼠标悬浮时高亮
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标悬停时显示按钮
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子分类容器样式
|
||||
.category-children {
|
||||
margin-left: 0;
|
||||
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus 对话框深度样式美化
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
.el-dialog__header {
|
||||
padding: 20px 24px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.category-manager {
|
||||
padding: 12px;
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
.page-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-list .category-tree .category-item .category-main {
|
||||
padding: 12px !important; // 移动端取消大缩进,改用其他视觉暗示
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.category-desc {
|
||||
display: none;
|
||||
} // 隐藏描述节省空间
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="category-manager">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h3>文章分类</h3>
|
||||
<p>共 {{ totalCount }} 个分类</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">
|
||||
新增分类
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-list" v-loading="loading">
|
||||
<div v-if="filteredCategories.length === 0" class="empty-state">
|
||||
<el-empty description="暂无文章分类" />
|
||||
</div>
|
||||
|
||||
<div v-else class="category-tree">
|
||||
<category-node
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:item="category"
|
||||
:level="0"
|
||||
@edit="handleEdit"
|
||||
@add-child="handleAddChild"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-cate
|
||||
v-model="dialogVisible"
|
||||
:category="currentEdit"
|
||||
@saved="fetchCategories"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Check,
|
||||
Close,
|
||||
Search,
|
||||
Refresh,
|
||||
Document,
|
||||
Picture,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { allCategories, deleteCategory } from "@/api/article";
|
||||
import EditCate from "./components/edit.vue";
|
||||
import CategoryNode from "./components/CategoryNode.vue";
|
||||
|
||||
// 颜色预设
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const currentEdit = ref(null);
|
||||
const searchText = ref("");
|
||||
const categories = ref([]);
|
||||
|
||||
// 新增分类
|
||||
const addCategory = ref(null);
|
||||
const handleAdd = () => {
|
||||
addCategory.value = { parentId: 0 };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => categories.value.length);
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (!searchText.value) {
|
||||
return categories.value;
|
||||
}
|
||||
|
||||
const search = searchText.value.toLowerCase();
|
||||
return categories.value.filter(
|
||||
(category) =>
|
||||
category.label.toLowerCase().includes(search) ||
|
||||
(category.remark && category.remark.toLowerCase().includes(search)),
|
||||
);
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
function buildTree(data) {
|
||||
// 1. 标准化数据字段
|
||||
const transformed = data.map((item) => ({
|
||||
...item,
|
||||
label: item.name ?? item.label ?? "",
|
||||
remark: item.desc ?? item.remark ?? "",
|
||||
parentId: item.parentId ?? item.cid ?? 0,
|
||||
children: [],
|
||||
expanded: true, // 默认展开所有级
|
||||
}));
|
||||
|
||||
const map = new Map();
|
||||
transformed.forEach((item) => map.set(item.id, item));
|
||||
|
||||
const roots = [];
|
||||
transformed.forEach((item) => {
|
||||
const parent = map.get(item.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
// 如果没有父节点,或者是顶级节点 (parentId 为 0)
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function handleCreate() {
|
||||
currentEdit.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(category) {
|
||||
currentEdit.value = category;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleAddChild(parent) {
|
||||
currentEdit.value = { parentId: parent.id };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
await fetchCategories();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchText.value = "";
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
function toggleExpand(category) {
|
||||
category.expanded = !category.expanded;
|
||||
}
|
||||
|
||||
function handleDelete(category) {
|
||||
if (Number(category.tid) === 0 || category.is_global === true) {
|
||||
ElMessage.warning("全局分类不可删除");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
deleteCategory(category.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
fetchCategories();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("删除分类失败:", error);
|
||||
ElMessage.error("删除失败");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// API调用
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await allCategories({ keyword: searchText.value });
|
||||
if (response.code === 200) {
|
||||
const categoryList = response.data || [];
|
||||
// 这里会递归生成三级、四级等
|
||||
categories.value = buildTree(categoryList);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.category-manager {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.page-title {
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏样式
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.el-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.search-stats {
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 分类列表容器
|
||||
.category-list {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.empty-state {
|
||||
padding: 80px 40px;
|
||||
.empty-icon {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归树结构核心样式
|
||||
.category-tree {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
&.disabled {
|
||||
.category-main {
|
||||
opacity: 0.6;
|
||||
background-color: var(--el-fill-color-lightest);
|
||||
}
|
||||
}
|
||||
|
||||
// 层级背景区分 (可选:让子级背景稍微深一点点)
|
||||
&.child-item {
|
||||
.category-main {
|
||||
background-color: rgba(245, 247, 250, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.category-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px; // 左右 padding 保持,左侧缩进通过内联 style 控制
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
// 展开收起按钮占位
|
||||
.expand-btn,
|
||||
.expand-spacer {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
.expand-button {
|
||||
padding: 4px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分类具体内容
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.status-tag {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
.category-desc {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮组
|
||||
.category-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
opacity: 0.4; // 默认低透明度,鼠标悬浮时高亮
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button-group .el-button {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
&.danger:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标悬停时显示按钮
|
||||
&:hover .category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子分类容器样式
|
||||
.category-children {
|
||||
margin-left: 0;
|
||||
|
||||
.category-node-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus 对话框深度样式美化
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
.el-dialog__header {
|
||||
padding: 20px 24px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.el-input__inner,
|
||||
.el-textarea__inner {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.category-manager {
|
||||
padding: 12px;
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
.page-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-list .category-tree .category-item .category-main {
|
||||
padding: 12px !important; // 移动端取消大缩进,改用其他视觉暗示
|
||||
.category-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.category-desc {
|
||||
display: none;
|
||||
} // 隐藏描述节省空间
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -56,6 +56,9 @@
|
||||
class="menu-icon"
|
||||
></i>
|
||||
<span>{{ scope.row.title }}</span>
|
||||
<el-tag v-if="scope.row.tenant_id === 0" size="small" type="warning">
|
||||
系统默认
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -110,7 +113,12 @@
|
||||
<span>子菜单</span>
|
||||
</el-button>
|
||||
|
||||
<el-button size="small" text @click="handleEditMenu(scope.row)">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="handleEditMenu(scope.row)"
|
||||
:disabled="scope.row.tenant_id === 0"
|
||||
>
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
@@ -122,6 +130,7 @@
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDeleteMenu(scope.row)"
|
||||
:disabled="scope.row.tenant_id === 0"
|
||||
>
|
||||
<el-icon>
|
||||
<Delete />
|
||||
@@ -168,6 +177,7 @@ import MenuEdit from "./components/edit.vue";
|
||||
// 定义菜单数据类型
|
||||
interface Menu {
|
||||
id: number;
|
||||
tenant_id?: number; // 0 表示系统默认菜单(不可编辑/删除)
|
||||
pid: number;
|
||||
title: string;
|
||||
type: number;
|
||||
|
||||
+321
-319
@@ -1,319 +1,321 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="产品名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入产品名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入产品描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品内容" prop="content">
|
||||
<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"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.thumb"
|
||||
:src="getImageUrl(formData.thumb)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传产品图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.thumb"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
删除产品图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { addProducts, updateProducts } from "@/api/products";
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加产品",
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("请上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error("图片大小不能超过 2MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.thumb = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.thumb = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
title: "",
|
||||
desc: "",
|
||||
thumb: "",
|
||||
url: "",
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: "请输入产品名称", trigger: "blur" },
|
||||
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
|
||||
],
|
||||
desc: [
|
||||
{ required: true, message: "请输入产品描述", trigger: "blur" },
|
||||
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = "";
|
||||
formData.desc = "";
|
||||
formData.thumb = "";
|
||||
formData.url = "";
|
||||
formData.sort = 0;
|
||||
formData.status = 1;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.isEdit) {
|
||||
res = await updateProducts(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addProducts(formData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
|
||||
handleClose();
|
||||
emit("success");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.flex-direction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="产品名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入产品名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入产品描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品内容" prop="content">
|
||||
<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"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.thumb"
|
||||
:src="getImageUrl(formData.thumb)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传产品图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.thumb"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
删除产品图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { addProducts, updateProducts } from "@/api/products";
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加产品",
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("请上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error("图片大小不能超过 2MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.thumb = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.thumb = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
title: "",
|
||||
desc: "",
|
||||
content: "",
|
||||
thumb: "",
|
||||
url: "",
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: "请输入产品名称", trigger: "blur" },
|
||||
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
|
||||
],
|
||||
desc: [
|
||||
{ required: true, message: "请输入产品描述", trigger: "blur" },
|
||||
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = "";
|
||||
formData.desc = "";
|
||||
formData.content = "";
|
||||
formData.thumb = "";
|
||||
formData.url = "";
|
||||
formData.sort = 0;
|
||||
formData.status = 1;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.isEdit) {
|
||||
res = await updateProducts(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addProducts(formData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
|
||||
handleClose();
|
||||
emit("success");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.flex-direction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
+295
-295
@@ -1,295 +1,295 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加产品
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<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" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="productsList" 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">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.thumb" :src="getImageUrl(row.thumb)" :preview-src-list="[getImageUrl(row.thumb)]"
|
||||
fit="cover" class="product-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" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
getProductsList,
|
||||
updateProducts,
|
||||
deleteProducts
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整URL
|
||||
// 接口可能返回:
|
||||
// 1) 绝对地址:http(s)://...
|
||||
// 2) 以 / 开头的相对路径:/storage/uploads/xxx.png
|
||||
// 3) 不以 / 开头的相对路径:storage/uploads/xxx.png
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (typeof imagePath !== 'string') imagePath = String(imagePath);
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
|
||||
const base = (API_BASE_URL ?? '').replace(/\/$/, '');
|
||||
const path = imagePath.startsWith('/') ? imagePath : `/${imagePath}`;
|
||||
return base + path;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const productsList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductsList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
productsList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业产品列表失败:', error)
|
||||
ElMessage.error('获取企业产品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加企业产品'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑企业产品'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该企业产品吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteProducts(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除企业产品失败:', error)
|
||||
ElMessage.error('删除企业产品失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 跳转到产品分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/products/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加产品
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<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" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="productsList" 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">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.thumb" :src="getImageUrl(row.thumb)" :preview-src-list="[getImageUrl(row.thumb)]"
|
||||
fit="cover" class="product-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" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
getProductsList,
|
||||
updateProducts,
|
||||
deleteProducts
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整URL
|
||||
// 接口可能返回:
|
||||
// 1) 绝对地址:http(s)://...
|
||||
// 2) 以 / 开头的相对路径:/storage/uploads/xxx.png
|
||||
// 3) 不以 / 开头的相对路径:storage/uploads/xxx.png
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (typeof imagePath !== 'string') imagePath = String(imagePath);
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
|
||||
const base = (API_BASE_URL ?? '').replace(/\/$/, '');
|
||||
const path = imagePath.startsWith('/') ? imagePath : `/${imagePath}`;
|
||||
return base + path;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const productsList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductsList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
productsList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业产品列表失败:', error)
|
||||
ElMessage.error('获取企业产品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加企业产品'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑企业产品'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该企业产品吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteProducts(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除企业产品失败:', error)
|
||||
ElMessage.error('删除企业产品失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 跳转到产品分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/products/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
+211
-211
@@ -1,211 +1,211 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="父级分类" prop="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
addProductsTypes,
|
||||
updateProductsTypes,
|
||||
getProductsTypesList
|
||||
} from '@/api/products'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '添加分类'
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getProductsTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateProductsTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addProductsTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="父级分类" prop="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
addProductsTypes,
|
||||
updateProductsTypes,
|
||||
getProductsTypesList
|
||||
} from '@/api/products'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '添加分类'
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getProductsTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateProductsTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addProductsTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+239
-239
@@ -1,240 +1,240 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入分类名称搜索"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getProductsTypesList,
|
||||
deleteProductsTypes
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductsTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业产品分类列表失败:', error)
|
||||
ElMessage.error('获取企业产品分类列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加企业产品分类'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑企业产品分类'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该企业产品分类吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteProductsTypes(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除分类成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除企业产品分类失败:', error)
|
||||
ElMessage.error('删除企业产品分类失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入分类名称搜索"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getProductsTypesList,
|
||||
deleteProductsTypes
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductsTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业产品分类列表失败:', error)
|
||||
ElMessage.error('获取企业产品分类列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加企业产品分类'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑企业产品分类'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该企业产品分类吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteProductsTypes(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除分类成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除企业产品分类失败:', error)
|
||||
ElMessage.error('删除企业产品分类失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
+315
-315
@@ -1,315 +1,315 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="服务名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入服务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入服务描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Logo" prop="thumb">
|
||||
<div class="flex-direction">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.thumb"
|
||||
:src="getImageUrl(formData.thumb)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传服务图标</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.thumb"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
删除图标
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { addService, updateService } from "@/api/services";
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加服务",
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("请上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error("图片大小不能超过 2MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.link_logo = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.link_logo = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
title: "",
|
||||
desc: "",
|
||||
thumb: "",
|
||||
url: "",
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: "请输入服务名称", trigger: "blur" },
|
||||
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
|
||||
],
|
||||
desc: [
|
||||
{ required: true, message: "请输入服务描述", trigger: "blur" },
|
||||
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = "";
|
||||
formData.desc = "";
|
||||
formData.thumb = "";
|
||||
formData.url = "";
|
||||
formData.sort = 0;
|
||||
formData.status = 1;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.isEdit) {
|
||||
res = await updateService(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addService(formData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
|
||||
handleClose();
|
||||
emit("success");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.flex-direction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="服务名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入服务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入服务描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Logo" prop="thumb">
|
||||
<div class="flex-direction">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.thumb"
|
||||
:src="getImageUrl(formData.thumb)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传服务图标</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.thumb"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
删除图标
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { addService, updateService } from "@/api/services";
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加服务",
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("请上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error("图片大小不能超过 2MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.link_logo = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.link_logo = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
title: "",
|
||||
desc: "",
|
||||
thumb: "",
|
||||
url: "",
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: "请输入服务名称", trigger: "blur" },
|
||||
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
|
||||
],
|
||||
desc: [
|
||||
{ required: true, message: "请输入服务描述", trigger: "blur" },
|
||||
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = "";
|
||||
formData.desc = "";
|
||||
formData.thumb = "";
|
||||
formData.url = "";
|
||||
formData.sort = 0;
|
||||
formData.status = 1;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.isEdit) {
|
||||
res = await updateService(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addService(formData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
|
||||
handleClose();
|
||||
emit("success");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.flex-direction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
+295
-281
@@ -1,281 +1,295 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>特色服务管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加内容
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<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" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<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="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" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getServiceList,
|
||||
updateService,
|
||||
deleteService
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const servicesList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getServiceList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
servicesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取特色服务列表失败:', error)
|
||||
ElMessage.error('获取特色服务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加特色服务'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑特色服务'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该特色服务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteService(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除特色服务失败:', error)
|
||||
ElMessage.error('删除特色服务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变化
|
||||
const handleStatusChange = async (row, val) => {
|
||||
try {
|
||||
const res = await updateService(row.id, { status: val })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('状态更新成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '状态更新失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新特色服务状态失败:', error)
|
||||
ElMessage.error('更新特色服务状态失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>特色服务管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<el-icon>
|
||||
<Menu />
|
||||
</el-icon>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加内容
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<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" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<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="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" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete, Menu } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
getServiceList,
|
||||
updateService,
|
||||
deleteService
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const servicesList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getServiceList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
servicesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取特色服务列表失败:', error)
|
||||
ElMessage.error('获取特色服务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加特色服务'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑特色服务'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该特色服务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteService(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除特色服务失败:', error)
|
||||
ElMessage.error('删除特色服务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变化
|
||||
const handleStatusChange = async (row, val) => {
|
||||
try {
|
||||
const res = await updateService(row.id, { status: val })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('状态更新成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '状态更新失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新特色服务状态失败:', error)
|
||||
ElMessage.error('更新特色服务状态失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 跳转到解决方案分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/solutions/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="父级分类" prop="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
addServicesTypes,
|
||||
updateServicesTypes,
|
||||
getServicesTypesList
|
||||
} from '@/api/services'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '添加分类'
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getServicesTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateServicesTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addServicesTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>解决方案分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入分类名称搜索"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getServicesTypesList,
|
||||
deleteServicesTypes
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getServicesTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取解决方案分类列表失败:', error)
|
||||
ElMessage.error('获取解决方案分类列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加解决方案分类'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑解决方案分类'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该解决方案分类吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteServicesTypes(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除分类成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除解决方案分类失败:', error)
|
||||
ElMessage.error('删除解决方案分类失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -103,6 +103,29 @@ location /api/ {
|
||||
|
||||
Go 的路由注册在 `/backend/*`、`/platform/*`(没有 `/api` 前缀),靠这个尾部斜杠剥前缀。
|
||||
|
||||
### 2.5 七牛图片统一通过官网域名代理
|
||||
|
||||
完整配置已在两个主域名的 `server` 块中加入 `location /qiniu/`。该规则会把官网域名下的 `/qiniu/` 请求通过 **HTTP** 代理到历史七牛域名 `http://7colud.yunzer.cn/`,并去掉 `/qiniu/` 前缀;浏览器仍通过官网 HTTPS 接收图片。
|
||||
|
||||
例如,浏览器请求:
|
||||
|
||||
```text
|
||||
https://yunzer.com.cn/qiniu/2026/06/02/1780371759517804300.png
|
||||
```
|
||||
|
||||
nginx 实际请求:
|
||||
|
||||
```text
|
||||
http://7colud.yunzer.cn/2026/06/02/1780371759517804300.png
|
||||
```
|
||||
|
||||
**存储配置填写规则**:平台后台“存储配置”中的 `qiniu_domain` 必须与实际复制的主站配置一致,且**末尾不要加 `/`**:
|
||||
|
||||
- 复制 `yunzer.com.cn` 配置时填写 `https://yunzer.com.cn/qiniu`;
|
||||
- 复制 `dh2.fun` 配置时填写 `https://dh2.fun/qiniu`。
|
||||
|
||||
Go 官网渲染会将数据库中已有的 `http(s)://7colud.yunzer.cn/路径`(兼容历史拼写 `7cloud.yunzer.cn`)替换为此配置的地址;之后新上传的七牛文件也会直接使用该代理地址。`location /qiniu/` 的 `proxy_pass` **末尾斜杠不能删除**,否则七牛会收到包含 `/qiniu/` 的错误对象路径。
|
||||
|
||||
---
|
||||
|
||||
## 三、完整配置 —— 宝塔站点 `dh2.fun`
|
||||
@@ -186,6 +209,19 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 七牛图片代理:/qiniu/ 前缀会被剥掉,转发到七牛对象路径
|
||||
# 存储配置 qiniu_domain 填 https://dh2.fun/qiniu(末尾不要加 /)
|
||||
location /qiniu/ {
|
||||
proxy_pass http://7colud.yunzer.cn/;
|
||||
proxy_set_header Host 7colud.yunzer.cn;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 浏览器缓存图片 7 天,减少重复请求
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
}
|
||||
|
||||
# 其余一切请求全部交给 Go:首页、/news、/page/xxx、/themes/ 静态资源、
|
||||
# /uploads/ 以及以后新增的任何路由,自动生效,nginx 永远不用再改
|
||||
location / {
|
||||
@@ -339,6 +375,19 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 七牛图片代理:/qiniu/ 前缀会被剥掉,转发到七牛对象路径
|
||||
# 存储配置 qiniu_domain 填 https://yunzer.com.cn/qiniu(末尾不要加 /)
|
||||
location /qiniu/ {
|
||||
proxy_pass http://7colud.yunzer.cn/;
|
||||
proxy_set_header Host 7colud.yunzer.cn;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 浏览器缓存图片 7 天,减少重复请求
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
}
|
||||
|
||||
# 其余一切请求全部交给 Go:首页、/news、/page/xxx、/themes/ 静态资源、
|
||||
# /uploads/ 以及以后新增的任何路由,自动生效,nginx 永远不用再改
|
||||
location / {
|
||||
@@ -416,17 +465,18 @@ server {
|
||||
|
||||
1. **子域名必须用泛域名证书**。租户域名是二级域名(`ceshi.yunzer.com.cn`),单域名证书会导致 HTTPS 证书错误。需在宝塔为 `*.yunzer.com.cn`、`*.dh2.fun` 申请泛域名证书(DNS 验证),并替换配置中的证书路径。
|
||||
2. **upstream 不能重名**。两个站点配置都在同一 http 上下文,本文分别用 `go_backend_dh2`、`go_backend_yunzer`;重名会 `duplicate upstream` 启动失败。
|
||||
3. **`/api/` 的 `proxy_pass` 尾部斜杠不能删**,它负责剥掉 `/api` 前缀(Go 路由注册在 `/backend/*`、`/platform/*`)。兜底 `location /` 的 `proxy_pass` 则**不带**尾部斜杠(原样转发 URI)。
|
||||
4. **不要再给官网域名加 `js|css` 正则缓存 location**。正则 location 优先级高于兜底前缀 location,会拦截 `/themes/xxx/static/main.js` 去本地 root 找文件导致 404。模板静态资源由 Go(beego)直接提供。
|
||||
5. **`proxy_set_header Host $host;` 必须保留**。Go 完全依赖 Host 识别租户;若透传成 upstream 地址,所有请求都会被识别失败。
|
||||
6. **Go 侧前置条件**:
|
||||
3. **`/api/` 和 `/qiniu/` 的 `proxy_pass` 尾部斜杠不能删**。`/api/` 的斜杠负责剥掉 `/api` 前缀(Go 路由注册在 `/backend/*`、`/platform/*`);`/qiniu/` 的斜杠负责剥掉 `/qiniu` 前缀,使七牛收到正确对象路径。兜底 `location /` 的 `proxy_pass` 则**不带**尾部斜杠(原样转发 URI)。
|
||||
4. **存储域名必须与复制的主站配置对应**。使用 `yunzer.com.cn` 配置则后台“存储配置”中的 `qiniu_domain` 填 `https://yunzer.com.cn/qiniu`;使用 `dh2.fun` 配置则填 `https://dh2.fun/qiniu`。均不带末尾 `/`。
|
||||
5. **不要再给官网域名加 `js|css` 正则缓存 location**。正则 location 优先级高于兜底前缀 location,会拦截 `/themes/xxx/static/main.js` 去本地 root 找文件导致 404。模板静态资源由 Go(beego)直接提供。
|
||||
6. **`proxy_set_header Host $host;` 必须保留**。Go 完全依赖 Host 识别租户;若透传成 upstream 地址,所有请求都会被识别失败。
|
||||
7. **Go 侧前置条件**:
|
||||
- 启动模式 `APP_MODE` 为 `all`(默认)或 `index`,否则官网渲染路由未注册;
|
||||
- `go/themes/` 目录与 Go 服务启动目录同级(模板文件在 **Go 项目目录** `go/themes/{编码}/`,与 platform/backend 前端项目无关);
|
||||
- 数据库已执行 `sql/yz_cms_frontend_tables.sql`。
|
||||
7. **域名发放数据必须精确**。`yz_system_tenant_domain.full_domain` 要与实际访问域名完全一致(不带协议、不带端口、不带路径),且 `status=1`、`delete_time` 为 NULL。
|
||||
8. **DNS 泛解析**。`*.yunzer.com.cn`、`*.dh2.fun` 需要配置泛解析 A 记录指向服务器,否则新发放的子域名无法访问(与 nginx 无关)。
|
||||
9. **HSTS 提醒**。配置了 `Strict-Transport-Security`,一旦浏览器记住,证书出问题期间该域名将无法用 HTTP 访问;测试阶段可先注释掉该行。
|
||||
10. **宝塔面板操作提示**。直接编辑站点配置文件后,用 `nginx -t` 校验再重载;宝塔后续"站点设置"的某些操作可能重写配置文件,改前建议备份。
|
||||
8. **域名发放数据必须精确**。`yz_system_tenant_domain.full_domain` 要与实际访问域名完全一致(不带协议、不带端口、不带路径),且 `status=1`、`delete_time` 为 NULL。
|
||||
9. **DNS 泛解析**。`*.yunzer.com.cn`、`*.dh2.fun` 需要配置泛解析 A 记录指向服务器,否则新发放的子域名无法访问(与 nginx 无关)。
|
||||
10. **HSTS 提醒**。配置了 `Strict-Transport-Security`,一旦浏览器记住,证书出问题期间该域名将无法用 HTTP 访问;测试阶段可先注释掉该行。
|
||||
11. **宝塔面板操作提示**。直接编辑站点配置文件后,用 `nginx -t` 校验再重载;宝塔后续"站点设置"的某些操作可能重写配置文件,改前建议备份。
|
||||
|
||||
---
|
||||
|
||||
@@ -441,7 +491,10 @@ curl -H "Host: ceshi.yunzer.com.cn" http://10.31.100.2:8081/
|
||||
# 2. 验证 nginx → Go 链路
|
||||
curl -k -H "Host: ceshi.yunzer.com.cn" https://127.0.0.1/
|
||||
|
||||
# 3. 浏览器访问 https://ceshi.yunzer.com.cn
|
||||
# 3. 验证七牛代理:应返回图片内容或 200/304,而不是 Go 的 404 页面
|
||||
curl -I https://yunzer.com.cn/qiniu/2026/06/02/1780371759517804300.png
|
||||
|
||||
# 4. 浏览器访问 https://ceshi.yunzer.com.cn
|
||||
```
|
||||
|
||||
### 6.2 常见现象对照表
|
||||
|
||||
@@ -88,12 +88,6 @@ func cmsEnsureTables(c *beego.Controller) bool {
|
||||
_ = c.ServeJSON()
|
||||
return false
|
||||
}
|
||||
if err := models.EnsureCmsArticleDefaultCategories(); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化文章分类失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ func (c *BackendMenuFrontController) checkAuth() (uint64, bool) {
|
||||
_ = c.ServeJSON()
|
||||
return 0, false
|
||||
}
|
||||
// tenant_id=0 为全局默认菜单保留值,不允许任何接口以租户身份操作
|
||||
if claims.TenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "租户ID缺失"}
|
||||
_ = c.ServeJSON()
|
||||
return 0, false
|
||||
}
|
||||
return uint64(claims.TenantId), true
|
||||
}
|
||||
|
||||
@@ -43,11 +49,15 @@ func (c *BackendMenuFrontController) List() {
|
||||
return
|
||||
}
|
||||
|
||||
// 保证全局默认导航(tenant_id=0)齐全,缺失自动补齐
|
||||
models.EnsureGlobalDefaultFrontMenus()
|
||||
|
||||
// 全局默认菜单(tenant_id=0,不可删改)+ 租户自定义菜单
|
||||
var menus []models.BackendMenuFront
|
||||
_, err := models.Orm.QueryTable("yz_backend_menu_front").
|
||||
Filter("tenant_id", tid).
|
||||
Filter("tenant_id__in", []uint64{0, tid}).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort").
|
||||
OrderBy("sort", "id").
|
||||
All(&menus)
|
||||
|
||||
if err != nil {
|
||||
@@ -151,4 +161,4 @@ func (c *BackendMenuFrontController) Delete() {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendProductController CMS 产品管理
|
||||
type BackendProductController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// BackendProductCategoryController CMS 产品分类管理
|
||||
type BackendProductCategoryController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendProductController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendProductCategoryController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendProductController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendProductCategoryController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func cmsEnsureProductTables(c *beego.Controller) bool {
|
||||
if err := models.EnsureCmsProductTables(); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化产品表失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cmsProductToMap(row models.CmsProduct) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"tid": row.Tid,
|
||||
"title": row.Title,
|
||||
"thumb": row.Thumb,
|
||||
"desc": row.Desc,
|
||||
"content": row.Content,
|
||||
"url": row.URL,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": models.CmsFormatTime(row.UpdateTime),
|
||||
}
|
||||
}
|
||||
|
||||
func cmsProductCateToMap(row models.CmsProductCategory) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"tid": row.Tid,
|
||||
"title": row.Title,
|
||||
"pid": row.Pid,
|
||||
"desc": row.Desc,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": models.CmsFormatTime(row.UpdateTime),
|
||||
}
|
||||
}
|
||||
|
||||
// List GET /backend/productsList
|
||||
func (c *BackendProductController) List() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
limit, _ := c.GetInt("limit", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.CmsProduct)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
if status, err := c.GetInt("status", -1); err == nil && status >= 0 {
|
||||
qs = qs.Filter("status", status)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsProduct
|
||||
offset := (page - 1) * limit
|
||||
_, err = qs.OrderBy("sort", "-id").Limit(limit, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取产品列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsProductToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type cmsProductPayload struct {
|
||||
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/addProducts
|
||||
func (c *BackendProductController) Create() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsProductPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "产品名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsProduct{
|
||||
Tid: tid,
|
||||
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,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "添加失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "添加成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update PUT /backend/editProducts/:id
|
||||
func (c *BackendProductController) Update() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsProductPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "产品名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsProduct)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{
|
||||
"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,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "产品不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deleteProducts/:id
|
||||
func (c *BackendProductController) Delete() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsProduct)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"delete_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "产品不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// List GET /backend/productsTypesList
|
||||
func (c *BackendProductCategoryController) List() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
limit, _ := c.GetInt("limit", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 1000 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.CmsProductCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsProductCategory
|
||||
offset := (page - 1) * limit
|
||||
_, err = qs.OrderBy("sort", "id").Limit(limit, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取产品分类列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsProductCateToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type cmsProductCategoryPayload struct {
|
||||
Title string `json:"title"`
|
||||
Pid uint64 `json:"pid"`
|
||||
Desc string `json:"desc"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// Create POST /backend/addProductsTypes
|
||||
func (c *BackendProductCategoryController) Create() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsProductCategoryPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsProductCategory{
|
||||
Tid: tid,
|
||||
Title: title,
|
||||
Pid: p.Pid,
|
||||
Desc: strings.TrimSpace(p.Desc),
|
||||
Sort: p.Sort,
|
||||
Status: 1,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "添加失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "添加成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update PUT /backend/editProductsTypes/:id
|
||||
func (c *BackendProductCategoryController) Update() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsProductCategoryPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
if p.Pid == id {
|
||||
c.cmsJSONErr(400, 400, "父级分类不能是自己")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsProductCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{
|
||||
"title": title,
|
||||
"pid": p.Pid,
|
||||
"desc": strings.TrimSpace(p.Desc),
|
||||
"sort": p.Sort,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deleteProductsTypes/:id
|
||||
func (c *BackendProductCategoryController) Delete() {
|
||||
if !cmsEnsureProductTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
// 存在子分类时不允许删除
|
||||
childCnt, _ := models.Orm.QueryTable(new(models.CmsProductCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("pid", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if childCnt > 0 {
|
||||
c.cmsJSONErr(400, 400, "该分类下存在子分类,请先删除子分类")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsProductCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"delete_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendSolutionController CMS 解决方案(特色服务)管理
|
||||
type BackendSolutionController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// BackendSolutionCategoryController CMS 解决方案分类管理
|
||||
type BackendSolutionCategoryController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendSolutionController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendSolutionCategoryController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendSolutionController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendSolutionCategoryController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func cmsEnsureSolutionTables(c *beego.Controller) bool {
|
||||
if err := models.EnsureCmsSolutionTables(); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化解决方案表失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cmsSolutionToMap(row models.CmsSolution) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"tid": row.Tid,
|
||||
"title": row.Title,
|
||||
"thumb": row.Thumb,
|
||||
"desc": row.Desc,
|
||||
"url": row.URL,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": models.CmsFormatTime(row.UpdateTime),
|
||||
}
|
||||
}
|
||||
|
||||
func cmsSolutionCateToMap(row models.CmsSolutionCategory) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"tid": row.Tid,
|
||||
"title": row.Title,
|
||||
"pid": row.Pid,
|
||||
"desc": row.Desc,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": models.CmsFormatTime(row.UpdateTime),
|
||||
}
|
||||
}
|
||||
|
||||
// List GET /backend/servicesList
|
||||
func (c *BackendSolutionController) List() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
limit, _ := c.GetInt("limit", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.CmsSolution)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
if status, err := c.GetInt("status", -1); err == nil && status >= 0 {
|
||||
qs = qs.Filter("status", status)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsSolution
|
||||
offset := (page - 1) * limit
|
||||
_, err = qs.OrderBy("sort", "-id").Limit(limit, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取解决方案列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsSolutionToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// Create POST /backend/addServices
|
||||
func (c *BackendSolutionController) Create() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsSolutionPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsSolution{
|
||||
Tid: tid,
|
||||
Title: title,
|
||||
Thumb: strings.TrimSpace(p.Thumb),
|
||||
Desc: strings.TrimSpace(p.Desc),
|
||||
URL: strings.TrimSpace(p.URL),
|
||||
Sort: p.Sort,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "添加失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "添加成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update PUT /backend/editServices/:id
|
||||
func (c *BackendSolutionController) Update() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsSolutionPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsSolution)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{
|
||||
"title": title,
|
||||
"thumb": strings.TrimSpace(p.Thumb),
|
||||
"desc": strings.TrimSpace(p.Desc),
|
||||
"url": strings.TrimSpace(p.URL),
|
||||
"sort": p.Sort,
|
||||
"status": p.Status,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deleteServices/:id
|
||||
func (c *BackendSolutionController) Delete() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsSolution)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"delete_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// List GET /backend/servicesTypesList
|
||||
func (c *BackendSolutionCategoryController) List() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
limit, _ := c.GetInt("limit", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 1000 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.CmsSolutionCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsSolutionCategory
|
||||
offset := (page - 1) * limit
|
||||
_, err = qs.OrderBy("sort", "id").Limit(limit, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取解决方案分类列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsSolutionCateToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type cmsSolutionCategoryPayload struct {
|
||||
Title string `json:"title"`
|
||||
Pid uint64 `json:"pid"`
|
||||
Desc string `json:"desc"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// Create POST /backend/addServicesTypes
|
||||
func (c *BackendSolutionCategoryController) Create() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsSolutionCategoryPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsSolutionCategory{
|
||||
Tid: tid,
|
||||
Title: title,
|
||||
Pid: p.Pid,
|
||||
Desc: strings.TrimSpace(p.Desc),
|
||||
Sort: p.Sort,
|
||||
Status: 1,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "添加失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "添加成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update PUT /backend/editServicesTypes/:id
|
||||
func (c *BackendSolutionCategoryController) Update() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsSolutionCategoryPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
if p.Pid == id {
|
||||
c.cmsJSONErr(400, 400, "父级分类不能是自己")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsSolutionCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{
|
||||
"title": title,
|
||||
"pid": p.Pid,
|
||||
"desc": strings.TrimSpace(p.Desc),
|
||||
"sort": p.Sort,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deleteServicesTypes/:id
|
||||
func (c *BackendSolutionCategoryController) Delete() {
|
||||
if !cmsEnsureSolutionTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
// 存在子分类时不允许删除
|
||||
childCnt, _ := models.Orm.QueryTable(new(models.CmsSolutionCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("pid", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if childCnt > 0 {
|
||||
c.cmsJSONErr(400, 400, "该分类下存在子分类,请先删除子分类")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsSolutionCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(orm.Params{"delete_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -120,32 +120,43 @@ func (c *TenantSiteController) resolveTemplateCode(tid uint64) string {
|
||||
func (c *TenantSiteController) writeHTML(status int, html string) {
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Ctx.WriteString(c.forceHTTPSStorageURLs(html))
|
||||
c.Ctx.WriteString(c.rewriteStorageURLs(html))
|
||||
}
|
||||
|
||||
// forceHTTPSStorageURLs 访客以 HTTPS 访问站点时,把页面中存储(CDN)域名的
|
||||
// http:// 资源升级为 https://,避免混合内容(Mixed Content)被浏览器拦截。
|
||||
// 仅替换存储配置里那个域名,不动正文里的其他外部链接。
|
||||
func (c *TenantSiteController) forceHTTPSStorageURLs(html string) string {
|
||||
proto := c.Ctx.Input.Header("X-Forwarded-Proto")
|
||||
if proto == "" && c.Ctx.Input.IsSecure() {
|
||||
proto = "https"
|
||||
}
|
||||
if proto != "https" {
|
||||
return html
|
||||
}
|
||||
var legacyQiniuDomains = []string{
|
||||
"7colud.yunzer.cn",
|
||||
"7cloud.yunzer.cn",
|
||||
}
|
||||
|
||||
// rewriteStorageURLs 将历史七牛域名替换为当前存储配置的访问域名。
|
||||
// 这样已存入数据库的旧完整 URL 会跟随新的 CDN 或 /qiniu/ 反向代理入口,
|
||||
// 不需要批量更新业务数据。
|
||||
func (c *TenantSiteController) rewriteStorageURLs(html string) string {
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil || strings.TrimSpace(cfg.QiniuDomain) == "" {
|
||||
return html
|
||||
}
|
||||
host := strings.TrimSpace(cfg.QiniuDomain)
|
||||
host = strings.TrimPrefix(host, "http://")
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
host = strings.TrimRight(host, "/")
|
||||
if host == "" {
|
||||
return rewriteLegacyQiniuURLs(html, storagePublicBaseURL(cfg.QiniuDomain))
|
||||
}
|
||||
|
||||
func storagePublicBaseURL(domain string) string {
|
||||
base := strings.TrimRight(strings.TrimSpace(domain), "/")
|
||||
if base == "" || strings.HasPrefix(base, "/") || strings.HasPrefix(base, "http://") || strings.HasPrefix(base, "https://") {
|
||||
return base
|
||||
}
|
||||
return "https://" + base
|
||||
}
|
||||
|
||||
func rewriteLegacyQiniuURLs(html, targetBase string) string {
|
||||
if targetBase == "" {
|
||||
return html
|
||||
}
|
||||
return strings.ReplaceAll(html, "http://"+host, "https://"+host)
|
||||
for _, domain := range legacyQiniuDomains {
|
||||
for _, scheme := range []string{"http://", "https://"} {
|
||||
html = strings.ReplaceAll(html, scheme+domain+"/", targetBase+"/")
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
// rewriteThemeAssets 将模板内书写的相对资源路径(href/src/url() 等)
|
||||
@@ -211,6 +222,11 @@ func rewriteThemeAssets(html, code string) string {
|
||||
|
||||
// render 解析并输出模板文件
|
||||
func (c *TenantSiteController) render(file string, mod func(ctx *tagengine.RenderCtx)) {
|
||||
c.renderFile(file, "", mod)
|
||||
}
|
||||
|
||||
// renderFile 解析并输出模板;preferred 不存在时尝试 fallback(fallback 为空则直接 404)
|
||||
func (c *TenantSiteController) renderFile(preferred, fallback string, mod func(ctx *tagengine.RenderCtx)) {
|
||||
tid, ok := c.resolveTid()
|
||||
if !ok {
|
||||
c.writeHTML(404, siteNotFoundPage)
|
||||
@@ -219,6 +235,12 @@ func (c *TenantSiteController) render(file string, mod func(ctx *tagengine.Rende
|
||||
|
||||
code := c.resolveTemplateCode(tid)
|
||||
dir := filepath.Join(cmsThemesRoot(), code)
|
||||
file := preferred
|
||||
if _, err := os.Stat(filepath.Join(dir, file)); err != nil {
|
||||
if fallback != "" {
|
||||
file = fallback
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, file)); err != nil {
|
||||
c.writeHTML(404, fmt.Sprintf(renderErrorPage, "模板页面不存在"))
|
||||
return
|
||||
@@ -271,7 +293,7 @@ func (c *TenantSiteController) Page() {
|
||||
c.writeHTML(404, fmt.Sprintf(renderErrorPage, "页面不存在"))
|
||||
return
|
||||
}
|
||||
c.render("page.html", func(ctx *tagengine.RenderCtx) {
|
||||
c.renderFile(path+".html", "page.html", func(ctx *tagengine.RenderCtx) {
|
||||
ctx.PagePath = path
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ func (m *BackendMenuFront) TableName() string {
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(BackendMenuFront))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,47 +109,6 @@ CREATE TABLE IF NOT EXISTS yz_cms_article (
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CmsProduct CMS 产品 yz_cms_product
|
||||
type CmsProduct 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(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"`
|
||||
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 *CmsProduct) TableName() string {
|
||||
return "yz_cms_product"
|
||||
}
|
||||
|
||||
// CmsProductCategory CMS 产品分类 yz_cms_product_category
|
||||
type CmsProductCategory 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(100)" json:"title"`
|
||||
Pid uint64 `orm:"column(pid);default(0)" json:"pid"`
|
||||
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 *CmsProductCategory) TableName() string {
|
||||
return "yz_cms_product_category"
|
||||
}
|
||||
|
||||
var cmsProductTablesOnce sync.Once
|
||||
|
||||
// EnsureCmsProductTables 首次使用时自动建表(若不存在)。
|
||||
func EnsureCmsProductTables() error {
|
||||
var err error
|
||||
cmsProductTablesOnce.Do(func() {
|
||||
_, err = Orm.Raw(`
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_product (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0,
|
||||
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,
|
||||
create_time datetime NOT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
delete_time datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tid_status (tid, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = Orm.Raw(`
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_product_category (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0,
|
||||
title varchar(100) NOT NULL DEFAULT '',
|
||||
pid bigint unsigned NOT NULL DEFAULT 0,
|
||||
` + "`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_pid (tid, pid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CmsSolution CMS 解决方案(特色服务) yz_cms_solution
|
||||
type CmsSolution 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(100)" json:"title"`
|
||||
Thumb string `orm:"column(thumb);size(500);default()" json:"thumb"`
|
||||
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
|
||||
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"`
|
||||
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 *CmsSolution) TableName() string {
|
||||
return "yz_cms_solution"
|
||||
}
|
||||
|
||||
// CmsSolutionCategory CMS 解决方案分类 yz_cms_solution_category
|
||||
type CmsSolutionCategory 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(100)" json:"title"`
|
||||
Pid uint64 `orm:"column(pid);default(0)" json:"pid"`
|
||||
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 *CmsSolutionCategory) TableName() string {
|
||||
return "yz_cms_solution_category"
|
||||
}
|
||||
|
||||
var cmsSolutionTablesOnce sync.Once
|
||||
|
||||
// EnsureCmsSolutionTables 首次使用时自动建表(若不存在)。
|
||||
func EnsureCmsSolutionTables() error {
|
||||
var err error
|
||||
cmsSolutionTablesOnce.Do(func() {
|
||||
_, err = Orm.Raw(`
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_solution (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0,
|
||||
title varchar(100) NOT NULL DEFAULT '',
|
||||
thumb varchar(500) NOT NULL DEFAULT '',
|
||||
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
|
||||
url 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_status (tid, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = Orm.Raw(`
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_solution_category (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0,
|
||||
title varchar(100) NOT NULL DEFAULT '',
|
||||
pid bigint unsigned NOT NULL DEFAULT 0,
|
||||
` + "`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_pid (tid, pid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// FrontMenuDefault 全局默认前端导航项定义
|
||||
type FrontMenuDefault struct {
|
||||
Title string
|
||||
Path string
|
||||
Type int8 // 菜单类型:2 页面 / 4 单页
|
||||
Sort int
|
||||
}
|
||||
|
||||
// FrontMenuDefaults 所有租户共用的默认导航(不可删除)。
|
||||
// 以 tenant_id=0 存放于同一张菜单表,表示全局共享:
|
||||
// 租户端增删改接口都带 tenant_id 过滤,天然无法触碰这些行。
|
||||
// 路径对应官网前台路由:新闻中心 /news,其余为单页 /page/:path
|
||||
// (单页内容在 backend 端 - 单页管理 中按 path 维护)。
|
||||
var FrontMenuDefaults = []FrontMenuDefault{
|
||||
{Title: "新闻中心", Path: "/news", Type: 2, Sort: 1},
|
||||
{Title: "产品展示", Path: "/page/products", Type: 4, Sort: 2},
|
||||
{Title: "解决方案", Path: "/page/solutions", Type: 4, Sort: 3},
|
||||
{Title: "关于我们", Path: "/page/about", Type: 4, Sort: 4},
|
||||
{Title: "联系我们", Path: "/page/contact", Type: 4, Sort: 5},
|
||||
}
|
||||
|
||||
// EnsureGlobalDefaultFrontMenus 保证全局默认导航(tenant_id=0)齐全:
|
||||
// 缺失的自动新增;被误删(软删)的自动恢复;已存在(含改名)的不动。
|
||||
// 以"在库生效数量 >= 默认项总数"作为短路条件,避免改名后重复插入。
|
||||
// 供后台菜单列表与前台导航渲染前调用,任何失败均静默忽略不阻断主流程。
|
||||
func EnsureGlobalDefaultFrontMenus() {
|
||||
if Orm == nil {
|
||||
return
|
||||
}
|
||||
qs := Orm.QueryTable(new(BackendMenuFront)).Filter("tenant_id", 0)
|
||||
|
||||
activeCnt, err := qs.Filter("delete_time__isnull", true).Count()
|
||||
if err != nil || activeCnt >= int64(len(FrontMenuDefaults)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 含软删记录一起取,用于恢复被误删的默认菜单
|
||||
var rows []BackendMenuFront
|
||||
if _, err := qs.All(&rows); err != nil {
|
||||
return
|
||||
}
|
||||
exist := make(map[string]*BackendMenuFront, len(rows))
|
||||
for i := range rows {
|
||||
exist[rows[i].Title] = &rows[i]
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, def := range FrontMenuDefaults {
|
||||
if row, ok := exist[def.Title]; ok {
|
||||
if row.DeleteTime != nil {
|
||||
_, _ = Orm.QueryTable(new(BackendMenuFront)).
|
||||
Filter("id", row.ID).
|
||||
Update(map[string]interface{}{
|
||||
"delete_time": nil,
|
||||
"status": 1,
|
||||
"is_visible": 1,
|
||||
"update_time": now,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, _ = Orm.Insert(&BackendMenuFront{
|
||||
TenantID: 0,
|
||||
Pid: 0,
|
||||
Title: def.Title,
|
||||
Path: def.Path,
|
||||
Sort: def.Sort,
|
||||
Status: 1,
|
||||
IsVisible: 1,
|
||||
Type: def.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,12 @@ type SelfCloseHandler func(tid uint64, params map[string]string, ctx *RenderCtx)
|
||||
// GlobalProvider 全局标签提供器:返回 标签名->值 映射
|
||||
type GlobalProvider func(tid uint64, ctx *RenderCtx) (map[string]string, error)
|
||||
|
||||
// BodyProvider 带循环体的块标签:自行组装 HTML(如带 Tab 的新闻中心)
|
||||
type BodyProvider func(tid uint64, params map[string]string, ctx *RenderCtx, body string) (string, error)
|
||||
|
||||
var (
|
||||
blockProviders = map[string]Provider{}
|
||||
blockProviders = map[string]Provider{}
|
||||
bodyProviders = map[string]BodyProvider{}
|
||||
selfCloseHandlers = map[string]SelfCloseHandler{}
|
||||
globalProvider GlobalProvider
|
||||
)
|
||||
@@ -35,6 +39,9 @@ var (
|
||||
// RegisterBlock 注册循环标签({yz:name}...{/yz:name})
|
||||
func RegisterBlock(name string, p Provider) { blockProviders[name] = p }
|
||||
|
||||
// RegisterBodyBlock 注册自行组装循环体的块标签
|
||||
func RegisterBodyBlock(name string, p BodyProvider) { bodyProviders[name] = p }
|
||||
|
||||
// RegisterSelfClose 注册自闭合标签({yz:name ... /})
|
||||
func RegisterSelfClose(name string, h SelfCloseHandler) { selfCloseHandlers[name] = h }
|
||||
|
||||
@@ -67,6 +74,13 @@ func parseAttrs(attrStr string) map[string]string {
|
||||
return out
|
||||
}
|
||||
|
||||
func applyFields(body string, row map[string]string) string {
|
||||
return reField.ReplaceAllStringFunc(body, func(fm string) string {
|
||||
field := reField.FindStringSubmatch(fm)[1]
|
||||
return row[field]
|
||||
})
|
||||
}
|
||||
|
||||
// Render 渲染模板目录下的指定文件
|
||||
// 流水线:include 展开 → 块标签 → 自闭合标签 → 全局标签
|
||||
func Render(templateDir, file string, ctx *RenderCtx) (string, error) {
|
||||
@@ -152,18 +166,21 @@ func renderBlocks(content string, ctx *RenderCtx) (string, error) {
|
||||
}
|
||||
|
||||
rendered := ""
|
||||
if p, ok := blockProviders[closeName]; ok {
|
||||
body := out[openEnd:c[0]]
|
||||
if bp, ok := bodyProviders[closeName]; ok {
|
||||
s, err := bp(ctx.Tid, parseAttrs(attrs), ctx, body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rendered = s
|
||||
} else if p, ok := blockProviders[closeName]; ok {
|
||||
rows, err := p(ctx.Tid, parseAttrs(attrs), ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
body := out[openEnd:c[0]]
|
||||
var sb strings.Builder
|
||||
for _, row := range rows {
|
||||
sb.WriteString(reField.ReplaceAllStringFunc(body, func(fm string) string {
|
||||
field := reField.FindStringSubmatch(fm)[1]
|
||||
return row[field]
|
||||
}))
|
||||
sb.WriteString(applyFields(body, row))
|
||||
}
|
||||
rendered = sb.String()
|
||||
}
|
||||
|
||||
+63
-17
@@ -4,11 +4,11 @@ package tagengine
|
||||
// 新增标签时在此登记一条,文档页自动同步,避免文档与实现脱节。
|
||||
type TagMeta struct {
|
||||
Name string `json:"name"` // 标签名
|
||||
Category string `json:"category"` // 分类:global 全局 / loop 循环 / single 单页 / other 其他
|
||||
Category string `json:"category"` // 分类:guide 入门 / global 全局 / loop 循环 / single 单页 / other 其他
|
||||
Syntax string `json:"syntax"` // 语法示例
|
||||
Desc string `json:"desc"` // 用途说明
|
||||
Params []string `json:"params"` // 可用参数(key=说明)
|
||||
Fields []string `json:"fields"` // 循环体内可用 [field:xxx/] 字段
|
||||
Fields []string `json:"fields"` // 循环体内可用 [field:xxx/] 字段;global 分类下为标签清单(名称 说明 数据来源)
|
||||
Example string `json:"example"` // 模板内示例代码
|
||||
}
|
||||
|
||||
@@ -16,31 +16,68 @@ type TagMeta struct {
|
||||
func TagDocs() []TagMeta {
|
||||
return []TagMeta{
|
||||
{
|
||||
Name: "全局站点信息",
|
||||
Category: "global",
|
||||
Syntax: "{yz:sitename} {yz:logo} {yz:logow} {yz:ico} {yz:icp} {yz:copyright} {yz:companyname} {yz:description} {yz:companyintroduction}",
|
||||
Desc: "取自当前租户的站点设置(backend 端 - 站点设置 - 基本信息),直接替换为对应文本/地址,无需闭合。logo 彩色Logo / logow 白色Logo / ico 站点图标 / companyintroduction 企业介绍(富文本)。",
|
||||
Name: "基本用法",
|
||||
Category: "guide",
|
||||
Syntax: "{yz:标签} / {yz:标签 参数=\"值\"/} / {yz:标签}...[field:字段/]...{/yz:标签}",
|
||||
Desc: "模板中用 {yz:xxx} 调用数据。三种形态:① 全局标签直接替换为文本,无需闭合;② 自闭合标签带参数、以 /} 结尾;③ 循环标签成对出现,循环体内用 [field:字段/] 输出每一行数据的字段。未配置的项输出空字符串,不会报错。",
|
||||
Params: nil,
|
||||
Fields: nil,
|
||||
Example: "<title>{yz:sitename}</title>\n<img src=\"{yz:logo}\" alt=\"{yz:sitename}\" />\n<p>{yz:icp} {yz:copyright}</p>",
|
||||
Example: "<!-- 全局标签:直接替换 -->\n<h1>{yz:sitename}</h1>\n\n<!-- 自闭合标签:带参数 -->\n{yz:onepage path=\"about\" field=\"content\"/}\n\n<!-- 循环标签:成对 + 字段占位 -->\n{yz:arclist row=\"6\"}\n<li><a href=\"[field:arcurl/]\">[field:title/]</a></li>\n{/yz:arclist}",
|
||||
},
|
||||
{
|
||||
Name: "全局联系方式",
|
||||
Category: "global",
|
||||
Syntax: "{yz:phone} {yz:email} {yz:address} {yz:worktime}",
|
||||
Desc: "取自租户的公司信息(backend 端 - 站点设置 - 公司信息),直接替换为文本,无需闭合。phone 联系电话(别名 {yz:tel} / {yz:mobile}) / email 电子邮箱 / address 公司地址 / worktime 工作时间。",
|
||||
Name: "模板文件结构",
|
||||
Category: "guide",
|
||||
Syntax: "themes/{模板编码}/",
|
||||
Desc: "模板存放在服务器 themes/{编码}/ 目录,按请求域名识别租户后用对应模板渲染。路由与文件的对应关系:首页 / → index.html;新闻列表 /news → news.html;文章详情 /news/:id → news_detail.html;单页 /page/:path → page.html。公共头部/底部建议拆成 header.html / footer.html,用 {yz:include/} 引入。模板内的相对资源路径(assets/css/xxx.css、style.css 等)渲染时会自动改写为 /themes/{编码}/ 绝对路径,无需手工改。",
|
||||
Params: nil,
|
||||
Fields: nil,
|
||||
Example: "<a href=\"tel:{yz:phone}\">{yz:phone}</a>\n<a href=\"mailto:{yz:email}\">{yz:email}</a>\n<p>{yz:address}</p>\n<p>工作时间:{yz:worktime}</p>",
|
||||
Example: "themes/business/\n├── index.html ← 首页 /\n├── news.html ← 新闻列表 /news?page=N\n├── news_detail.html ← 文章详情 /news/:id\n├── page.html ← 单页 /page/about\n├── header.html ← 公共头部(include 引入)\n├── footer.html ← 公共底部(include 引入)\n└── assets/ ← 模板自带的 css/js/图片",
|
||||
},
|
||||
{
|
||||
Name: "全局 SEO 信息",
|
||||
Name: "站点基本信息",
|
||||
Category: "global",
|
||||
Syntax: "{yz:seo_title} {yz:keywords} {yz:seo_description}",
|
||||
Desc: "取自租户的 SEO 设置(backend 端 - 站点设置 - SEO 设置),用于 head 区域的 meta 标签。seo_title 未填时建议回退用 {yz:sitename}。",
|
||||
Syntax: "{yz:sitename}、{yz:logo}、{yz:ico} …直接写在模板任意位置",
|
||||
Desc: "取自当前租户的站点设置(backend 端 - 站点设置 - 基本信息)。直接替换为文本或地址,无需闭合;未配置的项输出空字符串。点击行末复制按钮可复制标签。",
|
||||
Params: nil,
|
||||
Fields: nil,
|
||||
Example: "<title>{yz:seo_title}</title>\n<meta name=\"keywords\" content=\"{yz:keywords}\">\n<meta name=\"description\" content=\"{yz:seo_description}\">",
|
||||
Fields: []string{
|
||||
"sitename 站点名称,常用于 <title> 和页头 来源:基本信息-站点名称",
|
||||
"logo 彩色 Logo 图片地址,适合浅色背景 来源:基本信息-站点LOGO",
|
||||
"logow 白色 Logo 图片地址,适合深色背景/页脚 来源:基本信息-白色LOGO",
|
||||
"ico 站点图标地址,用于 <link rel=\"icon\"> 来源:基本信息-站点图标",
|
||||
"companyname 公司全称 来源:基本信息-公司名称",
|
||||
"companyintroduction 企业介绍(富文本HTML,直接用 div 承接) 来源:基本信息-企业介绍",
|
||||
"description 站点简介,可用于 meta description 来源:基本信息-站点描述",
|
||||
"copyright 版权信息,常用于页脚 来源:基本信息-版权信息",
|
||||
"icp ICP 备案号,常用于页脚 来源:基本信息-ICP备案",
|
||||
},
|
||||
Example: "<head>\n <title>{yz:sitename}</title>\n <link rel=\"icon\" href=\"{yz:ico}\">\n <meta name=\"description\" content=\"{yz:description}\">\n</head>\n<img src=\"{yz:logo}\" alt=\"{yz:sitename}\">\n<div class=\"about\">{yz:companyintroduction}</div>\n<footer>\n <p>{yz:copyright}</p>\n <p>{yz:icp}</p>\n</footer>",
|
||||
},
|
||||
{
|
||||
Name: "联系方式",
|
||||
Category: "global",
|
||||
Syntax: "{yz:phone}、{yz:email}、{yz:address}、{yz:worktime}",
|
||||
Desc: "取自租户的公司信息(backend 端 - 公司信息),常用于页头联系栏和页脚。直接替换为文本,无需闭合。",
|
||||
Params: nil,
|
||||
Fields: []string{
|
||||
"phone 联系电话,别名 {yz:tel}、{yz:mobile} 三者等价 来源:公司信息-联系电话",
|
||||
"email 电子邮箱,可配合 mailto: 使用 来源:公司信息-邮箱",
|
||||
"address 公司地址 来源:公司信息-地址",
|
||||
"worktime 工作时间 来源:公司信息-工作时间",
|
||||
},
|
||||
Example: "<!-- 页头联系栏 -->\n<a href=\"tel:{yz:phone}\">服务热线:{yz:phone}</a>\n<a href=\"mailto:{yz:email}\">{yz:email}</a>\n\n<!-- 页脚 -->\n<p>地址:{yz:address}</p>\n<p>工作时间:{yz:worktime}</p>",
|
||||
},
|
||||
{
|
||||
Name: "SEO 信息",
|
||||
Category: "global",
|
||||
Syntax: "{yz:seotitle}、{yz:keywords}、{yz:seodescription}",
|
||||
Desc: "取自租户的 SEO 设置(backend 端 - SEO 设置),用于 <head> 区域。seotitle 未填时输出为空,建议模板里用 {yz:sitename} 兜底。",
|
||||
Params: nil,
|
||||
Fields: []string{
|
||||
"seotitle SEO 标题,未填时建议回退 {yz:sitename} 来源:SEO设置-SEO标题",
|
||||
"keywords SEO 关键词,用于 meta keywords 来源:SEO设置-关键词",
|
||||
"seodescription SEO 描述,用于 meta description 来源:SEO设置-描述",
|
||||
},
|
||||
Example: "<title>{yz:seotitle}</title>\n<meta name=\"keywords\" content=\"{yz:keywords}\">\n<meta name=\"description\" content=\"{yz:seodescription}\">",
|
||||
},
|
||||
{
|
||||
Name: "nav 导航菜单",
|
||||
@@ -69,6 +106,15 @@ func TagDocs() []TagMeta {
|
||||
Fields: []string{"id 文章ID", "title 标题(受 titlelen 影响)", "titlefull 完整标题", "desc 摘要", "image 缩略图", "arcurl 详情页链接(/news/ID)", "pubdate 发布日期", "views 阅读量", "author 作者"},
|
||||
Example: "{yz:arclist row=\"6\" titlelen=\"20\"}\n<li><a href=\"[field:arcurl/]\">[field:title/]</a><span>[field:pubdate/]</span></li>\n{/yz:arclist}",
|
||||
},
|
||||
{
|
||||
Name: "newscenter 新闻中心(带Tab)",
|
||||
Category: "loop",
|
||||
Syntax: "{yz:newscenter row=\"8\" titlelen=\"30\"}...{/yz:newscenter}",
|
||||
Desc: "首页新闻中心区块。循环体是单条新闻卡片。若「新闻中心」分类下有子分类,则按子分类输出 Tab 并可切换,每个 Tab 展示 row 条;没有子分类则不输出 Tab,直接列出 row 条(含新闻中心及其子类文章)。",
|
||||
Params: []string{"row 每个分类输出条数,默认 8,最大 50", "titlelen 标题截断字数,不填为完整标题"},
|
||||
Fields: []string{"id 文章ID", "title 标题(受 titlelen 影响)", "titlefull 完整标题", "desc 摘要", "image 缩略图", "arcurl 详情页链接(/news/ID)", "pubdate 发布日期", "views 阅读量", "author 作者"},
|
||||
Example: "{yz:newscenter row=\"8\" titlelen=\"28\"}\n<div class=\"col-lg-3 col-md-6\">\n <a href=\"[field:arcurl/]\">\n <img src=\"[field:image/]\" alt=\"[field:titlefull/]\">\n <h4>[field:title/]</h4>\n <p>[field:desc/]</p>\n <span>[field:pubdate/]</span>\n </a>\n</div>\n{/yz:newscenter}",
|
||||
},
|
||||
{
|
||||
Name: "arcview 文章详情",
|
||||
Category: "single",
|
||||
|
||||
@@ -2,6 +2,7 @@ package tagengine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -23,6 +24,7 @@ func init() {
|
||||
RegisterBlock("arclist", arclistProvider)
|
||||
RegisterBlock("arcview", arcviewProvider)
|
||||
RegisterBlock("friendlink", friendlinkProvider)
|
||||
RegisterBodyBlock("newscenter", newscenterHandler)
|
||||
RegisterSelfClose("onepage", onepageHandler)
|
||||
RegisterSelfClose("pagelist", pagelistHandler)
|
||||
}
|
||||
@@ -61,7 +63,8 @@ func globalSiteInfo(tid uint64, ctx *RenderCtx) (map[string]string, error) {
|
||||
"copyright": "", "companyname": "", "description": "",
|
||||
"companyintroduction": "",
|
||||
"phone": "", "tel": "", "mobile": "", "email": "", "address": "", "worktime": "",
|
||||
"keywords": "", "seo_title": "", "seo_description": "",
|
||||
"keywords": "", "seotitle": "", "seodescription": "",
|
||||
"aboutus": "/page/about", "contact": "/page/contact", "news": "/news", "team": "/page/team",
|
||||
}
|
||||
var row models.TenantSiteSetting
|
||||
err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
@@ -105,8 +108,8 @@ func globalSiteInfo(tid uint64, ctx *RenderCtx) (map[string]string, error) {
|
||||
// SEO 三项取自租户扩展设置表(backend 端 - 站点设置 - SEO 设置)
|
||||
if vals, err := queryTenantSettingItems(tid, "seoKeywords", "seoTitle", "seoDescription"); err == nil {
|
||||
out["keywords"] = vals["seoKeywords"]
|
||||
out["seo_title"] = vals["seoTitle"]
|
||||
out["seo_description"] = vals["seoDescription"]
|
||||
out["seotitle"] = vals["seoTitle"]
|
||||
out["seodescription"] = vals["seoDescription"]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -136,11 +139,14 @@ func queryTenantSettingItems(tid uint64, keys ...string) (map[string]string, err
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// navProvider 导航菜单:一级菜单循环,[field:children/] 输出子菜单 HTML
|
||||
// navProvider 导航菜单:一级菜单循环,[field:children/] 输出子菜单 HTML。
|
||||
// 一级菜单含全局默认(tenant_id=0,不可删)与租户自定义;子菜单仅租户自有。
|
||||
func navProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||
models.EnsureGlobalDefaultFrontMenus()
|
||||
|
||||
var rows []models.BackendMenuFront
|
||||
_, err := models.Orm.QueryTable(new(models.BackendMenuFront)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("tenant_id__in", []uint64{0, tid}).
|
||||
Filter("pid", 0).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
@@ -169,7 +175,7 @@ func navProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[st
|
||||
childHTML := ""
|
||||
if subs, ok := childMap[int64(m.ID)]; ok && len(subs) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<ul class="yz-sub-menu">`)
|
||||
sb.WriteString(`<ul class="sub-menu">`)
|
||||
for _, s := range subs {
|
||||
sb.WriteString(fmt.Sprintf(`<li><a href="%s">%s</a></li>`, s.Path, s.Title))
|
||||
}
|
||||
@@ -373,6 +379,102 @@ func onepageHandler(tid uint64, params map[string]string, ctx *RenderCtx) (strin
|
||||
}
|
||||
}
|
||||
|
||||
// newscenterHandler 首页新闻中心:循环体为单条新闻卡片。
|
||||
// 有「新闻中心」子分类时输出 Bootstrap Tab,每个 Tab 8 条;无子分类则不输出 Tab,直接列出 8 条。
|
||||
func newscenterHandler(tid uint64, params map[string]string, ctx *RenderCtx, body string) (string, error) {
|
||||
row := atoiDefault(params["row"], 8)
|
||||
if row <= 0 || row > 50 {
|
||||
row = 8
|
||||
}
|
||||
titleLen := atoiDefault(params["titlelen"], 0)
|
||||
|
||||
parent, err := findNewsCenterCategory(tid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
children := []models.CmsArticleCategory{}
|
||||
if parent != nil {
|
||||
_, _ = models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("cid", parent.ID).
|
||||
Filter("tid__in", []uint64{0, tid}).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&children)
|
||||
}
|
||||
|
||||
renderCards := func(cateID uint64) string {
|
||||
qs := publishedCond(models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true))
|
||||
if cateID > 0 {
|
||||
qs = qs.Filter("cate_id", cateID)
|
||||
} else if parent != nil {
|
||||
ids := []uint64{parent.ID}
|
||||
for _, c := range children {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
qs = qs.Filter("cate_id__in", ids)
|
||||
}
|
||||
var arts []models.CmsArticle
|
||||
_, _ = qs.OrderBy("-top", "-id").Limit(row).All(&arts)
|
||||
var sb strings.Builder
|
||||
for _, a := range articleRows(arts, titleLen) {
|
||||
sb.WriteString(applyFields(body, a))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
return `<div class="row">` + renderCards(0) + `</div>`, nil
|
||||
}
|
||||
|
||||
var tabs, panes strings.Builder
|
||||
tabs.WriteString(`<nav class="news-center-tabs"><div class="nav nav-tabs" role="tablist">`)
|
||||
panes.WriteString(`<div class="tab-content">`)
|
||||
for i, c := range children {
|
||||
paneID := fmt.Sprintf("news-cate-%d", c.ID)
|
||||
tabID := paneID + "-tab"
|
||||
active := ""
|
||||
show := ""
|
||||
selected := "false"
|
||||
if i == 0 {
|
||||
active = " active"
|
||||
show = " show active"
|
||||
selected = "true"
|
||||
}
|
||||
tabs.WriteString(fmt.Sprintf(
|
||||
`<button class="nav-link%s" id="%s" data-bs-toggle="tab" data-bs-target="#%s" type="button" role="tab" aria-controls="%s" aria-selected="%s">%s</button>`,
|
||||
active, tabID, paneID, paneID, selected, html.EscapeString(c.Name),
|
||||
))
|
||||
panes.WriteString(fmt.Sprintf(
|
||||
`<div class="tab-pane fade%s" id="%s" role="tabpanel" aria-labelledby="%s"><div class="row">%s</div></div>`,
|
||||
show, paneID, tabID, renderCards(c.ID),
|
||||
))
|
||||
}
|
||||
tabs.WriteString(`</div></nav>`)
|
||||
panes.WriteString(`</div>`)
|
||||
return tabs.String() + panes.String(), nil
|
||||
}
|
||||
|
||||
func findNewsCenterCategory(tid uint64) (*models.CmsArticleCategory, error) {
|
||||
var row models.CmsArticleCategory
|
||||
err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("name", "新闻中心").
|
||||
Filter("tid__in", []uint64{0, tid}).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-tid", "id").
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// newsQueryBase 新闻列表统一查询条件(pagelist 与 news.html 保持一致)
|
||||
func newsQueryBase(tid uint64) orm.QuerySeter {
|
||||
return publishedCond(models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
|
||||
@@ -132,6 +132,28 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update")
|
||||
beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus")
|
||||
|
||||
// 产品管理(yz_cms_product / yz_cms_product_category,按租户隔离)
|
||||
beego.Router("/backend/productsList", &controllers.BackendProductController{}, "get:List")
|
||||
beego.Router("/backend/addProducts", &controllers.BackendProductController{}, "post:Create")
|
||||
beego.Router("/backend/editProducts/:id", &controllers.BackendProductController{}, "put:Update")
|
||||
beego.Router("/backend/deleteProducts/:id", &controllers.BackendProductController{}, "delete:Delete")
|
||||
|
||||
beego.Router("/backend/productsTypesList", &controllers.BackendProductCategoryController{}, "get:List")
|
||||
beego.Router("/backend/addProductsTypes", &controllers.BackendProductCategoryController{}, "post:Create")
|
||||
beego.Router("/backend/editProductsTypes/:id", &controllers.BackendProductCategoryController{}, "put:Update")
|
||||
beego.Router("/backend/deleteProductsTypes/:id", &controllers.BackendProductCategoryController{}, "delete:Delete")
|
||||
|
||||
// 解决方案/特色服务管理(yz_cms_solution / yz_cms_solution_category,按租户隔离)
|
||||
beego.Router("/backend/servicesList", &controllers.BackendSolutionController{}, "get:List")
|
||||
beego.Router("/backend/addServices", &controllers.BackendSolutionController{}, "post:Create")
|
||||
beego.Router("/backend/editServices/:id", &controllers.BackendSolutionController{}, "put:Update")
|
||||
beego.Router("/backend/deleteServices/:id", &controllers.BackendSolutionController{}, "delete:Delete")
|
||||
|
||||
beego.Router("/backend/servicesTypesList", &controllers.BackendSolutionCategoryController{}, "get:List")
|
||||
beego.Router("/backend/addServicesTypes", &controllers.BackendSolutionCategoryController{}, "post:Create")
|
||||
beego.Router("/backend/editServicesTypes/:id", &controllers.BackendSolutionCategoryController{}, "put:Update")
|
||||
beego.Router("/backend/deleteServicesTypes/:id", &controllers.BackendSolutionCategoryController{}, "delete:Delete")
|
||||
|
||||
// 官网前台内容(Banner / 友链 / 单页,yz_cms_frontend_*,按租户隔离)
|
||||
beego.Router("/backend/allbanners", &controllers.BackendBannerController{}, "get:ListAll")
|
||||
beego.Router("/backend/createbanner", &controllers.BackendBannerController{}, "post:Create")
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="description" content="{yz:description}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>{yz:sitename}</title>
|
||||
<link rel="shortcut icon" href="{yz:ico}" type="image/svg" />
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="assets/css/lineicons.css" />
|
||||
<link rel="stylesheet" href="assets/css/tiny-slider.css" />
|
||||
<link rel="stylesheet" href="assets/css/glightbox.min.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section class="navbar-area navbar-nine">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse sub-menu-bar" id="navbarNine">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a href="/">主页</a></li>
|
||||
{yz:nav}
|
||||
<li class="nav-item">
|
||||
<a href="[field:url/]">[field:name/]</a>
|
||||
[field:children/]
|
||||
</li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-btn d-none d-lg-inline-block">
|
||||
<a class="menu-bar" href="#side-menu-left"><i class="lni lni-menu"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-close"><a class="close" href="#close"><i class="lni lni-close"></i></a></div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" /></a></div>
|
||||
<p class="text">{yz:description}</p>
|
||||
<div class="sidebar-menu">
|
||||
<h5 class="menu-title">快速链接</h5>
|
||||
<ul>
|
||||
{yz:nav}
|
||||
<li><a href="[field:url/]">[field:name/]</a></li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-social align-items-center justify-content-center">
|
||||
<h5 class="social-title">关注我们</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-facebook-filled"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-twitter-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-linkedin-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-youtube"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-left"></div>
|
||||
|
||||
<section class="about-five page-inner-page">
|
||||
<div class="article-detail-breadcrumb-wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb article-detail-breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">主页</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">关于我们</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6 order-lg-2 order-1">
|
||||
<div class="about-image-five">
|
||||
<img src="assets/images/about/about-img1.jpg" alt="关于我们" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 order-lg-1 order-2">
|
||||
<div class="about-five-content">
|
||||
<h6 class="small-title">{yz:companyname}</h6>
|
||||
<h2 class="main-title fw-bold">关于我们</h2>
|
||||
<div class="onepage-content">
|
||||
{yz:onepage path="about"/}
|
||||
{yz:companyintroduction}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer-area footer-eleven">
|
||||
<div class="footer-top">
|
||||
<div class="container">
|
||||
<div class="inner-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget f-about">
|
||||
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" /></a></div>
|
||||
<p>{yz:description}</p>
|
||||
<p class="copyright-text">
|
||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>解决方案</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">市场营销</a></li>
|
||||
<li><a href="javascript:void(0)">数据分析</a></li>
|
||||
<li><a href="javascript:void(0)">电子商务</a></li>
|
||||
<li><a href="javascript:void(0)">商业洞察</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>技术支持</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">价格方案</a></li>
|
||||
<li><a href="javascript:void(0)">开发文档</a></li>
|
||||
<li><a href="javascript:void(0)">使用指南</a></li>
|
||||
<li><a href="javascript:void(0)">API 状态</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget newsletter">
|
||||
<h5>订阅我们</h5>
|
||||
<p>订阅我们,获取最新资讯</p>
|
||||
<form action="#" method="get" target="_blank" class="newsletter-form">
|
||||
<input name="EMAIL" placeholder="邮箱地址" required="required" type="email" />
|
||||
<div class="button"><button class="sub-btn"><i class="lni lni-envelope"></i></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<a href="#" class="scroll-top btn-hover"><i class="lni lni-chevron-up"></i></a>
|
||||
|
||||
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/glightbox.min.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/tiny-slider.js"></script>
|
||||
<script>
|
||||
document.querySelector(".navbar-nine .navbar-toggler").addEventListener("click", function () {
|
||||
this.classList.toggle("active");
|
||||
});
|
||||
var sidebarLeft = document.querySelector(".sidebar-left");
|
||||
var overlayLeft = document.querySelector(".overlay-left");
|
||||
overlayLeft.addEventListener("click", function () {
|
||||
sidebarLeft.classList.toggle("open");
|
||||
overlayLeft.classList.toggle("open");
|
||||
});
|
||||
document.querySelector(".sidebar-close .close").addEventListener("click", function () {
|
||||
sidebarLeft.classList.remove("open");
|
||||
overlayLeft.classList.remove("open");
|
||||
});
|
||||
document.querySelector(".navbar-nine .menu-bar").addEventListener("click", function () {
|
||||
sidebarLeft.classList.add("open");
|
||||
overlayLeft.classList.add("open");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
// section menu active
|
||||
// section menu active(仅处理页内锚点)
|
||||
function onScroll(event) {
|
||||
var sections = document.querySelectorAll('.page-scroll');
|
||||
var scrollPos = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
|
||||
@@ -32,10 +32,16 @@
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var currLink = sections[i];
|
||||
var val = currLink.getAttribute('href');
|
||||
if (!val || val.charAt(0) !== '#') {
|
||||
continue;
|
||||
}
|
||||
var refElement = document.querySelector(val);
|
||||
if (!refElement) {
|
||||
continue;
|
||||
}
|
||||
var scrollTopMinus = scrollPos + 73;
|
||||
if (refElement.offsetTop <= scrollTopMinus && (refElement.offsetTop + refElement.offsetHeight > scrollTopMinus)) {
|
||||
document.querySelector('.page-scroll').classList.remove('active');
|
||||
sections.forEach(function (link) { link.classList.remove('active'); });
|
||||
currLink.classList.add('active');
|
||||
} else {
|
||||
currLink.classList.remove('active');
|
||||
@@ -45,15 +51,23 @@
|
||||
|
||||
window.document.addEventListener('scroll', onScroll);
|
||||
|
||||
// for menu scroll
|
||||
// 仅页内锚点(#xxx)平滑滚动;/news、/ 等路由链接正常跳转
|
||||
var pageLink = document.querySelectorAll('.page-scroll');
|
||||
|
||||
pageLink.forEach(elem => {
|
||||
elem.addEventListener('click', e => {
|
||||
var href = elem.getAttribute('href');
|
||||
if (!href || href.charAt(0) !== '#') {
|
||||
return;
|
||||
}
|
||||
var target = document.querySelector(href);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
document.querySelector(elem.getAttribute('href')).scrollIntoView({
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
offsetTop: 1 - 60,
|
||||
block: 'start',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="description" content="{yz:description}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>{yz:sitename}</title>
|
||||
<link rel="shortcut icon" href="{yz:ico}" type="image/svg" />
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="assets/css/lineicons.css" />
|
||||
<link rel="stylesheet" href="assets/css/tiny-slider.css" />
|
||||
<link rel="stylesheet" href="assets/css/glightbox.min.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section class="navbar-area navbar-nine">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse sub-menu-bar" id="navbarNine">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a href="/">主页</a></li>
|
||||
{yz:nav}
|
||||
<li class="nav-item">
|
||||
<a href="[field:url/]">[field:name/]</a>
|
||||
[field:children/]
|
||||
</li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-btn d-none d-lg-inline-block">
|
||||
<a class="menu-bar" href="#side-menu-left"><i class="lni lni-menu"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-close"><a class="close" href="#close"><i class="lni lni-close"></i></a></div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" /></a></div>
|
||||
<p class="text">{yz:description}</p>
|
||||
<div class="sidebar-menu">
|
||||
<h5 class="menu-title">快速链接</h5>
|
||||
<ul>
|
||||
{yz:nav}
|
||||
<li><a href="[field:url/]">[field:name/]</a></li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-social align-items-center justify-content-center">
|
||||
<h5 class="social-title">关注我们</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-facebook-filled"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-twitter-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-linkedin-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-youtube"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-left"></div>
|
||||
|
||||
<section id="contact" class="contact-section page-inner-page">
|
||||
<div class="article-detail-breadcrumb-wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb article-detail-breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">主页</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">联系我们</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xl-4">
|
||||
<div class="contact-item-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-xl-12">
|
||||
<div class="contact-item">
|
||||
<div class="contact-icon"><i class="lni lni-phone"></i></div>
|
||||
<div class="contact-content">
|
||||
<h4>联系方式</h4>
|
||||
<p>{yz:phone}</p>
|
||||
<p>{yz:email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-12">
|
||||
<div class="contact-item">
|
||||
<div class="contact-icon"><i class="lni lni-map-marker"></i></div>
|
||||
<div class="contact-content">
|
||||
<h4>公司地址</h4>
|
||||
<p>{yz:address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-12">
|
||||
<div class="contact-item">
|
||||
<div class="contact-icon"><i class="lni lni-alarm-clock"></i></div>
|
||||
<div class="contact-content">
|
||||
<h4>营业时间</h4>
|
||||
<p>{yz:worktime}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-8">
|
||||
<div class="contact-form-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-lg-8 mx-auto">
|
||||
<div class="section-title text-center">
|
||||
<span>联系我们</span>
|
||||
<h2>准备开始吧!</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="onepage-content contact-page-content">
|
||||
{yz:onepage path="contact"/}
|
||||
</div>
|
||||
<form action="#" class="contact-form">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" name="name" id="name" placeholder="姓名" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="email" name="email" id="email" placeholder="邮箱" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" name="phone" id="phone" placeholder="手机号" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" name="title" id="title" placeholder="标题" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<textarea name="message" id="message" placeholder="请输入内容" rows="5"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="button text-center rounded-buttons">
|
||||
<button type="submit" class="btn primary-btn rounded-full">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer-area footer-eleven">
|
||||
<div class="footer-top">
|
||||
<div class="container">
|
||||
<div class="inner-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget f-about">
|
||||
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" /></a></div>
|
||||
<p>{yz:description}</p>
|
||||
<p class="copyright-text">
|
||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>解决方案</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">市场营销</a></li>
|
||||
<li><a href="javascript:void(0)">数据分析</a></li>
|
||||
<li><a href="javascript:void(0)">电子商务</a></li>
|
||||
<li><a href="javascript:void(0)">商业洞察</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>技术支持</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">价格方案</a></li>
|
||||
<li><a href="javascript:void(0)">开发文档</a></li>
|
||||
<li><a href="javascript:void(0)">使用指南</a></li>
|
||||
<li><a href="javascript:void(0)">API 状态</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget newsletter">
|
||||
<h5>订阅我们</h5>
|
||||
<p>订阅我们,获取最新资讯</p>
|
||||
<form action="#" method="get" target="_blank" class="newsletter-form">
|
||||
<input name="EMAIL" placeholder="邮箱地址" required="required" type="email" />
|
||||
<div class="button"><button class="sub-btn"><i class="lni lni-envelope"></i></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<a href="#" class="scroll-top btn-hover"><i class="lni lni-chevron-up"></i></a>
|
||||
|
||||
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/glightbox.min.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/tiny-slider.js"></script>
|
||||
<script>
|
||||
document.querySelector(".navbar-nine .navbar-toggler").addEventListener("click", function () {
|
||||
this.classList.toggle("active");
|
||||
});
|
||||
var sidebarLeft = document.querySelector(".sidebar-left");
|
||||
var overlayLeft = document.querySelector(".overlay-left");
|
||||
overlayLeft.addEventListener("click", function () {
|
||||
sidebarLeft.classList.toggle("open");
|
||||
overlayLeft.classList.toggle("open");
|
||||
});
|
||||
document.querySelector(".sidebar-close .close").addEventListener("click", function () {
|
||||
sidebarLeft.classList.remove("open");
|
||||
overlayLeft.classList.remove("open");
|
||||
});
|
||||
document.querySelector(".navbar-nine .menu-bar").addEventListener("click", function () {
|
||||
sidebarLeft.classList.add("open");
|
||||
overlayLeft.classList.add("open");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+119
-404
@@ -5,11 +5,11 @@
|
||||
<!--====== 必需的元标签 ======-->
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="description" content="云泽网 - 数字化解决方案与商业服务" />
|
||||
<meta name="description" content="{yz:description}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
|
||||
<!--====== 标题 ======-->
|
||||
<title>云泽网 - {yz:sitename}</title>
|
||||
<title>{yz:sitename}</title>
|
||||
|
||||
<!--====== 网站图标 ======-->
|
||||
<link rel="shortcut icon" href="{yz:ico}" type="image/svg" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="index.html">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||
@@ -51,11 +51,11 @@
|
||||
<div class="collapse navbar-collapse sub-menu-bar" id="navbarNine">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="page-scroll" href="/">主页</a>
|
||||
<a href="/">主页</a>
|
||||
</li>
|
||||
{yz:nav}
|
||||
<li class="nav-item">
|
||||
<a class="page-scroll" href="[field:url/]">[field:name/]</a>
|
||||
<a href="[field:url/]">[field:name/]</a>
|
||||
[field:children/]
|
||||
</li>
|
||||
{/yz:nav}
|
||||
@@ -84,17 +84,16 @@
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-logo">
|
||||
<a href="index.html"><img src="assets/images/logo.svg" alt="云泽网" /></a>
|
||||
<a href="/"><img src="{yz:logo}" alt="{yz:sitename}" /></a>
|
||||
</div>
|
||||
<p class="text">我们致力于为品牌提供卓越的数字解决方案,用技术创造无限可能。</p>
|
||||
<p class="text">{yz:description}</p>
|
||||
<!-- logo -->
|
||||
<div class="sidebar-menu">
|
||||
<h5 class="menu-title">快速链接</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">关于我们</a></li>
|
||||
<li><a href="javascript:void(0)">我们的团队</a></li>
|
||||
<li><a href="javascript:void(0)">最新动态</a></li>
|
||||
<li><a href="javascript:void(0)">联系我们</a></li>
|
||||
{yz:nav}
|
||||
<li><a href="[field:url/]">[field:name/]</a></li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- menu -->
|
||||
@@ -148,137 +147,48 @@
|
||||
</section>
|
||||
<!-- 头部区域 结束 -->
|
||||
|
||||
<!--====== 关于我们 开始 ======-->
|
||||
<!--====== 新闻中心 开始 ======-->
|
||||
|
||||
<section class="about-area about-five">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="about-image-five">
|
||||
<svg class="shape" width="106" height="134" viewBox="0 0 106 134" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="1.66654" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="1.66654" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.3333" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.3333" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.3333" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.3333" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.333" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.333" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.333" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.333" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.333" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="16.333" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="30.9998" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6665" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="30.9998" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6665" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="30.9998" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6665" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="30.9998" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6665" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="31" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6668" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="31" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6668" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="31" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6668" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="31" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6668" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="31" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6668" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="31" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="74.6668" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="45.6665" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="89.3333" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.3333" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="1.66679" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.3333" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="16.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.3333" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="31.0001" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.3333" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="45.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.333" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="60.3335" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.333" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="88.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.333" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="117.667" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.333" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="74.6668" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.333" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="103" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="60.333" cy="132" r="1.66667" fill="#DADADA" />
|
||||
<circle cx="104" cy="132" r="1.66667" fill="#DADADA" />
|
||||
</svg>
|
||||
<img src="assets/images/about/about-img1.jpg" alt="关于我们 - 团队与经验" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="about-five-content">
|
||||
<h6 class="small-title text-lg">我们的故事</h6>
|
||||
<h2 class="main-title fw-bold">我们的团队拥有丰富的经验和专业知识</h2>
|
||||
<div class="about-five-tab">
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<button class="nav-link active" id="nav-who-tab" data-bs-toggle="tab" data-bs-target="#nav-who"
|
||||
type="button" role="tab" aria-controls="nav-who" aria-selected="true">关于我们</button>
|
||||
<button class="nav-link" id="nav-vision-tab" data-bs-toggle="tab" data-bs-target="#nav-vision"
|
||||
type="button" role="tab" aria-controls="nav-vision" aria-selected="false">我们的愿景</button>
|
||||
<button class="nav-link" id="nav-history-tab" data-bs-toggle="tab" data-bs-target="#nav-history"
|
||||
type="button" role="tab" aria-controls="nav-history" aria-selected="false">发展历程</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<div class="tab-pane fade show active" id="nav-who" role="tabpanel" aria-labelledby="nav-who-tab">
|
||||
<p>我们相信,优秀的设计和清晰的技术能够塑造品牌的未来。我们专注于提供定制化的数字解决方案,帮助客户在竞争中脱颖而出。</p>
|
||||
<p>从初创企业到行业巨头,我们与各类品牌合作,通过创新驱动业务增长。我们的团队始终以客户需求为核心,打造兼具美感与功能的产品。</p>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-vision" role="tabpanel" aria-labelledby="nav-vision-tab">
|
||||
<p>我们的愿景是成为数字创新领域的引领者,通过科技赋能商业,让每一个创意都能落地生根,创造真正的社会与商业价值。</p>
|
||||
<p>我们期待构建一个开放、协作的生态系统,与合作伙伴共同探索未来的可能性,推动行业持续进步。</p>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-history" role="tabpanel" aria-labelledby="nav-history-tab">
|
||||
<p>自成立以来,我们始终秉持匠心精神,不断打磨技术与服务。从第一个项目到如今服务全球客户,每一步都见证着我们的成长与坚持。</p>
|
||||
<p>我们珍视每一次合作,并将其视为推动自我革新的契机。未来的路,我们期待与更多伙伴携手同行。</p>
|
||||
</div>
|
||||
</div>
|
||||
<section id="news-center" class="news-center-area">
|
||||
<div class="section-title-five">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="content">
|
||||
<h6>资讯动态</h6>
|
||||
<h2 class="fw-bold">新闻中心</h2>
|
||||
<p>了解最新动态、行业资讯与公司公告。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- container -->
|
||||
<div class="container">
|
||||
{yz:newscenter row="8" titlelen="28"}
|
||||
<div class="col-lg-3 col-md-6 col-12">
|
||||
<div class="single-news news-center-item">
|
||||
<div class="image">
|
||||
<a href="[field:arcurl/]"><img class="thumb" src="[field:image/]" alt="[field:titlefull/]" onerror="this.style.display='none'" /></a>
|
||||
<div class="meta-details">
|
||||
<span>[field:pubdate/]</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<h4 class="title">
|
||||
<a href="[field:arcurl/]" title="[field:titlefull/]">[field:title/]</a>
|
||||
</h4>
|
||||
<p>[field:desc/]</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/yz:newscenter}
|
||||
<div class="news-center-more">
|
||||
<a href="/news" class="btn primary-btn-outline">查看更多</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--====== 关于我们 结束 ======-->
|
||||
<!--====== 新闻中心 结束 ======-->
|
||||
|
||||
<!-- ===== 服务区域 开始 ===== -->
|
||||
<section id="services" class="services-area services-eight">
|
||||
@@ -288,10 +198,10 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="content">
|
||||
<h6>我们的服务</h6>
|
||||
<h2 class="fw-bold">核心业务</h2>
|
||||
<h6>产品中心</h6>
|
||||
<h2 class="fw-bold">精选产品展示</h2>
|
||||
<p>
|
||||
我们提供从策略到执行的全链路服务,助力品牌实现数字化升级。
|
||||
面向企业数字化运营,提供开箱即用、可持续扩展的产品能力。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,84 +212,33 @@
|
||||
</div>
|
||||
<!--====== 标题区域 结束 ======-->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="row product-showcase-grid">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="single-services">
|
||||
<div class="service-icon">
|
||||
<i class="lni lni-capsule"></i>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
<h4>焕新设计</h4>
|
||||
<p>
|
||||
以用户为中心,打造兼具视觉冲击与极致体验的界面设计,让品牌形象焕然一新。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<article class="product-showcase-card product-showcase-card--primary">
|
||||
<div class="product-showcase-icon"><i class="lni lni-dashboard"></i></div>
|
||||
<span class="product-showcase-tag">运营增长</span>
|
||||
<h3>企业运营中台</h3>
|
||||
<p>统一连接客户、业务和数据,帮助团队快速构建可度量的运营闭环。</p>
|
||||
<a href="#contact" class="product-showcase-link">了解产品 <i class="lni lni-arrow-right"></i></a>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="single-services">
|
||||
<div class="service-icon">
|
||||
<i class="lni lni-bootstrap"></i>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
<h4>坚实 Bootstrap 5</h4>
|
||||
<p>
|
||||
基于最新 Bootstrap 5 框架,确保项目具有出色的响应式布局和稳定的性能表现。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<article class="product-showcase-card">
|
||||
<div class="product-showcase-icon"><i class="lni lni-layers"></i></div>
|
||||
<span class="product-showcase-tag">协同办公</span>
|
||||
<h3>智能协作平台</h3>
|
||||
<p>聚合任务、审批与知识资产,让跨部门协作清晰有序、高效推进。</p>
|
||||
<a href="#contact" class="product-showcase-link">了解产品 <i class="lni lni-arrow-right"></i></a>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="single-services">
|
||||
<div class="service-icon">
|
||||
<i class="lni lni-shortcode"></i>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
<h4>100+ 组件库</h4>
|
||||
<p>
|
||||
内置丰富的组件与模块,灵活组合,快速搭建企业级应用与营销页面。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="single-services">
|
||||
<div class="service-icon">
|
||||
<i class="lni lni-dashboard"></i>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
<h4>性能优化</h4>
|
||||
<p>
|
||||
从代码到资源加载,全方位优化,确保网站与应用拥有闪电般的访问速度。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="single-services">
|
||||
<div class="service-icon">
|
||||
<i class="lni lni-layers"></i>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
<h4>完全自定义</h4>
|
||||
<p>
|
||||
所有设计均支持深度定制,贴合品牌调性与业务需求,拒绝模板化。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="single-services">
|
||||
<div class="service-icon">
|
||||
<i class="lni lni-reload"></i>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
<h4>持续迭代</h4>
|
||||
<p>
|
||||
我们提供长期的技术支持与功能更新,保障产品始终与时俱进。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<article class="product-showcase-card">
|
||||
<div class="product-showcase-icon"><i class="lni lni-bar-chart"></i></div>
|
||||
<span class="product-showcase-tag">数据洞察</span>
|
||||
<h3>经营分析系统</h3>
|
||||
<p>将分散经营数据转化为实时洞察,为每一次关键决策提供可靠依据。</p>
|
||||
<a href="#contact" class="product-showcase-link">了解产品 <i class="lni lni-arrow-right"></i></a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,117 +246,71 @@
|
||||
<!-- ===== 服务区域 结束 ===== -->
|
||||
|
||||
|
||||
<!-- 价格区域 开始 -->
|
||||
<section id="pricing" class="pricing-area pricing-fourteen">
|
||||
<!--====== 标题区域 开始 ======-->
|
||||
<!-- 解决方案区域 开始 -->
|
||||
<section id="pricing" class="solution-area">
|
||||
<div class="section-title-five">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="content">
|
||||
<h6>定价方案</h6>
|
||||
<h2 class="fw-bold">灵活的价格策略</h2>
|
||||
<h6>解决方案</h6>
|
||||
<h2 class="fw-bold">适配不同业务场景</h2>
|
||||
<p>
|
||||
我们提供多种套餐,满足不同规模企业的需求。所有方案均包含核心功能,无隐藏费用。
|
||||
从业务诊断到平台落地,为不同阶段、不同规模的企业提供针对性支持。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- row -->
|
||||
</div>
|
||||
<!-- container -->
|
||||
</div>
|
||||
<!--====== 标题区域 结束 ======-->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="pricing-style-fourteen">
|
||||
<div class="table-head">
|
||||
<h6 class="title">入门版</h4>
|
||||
<p>适合个人开发者或小型项目,快速起步。</p>
|
||||
<div class="price">
|
||||
<h2 class="amount">
|
||||
<span class="currency">$</span>0<span class="duration">/月 </span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="light-rounded-buttons">
|
||||
<a href="javascript:void(0)" class="btn primary-btn-outline">
|
||||
免费试用
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-content">
|
||||
<ul class="table-list">
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 基础组件使用</li>
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 社区支持</li>
|
||||
<li> <i class="lni lni-checkmark-circle deactive"></i> 高级组件库</li>
|
||||
<li> <i class="lni lni-checkmark-circle deactive"></i> 专属技术支持</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<article class="solution-showcase-card">
|
||||
<span class="solution-showcase-index">01</span>
|
||||
<div class="solution-showcase-icon"><i class="lni lni-briefcase"></i></div>
|
||||
<h3>中小企业数字化</h3>
|
||||
<p>以轻量化产品快速梳理业务流程,降低协同和管理成本。</p>
|
||||
<ul>
|
||||
<li>业务流程在线化</li>
|
||||
<li>经营数据可视化</li>
|
||||
<li>模块化按需启用</li>
|
||||
</ul>
|
||||
<a href="#contact" class="solution-showcase-link">咨询方案 <i class="lni lni-arrow-right"></i></a>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="pricing-style-fourteen middle">
|
||||
<div class="table-head">
|
||||
<h6 class="title">进阶版</h4>
|
||||
<p>为成长型企业打造,提供更多扩展功能。</p>
|
||||
<div class="price">
|
||||
<h2 class="amount">
|
||||
<span class="currency">$</span>99<span class="duration">/月 </span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="light-rounded-buttons">
|
||||
<a href="javascript:void(0)" class="btn primary-btn">
|
||||
免费试用
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-content">
|
||||
<ul class="table-list">
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 全组件使用</li>
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 优先邮件支持</li>
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 自定义主题</li>
|
||||
<li> <i class="lni lni-checkmark-circle deactive"></i> 专属客户经理</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<article class="solution-showcase-card solution-showcase-card--featured">
|
||||
<span class="solution-showcase-index">02</span>
|
||||
<div class="solution-showcase-icon"><i class="lni lni-network"></i></div>
|
||||
<h3>集团协同管理</h3>
|
||||
<p>构建跨组织、跨区域的一体化管理平台,让总部与业务单元高效联动。</p>
|
||||
<ul>
|
||||
<li>多组织权限体系</li>
|
||||
<li>统一业务数据标准</li>
|
||||
<li>全链路经营管控</li>
|
||||
</ul>
|
||||
<a href="#contact" class="solution-showcase-link">咨询方案 <i class="lni lni-arrow-right"></i></a>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="pricing-style-fourteen">
|
||||
<div class="table-head">
|
||||
<h6 class="title">尊享版</h4>
|
||||
<p>面向大型组织,提供全方位定制与专属服务。</p>
|
||||
<div class="price">
|
||||
<h2 class="amount">
|
||||
<span class="currency">$</span>150<span class="duration">/月 </span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="light-rounded-buttons">
|
||||
<a href="javascript:void(0)" class="btn primary-btn-outline">
|
||||
免费试用
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-content">
|
||||
<ul class="table-list">
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 所有高级功能</li>
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 7x24 技术支持</li>
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 专属定制开发</li>
|
||||
<li> <i class="lni lni-checkmark-circle"></i> 战略咨询顾问</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<article class="solution-showcase-card">
|
||||
<span class="solution-showcase-index">03</span>
|
||||
<div class="solution-showcase-icon"><i class="lni lni-rocket"></i></div>
|
||||
<h3>业务增长与创新</h3>
|
||||
<p>以数据驱动产品迭代和客户运营,持续验证并放大增长机会。</p>
|
||||
<ul>
|
||||
<li>客户全生命周期运营</li>
|
||||
<li>实时指标监测分析</li>
|
||||
<li>敏捷迭代支持</li>
|
||||
</ul>
|
||||
<a href="#contact" class="solution-showcase-link">咨询方案 <i class="lni lni-arrow-right"></i></a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!--/ 价格区域 结束 -->
|
||||
<!-- 解决方案区域 结束 -->
|
||||
|
||||
|
||||
|
||||
@@ -521,103 +334,6 @@
|
||||
</section>
|
||||
<!-- 行动号召区域 结束 -->
|
||||
|
||||
|
||||
|
||||
<!-- 最新动态区域 开始 -->
|
||||
<div id="blog" class="latest-news-area section">
|
||||
<!--====== 标题区域 开始 ======-->
|
||||
<div class="section-title-five">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="content">
|
||||
<h6>最新资讯</h6>
|
||||
<h2 class="fw-bold">新闻与博客</h2>
|
||||
<p>
|
||||
分享行业洞察、技术干货与团队动态,与您一同探索数字世界的边界。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- row -->
|
||||
</div>
|
||||
<!-- container -->
|
||||
</div>
|
||||
<!--====== 标题区域 结束 ======-->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<!-- 单篇新闻 -->
|
||||
<div class="single-news">
|
||||
<div class="image">
|
||||
<a href="javascript:void(0)"><img class="thumb" src="assets/images/blog/1.jpg" alt="博客文章" /></a>
|
||||
<div class="meta-details">
|
||||
<img class="thumb" src="assets/images/blog/b6.jpg" alt="作者头像" />
|
||||
<span>作者:张明</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<h4 class="title">
|
||||
<a href="javascript:void(0)"> 如何打造设计驱动的团队文化 </a>
|
||||
</h4>
|
||||
<p>
|
||||
设计不仅仅是美学,更是解决问题的思维方式。本文分享了我们在团队中推行设计驱动的实践与经验。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单篇新闻结束 -->
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<!-- 单篇新闻 -->
|
||||
<div class="single-news">
|
||||
<div class="image">
|
||||
<a href="javascript:void(0)"><img class="thumb" src="assets/images/blog/2.jpg" alt="博客文章" /></a>
|
||||
<div class="meta-details">
|
||||
<img class="thumb" src="assets/images/blog/b6.jpg" alt="作者头像" />
|
||||
<span>作者:李薇</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<h4 class="title">
|
||||
<a href="javascript:void(0)">
|
||||
2026 年最值得关注的前端框架
|
||||
</a>
|
||||
</h4>
|
||||
<p>
|
||||
技术演进日新月异,我们梳理了当前最具潜力与社区活力的前端框架,助你做出更明智的技术选型。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单篇新闻结束 -->
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<!-- 单篇新闻 -->
|
||||
<div class="single-news">
|
||||
<div class="image">
|
||||
<a href="javascript:void(0)"><img class="thumb" src="assets/images/blog/3.jpg" alt="博客文章" /></a>
|
||||
<div class="meta-details">
|
||||
<img class="thumb" src="assets/images/blog/b6.jpg" alt="作者头像" />
|
||||
<span>作者:王磊</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<h4 class="title">
|
||||
<a href="javascript:void(0)">
|
||||
提升用户留存的 5 个关键策略
|
||||
</a>
|
||||
</h4>
|
||||
<p>
|
||||
用户留存是增长的基石。我们结合案例,总结了提升用户粘性与活跃度的实用方法,值得收藏。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单篇新闻结束 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 最新动态区域 结束 -->
|
||||
|
||||
<!-- 品牌展示区域 开始 -->
|
||||
<div id="clients" class="brand-area section">
|
||||
<!--====== 标题区域 开始 ======-->
|
||||
@@ -682,8 +398,8 @@
|
||||
</div>
|
||||
<div class="contact-content">
|
||||
<h4>联系方式</h4>
|
||||
<p>0984537278623</p>
|
||||
<p>yourmail@yunzer.cn</p>
|
||||
<p>{yz:phone}</p>
|
||||
<p>{yz:email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -694,8 +410,7 @@
|
||||
</div>
|
||||
<div class="contact-content">
|
||||
<h4>公司地址</h4>
|
||||
<p>北京市朝阳区建国路88号SOHO现代城</p>
|
||||
<p>中国 · 北京</p>
|
||||
<p>{yz:address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,7 +501,7 @@
|
||||
<!-- 单个小组件 -->
|
||||
<div class="footer-widget f-about">
|
||||
<div class="logo">
|
||||
<a href="index.html">
|
||||
<a href="/">
|
||||
<img src="{yz:logo}" alt="云泽网" class="img-fluid" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="description" content="{yz:description}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>{yz:sitename}</title>
|
||||
<link rel="shortcut icon" href="{yz:ico}" type="image/svg" />
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="assets/css/lineicons.css" />
|
||||
<link rel="stylesheet" href="assets/css/tiny-slider.css" />
|
||||
<link rel="stylesheet" href="assets/css/glightbox.min.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!--====== 导航栏 开始 ======-->
|
||||
<section class="navbar-area navbar-nine">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse sub-menu-bar" id="navbarNine">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a href="/">主页</a>
|
||||
</li>
|
||||
{yz:nav}
|
||||
<li class="nav-item">
|
||||
<a href="[field:url/]">[field:name/]</a>
|
||||
[field:children/]
|
||||
</li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="navbar-btn d-none d-lg-inline-block">
|
||||
<a class="menu-bar" href="#side-menu-left"><i class="lni lni-menu"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!--====== 导航栏 结束 ======-->
|
||||
|
||||
<!--====== 侧边栏 开始 ======-->
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-close">
|
||||
<a class="close" href="#close"><i class="lni lni-close"></i></a>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-logo">
|
||||
<a href="/"><img src="{yz:logo}" alt="{yz:sitename}" /></a>
|
||||
</div>
|
||||
<p class="text">{yz:description}</p>
|
||||
<div class="sidebar-menu">
|
||||
<h5 class="menu-title">快速链接</h5>
|
||||
<ul>
|
||||
{yz:nav}
|
||||
<li><a href="[field:url/]">[field:name/]</a></li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-social align-items-center justify-content-center">
|
||||
<h5 class="social-title">关注我们</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-facebook-filled"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-twitter-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-linkedin-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-youtube"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-left"></div>
|
||||
<!--====== 侧边栏 结束 ======-->
|
||||
|
||||
<!--====== 新闻中心 开始 ======-->
|
||||
<section id="news-center" class="news-center-area news-list-page">
|
||||
<div class="article-detail-breadcrumb-wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb article-detail-breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">主页</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">新闻中心</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title-five">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="content">
|
||||
<h6>资讯动态</h6>
|
||||
<h2 class="fw-bold">新闻中心</h2>
|
||||
<p>了解最新动态、行业资讯与公司公告。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{yz:arclist row="10" titlelen="40"}
|
||||
<div class="col-lg-3 col-md-6 col-12">
|
||||
<div class="single-news news-center-item">
|
||||
<div class="image">
|
||||
<a href="[field:arcurl/]"><img class="thumb" src="[field:image/]" alt="[field:titlefull/]" onerror="this.style.display='none'" /></a>
|
||||
<div class="meta-details">
|
||||
<span>[field:pubdate/]</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<h4 class="title">
|
||||
<a href="[field:arcurl/]" title="[field:titlefull/]">[field:title/]</a>
|
||||
</h4>
|
||||
<p>[field:desc/]</p>
|
||||
<div class="news-item-meta">
|
||||
<span><i class="lni lni-user"></i> [field:author/]</span>
|
||||
<span><i class="lni lni-eye"></i> [field:views/]</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/yz:arclist}
|
||||
</div>
|
||||
|
||||
<div class="news-list-pager">{yz:pagelist/}</div>
|
||||
</div>
|
||||
</section>
|
||||
<!--====== 新闻中心 结束 ======-->
|
||||
|
||||
<!-- 页脚区域 开始 -->
|
||||
<footer class="footer-area footer-eleven">
|
||||
<div class="footer-top">
|
||||
<div class="container">
|
||||
<div class="inner-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget f-about">
|
||||
<div class="logo">
|
||||
<a href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" />
|
||||
</a>
|
||||
</div>
|
||||
<p>{yz:description}</p>
|
||||
<p class="copyright-text">
|
||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>解决方案</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">市场营销</a></li>
|
||||
<li><a href="javascript:void(0)">数据分析</a></li>
|
||||
<li><a href="javascript:void(0)">电子商务</a></li>
|
||||
<li><a href="javascript:void(0)">商业洞察</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>技术支持</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">价格方案</a></li>
|
||||
<li><a href="javascript:void(0)">开发文档</a></li>
|
||||
<li><a href="javascript:void(0)">使用指南</a></li>
|
||||
<li><a href="javascript:void(0)">API 状态</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget newsletter">
|
||||
<h5>订阅我们</h5>
|
||||
<p>订阅我们,获取最新资讯</p>
|
||||
<form action="#" method="get" target="_blank" class="newsletter-form">
|
||||
<input name="EMAIL" placeholder="邮箱地址" required="required" type="email" />
|
||||
<div class="button">
|
||||
<button class="sub-btn">
|
||||
<i class="lni lni-envelope"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- 页脚区域 结束 -->
|
||||
|
||||
<a href="#" class="scroll-top btn-hover">
|
||||
<i class="lni lni-chevron-up"></i>
|
||||
</a>
|
||||
|
||||
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/glightbox.min.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/tiny-slider.js"></script>
|
||||
<script>
|
||||
let navbarTogglerNine = document.querySelector(".navbar-nine .navbar-toggler");
|
||||
navbarTogglerNine.addEventListener("click", function () {
|
||||
navbarTogglerNine.classList.toggle("active");
|
||||
});
|
||||
|
||||
let sidebarLeft = document.querySelector(".sidebar-left");
|
||||
let overlayLeft = document.querySelector(".overlay-left");
|
||||
let sidebarClose = document.querySelector(".sidebar-close .close");
|
||||
|
||||
overlayLeft.addEventListener("click", function () {
|
||||
sidebarLeft.classList.toggle("open");
|
||||
overlayLeft.classList.toggle("open");
|
||||
});
|
||||
sidebarClose.addEventListener("click", function () {
|
||||
sidebarLeft.classList.remove("open");
|
||||
overlayLeft.classList.remove("open");
|
||||
});
|
||||
|
||||
let sideMenuLeftNine = document.querySelector(".navbar-nine .menu-bar");
|
||||
sideMenuLeftNine.addEventListener("click", function () {
|
||||
sidebarLeft.classList.add("open");
|
||||
overlayLeft.classList.add("open");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="description" content="{yz:description}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>{yz:sitename}</title>
|
||||
<link rel="shortcut icon" href="{yz:ico}" type="image/svg" />
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="assets/css/lineicons.css" />
|
||||
<link rel="stylesheet" href="assets/css/tiny-slider.css" />
|
||||
<link rel="stylesheet" href="assets/css/glightbox.min.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!--====== 导航栏 开始 ======-->
|
||||
<section class="navbar-area navbar-nine">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse sub-menu-bar" id="navbarNine">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a href="/">主页</a>
|
||||
</li>
|
||||
{yz:nav}
|
||||
<li class="nav-item">
|
||||
<a href="[field:url/]">[field:name/]</a>
|
||||
[field:children/]
|
||||
</li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="navbar-btn d-none d-lg-inline-block">
|
||||
<a class="menu-bar" href="#side-menu-left"><i class="lni lni-menu"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!--====== 导航栏 结束 ======-->
|
||||
|
||||
<!--====== 侧边栏 开始 ======-->
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-close">
|
||||
<a class="close" href="#close"><i class="lni lni-close"></i></a>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-logo">
|
||||
<a href="/"><img src="{yz:logo}" alt="{yz:sitename}" /></a>
|
||||
</div>
|
||||
<p class="text">{yz:description}</p>
|
||||
<div class="sidebar-menu">
|
||||
<h5 class="menu-title">快速链接</h5>
|
||||
<ul>
|
||||
{yz:nav}
|
||||
<li><a href="[field:url/]">[field:name/]</a></li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-social align-items-center justify-content-center">
|
||||
<h5 class="social-title">关注我们</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-facebook-filled"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-twitter-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-linkedin-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-youtube"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-left"></div>
|
||||
<!--====== 侧边栏 结束 ======-->
|
||||
|
||||
<!--====== 文章详情 开始 ======-->
|
||||
<section class="article-detail-area">
|
||||
<div class="article-detail-breadcrumb-wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb article-detail-breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">主页</a></li>
|
||||
<li class="breadcrumb-item"><a href="/news">新闻中心</a></li>
|
||||
{yz:arcview}
|
||||
<li class="breadcrumb-item active" aria-current="page">[field:title/]</li>
|
||||
{/yz:arcview}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10 col-xl-9">
|
||||
{yz:arcview}
|
||||
<article class="article-detail-card">
|
||||
<div class="article-detail-thumb" style="display:none;">
|
||||
<img src="[field:image/]" alt="[field:title/]" class="article-cover" onerror="this.closest('.article-detail-thumb').style.display='none'" onload="this.closest('.article-detail-thumb').style.display='block'" />
|
||||
</div>
|
||||
|
||||
<h1 class="article-detail-title">[field:title/]</h1>
|
||||
|
||||
<div class="article-detail-meta">
|
||||
<span><i class="lni lni-calendar"></i> [field:pubdate/]</span>
|
||||
<span><i class="lni lni-user"></i> [field:author/]</span>
|
||||
<span><i class="lni lni-eye"></i> [field:views/] 阅读</span>
|
||||
</div>
|
||||
|
||||
<div class="article-detail-content">
|
||||
[field:content/]
|
||||
</div>
|
||||
</article>
|
||||
{/yz:arcview}
|
||||
|
||||
<div class="article-detail-back">
|
||||
<a href="/news" class="btn primary-btn-outline">
|
||||
<i class="lni lni-arrow-left"></i> 返回新闻中心
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!--====== 文章详情 结束 ======-->
|
||||
|
||||
<!-- 页脚区域 开始 -->
|
||||
<footer class="footer-area footer-eleven">
|
||||
<div class="footer-top">
|
||||
<div class="container">
|
||||
<div class="inner-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget f-about">
|
||||
<div class="logo">
|
||||
<a href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" />
|
||||
</a>
|
||||
</div>
|
||||
<p>{yz:description}</p>
|
||||
<p class="copyright-text">
|
||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>解决方案</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">市场营销</a></li>
|
||||
<li><a href="javascript:void(0)">数据分析</a></li>
|
||||
<li><a href="javascript:void(0)">电子商务</a></li>
|
||||
<li><a href="javascript:void(0)">商业洞察</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>技术支持</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">价格方案</a></li>
|
||||
<li><a href="javascript:void(0)">开发文档</a></li>
|
||||
<li><a href="javascript:void(0)">使用指南</a></li>
|
||||
<li><a href="javascript:void(0)">API 状态</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget newsletter">
|
||||
<h5>订阅我们</h5>
|
||||
<p>订阅我们,获取最新资讯</p>
|
||||
<form action="#" method="get" target="_blank" class="newsletter-form">
|
||||
<input name="EMAIL" placeholder="邮箱地址" required="required" type="email" />
|
||||
<div class="button">
|
||||
<button class="sub-btn">
|
||||
<i class="lni lni-envelope"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- 页脚区域 结束 -->
|
||||
|
||||
<a href="#" class="scroll-top btn-hover">
|
||||
<i class="lni lni-chevron-up"></i>
|
||||
</a>
|
||||
|
||||
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/glightbox.min.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/tiny-slider.js"></script>
|
||||
<script>
|
||||
let navbarTogglerNine = document.querySelector(".navbar-nine .navbar-toggler");
|
||||
navbarTogglerNine.addEventListener("click", function () {
|
||||
navbarTogglerNine.classList.toggle("active");
|
||||
});
|
||||
|
||||
let sidebarLeft = document.querySelector(".sidebar-left");
|
||||
let overlayLeft = document.querySelector(".overlay-left");
|
||||
let sidebarClose = document.querySelector(".sidebar-close .close");
|
||||
|
||||
overlayLeft.addEventListener("click", function () {
|
||||
sidebarLeft.classList.toggle("open");
|
||||
overlayLeft.classList.toggle("open");
|
||||
});
|
||||
sidebarClose.addEventListener("click", function () {
|
||||
sidebarLeft.classList.remove("open");
|
||||
overlayLeft.classList.remove("open");
|
||||
});
|
||||
|
||||
let sideMenuLeftNine = document.querySelector(".navbar-nine .menu-bar");
|
||||
sideMenuLeftNine.addEventListener("click", function () {
|
||||
sidebarLeft.classList.add("open");
|
||||
overlayLeft.classList.add("open");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="description" content="{yz:description}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>{yz:sitename}</title>
|
||||
<link rel="shortcut icon" href="{yz:ico}" type="image/svg" />
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="assets/css/lineicons.css" />
|
||||
<link rel="stylesheet" href="assets/css/tiny-slider.css" />
|
||||
<link rel="stylesheet" href="assets/css/glightbox.min.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section class="navbar-area navbar-nine">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{yz:logo}" alt="{yz:sitename}" />
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNine"
|
||||
aria-controls="navbarNine" aria-expanded="false" aria-label="切换导航">
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
<span class="toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse sub-menu-bar" id="navbarNine">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a href="/">主页</a></li>
|
||||
{yz:nav}
|
||||
<li class="nav-item">
|
||||
<a href="[field:url/]">[field:name/]</a>
|
||||
[field:children/]
|
||||
</li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-btn d-none d-lg-inline-block">
|
||||
<a class="menu-bar" href="#side-menu-left"><i class="lni lni-menu"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="sidebar-left">
|
||||
<div class="sidebar-close"><a class="close" href="#close"><i class="lni lni-close"></i></a></div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" /></a></div>
|
||||
<p class="text">{yz:description}</p>
|
||||
<div class="sidebar-menu">
|
||||
<h5 class="menu-title">快速链接</h5>
|
||||
<ul>
|
||||
{yz:nav}
|
||||
<li><a href="[field:url/]">[field:name/]</a></li>
|
||||
{/yz:nav}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar-social align-items-center justify-content-center">
|
||||
<h5 class="social-title">关注我们</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-facebook-filled"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-twitter-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-linkedin-original"></i></a></li>
|
||||
<li><a href="javascript:void(0)"><i class="lni lni-youtube"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-left"></div>
|
||||
|
||||
<section class="article-detail-area page-inner-page">
|
||||
<div class="article-detail-breadcrumb-wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb article-detail-breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">主页</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{yz:onepage field="title"/}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10 col-xl-9">
|
||||
<article class="article-detail-card">
|
||||
<h1 class="article-detail-title">{yz:onepage field="title"/}</h1>
|
||||
<div class="onepage-content">
|
||||
{yz:onepage/}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer-area footer-eleven">
|
||||
<div class="footer-top">
|
||||
<div class="container">
|
||||
<div class="inner-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget f-about">
|
||||
<div class="logo"><a href="/"><img src="{yz:logo}" alt="{yz:sitename}" class="img-fluid" /></a></div>
|
||||
<p>{yz:description}</p>
|
||||
<p class="copyright-text">
|
||||
<span>备案号:{yz:icp}<br>{yz:copyright} <a href="www.yunzer.cn" rel="nofollow"> 云泽网 </a></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>解决方案</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">市场营销</a></li>
|
||||
<li><a href="javascript:void(0)">数据分析</a></li>
|
||||
<li><a href="javascript:void(0)">电子商务</a></li>
|
||||
<li><a href="javascript:void(0)">商业洞察</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-12">
|
||||
<div class="footer-widget f-link">
|
||||
<h5>技术支持</h5>
|
||||
<ul>
|
||||
<li><a href="javascript:void(0)">价格方案</a></li>
|
||||
<li><a href="javascript:void(0)">开发文档</a></li>
|
||||
<li><a href="javascript:void(0)">使用指南</a></li>
|
||||
<li><a href="javascript:void(0)">API 状态</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 col-12">
|
||||
<div class="footer-widget newsletter">
|
||||
<h5>订阅我们</h5>
|
||||
<p>订阅我们,获取最新资讯</p>
|
||||
<form action="#" method="get" target="_blank" class="newsletter-form">
|
||||
<input name="EMAIL" placeholder="邮箱地址" required="required" type="email" />
|
||||
<div class="button"><button class="sub-btn"><i class="lni lni-envelope"></i></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<a href="#" class="scroll-top btn-hover"><i class="lni lni-chevron-up"></i></a>
|
||||
|
||||
<script src="assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/glightbox.min.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/tiny-slider.js"></script>
|
||||
<script>
|
||||
document.querySelector(".navbar-nine .navbar-toggler").addEventListener("click", function () {
|
||||
this.classList.toggle("active");
|
||||
});
|
||||
var sidebarLeft = document.querySelector(".sidebar-left");
|
||||
var overlayLeft = document.querySelector(".overlay-left");
|
||||
overlayLeft.addEventListener("click", function () {
|
||||
sidebarLeft.classList.toggle("open");
|
||||
overlayLeft.classList.toggle("open");
|
||||
});
|
||||
document.querySelector(".sidebar-close .close").addEventListener("click", function () {
|
||||
sidebarLeft.classList.remove("open");
|
||||
overlayLeft.classList.remove("open");
|
||||
});
|
||||
document.querySelector(".navbar-nine .menu-bar").addEventListener("click", function () {
|
||||
sidebarLeft.classList.add("open");
|
||||
overlayLeft.classList.add("open");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1478,6 +1478,167 @@ p {
|
||||
|
||||
|
||||
|
||||
/*===== news-center-area =====*/
|
||||
.news-center-area {
|
||||
background-color: var(--light-3);
|
||||
padding-top: 100px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
@media only screen and (min-width: 768px) and (max-width: 991px) {
|
||||
.news-center-area {
|
||||
padding-top: 80px;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.news-center-area {
|
||||
padding-top: 60px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
}
|
||||
.news-center-area .news-center-tabs {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.news-center-area .news-center-tabs .nav-tabs {
|
||||
border: none;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.news-center-area .news-center-tabs .nav-link {
|
||||
border: none;
|
||||
color: var(--dark-1);
|
||||
font-weight: 600;
|
||||
background-color: var(--white);
|
||||
padding: 10px 22px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.news-center-area .news-center-tabs .nav-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
.news-center-area .news-center-tabs .nav-link.active {
|
||||
background-color: var(--primary);
|
||||
color: var(--white);
|
||||
}
|
||||
.news-center-area .news-center-item {
|
||||
margin-top: 30px;
|
||||
background: var(--white);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
.news-center-area .news-center-item .image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 160px;
|
||||
background: var(--light-2);
|
||||
}
|
||||
.news-center-area .news-center-item .image img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
.news-center-area .news-center-item .image .meta-details {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
background-color: var(--primary);
|
||||
color: var(--white);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.news-center-area .news-center-item .content-body {
|
||||
padding: 18px 16px 20px;
|
||||
}
|
||||
.news-center-area .news-center-item .content-body .title {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.news-center-area .news-center-item .content-body .title a {
|
||||
color: var(--black);
|
||||
}
|
||||
.news-center-area .news-center-item .content-body .title a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
.news-center-area .news-center-item .content-body p {
|
||||
color: var(--dark-3);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.news-center-area .news-center-item:hover .image .thumb {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.news-center-area .news-center-more {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/*===== news-list-page =====*/
|
||||
.news-list-page {
|
||||
padding-top: 0;
|
||||
}
|
||||
.news-list-page .section-title-five {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.news-list-page .news-item-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 16px;
|
||||
margin-top: 12px;
|
||||
color: var(--gray-1);
|
||||
font-size: 12px;
|
||||
}
|
||||
.news-list-page .news-item-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.news-list-page .news-item-meta i {
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.news-list-pager {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.news-list-pager .yz-pagination {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.news-list-pager .yz-page {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--gray-4);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--dark-2);
|
||||
background: var(--white);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.news-list-pager .yz-page:hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.news-list-pager .yz-page.current {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/*===== latest-news-area =====*/
|
||||
.latest-news-area {
|
||||
background: var(--white);
|
||||
@@ -1905,3 +2066,400 @@ p {
|
||||
color: var(--white);
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/*===== article-detail-area =====*/
|
||||
.article-detail-area {
|
||||
padding-bottom: 80px;
|
||||
background-color: var(--light-3);
|
||||
}
|
||||
.article-detail-breadcrumb-wrap {
|
||||
padding-top: 120px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.article-detail-breadcrumb-wrap {
|
||||
padding-top: 100px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.article-detail-breadcrumb {
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
.article-detail-breadcrumb .breadcrumb-item a {
|
||||
color: var(--dark-3);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.article-detail-breadcrumb .breadcrumb-item a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
.article-detail-breadcrumb .breadcrumb-item.active {
|
||||
color: var(--black);
|
||||
font-weight: 500;
|
||||
max-width: 420px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.article-detail-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--gray-2);
|
||||
}
|
||||
.article-detail-card {
|
||||
background: var(--white);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-2);
|
||||
padding: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.article-detail-card {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
.article-detail-thumb {
|
||||
margin-bottom: 28px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.article-detail-thumb .article-cover {
|
||||
width: 100%;
|
||||
max-height: 420px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.article-detail-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: var(--black);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.article-detail-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.article-detail-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 24px;
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid var(--gray-4);
|
||||
color: var(--dark-3);
|
||||
font-size: 14px;
|
||||
}
|
||||
.article-detail-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.article-detail-meta i {
|
||||
color: var(--primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
.article-detail-content {
|
||||
color: var(--dark-2);
|
||||
font-size: 16px;
|
||||
line-height: 1.85;
|
||||
word-break: break-word;
|
||||
}
|
||||
.article-detail-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.article-detail-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
.article-detail-content h2,
|
||||
.article-detail-content h3,
|
||||
.article-detail-content h4 {
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
margin: 28px 0 14px;
|
||||
}
|
||||
.article-detail-content h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.article-detail-content h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.article-detail-content h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.article-detail-content ul,
|
||||
.article-detail-content ol {
|
||||
margin: 0 0 16px 20px;
|
||||
padding: 0;
|
||||
}
|
||||
.article-detail-content li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.article-detail-content blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 16px 20px;
|
||||
border-left: 4px solid var(--primary);
|
||||
background: var(--primary-light);
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: var(--dark-2);
|
||||
}
|
||||
.article-detail-content a {
|
||||
color: var(--primary);
|
||||
}
|
||||
.article-detail-content a:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.article-detail-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.article-detail-content table th,
|
||||
.article-detail-content table td {
|
||||
border: 1px solid var(--gray-4);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.article-detail-back {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.article-detail-back .btn i {
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/*===== 首页产品与解决方案展示 =====*/
|
||||
.product-showcase-grid {
|
||||
row-gap: 24px;
|
||||
}
|
||||
.product-showcase-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 320px;
|
||||
padding: 36px 32px 30px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-4);
|
||||
border-radius: 16px;
|
||||
background: var(--white);
|
||||
box-shadow: var(--shadow-2);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.product-showcase-card::after {
|
||||
position: absolute;
|
||||
right: -46px;
|
||||
bottom: -58px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
content: "";
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.product-showcase-card:hover {
|
||||
transform: translateY(-8px);
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-5);
|
||||
}
|
||||
.product-showcase-card:hover::after {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
.product-showcase-card--primary {
|
||||
border-color: var(--primary);
|
||||
background: var(--gradient-1);
|
||||
}
|
||||
.product-showcase-card--primary::after {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.product-showcase-icon,
|
||||
.product-showcase-tag,
|
||||
.product-showcase-card h3,
|
||||
.product-showcase-card p,
|
||||
.product-showcase-link {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.product-showcase-icon {
|
||||
display: flex;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 26px;
|
||||
border-radius: 14px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 24px;
|
||||
}
|
||||
.product-showcase-tag {
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.product-showcase-card h3 {
|
||||
margin-bottom: 14px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.product-showcase-card p {
|
||||
margin-bottom: 25px;
|
||||
color: var(--dark-3);
|
||||
line-height: 1.75;
|
||||
}
|
||||
.product-showcase-link,
|
||||
.solution-showcase-link {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
.product-showcase-link i,
|
||||
.solution-showcase-link i {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.product-showcase-link:hover i,
|
||||
.solution-showcase-link:hover i {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
.product-showcase-card--primary .product-showcase-icon {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: var(--white);
|
||||
}
|
||||
.product-showcase-card--primary .product-showcase-tag,
|
||||
.product-showcase-card--primary .product-showcase-link,
|
||||
.product-showcase-card--primary h3,
|
||||
.product-showcase-card--primary p {
|
||||
color: var(--white);
|
||||
}
|
||||
.solution-area {
|
||||
padding: 100px 0;
|
||||
background: var(--light-3);
|
||||
}
|
||||
.solution-showcase-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 38px 34px 32px;
|
||||
border: 1px solid var(--gray-4);
|
||||
border-radius: 16px;
|
||||
background: var(--white);
|
||||
box-shadow: var(--shadow-2);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.solution-showcase-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: var(--shadow-5);
|
||||
}
|
||||
.solution-showcase-card--featured {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.solution-showcase-index {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 28px;
|
||||
color: var(--gray-3);
|
||||
font-size: 42px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
.solution-showcase-icon {
|
||||
display: flex;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
.solution-showcase-card h3 {
|
||||
margin-bottom: 14px;
|
||||
font-size: 23px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.solution-showcase-card p {
|
||||
min-height: 78px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--dark-3);
|
||||
line-height: 1.75;
|
||||
}
|
||||
.solution-showcase-card ul {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.solution-showcase-card li {
|
||||
margin-bottom: 10px;
|
||||
color: var(--dark-2);
|
||||
}
|
||||
.solution-showcase-card li::before {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
color: var(--primary);
|
||||
content: "✓";
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.product-showcase-card,
|
||||
.solution-showcase-card {
|
||||
min-height: 0;
|
||||
padding: 30px 24px;
|
||||
}
|
||||
.solution-area {
|
||||
padding: 70px 0;
|
||||
}
|
||||
.solution-showcase-card p {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-inner-page.about-five,
|
||||
.page-inner-page.contact-section {
|
||||
padding-top: 0;
|
||||
padding-bottom: 80px;
|
||||
background-color: var(--light-3);
|
||||
}
|
||||
.page-inner-page.contact-section {
|
||||
background-color: var(--white);
|
||||
}
|
||||
.onepage-content {
|
||||
color: var(--dark-2);
|
||||
font-size: 16px;
|
||||
line-height: 1.85;
|
||||
word-break: break-word;
|
||||
}
|
||||
.onepage-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.onepage-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
.onepage-content h2,
|
||||
.onepage-content h3,
|
||||
.onepage-content h4 {
|
||||
color: var(--black);
|
||||
font-weight: 700;
|
||||
margin: 28px 0 14px;
|
||||
}
|
||||
.onepage-content a {
|
||||
color: var(--primary);
|
||||
}
|
||||
.onepage-content a:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.about-five-content .onepage-content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.contact-page-content {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -7,18 +7,34 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
|
||||
模板中使用 <b>{yz:标签}</b> 调用数据,循环标签内用 <b>[field:字段/]</b> 输出字段。
|
||||
本页由引擎注册表自动生成,新增标签后无需改文档。
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
||||
模板中使用 <b>{yz:标签}</b> 调用数据:全局标签直接替换为文本;循环标签成对出现,循环体内用
|
||||
<b>[field:字段/]</b> 输出字段。本页由引擎注册表自动生成,新增标签后无需改文档。
|
||||
</el-alert>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<div class="quick-nav" v-if="docs.length">
|
||||
<template v-for="cat in categories" :key="cat.key">
|
||||
<template v-if="grouped[cat.key]?.length">
|
||||
<span class="nav-cat">{{ cat.label }}:</span>
|
||||
<a
|
||||
v-for="tag in grouped[cat.key]"
|
||||
:key="cat.key + tag.name"
|
||||
class="nav-link"
|
||||
:href="'#tag-' + anchor(tag.name)"
|
||||
>{{ tag.name }}</a>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading">
|
||||
<template v-for="cat in categories" :key="cat.key">
|
||||
<div v-if="grouped[cat.key]?.length" class="cat-block">
|
||||
<h3 class="cat-title">{{ cat.label }}</h3>
|
||||
|
||||
<el-card v-for="tag in grouped[cat.key]" :key="tag.name" class="tag-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="tag-head">
|
||||
<div class="tag-head" :id="'tag-' + anchor(tag.name)">
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
<code class="tag-syntax">{{ tag.syntax }}</code>
|
||||
<el-button size="small" text type="primary" @click="copy(tag.syntax)">复制语法</el-button>
|
||||
@@ -27,36 +43,59 @@
|
||||
|
||||
<p class="tag-desc">{{ tag.desc }}</p>
|
||||
|
||||
<!-- 参数:表格化 -->
|
||||
<div v-if="tag.params?.length" class="tag-section">
|
||||
<div class="sec-label">参数</div>
|
||||
<ul>
|
||||
<li v-for="p in tag.params" :key="p"><code>{{ paramName(p) }}</code>{{ paramDesc(p) }}</li>
|
||||
</ul>
|
||||
<div class="sec-label">可用参数</div>
|
||||
<el-table :data="paramRows(tag.params)" size="small" border class="mini-table">
|
||||
<el-table-column prop="name" label="参数" width="140">
|
||||
<template #default="{ row }"><code>{{ row.name }}</code></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="desc" label="说明" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-if="tag.fields?.length" class="tag-section">
|
||||
<div class="sec-label">可用字段</div>
|
||||
<div class="field-tags">
|
||||
<el-tag
|
||||
v-for="f in tag.fields"
|
||||
:key="f"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="field-tag"
|
||||
@click="copy('[field:' + fieldName(f) + '/]')"
|
||||
>
|
||||
[field:{{ fieldName(f) }}/]
|
||||
</el-tag>
|
||||
</div>
|
||||
<ul class="field-desc">
|
||||
<li v-for="f in tag.fields" :key="f"><code>[field:{{ fieldName(f) }}/]</code>{{ fieldDesc(f) }}</li>
|
||||
</ul>
|
||||
<!-- 全局标签:标签清单表格(标签 / 用法 / 数据来源 / 复制) -->
|
||||
<div v-else-if="cat.key === 'global' && tag.fields?.length" class="tag-section">
|
||||
<div class="sec-label">标签清单(点击"复制"后可直接粘贴到模板)</div>
|
||||
<el-table :data="globalRows(tag.fields)" size="small" border class="mini-table">
|
||||
<el-table-column prop="name" label="标签" width="220">
|
||||
<template #default="{ row }">
|
||||
<code class="gtag">{yz:{{ row.name }}}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="desc" label="用法说明" />
|
||||
<el-table-column prop="source" label="数据来源" width="200" />
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text type="primary" @click="copy('{yz:' + row.name + '}')">复制</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 循环/单条标签:字段清单表格(字段 / 说明 / 复制) -->
|
||||
<div v-else-if="tag.fields?.length" class="tag-section">
|
||||
<div class="sec-label">循环体内可用字段</div>
|
||||
<el-table :data="fieldRows(tag.fields)" size="small" border class="mini-table">
|
||||
<el-table-column prop="name" label="字段" width="220">
|
||||
<template #default="{ row }">
|
||||
<code class="gtag">[field:{{ row.name }}/]</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="desc" label="说明" />
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text type="primary" @click="copy('[field:' + row.name + '/]')">复制</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 示例 -->
|
||||
<div v-if="tag.example" class="tag-section">
|
||||
<div class="sec-label">
|
||||
示例
|
||||
<el-button size="small" text type="primary" @click="copy(tag.example)">复制</el-button>
|
||||
<el-button size="small" text type="primary" @click="copy(tag.example)">复制示例</el-button>
|
||||
</div>
|
||||
<pre class="tag-example">{{ tag.example }}</pre>
|
||||
</div>
|
||||
@@ -77,6 +116,7 @@ const docs = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const categories = [
|
||||
{ key: 'guide', label: '入门指南' },
|
||||
{ key: 'global', label: '全局标签' },
|
||||
{ key: 'loop', label: '循环标签' },
|
||||
{ key: 'single', label: '单条内容标签' },
|
||||
@@ -92,17 +132,35 @@ const grouped = computed(() => {
|
||||
return out
|
||||
})
|
||||
|
||||
// "row 输出条数,默认 10" -> 名称与说明拆分
|
||||
const paramName = (p) => (p || '').split(' ')[0]
|
||||
const paramDesc = (p) => {
|
||||
const i = (p || '').indexOf(' ')
|
||||
return i > 0 ? ' — ' + p.slice(i + 1) : ''
|
||||
}
|
||||
const fieldName = (f) => (f || '').split(' ')[0]
|
||||
const fieldDesc = (f) => {
|
||||
const i = (f || '').indexOf(' ')
|
||||
return i > 0 ? ' — ' + f.slice(i + 1) : ''
|
||||
}
|
||||
const anchor = (name) => encodeURIComponent(name || '')
|
||||
|
||||
// "row 输出条数,默认 10" -> { name, desc }
|
||||
const paramRows = (params) =>
|
||||
(params || []).map((p) => {
|
||||
const i = (p || '').indexOf(' ')
|
||||
return i > 0 ? { name: p.slice(0, i), desc: p.slice(i + 1) } : { name: p, desc: '' }
|
||||
})
|
||||
|
||||
// "sitename 站点名称,常用于… 来源:基本信息-站点名称" -> { name, desc, source }
|
||||
const globalRows = (fields) =>
|
||||
(fields || []).map((f) => {
|
||||
const name = (f || '').split(' ')[0] || ''
|
||||
let rest = (f || '').slice(name.length).trim()
|
||||
let source = ''
|
||||
const si = rest.lastIndexOf(' 来源:')
|
||||
if (si > 0) {
|
||||
source = rest.slice(si + 4)
|
||||
rest = rest.slice(0, si)
|
||||
}
|
||||
return { name, desc: rest, source }
|
||||
})
|
||||
|
||||
// "[field:xxx/] 字段" 形式 -> { name, desc }
|
||||
const fieldRows = (fields) =>
|
||||
(fields || []).map((f) => {
|
||||
const i = (f || '').indexOf(' ')
|
||||
return i > 0 ? { name: f.slice(0, i), desc: f.slice(i + 1) } : { name: f, desc: '' }
|
||||
})
|
||||
|
||||
const copy = async (text) => {
|
||||
try {
|
||||
@@ -149,6 +207,28 @@ onMounted(() => {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.quick-nav {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 2;
|
||||
}
|
||||
.nav-cat {
|
||||
color: #909399;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
margin-right: 14px;
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cat-block {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -166,6 +246,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
scroll-margin-top: 70px;
|
||||
}
|
||||
.tag-name {
|
||||
font-weight: 600;
|
||||
@@ -177,14 +258,16 @@ onMounted(() => {
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #d63384;
|
||||
word-break: break-all;
|
||||
}
|
||||
.tag-desc {
|
||||
margin: 0 0 12px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.tag-section {
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.sec-label {
|
||||
font-size: 13px;
|
||||
@@ -195,33 +278,17 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.tag-section ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.tag-section li {
|
||||
margin: 3px 0;
|
||||
}
|
||||
.field-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.field-tag {
|
||||
cursor: pointer;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
}
|
||||
.field-desc code,
|
||||
.tag-section code {
|
||||
.mini-table code {
|
||||
background: #f5f7fa;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.mini-table code.gtag {
|
||||
color: #d63384;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tag-example {
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 清理旧版本自动插入的文章分类(旧代码 cmsEnsureTables 每次请求都会调
|
||||
-- EnsureCmsArticleDefaultCategories,向 yz_cms_article_category 插入
|
||||
-- tid=0 的「文章中心」「新闻中心」,导致刷新页面分类不断增多)。
|
||||
-- 新代码已移除该逻辑,分类完全由后台手动维护。
|
||||
-- 执行前请先备份;如需保留其中一条,请自行注释对应语句。
|
||||
|
||||
DELETE FROM yz_cms_article_category WHERE tid = 0 AND `name` IN ('文章中心', '新闻中心');
|
||||
@@ -0,0 +1,35 @@
|
||||
-- CMS 产品与产品分类表(对应 backend 租户端 apps/cms/product 模块)
|
||||
-- 接口:/backend/productsList、/backend/addProducts、/backend/editProducts/:id、/backend/deleteProducts/:id
|
||||
-- /backend/productsTypesList、/backend/addProductsTypes、/backend/editProductsTypes/:id、/backend/deleteProductsTypes/:id
|
||||
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_product (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
title varchar(100) NOT NULL DEFAULT '' COMMENT '产品名称',
|
||||
thumb varchar(500) NOT NULL DEFAULT '' COMMENT '产品图片',
|
||||
`desc` varchar(500) NOT NULL DEFAULT '' COMMENT '产品描述',
|
||||
content mediumtext COMMENT '产品内容(富文本)',
|
||||
url varchar(500) NOT NULL DEFAULT '' COMMENT '跳转地址',
|
||||
sort int NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用',
|
||||
create_time datetime NOT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
delete_time datetime DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tid_status (tid, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS产品';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_product_category (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
title varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||
pid bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级分类ID,0为顶级',
|
||||
`desc` varchar(500) NOT NULL DEFAULT '' COMMENT '分类描述',
|
||||
sort int NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用',
|
||||
create_time datetime NOT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
delete_time datetime DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tid_pid (tid, pid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS产品分类';
|
||||
@@ -0,0 +1,34 @@
|
||||
-- CMS 解决方案(特色服务)与分类表(对应 backend 租户端 apps/cms/solution 模块)
|
||||
-- 接口:/backend/servicesList、/backend/addServices、/backend/editServices/:id、/backend/deleteServices/:id
|
||||
-- /backend/servicesTypesList、/backend/addServicesTypes、/backend/editServicesTypes/:id、/backend/deleteServicesTypes/:id
|
||||
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_solution (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
title varchar(100) NOT NULL DEFAULT '' COMMENT '名称',
|
||||
thumb varchar(500) NOT NULL DEFAULT '' COMMENT '图片',
|
||||
`desc` varchar(500) NOT NULL DEFAULT '' COMMENT '描述',
|
||||
url varchar(500) NOT NULL DEFAULT '' COMMENT '跳转地址',
|
||||
sort int NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用',
|
||||
create_time datetime NOT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
delete_time datetime DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tid_status (tid, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS解决方案(特色服务)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS yz_cms_solution_category (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
title varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称',
|
||||
pid bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级分类ID,0为顶级',
|
||||
`desc` varchar(500) NOT NULL DEFAULT '' COMMENT '分类描述',
|
||||
sort int NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用',
|
||||
create_time datetime NOT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
delete_time datetime DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tid_pid (tid, pid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS解决方案分类';
|
||||
Reference in New Issue
Block a user