优化企业网站功能

This commit is contained in:
2026-08-20 16:21:07 +08:00
parent 112d60b993
commit 3f0fcb88e7
45 changed files with 8758 additions and 5058 deletions
+54
View File
@@ -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'
})
}
+35 -3
View File
@@ -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: []
}
]
};
+46 -6
View File
@@ -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] // 合并静态和动态子路由
});
@@ -213,8 +213,8 @@
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
import Edit from "./components/edit.vue";
import Preview from "./components/preview.vue";
import Edit from "./index/components/edit.vue";
import Preview from "./index/components/preview.vue";
import { useAuthStore } from "@/stores/auth";
import {
listArticles,
@@ -57,7 +57,7 @@ import {
ArrowRight,
} from "@element-plus/icons-vue";
import { allCategories, deleteCategory } from "@/api/article";
import EditCate from "@/views/apps/cms/articles/components/edit-cate.vue";
import EditCate from "./components/edit.vue";
import CategoryNode from "./components/CategoryNode.vue";
// 颜色预设
+11 -1
View File
@@ -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;
@@ -178,6 +178,7 @@ const getImageUrl = (imagePath) => {
const formData = reactive({
title: "",
desc: "",
content: "",
thumb: "",
url: "",
sort: 0,
@@ -218,6 +219,7 @@ watch(visible, (val) => {
const resetForm = () => {
formData.title = "";
formData.desc = "";
formData.content = "";
formData.thumb = "";
formData.url = "";
formData.sort = 0;
@@ -3,6 +3,12 @@
<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 />
@@ -80,7 +86,8 @@
<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 { Plus, Refresh, Search, Edit, Delete, Menu } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import {
getServiceList,
updateService,
@@ -91,6 +98,8 @@ 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 "";
@@ -243,6 +252,11 @@ const handleSuccess = () => {
fetchList()
}
// 跳转到解决方案分类管理页
const handleTypes = () => {
router.push('/apps/cms/solutions/types')
}
// 初始化
onMounted(() => {
fetchList()
@@ -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 常见现象对照表
-6
View File
@@ -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
}
+12 -2
View File
@@ -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 {
+528
View File
@@ -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()
}
+524
View File
@@ -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()
}
+40 -18
View File
@@ -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
})
}
-41
View File
@@ -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 {
+89
View File
@@ -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
}
+87
View File
@@ -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
}
+77
View File
@@ -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,
})
}
}
+23 -6
View File
@@ -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{}
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
View File
@@ -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",
+108 -6
View File
@@ -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)).
+22
View File
@@ -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")
+196
View File
@@ -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}&nbsp;&nbsp;<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>
+19 -5
View File
@@ -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',
});
});
});
+260
View File
@@ -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}&nbsp;&nbsp;<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>
+113 -398
View File
@@ -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">
<section id="news-center" class="news-center-area">
<div class="section-title-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 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">
{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>
<!-- container -->
</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>
<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>
</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>
<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>
</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>
<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>
</div>
</div>
<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>
+251
View File
@@ -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}&nbsp;&nbsp;<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>
+243
View File
@@ -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}&nbsp;&nbsp;<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>
+189
View File
@@ -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}&nbsp;&nbsp;<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>
+558
View File
@@ -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;
}
+123 -56
View File
@@ -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 anchor = (name) => encodeURIComponent(name || '')
// "row 输出条数,默认 10" -> { name, desc }
const paramRows = (params) =>
(params || []).map((p) => {
const i = (p || '').indexOf(' ')
return i > 0 ? ' — ' + p.slice(i + 1) : ''
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)
}
const fieldName = (f) => (f || '').split(' ')[0]
const fieldDesc = (f) => {
return { name, desc: rest, source }
})
// "[field:xxx/] 字段" 形式 -> { name, desc }
const fieldRows = (fields) =>
(fields || []).map((f) => {
const i = (f || '').indexOf(' ')
return i > 0 ? ' — ' + f.slice(i + 1) : ''
}
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;
+7
View File
@@ -0,0 +1,7 @@
-- 清理旧版本自动插入的文章分类(旧代码 cmsEnsureTables 每次请求都会调
-- EnsureCmsArticleDefaultCategories,向 yz_cms_article_category 插入
-- tid=0 的「文章中心」「新闻中心」,导致刷新页面分类不断增多)。
-- 新代码已移除该逻辑,分类完全由后台手动维护。
-- 执行前请先备份;如需保留其中一条,请自行注释对应语句。
DELETE FROM yz_cms_article_category WHERE tid = 0 AND `name` IN ('文章中心', '新闻中心');
+35
View File
@@ -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产品分类';
+34
View File
@@ -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解决方案分类';