初始化仓库

This commit is contained in:
tianshixing 2025-03-13 09:52:18 +08:00
commit a3e1479bda
150 changed files with 33505 additions and 0 deletions

24
.env Normal file
View File

@ -0,0 +1,24 @@
# 代理接口前缀
VITE_API_PREFIX = '/api'
# 接口地址,可在其它环境配置文件进行覆盖
VITE_API_URL = 'http://localhost:10010'
# 静态资源地址
VITE_RESOURCE_PREFIX = 'http://localhost:10010/resource'
# 站点配置
# - 站点标题
VITE_SITE_TITLE = '天津市南开区民政局'
# - 站点副标题
VITE_SITE_SUB_TITLE = ''
# - 主办单位
VITE_SITE_ORGANIZATION = '天津市南开区民政局'
# - 备案号
VITE_SITE_ICP = '津ICP备2021010010号-3'
# - 地址
VITE_SITE_ADDRESS = '天津市南开区'
# SEO配置
VITE_SEO_KEYWORDS = '天津市南开区民政局'
VITE_SEO_DESCRIPTION = '天津市南开区民政局'

1
.env.development Normal file
View File

@ -0,0 +1 @@
VITE_APP_ENV = 'development'

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
VITE_APP_ENV = 'production'
# 接口地址
VITE_API_URL = 'http://localhost:10010/'

4
.env.staging Normal file
View File

@ -0,0 +1,4 @@
VITE_APP_ENV = 'staging'
# 接口地址
VITE_API_URL = 'http://localhost:10010/'

18
.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'plugin:nuxt/recommended',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 0, // 针对单个单词组件报错的规则关闭
}
}

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea

75
.kit/kit.json Normal file
View File

@ -0,0 +1,75 @@
{
"name": "eva-cms-website-gov",
"label": "eva-cms-website-gov",
"version": "1.0.0.3",
"private": false,
"receivable": false,
"compiler": "static",
"repository": "",
"branch": "",
"supportedDatabases": [],
"presetPlugins": [],
"prices": [
{
"type": "FREE",
"value": 0
}
],
"builds": [
{
"name": "安装项目依赖",
"type": "Markdown",
"content": "执行以下命令安装项目依赖\n```shell\nnpm install --registry https://registry.npmmirror.com\n```",
"contentType": "string"
},
{
"name": "启动项目",
"type": "Markdown",
"content": "待项目依赖安装完成后,执行以下命令即可启动项目\n```shell\nnpm run dev\n```",
"contentType": "string"
}
],
"unbuilds": [],
"variables": [
{
"id": "4DJOSGBHZ22OWW",
"type": "variable",
"name": "title",
"label": "站点标题",
"inputType": "input",
"required": true,
"hidden": false,
"defaultValue": "伊娃CMS政企门户主题",
"compiler": "static",
"remark": ""
},
{
"id": "1YFFECYGK5GGOC",
"type": "variable",
"name": "subTitle",
"label": "站点副标题",
"inputType": "input",
"required": true,
"hidden": false,
"defaultValue": "金镐开源组织研发,免费、开源、轻量、高规范",
"compiler": "static",
"remark": ""
}
],
"translator": {
"output": ".kit/translated",
"filepath": "",
"content": "if (filename === '.env') {\n content = content\n .replace('伊娃CMS政企门户主题', '${title}')\n .replace('金镐开源组织研发,免费、开源、轻量、高规范', '${subTitle}')\n}\nreturn content"
},
"settings": [
{
"path": ".env",
"compiler": "freemarker",
"withoutIfNotExists": false,
"enableExpress": "",
"variables": []
}
],
"introduce": "🇨🇳Eva CMS 政企门户站点主题采用Nuxt3 + Element Plus + Sass实现。前后端分离扩展功能更方便。",
"readme": "<div align=\"center\">\n <img src=\"https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/logo.png\" width=\"120px\" style=\"width:80px;height:80px;\" />\n <h1>伊娃CMS政企门户主题</h1>\n</div>\n\n## 在线演示 & 技术文档\n\n- 演示地址/政企门户主题:[http://gov.eva-cms.goldpankit.com/](http://gov.eva-cms.goldpankit.com/)\n- 演示地址/后台管理:[http://admin.eva-cms.goldpankit.com/](http://admin.eva-cms.goldpankit.com/)\n- 官方文档:[https://www.yuque.com/u21334242/eva-cms](https://www.yuque.com/u21334242/eva-cms)\n\n\n## 金镐开源组织生态项目\n\n| 版本 | 说明 | 开源地址 | 构建地址 |\n|---------------------|-------------------|--------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|\n| eva-cms | 伊娃CMS后端 | [https://gitee.com/goldpankit/eva-cms](https://gitee.com/goldpankit/eva-cms) | [http://goldpankit.com/space/Eva/eva-cms](http://goldpankit.com/space/Eva/eva-cms) |\n| eva-cms-admin-front | 伊娃CMS后台管理前端 | [https://gitee.com/goldpankit/eva-cms-admin-front](https://gitee.com/goldpankit/eva-cms-admin-front) | [http://goldpankit.com/space/Eva/eva-cms-admin-front](http://goldpankit.com/space/Eva/eva-cms-admin-front) |\n| eva-cms-website-gov | 伊娃CMS政企门户主题站点前端 | [https://gitee.com/goldpankit/eva-cms-website-gov](https://gitee.com/goldpankit/eva-cms-website-gov) | [http://goldpankit.com/space/Eva/eva-cms-website-gov](http://goldpankit.com/space/Eva/eva-cms-website-gov) |\n| eva-server | 伊娃权限系统单工程版本 | [https://gitee.com/goldpankit/eva-server](https://gitee.com/goldpankit/eva-server) | [http://goldpankit.com/space/Eva/eva-server](http://goldpankit.com/space/Eva/eva-server) |\n| eva-server-modules | 伊娃权限系统Maven多模块版本 | [https://gitee.com/goldpankit/eva-server-modules](https://gitee.com/goldpankit/eva-server-modules) | [http://www.goldpankit.com/space/Eva/eva-server-modules](http://www.goldpankit.com/space/Eva/eva-server-modules) |\n| eva-vue2 | 伊娃权限系统前端vue2版本 | [https://gitee.com/goldpankit/eva-vue/tree/vue2/](https://gitee.com/goldpankit/eva-vue/tree/vue2/) | [http://goldpankit.com/space/Eva/eva-vue2](http://goldpankit.com/space/Eva/eva-vue2) |\n| eva-vue3-options | 伊娃权限系统前端vue3选项式版本 | [https://gitee.com/goldpankit/eva-vue/tree/vue3-options/](https://gitee.com/goldpankit/eva-vue/tree/vue3-options/) | [http://www.goldpankit.com/space/Eva/eva-vue3-options](http://www.goldpankit.com/space/Eva/eva-vue3-options) |\n\n\n## 项目特点\n\n1. 可扩展的功能模块默认情况下提供了文章管理、栏目管理、资源管理、模板管理等CMS系统的基础模块以及用户管理、角色管理、菜单管理等权限系统基础模块使用GoldPanKit可进一步进行源码级功能模块的扩展。\n2. 不用担心存在BUG如果存在BUG使用GoldPanKit可实现一键升级。\n3. 不用担心存在安全漏洞如果存在安全漏洞GoldPanKit会进行提醒并支持一键升级。\n4. 规范化代码 + 详细的代码注释。\n5. 基于Eva 4权限系统进行研发安全稳定\n6. 丰富的插件市场可使用GoldPanKit进行单表、多表的页面生成。\n\n## 绝对优势\n\n结合GoldPanKit可实现代码直接生成到项目中安装更多的功能模块一键修复BUG等。\n\n## 项目预览\n\n**登录页**\n![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/1.png)\n\n**文章管理**\n![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/2.png)\n\n**栏目管理**\n![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/3.png)\n\n**资源管理**\n![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/4.png)\n![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/5.png)\n\n**模板管理**\n![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/6.png)\n"
}

24
.kit/translated/.env Normal file
View File

@ -0,0 +1,24 @@
# 代理接口前缀
VITE_API_PREFIX = '/api'
# 接口地址,可在其它环境配置文件进行覆盖
VITE_API_URL = 'http://localhost:10020'
# 静态资源地址
VITE_RESOURCE_PREFIX = '/resource'
# 站点配置
# - 站点标题
VITE_SITE_TITLE = '${title}'
# - 站点副标题
VITE_SITE_SUB_TITLE = '${subTitle}'
# - 主办单位
VITE_SITE_ORGANIZATION = '金镐开源组织'
# - 备案号
VITE_SITE_ICP = '湘ICP备2021010010号-3'
# - 地址
VITE_SITE_ADDRESS = '湖南省郴州市桂阳县'
# SEO配置
VITE_SEO_KEYWORDS = '伊娃CMS,轻量级CMS系统,政府门户主题'
VITE_SEO_DESCRIPTION = '伊娃CMS政府门户主题基于Nuxt3、Element Plus、SpringBoot、MyBatisPlus、MySQL等主流技术栈构建轻量、简洁、质量。'

View File

@ -0,0 +1 @@
VITE_APP_ENV = 'development'

View File

@ -0,0 +1,4 @@
VITE_APP_ENV = 'production'
# 接口地址
VITE_API_URL = 'http://localhost:10020/'

View File

@ -0,0 +1,4 @@
VITE_APP_ENV = 'staging'
# 接口地址
VITE_API_URL = 'http://localhost:10020/'

View File

@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'plugin:nuxt/recommended',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 0, // 针对单个单词组件报错的规则关闭
}
}

19
.kit/translated/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea

View File

@ -0,0 +1,5 @@
# 规范代码格式时的忽略文件
.*
*.json
*.md
node_modules/**

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

21
.kit/translated/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 金镐开源组织
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
.kit/translated/README.md Normal file
View File

@ -0,0 +1,55 @@
<div align="center">
<img src="https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/logo.png" width="120px" style="width:80px;height:80px;" />
<h1>伊娃CMS政企门户主题</h1>
</div>
## 在线演示 & 技术文档
- 演示地址/政企门户主题:[http://gov.eva-cms.goldpankit.com/](http://gov.eva-cms.goldpankit.com/)
- 演示地址/后台管理:[http://admin.eva-cms.goldpankit.com/](http://admin.eva-cms.goldpankit.com/)
- 官方文档:[https://www.yuque.com/u21334242/eva-cms](https://www.yuque.com/u21334242/eva-cms)
## 金镐开源组织生态项目
| 版本 | 说明 | 开源地址 | 构建地址 |
|---------------------|-------------------|--------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| eva-cms | 伊娃CMS后端 | [https://gitee.com/goldpankit/eva-cms](https://gitee.com/goldpankit/eva-cms) | [http://goldpankit.com/space/Eva/eva-cms](http://goldpankit.com/space/Eva/eva-cms) |
| eva-cms-admin-front | 伊娃CMS后台管理前端 | [https://gitee.com/goldpankit/eva-cms-admin-front](https://gitee.com/goldpankit/eva-cms-admin-front) | [http://goldpankit.com/space/Eva/eva-cms-admin-front](http://goldpankit.com/space/Eva/eva-cms-admin-front) |
| eva-cms-website-gov | 伊娃CMS政企门户主题站点前端 | [https://gitee.com/goldpankit/eva-cms-website-gov](https://gitee.com/goldpankit/eva-cms-website-gov) | [http://goldpankit.com/space/Eva/eva-cms-website-gov](http://goldpankit.com/space/Eva/eva-cms-website-gov) |
| eva-server | 伊娃权限系统单工程版本 | [https://gitee.com/goldpankit/eva-server](https://gitee.com/goldpankit/eva-server) | [http://goldpankit.com/space/Eva/eva-server](http://goldpankit.com/space/Eva/eva-server) |
| eva-server-modules | 伊娃权限系统Maven多模块版本 | [https://gitee.com/goldpankit/eva-server-modules](https://gitee.com/goldpankit/eva-server-modules) | [http://www.goldpankit.com/space/Eva/eva-server-modules](http://www.goldpankit.com/space/Eva/eva-server-modules) |
| eva-vue2 | 伊娃权限系统前端vue2版本 | [https://gitee.com/goldpankit/eva-vue/tree/vue2/](https://gitee.com/goldpankit/eva-vue/tree/vue2/) | [http://goldpankit.com/space/Eva/eva-vue2](http://goldpankit.com/space/Eva/eva-vue2) |
| eva-vue3-options | 伊娃权限系统前端vue3选项式版本 | [https://gitee.com/goldpankit/eva-vue/tree/vue3-options/](https://gitee.com/goldpankit/eva-vue/tree/vue3-options/) | [http://www.goldpankit.com/space/Eva/eva-vue3-options](http://www.goldpankit.com/space/Eva/eva-vue3-options) |
## 项目特点
1. 可扩展的功能模块默认情况下提供了文章管理、栏目管理、资源管理、模板管理等CMS系统的基础模块以及用户管理、角色管理、菜单管理等权限系统基础模块使用GoldPanKit可进一步进行源码级功能模块的扩展。
2. 不用担心存在BUG如果存在BUG使用GoldPanKit可实现一键升级。
3. 不用担心存在安全漏洞如果存在安全漏洞GoldPanKit会进行提醒并支持一键升级。
4. 规范化代码 + 详细的代码注释。
5. 基于Eva 4权限系统进行研发安全稳定
6. 丰富的插件市场可使用GoldPanKit进行单表、多表的页面生成。
## 绝对优势
结合GoldPanKit可实现代码直接生成到项目中安装更多的功能模块一键修复BUG等。
## 项目预览
**登录页**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/1.png)
**文章管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/2.png)
**栏目管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/3.png)
**资源管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/4.png)
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/5.png)
**模板管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/6.png)

View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取客户端配置
export function fetchConfig () {
return request.get('/client/config')
}

View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取文章分页数据
export function fetchArticlePage (data) {
return request.post('/article/profile/page', data)
}

View File

@ -0,0 +1,11 @@
import request from '@/utils/request.js'
// 获取栏目树
export function fetchCategoryTree () {
return request.post('/category/tree')
}
// 获取子栏目树
export function fetchSubCategoryTree (parentCategoryUid) {
return request.post(`/category/${parentCategoryUid}/tree`)
}

View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取资源列表
export function fetchResources (groupUid) {
return request.post(`/resource/${groupUid}/list`)
}

View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 站内搜索
export function search (data) {
return request.post('/search', data)
}

View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取模板数据
export function fetchTemplateData (data) {
return request.post('/template/data', data)
}

5
.kit/translated/app.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

View File

@ -0,0 +1,64 @@
html body {
padding: 0;
margin: 0;
background: url("/public/images/background.jpg") repeat;
background-color: var(--page-background);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
min-width: var(--body-min-width);
.content-wrap {
width: var(--page-width);
margin: 0 auto;
box-sizing: border-box;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
a {
text-decoration: none;
&:link {
color: var(--font-color);
}
}
// 加载动画
#nprogress {
--loading-color: red;
// 改变进度条的颜色
.bar {
background: var(--loading-color) !important;
}
// 改变进度条的阴影颜色
.peg {
box-shadow: 0 0 10px var(--loading-color), 0 0 5px var(--loading-color) !important; /* 这里设置阴影颜色 */
}
// 改变进度条的旋转动画颜色
.spinner-icon {
border-top-color: var(--loading-color) !important;
border-left-color: var(--loading-color) !important;
}
}
}
// 菜单
.el-menu {
--el-menu-bg-color: var(--primary-color-dark);
--el-menu-text-color: var(--color-white);
--el-menu-hover-bg-color: var(--primary-color-dark);
--el-bg-color-overlay: var(--primary-color-dark);
--el-menu-item-font-size: var(--font-size-large);
--el-border-color-light: var(--primary-color-dark);
}
// 页面标题
.page-title {
--font-size-title: 25px;
color: var(--primary-color-dark);
font-size: var(--font-size-title);
margin: 0;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
font-weight: bold;
}

View File

@ -0,0 +1,34 @@
body {
// 页面
--page-width: 1280px;
--page-background: #f7f7f7;
// - body最小宽度避免屏幕过窄时页面溢出
--body-min-width: calc(var(--page-width) + 60px);
// 主题色
--primary-color: #224b9d;
--primary-color-light: #285dc4;
--primary-color-light-deep: #e5ebff;
--primary-color-dark: #224da2;
--primary-color-dark-deep: #16336b;
// 字体
--font-size-base: 16px;
--font-size-small: 14px;
--font-size-middle: 18px;
--font-size-large: 20px;
--font-family-base: "微软雅黑";
--font-color: #333;
// 背景色
--background-color: #f7f7f7;
// 颜色
--color-white: #ffffff;
--color-gray: #999;
--color-gray-light: #ccc;
--color-keyword: #ff0000;
// 边框
--border-color: #eee;
// 子栏目树
--sub-category-width: 275px;
// 覆盖element-plus的默认字体大小
--el-font-size-base: var(--font-size-base);
}

View File

@ -0,0 +1,95 @@
<template>
<ul v-if="articles.length > 0" class="article-list">
<li v-for="article in articles" :key="article.uid">
<nuxt-link :to="`/article/${article.uid}`" target="_blank">
<span class="title" v-html="getTitle(article)"></span>
<span class="date">{{ article.updatedAt }}</span>
</nuxt-link>
</li>
</ul>
<EmptyTip v-else/>
</template>
<script>
import EmptyTip from '@/components/common/EmptyTip'
export default {
name: 'ArticleList',
components: { EmptyTip },
props: {
//
articles: {
type: Array,
required: true
},
//
keyword: {
type: String
}
},
methods: {
//
getTitle (article) {
// em
if (this.keyword != null && this.keyword.trim().length > 0) {
return article.title.replace(new RegExp(this.keyword, 'g'), `<em>${this.keyword}</em>`)
}
return article.title
}
}
}
</script>
<style scoped lang="scss">
ul.article-list {
padding: 10px 0 15px;
li {
border-bottom: 1px dashed var(--color-gray-light);
&:last-of-type {
border-bottom: 0;
}
}
li a {
position: relative;
display: flex;
justify-content: space-between;
padding: 15px 10px 15px 25px;
color: var(--font-color);
text-decoration: none;
&:hover {
color: var(--primary-color);
}
//
&::before {
content: '';
width: 5px;
height: 5px;
background-color: var(--color-gray);
border-radius: 50%;
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
}
//
.title {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 20px;
//
:deep(em) {
font-style: normal;
color: var(--color-keyword);
}
}
//
.date {
flex-shrink: 0;
color: var(--color-gray);
font-size: var(--font-size-small);
}
}
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<div class="article-preview">
<!-- 标题 -->
<h2>{{ data.title }}</h2>
<!-- 信息 -->
<ul class="article-information">
<li>
<label>发布时间</label>
{{ data.updatedAt || data.createdAt }}
</li>
</ul>
<!-- 内容 -->
<article v-html="data.content"></article>
</div>
</template>
<script lang="ts">
export default {
name: 'ArticlePreview',
props: {
data: {
required: true
}
}
}
</script>
<style scoped lang="scss">
.article-preview {
--article-title: 30px;
--article-content-font-size: 16px;
--article-content-font-color: #555;
//
h2 {
text-align: center;
color: var(--primary-color);
font-size: var(--article-title);
line-height: 50px;
font-weight: bold;
padding: 0 20px;
word-break: break-all;
}
//
.article-information {
color: var(--color-gray);
line-height: 36px;
border-bottom: 1px solid var(--border-color);
padding: 0 20px;
}
//
article {
padding: 0 30px;
overflow: hidden;
font-size: var(--article-content-font-size);
color: var(--article-content-font-color);
line-height: 1.8;
:deep(img) {
max-width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<!-- 网络图标 -->
<img
v-if="isNetworkIcon"
class="category-icon image-icon"
:src="value"
alt="栏目图标"
:style="{ width: sizeWidth }"
/>
<!-- class图标 -->
<i
v-else-if="isClassIcon"
class="category-icon"
:class="value"
:style="{ 'font-size': size }"
></i>
<!-- 不正确的图标 -->
<span class="category-icon holder" v-else-if="withHolder"><em>x</em></span>
</template>
<script>
export default {
name: 'CategoryIcon',
props: {
//
value: {},
//
size: {
default: 'inherit'
},
//
withHolder: {
default: true
}
},
computed: {
// sizewidth
sizeWidth () {
if (this.size === 'inherit') {
return '16px'
}
return this.size
},
// class
isClassIcon () {
if (this.value == null || this.value === '') {
return false
}
return !this.value.startsWith('/') &&
!this.value.startsWith('http://') && !this.value.startsWith('https://')
},
//
isNetworkIcon () {
if (this.value == null || this.value === '') {
return false
}
return this.value.startsWith('/') ||
this.value.startsWith('http://') || this.value.startsWith('https://')
}
}
}
</script>
<style scoped lang="scss">
.holder {
width: 16px !important;
height: 16px !important;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 12px;
color: red !important;
background: #e0e0e0;
em {
position: relative;
top: -1px;
font-style: normal;
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="category-tree">
<el-tree
:data="data"
node-key="uid"
:props="{
children: 'children',
label: 'title'
}"
:current-node-key="selected"
:highlight-current="true"
:default-expand-all="true"
:expand-on-click-node="false"
>
<template #default="{ data }">
<nuxt-link :to="data.uri">{{ data.title }}</nuxt-link>
</template>
</el-tree>
</div>
</template>
<script>
export default {
name: 'CategoryTree',
props: {
//
data : {},
//
selected: {}
},
data() {
return {
}
},
methods: {
}
}
</script>
<style scoped lang="scss">
.category-tree {
height: 800px;
background-color: var(--background-color);
padding: 15px;
box-sizing: border-box;
:deep(.el-tree) {
--el-tree-node-content-height: auto;
background-color: var(--background-color);
.el-tree-node__content {
border-radius: 8px;
a {
height: 100%;
min-height: 50px;
flex-grow: 1;
display: flex;
align-items: center;
padding: 10px;
box-sizing: border-box;
text-wrap: initial;
}
.el-tree-node__expand-icon {
display: none;
}
}
//
.is-current {
& > .el-tree-node__content {
background-color: var(--primary-color-light);
color: var(--color-white);
a {
color: var(--color-white) !important;
text-decoration: none;
}
}
}
a {
color: var(--font-color) !important;
text-decoration: none;
}
}
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<div class="special-column">
<div class="title-wrap">
<label>专题专栏</label>
<!-- 箭头 -->
<span/>
</div>
<div class="swiper-wrap">
<swiper
class="swiper-container"
ref="swiper"
:speed="500"
:slidesPerView="4"
:loop="true"
:autoplay="{
delay: 1500,
disableOnInteraction: false //
}"
:modules="modules"
@mouseenter="$refs.swiper.$el.swiper.autoplay.stop()"
@mouseleave="$refs.swiper.$el.swiper.autoplay.start()"
>
<swiper-slide v-for="(item, index) in getIcons" :key="index">
<nuxt-link target="_blank" :to="JSON.parse(item.value).link">
<img
:src="getImageURL(JSON.parse(item.value).image)"
:alt="item.title"
/>
</nuxt-link>
</swiper-slide>
</swiper>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay } from 'swiper/modules'
import 'swiper/scss'
import 'swiper/scss/free-mode'
import { getImageURL } from '@/utils/util'
export default {
name: 'SpecialColumn',
components: { Swiper, SwiperSlide },
props: {
icons: {
default: () => {
return []
}
}
},
computed: {
//
getIcons () {
if (this.icons.length < 5 || this.icons.length > 7) {
return this.icons
}
return [...this.icons, ...this.icons]
}
},
data() {
return {
modules: []
}
},
methods: {
getImageURL
},
created () {
this.modules = [Autoplay]
}
}
</script>
<style lang="scss" scoped>
.special-column {
display: flex;
//
.title-wrap {
height: 100px;
width: 40px;
margin: auto 20px auto 0;
padding: 0 14px;
background: linear-gradient(90deg, var(--primary-color-light) 0%, var(--primary-color) 100%);
border-radius: 10px;
text-align: center;
display: flex;
align-items: center;
position: relative;
//
& > span {
position: absolute;
right: -12px;
top: calc(50% - 10px);
width: 0;
height: 0;
border-bottom:10px solid transparent;
border-top: 10px solid transparent;
border-left: 12px solid var(--primary-color);
}
//
label {
display: block;
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
}
//
.swiper-wrap {
width: calc(100% - 80px);
height: 100px;
flex-grow: 1;
overflow: hidden;
//
.swiper-slide {
// swiperwidth
max-width: 25%;
height: 100%;
overflow: hidden;
padding: 0 10px;
box-sizing: border-box;
&:first-of-type {
padding-left: 0;
}
&:last-of-type {
padding-right: 0;
}
& > a {
display: block;
height: 100px;
cursor: pointer;
img {
border-radius: 20px;
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div class="data-list-tabs">
<!-- 页签 -->
<div class="tabs-header">
<ul>
<li
v-for="(tab, index) in data"
:key="tab.label"
:class="{ hover: activeIndex === index }"
@mouseover="trigger === 'hover' ? changeTab(index) : null"
@click="trigger === 'click' ? changeTab(index) : null"
>
{{ tab.label }}
</li>
</ul>
<nuxt-link
v-if="withMore && currentTab.moreLink != null"
:to="currentTab.moreLink"
>
更多<el-icon><ElIconDArrowRight /></el-icon>
</nuxt-link>
</div>
<!-- 搜索表单 -->
<slot name="search"></slot>
<!-- 数据列表 -->
<slot v-if="items.length > 0" :items="items" :tab="data[activeIndex]">
<ArticleList :articles="items"/>
</slot>
<!-- 暂无数据 -->
<EmptyTip v-else/>
</div>
</template>
<script>
import EmptyTip from "~/components/common/EmptyTip.vue";
import ArticleList from "~/components/cms/ArticleList.vue";
export default {
name: 'DataListTabs',
components: {ArticleList, EmptyTip},
emits: ['changeTab'],
props: {
// [{ label: '', items: [ { title: '', updateTime: '' } ] }]
data: {
default: () => {
return []
}
},
//
withMore: {
default: true
},
//
trigger: {
default: 'hover'
}
},
computed: {
// tab
currentTab () {
return this.data[this.activeIndex]
},
//
items () {
if (this.currentTab == null) {
return
}
return this.currentTab.items
}
},
methods: {
/**
* 切换tab
* @param index 坐标
*/
changeTab (index) {
this.activeIndex = index
this.$emit('changeTab', index)
}
},
data() {
return {
// tab
activeIndex: 0
}
}
}
</script>
<style scoped lang="scss">
.data-list-tabs {
width: 100%;
min-height: 200px;
overflow: hidden;
//
.tabs-header {
position: relative;
border-bottom: 1px solid #eee;
color: var(--primary-color);
ul {
width: 100%;
display: flex;
padding-right: 100px;
overflow: hidden;
box-sizing: border-box;
li {
padding: 8px 15px;
cursor: default;
font-size: var(--font-size-middle);
flex-shrink: 0;
&.hover {
font-weight: bold;
border-bottom: 3px solid var(--primary-color);
}
}
}
//
a {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: var(--font-size-small);
display: flex;
align-items: center;
color: var(--primary-color) !important;
text-decoration: none;
}
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<el-empty
:image-size="imageSize"
:description="description"
:class="{ 'el-empty__no-text': !description }"
>
<slot></slot>
</el-empty>
</template>
<script>
export default {
name: 'EmptyTip',
props: {
// el-empty image-size
imageSize: {
default: 55
},
// el-empty description
description: {
default: '暂无数据'
}
}
}
</script>
<style scoped lang="scss">
.el-empty.el-empty__no-text {
:deep(.el-empty__description) {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="page-breadcrumb">
<span>当前所在位置</span>
<ul>
<li v-for="(item,index) in data" :key="item.uid">
<nuxt-link v-if="item.uri != null && item.uri !== ''" :to="item.uri">
{{ item.title }}
</nuxt-link>
<span v-else>{{ item.title }}</span>
<el-icon v-if="index < data.length - 1"><ArrowRight /></el-icon>
</li>
</ul>
</div>
</template>
<script>
import { ArrowRight } from '@element-plus/icons-vue'
export default {
name: 'PageBreadCrumb',
components: { ArrowRight },
props: {
data: {
required: true
}
}
}
</script>
<style scoped lang="scss">
.page-breadcrumb {
background-color: #fff;
height: 50px;
line-height: 40px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
color: var(--color-gray);
ul {
display: flex;
align-items: center;
li {
display: flex;
align-items: center;
color: var(--font-color);
a {
color: var(--primary-color);
}
.el-icon {
margin: 0 10px;
}
}
}
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div v-if="pageCount > 1" class="pagination">
<div class="pagination-content">
<!-- 总数 -->
<span class="pagination-info"> <em>{{ modelValue.total }}</em> </span>
<ul>
<!-- 上一页 -->
<li :class="{ disabled: modelValue.page <= 1 }">
<el-icon v-if="modelValue.page <= 1"><ArrowLeft/></el-icon>
<nuxt-link v-else :to="getPageUrl(modelValue.page - 1)">
<el-icon><ArrowLeft/></el-icon>
</nuxt-link>
</li>
<!-- 前置分页 -->
<li
v-for="pageIndex in prevPages"
:key="pageIndex"
:class="{ selected: pageIndex === modelValue.page }"
>
<nuxt-link :to="getPageUrl(pageIndex)">{{ pageIndex }}</nuxt-link>
</li>
<!-- 前置翻页器存在中间页码 || 完全展示了后置页码 -->
<li v-if="centerPages.length > 0 || afterPages.length === limitPageCount" class="page-control">
<nuxt-link :to="getPageUrl(modelValue.page-limitPageCount < 1 ? 1 : modelValue.page-limitPageCount)">
<span>...</span>
<el-icon><DArrowLeft /></el-icon>
</nuxt-link>
</li>
<!-- 中间分页 -->
<li
v-for="pageIndex in centerPages"
:key="pageIndex"
:class="{ selected: pageIndex === modelValue.page }"
>
<nuxt-link :to="getPageUrl(pageIndex)">{{ pageIndex }}</nuxt-link>
</li>
<!-- 后置翻页器存在中间页码 || 完全展示了前面的页码但总页数要大于前方展示的页码数 -->
<li v-if="centerPages.length > 0 || (prevPages.length === limitPageCount && pageCount > limitPageCount)" class="page-control">
<nuxt-link :to="getPageUrl(modelValue.page+limitPageCount > pageCount ? pageCount : modelValue.page+limitPageCount)">
<span>...</span>
<el-icon><DArrowRight /></el-icon>
</nuxt-link>
</li>
<!-- 后置分页 -->
<li
v-for="pageIndex in afterPages"
:key="pageIndex"
:class="{ selected: pageIndex === modelValue.page }"
>
<nuxt-link :to="getPageUrl(pageIndex)">{{ pageIndex }}</nuxt-link>
</li>
<!-- 下一页 -->
<li :class="{ disabled: modelValue.page >= pageCount }">
<el-icon v-if="modelValue.page >= pageCount"><ArrowRight /></el-icon>
<nuxt-link v-else :to="getPageUrl(modelValue.page + 1)">
<el-icon><ArrowRight/></el-icon>
</nuxt-link>
</li>
</ul>
<!-- 跳转 -->
<div class="pagination-info pagination-jump">
前往
<el-input-number
v-model="targetPage"
:controls="false"
:max="pageCount"
@keydown.enter="jump"
/>
</div>
</div>
</div>
</template>
<script>
import { ArrowLeft, DArrowLeft, ArrowRight, DArrowRight } from '@element-plus/icons-vue'
export default {
name: 'Pagination',
components: { ArrowLeft, DArrowLeft, ArrowRight, DArrowRight },
emits: ['update:modelValue', 'page-change'],
props: {
// e.g: { page: 1, capacity: 10, total: 100 }
modelValue: {
type: Object,
required: true,
default: function () {
return {}
}
},
//
getPageUrl: {
type: Function,
default: function (page) {
return `?page=${page}`
}
},
//
limitPageCount: {
type: Number,
default: 5
}
},
data () {
return {
targetPage: 1,
}
},
computed: {
// 4 = + + +
maxPageCount () {
return this.limitPageCount + 4
},
//
pageCount () {
return Math.ceil(this.modelValue.total / this.modelValue.capacity)
},
//
limitPageCountHalf () {
return Math.floor(this.limitPageCount / 2)
},
//
prevPages () {
// maxPageCount
if (this.pageCount <= this.maxPageCount) {
return new Array(this.pageCount)
.fill(0)
.map((item, index) => index + 1)
}
// <= limitPageCount
if (this.modelValue.page < this.limitPageCount) {
return new Array(this.limitPageCount)
.fill(0)
.map((item, index) => index + 1)
}
return [1]
},
// 5
centerPages () {
if (this.pageCount <= this.maxPageCount) {
return []
}
// > limitPageCount && < pageCount - limitPageCount
if (this.modelValue.page >= this.limitPageCount && this.modelValue.page <= this.pageCount - this.limitPageCount + 1) {
const pages = []
for (let i = 0; i < this.limitPageCount; i++) {
//
if (i === this.limitPageCountHalf) {
pages.push(this.modelValue.page)
continue
}
//
pages.push(this.modelValue.page - this.limitPageCountHalf + i)
}
return pages
}
return []
},
//
afterPages () {
if (this.pageCount <= this.maxPageCount) {
return []
}
// >= pageCount - limitPageCount + 1
if (this.modelValue.page > this.pageCount - this.limitPageCount + 1) {
return new Array(this.limitPageCount)
.fill(0)
.map((item, index) => {
return this.pageCount - this.limitPageCount + index + 1
})
}
return [this.pageCount]
}
},
watch: {
//
'modelValue.page': {
immediate: true,
handler (newValue) {
if (this.targetPage !== newValue) {
this.targetPage = newValue
this.$router.replace(this.getPageUrl(newValue))
}
}
}
},
methods: {
//
jump () {
if (this.targetPage < 1) {
this.targetPage = 1
}
this.$router.push(this.getPageUrl(this.targetPage))
}
}
}
</script>
<style scoped lang="scss">
.pagination {
--page-item-size: 35px;
width: 100%;
text-align: center;
.pagination-content {
width: 800px;
overflow: hidden;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
}
//
ul {
display: flex;
margin: 0 30px;
li {
width: var(--page-item-size);
height: var(--page-item-size);
background: var(--background-color);
margin-right: 3px;
display: flex;
justify-content: center;
align-items: center;
a {
display: block;
width: 100%;
height: 100%;
line-height: var(--page-item-size);
text-align: center;
color: #333;
&:hover {
color: var(--primary-color);
}
.el-icon {
position: relative;
top: 2px;
}
}
//
&.selected {
a {
color: var(--color-white);
background: var(--primary-color);
&:hover {
color: var(--color-white);
}
}
}
//
&.disabled {
color: var(--color-gray);
cursor: not-allowed;
}
//
&.page-control {
a {
display: flex;
justify-content: center;
align-items: center;
}
.el-icon {
display: none;
position: relative;
top: 0;
}
&:hover {
span {
display: none;
}
.el-icon {
display: block;
}
}
}
}
}
//
.pagination-info {
color: var(--color-gray);
em {
color: var(--font-color);
font-style: normal;
}
}
//
.pagination-jump {
display: flex;
align-items: center;
.el-input-number {
width: 70px;
text-align: center;
margin: 0 10px;
}
}
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="pop-up-select" @mouseover="active = true" @mouseout="active = false">
<span>{{ title }} <el-icon><ElIconArrowUpBold/></el-icon></span>
<ul
:class="{ active: active, leave: !active }"
@mouseover="active = true"
@mouseout="active = false"
>
<li v-for="option in data" :key="option.title">
<nuxt-link target="_blank" :to="option.value">{{ option.title }}</nuxt-link>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'PopUpSelect',
props: {
title: String,
data: {
default: () => {
return []
}
}
},
data () {
return {
active: false
}
}
}
</script>
<style scoped lang="scss">
.pop-up-select {
position: relative;
height: 40px;
background-color: #FFFFFF;
border: 1px solid #e4e7ed;
display: flex;
align-items: center;
&>span {
width: 100%;
padding: 5px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.el-icon {
color: #aaa;
font-size: 18px;
line-height: 18px;
margin-top: 2px;
}
}
ul {
width: 100%;
position: absolute;
bottom: 30px;
left: -1px;
background-color: #FFFFFF;
overflow: hidden;
border: 1px solid #e4e7ed;
border-bottom: 0;
z-index: 9;
height: 0;
&.active {
height: auto;
animation: pop-up linear 0.5s 1;
animation-fill-mode: forwards;
}
&.leave {
display: none;
}
li {
&:hover {
background-color: rgb(216.8, 235.6, 255);
}
a {
position: relative;
display: flex;
padding: 10px 10px 10px 25px;
color: var(--font-color);
}
&:last-of-type {
margin-bottom: 10px;
}
}
}
}
@keyframes pop-up {
0% {
max-height: 0;
}
100% {
max-height: 300px;
}
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div class="segment-selector">
<label>{{ data.title }}</label>
<ul>
<li
v-for="item in items"
:class="{'is-active': modelValue === item.value}"
:key="item.id"
@click="handleSelect(item.value)"
>{{ item.label }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'SegmentedFilter',
emits: ['update:modelValue', 'change'],
props: {
data: {
default: () => {
return {
children: []
}
}
},
modelValue: {}
},
computed: {
items () {
return [
{
id: 0,
label: '不限',
value: ''
},
...this.data.items
]
}
},
methods: {
/**
* 处理选中
*
* @param value 触发对象
*/
handleSelect (value) {
if (value === this.modelValue) {
return
}
this.$emit('update:modelValue', value)
this.$emit('change', value)
}
}
}
</script>
<style scoped lang="scss">
.segment-selector {
font-size: 14px;
display: flex;
label {
width: 75px;
font-size: 15px;
flex-shrink: 0;
line-height: 30px;
color: var(--color-gray);
text-align: right;
}
ul {
display: flex;
flex-wrap: wrap;
line-height: 30px;
gap: 5px;
li {
height: 30px;
padding: 0 10px;
margin-bottom: 10px;
cursor: pointer;
background-color: var(--background-color);
&:hover {
color: var(--primary-color-dark);
}
&.is-active {
background-color: var(--primary-color-light-deep);
}
}
}
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="carousel">
<el-carousel
ref="carousel"
motion-blur
:pause-on-hover="true"
>
<el-carousel-item v-for="item in data" :key="item.id">
<nuxt-link target="_blank" :to="JSON.parse(item.value).link">
<img :src="getImageURL(JSON.parse(item.value).image)" :alt="item.title"/>
<p>{{ item.title }}</p>
</nuxt-link>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script>
import { getImageURL } from '@/utils/util'
export default {
name: 'Carousel',
props: {
//
data: {
type: Array,
required: true
}
},
methods: {
getImageURL
}
}
</script>
<style scoped lang="scss">
.carousel {
width: 100%;
height: 100%;
:deep(.el-carousel) {
//
--indicators-width: 200px;
height: 100%;
position: relative;
.el-carousel__container {
height: 100% !important;
}
//
.el-carousel__item {
background-color: #efdfdf;
a {
display: block;
width: 100%;
height: 100%;
position: relative;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
p {
position: absolute;
width: 100%;
box-sizing: border-box;
left: 0;
margin: 0;
bottom: 0;
padding: 8px var(--indicators-width) 8px 20px;
background-color: rgba(0, 0, 0, .3);
color: var(--color-white);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
//
.el-carousel__indicators {
width: var(--indicators-width);
position: absolute;
right: 0;
bottom: 5px;
left: initial;
transform: none;
display: flex;
justify-content: flex-end;
}
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<div class="website-nav-wrap">
<div class="title-wrap">
<img src="/images/link.png" alt="">
<h3>网站导航</h3>
</div>
<ul>
<li
v-for="(title, index) in navList"
v-show="data[index] != null && data[index].length > 0"
:key="title"
>
<PopUpSelect :title="title" :data="data[index]"/>
</li>
</ul>
</div>
</template>
<script>
import PopUpSelect from '@/components/common/PopUpSelect.vue'
export default {
name: 'WebsiteNav',
components: { PopUpSelect },
props: {
data: {
default: () => {
return []
}
}
},
data () {
return {
navList: ['国家级链接', '省级链接', '市级链接', '其他链接']
}
}
}
</script>
<style scoped lang="scss">
.website-nav-wrap {
margin: 0 auto;
display: flex;
//
.title-wrap {
color: var(--primary-color);
display: flex;
align-items: center;
flex-shrink: 0;
margin-right: 30px;
h3 {
margin: 0;
padding-left: 8px;
font-size: 16px;
}
img {
width: 20px;
padding-top: 2px;
}
}
//
ul {
display: flex;
flex-grow: 1;
gap: 20px;
&>li {
flex: 1;
cursor: pointer;
&:first-of-type {
justify-content: flex-start;
padding-left: 10px;
}
.el-tooltip__trigger:focus {
outline: none;
}
.el-dropdown {
width: 100%;
.dropdown-link {
display: flex;
align-items: center;
font-size: 14px;
color: #1b1b1b;
padding: 10px 20px;
}
.el-icon--right {
padding-left: 100px;
color: #aaa;
font-size: 18px;
line-height: 18px;
margin-top: 2px;
}
}
}
}
}
:deep(.el-dropdown-menu__item) {
min-width: 220px;
a {
color: #1b1b1b;
}
}
</style>

View File

@ -0,0 +1,137 @@
<template>
<div class="categories">
<div class="content-wrap">
<el-menu
:popper-offset="0"
popper-class="categories-popper"
mode="horizontal"
:show-timeout="0"
:hide-timeout="0"
>
<CategoryChildren
v-for="category in categories"
:key="category.uid"
:index="category.uid"
:category="category"
/>
</el-menu>
</div>
</div>
</template>
<script>
import CategoryChildren from '@/components/layout/CategoryChildren'
export default {
name: 'Categories',
components: { CategoryChildren },
props: {
//
categories: {
required: true
}
}
}
</script>
<style scoped lang="scss">
.categories {
background-color: var(--primary-color-dark);
:deep(.el-menu) {
// Nuxtclass
.router-link-active {
background-color: var(--color-white);
color: var(--primary-color-dark) !important;
border-radius: 8px;
line-height: 38px;
font-weight: bold;
transition: all .3s;
&:hover {
border-radius: 0;
}
}
//
a,label {
display: flex;
align-items: center;
width: 100%;
color: var(--color-white);
cursor: pointer;
padding: 0 35px;
.category-icon {
margin-right: 5px;
}
.image-icon {
width: 20px !important;
}
}
//
.el-sub-menu__icon-arrow {
display: none;
}
//
.el-menu-item,.el-sub-menu {
padding: 10px 0;
box-sizing: border-box;
text-align: center;
border-bottom: 0 !important;
&.is-active {
color: var(--primary-color-dark) !important;
}
}
//
.el-sub-menu__title {
padding: 0;
border-bottom: 0;
}
}
}
</style>
<style lang="scss">
.categories-popper {
border: 0 !important;
.el-menu {
background-color: #fff !important;
//
a,label {
display: block;
width: 100%;
height: 100%;
text-align: left;
color: var(--font-color);
&:hover {
color: var(--color-white);
}
}
.el-menu-item {
min-width: 200px;
background-color: #fff;
font-size: var(--font-size-middle);
text-align: center;
transition: none;
&:hover {
background-color: var(--primary-color-light);
color: var(--color-white);
}
}
//
.el-sub-menu__title {
background-color: var(--color-white);
font-size: var(--font-size-middle);
//
.el-sub-menu__icon-arrow {
color: var(--font-color);
}
&:hover {
background-color: var(--primary-color-light);
a {
color: var(--color-white) !important;
}
.el-sub-menu__icon-arrow {
color: var(--color-white) !important;
}
}
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<el-menu-item
v-if="category.children == null || category.children.length == 0"
:key="index"
:index="index + '#' + category.path"
>
<template v-if="category.uri != null && category.uri !== ''">
<!-- 外部链接新窗口打开 -->
<nuxt-link v-if="category.type === 'OUT_LINK'" :to="category.uri" target="_blank">
<CategoryIcon :value="category.icon" :with-holder="false"/>
{{category.title}}
</nuxt-link>
<!-- 内部链接 || 常规栏目配置了模板当前页打开 -->
<nuxt-link v-else :to="category.uri">
<CategoryIcon :value="category.icon" :with-holder="false"/>
{{category.title}}
</nuxt-link>
</template>
<!-- 常规栏目 -->
<label v-else>{{ category.title }}</label>
</el-menu-item>
<el-sub-menu v-else :index="index">
<template #title>
<template v-if="category.uri != null && category.uri !== ''">
<!-- 外部链接新窗口打开 -->
<nuxt-link v-if="category.type === 'OUT_LINK'" :to="category.uri" target="_blank">
<CategoryIcon :value="category.icon" :with-holder="false"/>
{{category.title}}
</nuxt-link>
<!-- 内部链接 || 常规栏目配置了模板当前页打开 -->
<nuxt-link v-else :to="category.uri">
<CategoryIcon :value="category.icon" :with-holder="false"/>
{{category.title}}
</nuxt-link>
</template>
<!-- 常规栏目未配置模板目录 -->
<label v-else>
<CategoryIcon :value="category.icon" :with-holder="false"/>
{{ category.title }}
</label>
</template>
<CategoryChildren
v-for="(child, idx) in category.children"
:category="child"
:key="index + '-' + idx"
:index="String(index + '-' + idx)"
/>
</el-sub-menu>
</template>
<script>
import CategoryIcon from '@/components/cms/CategoryIcon'
export default {
name: 'CategoryChildren',
components: { CategoryIcon },
props: {
//
category: {
type: Object,
required: true
},
index: String,
}
}
</script>

View File

@ -0,0 +1,85 @@
<template>
<el-input
v-model="keyword"
size="large"
placeholder="请输入搜索关键字"
clearable
@keydown.enter="search"
>
<template #append>
<el-button :icon="ElIconSearch" @click="search">搜索</el-button>
</template>
</el-input>
</template>
<script>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
export default {
name: 'SearchInput',
setup () {
const route = useRoute()
const keyword = route.query.kwd || ''
return {
keyword: ref(keyword)
}
},
watch: {
//
'$route.query': function (newQuery) {
this.keyword = newQuery.kwd == null ? '' : newQuery.kwd.trim()
}
},
methods: {
//
search () {
this.keyword = this.keyword.trim()
if (this.keyword === '') {
return
}
//
if (this.$route.path === '/search') {
this.$router.push({ path: '/search', query: { kwd: this.keyword } })
return
}
//
window.open(`/search?kwd=${this.keyword}`)
}
}
}
</script>
<style scoped lang="scss">
.el-input {
width: 300px;
border-radius: 40px;
overflow: hidden;
:deep(.el-input__wrapper) {
box-shadow: none;
.el-input__inner {
font-size: var(--font-size-base);
}
}
//
:deep(.el-input-group__append) {
padding: 0 10px;
width: 65px;
background-color: var(--primary-color-dark-deep);
box-shadow: none;
font-size: var(--font-size-base);
color: var(--color-white);
overflow: hidden;
.el-button {
display: flex;
align-items: center;
padding: 0;
width: 100%;
height: 100%;
.el-icon {
margin-right: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div class="web-footer">
<ul class="content-wrap">
<li>
主办单位{{ organization }}
</li>
<li>
网站标识码
<img class="icon-icp" src="/images/icp.png" alt="icp"/>
<nuxt-link to="https://beian.miit.gov.cn" target="_blank">{{ icp }}</nuxt-link>
</li>
<li>地址{{ address }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'WebFooter',
setup () {
return {
organization: import.meta.env.VITE_SITE_ORGANIZATION,
icp: import.meta.env.VITE_SITE_ICP,
address: import.meta.env.VITE_SITE_ADDRESS,
}
}
}
</script>
<style scoped lang="scss">
.web-footer {
height: 100%;
background-color: var(--primary-color-dark);
padding: 50px 0;
color: var(--color-white);
.content-wrap {
text-align: center;
margin-top: 20px;
font-size: 14px;
line-height: 24px;
color: var(--color-white);
background-color: transparent;
li {
display: flex;
justify-content: center;
align-items: center;
color: var(--color-white);
a {
color: var(--color-white);
}
.icon-icp {
margin-right: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div class="web-header">
<div class="content-wrap">
<div class="title-wrap">
<h1>{{ title }}</h1>
<h2>{{ subTitle }}</h2>
</div>
<div class="search-wrap">
<SearchInput/>
</div>
</div>
</div>
</template>
<script>
import SearchInput from './SearchInput'
export default {
name: 'WebHeader',
components: { SearchInput },
async setup () {
return {
title: import.meta.env.VITE_SITE_TITLE,
subTitle: import.meta.env.VITE_SITE_SUB_TITLE
}
}
}
</script>
<style scoped lang="scss">
.web-header {
padding: 50px 0;
background: linear-gradient(to bottom, var(--primary-color), var(--primary-color-light));
color: var(--color-white);
.content-wrap {
width: var(--page-width);
margin: 0 auto;
display: flex;
align-items: flex-end;
background-color: transparent;
//
.title-wrap {
flex-grow: 1;
h1,h2 {
margin: 0;
}
h1 {
font-size: 42px;
}
h2 {
color: var(--primary-color-light-deep);
font-size: 16px;
}
a {
color: var(--primary-color-light-deep);
}
}
//
.search-wrap {
flex-shrink: 0;
}
}
}
</style>

74
.kit/translated/error.vue Normal file
View File

@ -0,0 +1,74 @@
<template>
<div class="error-page">
<div class="content-wrap">
<h1 v-if="error.statusCode === 404">找不到您要访问的资源</h1>
<h1 v-else>{{ error.message }}</h1>
<p>{{ error.url }} - {{ error.statusCode }}</p>
<pre v-if="error.statusCode !== 404">错误详情<br/>{{ errorDetail }}</pre>
<div class="opera">
<nuxt-link to="/" class="button">返回首页</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
error: {}
},
computed: {
errorDetail () {
return JSON.stringify(this.error, null, 2)
}
}
}
</script>
<style scoped lang="scss">
.error-page {
width: 100%;
height: 100vh;
background-color: var(--background-color);
padding-top: 200px;
box-sizing: border-box;
.content-wrap {
width: 800px;
margin: 0 auto;
padding: 50px 100px;
background-color: var(--color-white);
text-align: center;
h1 {
margin: 0;
}
p {
color: var(--color-gray);
}
pre {
font-size: var(--font-size-small);
text-align: left;
white-space: pre-wrap;
word-break: break-all;
color: var(--color-gray);
line-height: 1.5;
}
.opera {
margin-top: 50px;
display: flex;
justify-content: center;
border-top: 1px solid var(--border-color);
padding-top: 20px;
}
.button {
display: flex;
justify-content: center;
align-items: center;
width: 120px;
height: 40px;
background-color: var(--primary-color);
color: var(--color-white);
}
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div class="category-detail-layout">
<Head>
<Title>{{ selectedCategory.title }}</Title>
<Meta type="keywords" :content="siteConfig.keywords"/>
<Meta type="description" :content="siteConfig.description"/>
</Head>
<header>
<WebHeader/>
<Categories :categories="categories"/>
</header>
<main>
<!-- 修改el语言为中文 -->
<el-config-provider :locale="zhCn">
<div class="content-wrap category-detail-wrap">
<!-- 页面面包屑 -->
<PageBreadCrumb :data="breadcrumbs"/>
<div class="detail-wrap__body">
<!-- 栏目树 -->
<div class="detail-wrap__categories">
<CategoryTree :data="subCategories" :selected="selectedCategory.uid"/>
</div>
<!-- 主要内容 -->
<div class="detail-wrap__content">
<slot></slot>
</div>
</div>
</div>
</el-config-provider>
</main>
<footer>
<WebFooter/>
</footer>
</div>
</template>
<script>
import WebHeader from '@/components/layout/WebHeader'
import WebFooter from '@/components/layout/WebFooter'
import Categories from '@/components/layout/Categories'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import CategoryTree from '@/components/cms/CategoryTree';
import PageBreadCrumb from '@/components/common/PageBreadCrumb'
export default {
components: { Categories, WebFooter, WebHeader, CategoryTree, PageBreadCrumb},
props: {
//
categories: {
required: true
},
//
subCategories: {
required: true
},
//
selectedCategory: {
required: true
},
//
breadcrumbs: {
required: true
}
},
data () {
return {
zhCn,
siteConfig: {
title: import.meta.env.VITE_SITE_TITLE,
keywords: import.meta.env.VITE_SEO_KEYWORDS,
description: import.meta.env.VITE_SEO_DESCRIPTION
}
}
}
}
</script>
<style scoped lang="scss">
//
.category-detail-wrap {
background-color: #fff;
padding: 20px;
.detail-wrap__body {
margin-top: 10px;
display: flex;
//
.detail-wrap__categories {
height: auto;
width: 275px;
flex-shrink: 0;
align-self: flex-start;
position: sticky;
top: 20px;
}
//
.detail-wrap__content {
flex-grow: 1;
padding: 10px 30px;
overflow: hidden;
& > h2 {
font-size: var(--font-size-large);
margin: 0;
color: var(--primary-color);
font-weight: normal;
}
}
}
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<div class="default-layout">
<Head>
<Title>{{ siteConfig.title }}</Title>
<Meta type="keywords" :content="siteConfig.keywords"/>
<Meta type="description" :content="siteConfig.description"/>
</Head>
<header>
<WebHeader/>
<Categories :categories="categories"/>
</header>
<main>
<!-- 修改el语言为中文 -->
<el-config-provider :locale="zhCn">
<slot></slot>
</el-config-provider>
</main>
<footer>
<WebFooter/>
</footer>
</div>
</template>
<script>
import WebHeader from '@/components/layout/WebHeader'
import WebFooter from '@/components/layout/WebFooter'
import Categories from '@/components/layout/Categories'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
export default {
components: { Categories, WebFooter, WebHeader},
props: {
//
categories: {
required: true
}
},
data () {
return {
zhCn,
siteConfig: {
title: import.meta.env.VITE_SITE_TITLE,
keywords: import.meta.env.VITE_SEO_KEYWORDS,
description: import.meta.env.VITE_SEO_DESCRIPTION
}
}
}
}
</script>

View File

@ -0,0 +1,71 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import sitemap from './utils/sitemap'
export default defineNuxtConfig({
compatibilityDate: '2024-07-10',
devtools: { enabled: true },
sitemap,
app: {
// head配置
head: {
title: import.meta.env.VITE_SITE_TITLE,
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
htmlAttrs: {
lang: 'zh',
},
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no' },
{ name: 'screen-orientation', content: 'portrait' },
],
script: [
],
}
},
css: [
// 全局样式
'@/assets/style/app.scss',
'@/assets/style/theme.scss',
'@/public/icons/cms/iconfont.css'
],
vite: {
css: {
// 修复启动时出现The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.
preprocessorOptions: {
scss: {
api: 'modern-compiler'
}
}
},
// 接口代理配置
server: {
proxy: {
// 接口代理
[import.meta.env.VITE_API_PREFIX]: {
target: import.meta.env.VITE_API_URL,
changeOrigin: true,
rewrite: (path:any) => path.replace(new RegExp(`^${import.meta.env.VITE_API_PREFIX}`), '')
},
// 资源代理
[import.meta.env.VITE_RESOURCE_PREFIX]: {
target: `${import.meta.env.VITE_API_URL}${import.meta.env.VITE_RESOURCE_PREFIX}`,
changeOrigin: true,
rewrite: (path:any) => path.replace(new RegExp(`^${import.meta.env.VITE_RESOURCE_PREFIX}`), '')
}
},
}
},
// 扩展内容
modules: [
'dayjs-nuxt',
'@element-plus/nuxt',
// 导入element-plus图标
'@pinia/nuxt',
'nuxt-swiper',
'@nuxtjs/sitemap'
],
plugins: [
'~/plugins/nprogress.js',
'~/plugins/messagebox.js',
'~/plugins/nprogress.js',
]
})

10511
.kit/translated/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "eva-cms-website-gov",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev --dotenv .env.development",
"staging": "nuxt dev --dotenv .env.staging",
"build": "nuxt build --dotenv .env.production",
"build:staging": "nuxt build --dotenv .env.staging",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint src",
"fix": "eslint layouts --fix"
},
"overrides": {
"vue": "latest"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@nuxtjs/sitemap": "^6.1.2",
"@pinia/nuxt": "^0.5.3",
"axios": "^1.7.4",
"element-plus": "^2.8.0",
"nprogress": "^0.2.0",
"nuxt": "^3.12.4",
"nuxt-swiper": "^1.2.2",
"pinia": "^2.2.1",
"vue": "latest"
},
"devDependencies": {
"@element-plus/nuxt": "^1.0.9",
"@rushstack/eslint-patch": "^1.8.0",
"@vue/eslint-config-prettier": "^9.0.0",
"dayjs-nuxt": "^2.1.9",
"eslint": "^8.57.0",
"eslint-plugin-nuxt": "^4.0.0",
"eslint-plugin-vue": "^9.23.0",
"prettier": "^3.2.5",
"sass": "^1.77.8"
},
"volta": {
"node": "18.15.0",
"npm": "8.19.4",
"yarn": "1.22.21"
}
}

View File

@ -0,0 +1,98 @@
<template>
<NuxtLayout name="default" :categories="categoryTree">
<Head>
<Title>{{ articleData.title }}</Title>
<Meta name="keywords" :content="articleData.keywords" />
<Meta name="description" :content="articleData.contentDigest" />
</Head>
<div class="content-wrap">
<div class="content">
<ArticlePreview :data="articleData"/>
<!-- 附件 -->
<div v-if="attachments.length > 0" class="attachment-wrap">
<h4>本文附件</h4>
<ul>
<li v-for="attach of attachments" :key="attach.uid">
<a :href="$getAttachURL(attach.fileKey, attach.name)" target="_blank">
<el-icon><Document /></el-icon>
<span>{{ attach.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</NuxtLayout>
</template>
<script>
import { definePageMeta } from '#imports'
import { useRoute } from 'vue-router'
import { Document } from '@element-plus/icons-vue'
import ArticlePreview from '@/components/cms/ArticlePreview'
import { fetchTemplateData } from '@/api/cms/template'
import { strictPackage } from '@/utils/util'
export default {
components: { ArticlePreview, Document },
async setup () {
//
definePageMeta({
layout: false,
})
const route = useRoute()
const data = strictPackage(await fetchTemplateData({
path: '/article/[articleUid].vue',
parameters: {
articleUid: route.params.articleUid,
mode: route.query.mode
}
}))
return {
//
categoryTree: data.CATEGORIES_TREE,
//
articleData: data.ARTICLE_DETAIL,
//
attachments: data.ARTICLE_DETAIL.attachments == null ?
[] : JSON.parse(data.ARTICLE_DETAIL.attachments)
}
}
}
</script>
<style scoped lang="scss">
.content-wrap {
background: var(--color-white);
padding: 30px;
.content {
border: 1px solid var(--border-color);
//
.attachment-wrap {
border-top: 1px solid var(--border-color);
padding: 20px 30px;
h4 {
color: var(--primary-color-dark);
margin: 0;
}
ul {
margin-top: 10px;
li {
padding: 3px 0;
a {
display: flex;
align-items: center;
color: var(--primary-color);
&:hover {
color: var(--primary-color-light);
}
.el-icon {
margin-right: 5px;
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<NuxtLayout name="default" :categories="categoryTree">
<Head>
<Title>{{ category.title }}</Title>
</Head>
<div class="content-wrap">
<!-- 页面面包屑 -->
<PageBreadCrumb :data="breadcrumbs"/>
<div class="wrap-body">
<!-- 主要内容 -->
<div class="content">
<h2 class="page-title">{{ category.title }}</h2>
<ArticleList :articles="articles" />
<Pagination v-model="pagination" />
</div>
</div>
</div>
</NuxtLayout>
</template>
<script>
import { definePageMeta } from '#imports'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import PageBreadCrumb from '@/components/common/PageBreadCrumb'
import Pagination from '@/components/common/Pagination'
import CategoryUtil from '@/utils/category'
import ArticleList from '@/components/cms/ArticleList'
import { strictPackage } from '@/utils/util'
import { fetchTemplateData } from '@/api/cms/template'
import { fetchArticlePage } from '@/api/cms/article'
export default {
components: {
ArticleList,
Pagination,
PageBreadCrumb
},
async setup () {
//
definePageMeta({
layout: false,
})
const route = useRoute()
const data = strictPackage(await fetchTemplateData({
path: '/category/[categoryUid]/articles.vue',
targetUid: route.params.categoryUid,
targetType: 'CATEGORY',
parameters: {
categoryUid: route.params.categoryUid
}
}))
// 使UID
const categoryUid = data.ARTICLE_PAGE_PARAMETERS.categoryUid
const categoryUtil = new CategoryUtil(data.CATEGORIES_TREE)
const category = categoryUtil.getCategory(categoryUid)
if (category == null) {
throw new Error(`栏目不存在栏目UID${categoryUid}`)
}
return {
category,
categoryTree: data.CATEGORIES_TREE,
// ID
categoryUid,
//
breadcrumbs: categoryUtil.getBreadcrumb(categoryUid),
//
articles: ref(data.ARTICLE_PAGE.records),
//
pagination: ref({
page: data.ARTICLE_PAGE.page,
capacity: data.ARTICLE_PAGE.capacity,
total: data.ARTICLE_PAGE.total
})
}
},
watch: {
//
'$route.query': function (newQuery) {
this.handlePageChange(Number(newQuery.page) || 1)
}
},
methods: {
/**
* 页码变更
*
* @param page 新页码
*/
handlePageChange (page) {
this.pagination.page = page
fetchArticlePage({
page: this.pagination.page,
capacity: this.pagination.capacity,
model: {
categoryUid: this.categoryUid
}
})
.then(data => {
this.pagination.page = page
this.pagination.total = data.total
this.articles = data.records
})
.catch(e => console.error(e))
}
}
}
</script>
<style scoped lang="scss">
.content-wrap {
background-color: #fff;
padding: 20px;
}
.wrap-body {
margin-top: 10px;
display: flex;
//
.category-wrap {
width: 275px;
flex-shrink: 0;
}
//
.content {
flex-grow: 1;
padding: 10px 30px;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<NuxtLayout
name="category"
:categories="categoryTree"
:subCategories="subCategoryTree"
:selected-category="{
uid: articlePageCategoryUid,
title: category.title
}"
:breadcrumbs="breadcrumbs"
>
<ArticleList :articles="articles" />
<Pagination v-model="pagination" />
</NuxtLayout>
</template>
<script>
import { definePageMeta } from '#imports'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import Pagination from '@/components/common/Pagination'
import CategoryUtil from '@/utils/category'
import ArticleList from '@/components/cms/ArticleList'
import { fetchTemplateData } from '@/api/cms/template'
import { fetchArticlePage } from '@/api/cms/article'
import { strictPackage } from '@/utils/util'
export default {
components: {
ArticleList,
Pagination
},
async setup () {
//
definePageMeta({
layout: false,
})
const route = useRoute()
const data = strictPackage(await fetchTemplateData({
path: '/category/[categoryUid]/index.vue',
targetUid: route.params.categoryUid,
targetType: 'CATEGORY',
parameters: {
categoryUid: route.params.categoryUid
}
}))
// 使UID
const articlePageCategoryUid = data.ARTICLE_PAGE_PARAMETERS.categoryUid
const categoryUtil = new CategoryUtil(data.CATEGORIES_TREE)
const category = categoryUtil.getCategory(articlePageCategoryUid)
if (category == null) {
throw new Error(`找不到栏目UID: ${articlePageCategoryUid}`)
}
return {
categoryUid: route.params.categoryUid,
categoryTree: data.CATEGORIES_TREE,
subCategoryTree: data.SUB_CATEGORIES_TREE,
category: categoryUtil.getCategory(articlePageCategoryUid),
// ID
articlePageCategoryUid,
//
breadcrumbs: categoryUtil.getBreadcrumb(articlePageCategoryUid),
//
articles: ref(data.ARTICLE_PAGE.records),
//
pagination: ref({
page: data.ARTICLE_PAGE.page,
capacity: data.ARTICLE_PAGE.capacity,
total: data.ARTICLE_PAGE.total
})
}
},
watch: {
//
'$route.query': function (newQuery) {
this.handlePageChange(Number(newQuery.page) || 1)
}
},
methods: {
/**
* 页码变更
*
* @param page 新页码
*/
handlePageChange (page) {
this.pagination.page = page
fetchArticlePage({
page: this.pagination.page,
capacity: this.pagination.capacity,
model: {
categoryUid: this.articlePageCategoryUid
}
})
.then(data => {
this.pagination.page = page
this.pagination.total = data.total
this.articles = data.records
})
.catch(e => console.error(e))
}
}
}
</script>

View File

@ -0,0 +1,179 @@
<template>
<NuxtLayout name="default" :categories="categoryTree">
<div class="content-wrap">
<!-- 第一部分 -->
<div class="block row row-wrap">
<!-- 轮播图 -->
<div>
<Carousel :data="carouselList"/>
</div>
<div>
<DataListTabs :data="dtData"/>
</div>
</div>
<!-- 第二部分 -->
<div class="block row">
<div>
<DataListTabs :data="ldzcData"/>
</div>
<div>
<DataListTabs :data="dwgkData"/>
</div>
</div>
<!-- 第三部分 -->
<ul class="block row row-wrap col-3" style="padding-bottom: 0;">
<li><DataListTabs :data="tzggData"/></li>
<li><DataListTabs :data="pqgsTabs"/></li>
<li><DataListTabs :data="phgsTabs"/></li>
<li><DataListTabs :data="bdcdjznTabs"/></li>
<li><DataListTabs :data="wlaqxcTabs"/></li>
<li><DataListTabs :data="djgzTabs"/></li>
</ul>
<!-- 专题专栏 -->
<div class="block">
<SpecialColumn :icons="subjectData"/>
</div>
<!-- 网站导航 -->
<div class="website-nav">
<WebsiteNav :data="websiteNavData"/>
</div>
</div>
</NuxtLayout>
</template>
<script>
import { definePageMeta } from '#imports'
import DataListTabs from '@/components/common/DataListTabs'
import SpecialColumn from '@/components/cms/SpecialColumn'
import Carousel from '@/components/home/Carousel'
import WebsiteNav from '@/components/home/WebsiteNav'
import CategoryUtil from '@/utils/category'
import { fetchTemplateData } from '@/api/cms/template'
import { strictPackage } from '@/utils/util'
export default {
components: {
WebsiteNav,
Carousel,
SpecialColumn,
DataListTabs
},
async setup () {
//
definePageMeta({
layout: false,
})
const pageData = strictPackage(await fetchTemplateData({
path: '/index.vue',
targetUid: 'sy',
targetType: 'CATEGORY'
}))
const categoryTitleMap = {
snyw: '省内要闻',
gnyw: '国内要闻',
ldzc: '领导之窗',
dwgk: '单位概况',
tzgg: '通知公告',
pqgs: '批前公示',
phgs: '批后公示',
bdcdjzn: '不动产登记指南',
wlaqxc: '网络安全宣传',
djgz: '党建工作',
}
const categoryUtil = new CategoryUtil(pageData.CATEGORIES_TREE)
const getTab = (categoryUid, data = []) => {
const category = categoryUtil.getCategory(categoryUid)
if (category == null) {
throw new Error(`找不到栏目UID: ${categoryUid}`)
}
return {
code: categoryUid,
label: categoryTitleMap[categoryUid],
moreLink: category.uri,
items: data
}
}
const dataListTabs = pageData.MULTIPLE_CATEGORY_ARTICLES
//
const resources = pageData.MULTIPLE_RESOURCES
return {
//
categoryTree: pageData.CATEGORIES_TREE,
//
carouselList: pageData.MULTIPLE_RESOURCES.HOME_CAROUSEL,
//
dtData: [
getTab('snyw', dataListTabs.snyw),
getTab('gnyw', dataListTabs.gnyw),
],
//
subjectData: resources.ZHUAN_LAN,
//
websiteNavData: [
resources.SITE_NAVIGAT_1,
resources.SITE_NAVIGAT_2,
resources.SITE_NAVIGAT_3,
resources.FRIENDLY_LINKS
],
ldzcData: [getTab('ldzc', dataListTabs.ldzc)],
dwgkData: [getTab('dwgk', dataListTabs.dwgk)],
tzggData: [getTab('tzgg', dataListTabs.tzgg)],
pqgsTabs: [getTab('pqgs', dataListTabs.pqgs)],
phgsTabs: [getTab('phgs', dataListTabs.phgs)],
bdcdjznTabs: [getTab('bdcdjzn', dataListTabs.bdcdjzn)],
wlaqxcTabs: [getTab('wlaqxc', dataListTabs.wlaqxc)],
djgzTabs: [getTab('djgz', dataListTabs.djgz)],
}
}
}
</script>
<style scoped lang="scss">
.default-layout {
.content-wrap {
//
--data-block-height: 360px;
//
.website-nav {
padding: 15px 20px;
background-color: #EAF1FB;
margin: 10px 0;
}
}
//
.data-list-tabs {
:deep(ul.article-list) {
height: var(--data-block-height);
box-sizing: border-box;
padding-bottom: 0;
& > li {
border-bottom: 0 !important;
a {
padding: 10px 10px 10px 25px;
}
}
}
}
.block {
padding: 20px;
background-color: #fff;
margin-bottom: 12px;
}
.row {
display: flex;
justify-content: space-between;
& > * {
width: 49%;
flex-shrink: 0;
overflow: hidden;
}
//
&.row-wrap {
flex-wrap: wrap;
}
&.col-3 {
& > * {
width: 32%;
margin-bottom: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<NuxtLayout name="default" :categories="categoryTree">
<div class="content-wrap">
<h2>搜索到 <em>{{ articlePage.pagination.total }}</em> 条记录</h2>
<ArticleList :articles="articlePage.data" :keyword="keyword"/>
<Pagination
v-model="articlePage.pagination"
:get-page-url="page => {
return `/search?kwd=${keyword}&page=${page}`
}"
/>
</div>
</NuxtLayout>
</template>
<script>
import { definePageMeta } from '#imports'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import Pagination from '@/components/common/Pagination'
import ArticleList from '@/components/cms/ArticleList'
import { fetchTemplateData } from '@/api/cms/template'
import { search } from '@/api/cms/search'
import { strictPackage } from '@/utils/util'
export default {
components: { ArticleList, Pagination },
async setup () {
//
definePageMeta({
layout: false,
})
const route = useRoute()
const keyword = route.query.kwd || ''
//
const data = strictPackage(await fetchTemplateData({
path: '/search.vue',
parameters: {
keyword
}
}))
return {
keyword,
//
categoryTree: data.CATEGORIES_TREE,
//
articlePage: ref({
//
data: data.SEARCH_PAGE.articlePage.records,
//
pagination: {
page: data.SEARCH_PAGE.articlePage.page,
capacity: data.SEARCH_PAGE.articlePage.capacity,
total: data.SEARCH_PAGE.articlePage.total
}
})
}
},
watch: {
//
'$route.query': function (newQuery) {
this.keyword = newQuery.kwd || ''
this.handlePageChange(Number(newQuery.page) || 1)
}
},
methods: {
/**
* 页码变更
*
* @param page 新页码
*/
handlePageChange (page) {
this.articlePage.pagination.page = page
search({
page: this.articlePage.pagination.page,
capacity: this.articlePage.pagination.capacity,
model: this.keyword
})
.then(data => {
this.articlePage.data = data.articlePage.records
this.articlePage.pagination.total = data.articlePage.total
})
.catch(e => {
this.$tip.apiFailed(e)
})
}
}
}
</script>
<style scoped lang="scss">
.content-wrap {
background-color: var(--color-white);
padding: 20px;
h2 {
margin: 0;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
font-weight: normal;
em {
font-weight: bold;
font-style: normal;
color: var(--primary-color);
}
}
}
</style>

View File

@ -0,0 +1,28 @@
// imports
import request from '@/utils/request'
import {getImageURL, getDictLabel, getAttachURL} from '@/utils/util'
/**
* 扩展全局属性此处封装为方法方便做框架级调整
*
* @param app 挂载对象
* @param key 属性
* @param value
*/
const extendsProperty = (app, key, value) => {
app[key] = value
}
export default defineNuxtPlugin(nuxtApp => {
const app = nuxtApp.vueApp.config.globalProperties
nuxtApp.provide('request', request)
// 去掉警告信息
nuxtApp.vueApp.config.warnHandler = (msg, instance, trace) => {
console.warn('此处忽略WARN内容避免展示大量不必要的WARN信息');
}
// 获取图片地址
app.$getImageURL = getImageURL
// 获取附件地址
app.$getAttachURL = getAttachURL
// 获取字典数据
app.$d = getDictLabel
})

View File

@ -0,0 +1,24 @@
import { ElMessage } from 'element-plus'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.config.globalProperties.$tip = {
...ElMessage,
apiSuccess (message) {
return ElMessage.success(message)
},
apiFailed (e) {
if (e === 'cancel') {
return
}
console.error && console.error('接口提示错误', e)
// 检查是否存在全局错误
const globalErrorDom = document.querySelector('.el-message--error')
if (globalErrorDom != null) {
return
}
if (typeof e === 'string') {
return ElMessage.error(e)
}
return ElMessage.error(e.message)
}
}
})

View File

@ -0,0 +1,24 @@
import { ElMessageBox } from 'element-plus'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.config.globalProperties.$dialog = {
...ElMessageBox,
/**
* 重要提醒
*
* @param message 消息内容
* @param title 提醒标题
* @param extConfig 扩展配置
* @returns {Promise}
*/
attentionConfirm (message, title = '重要提醒', extConfig = {}) {
return ElMessageBox.confirm(message, title, {
showCancelButton: false,
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false,
type: 'warning',
...extConfig
})
}
}
})

View File

@ -0,0 +1,11 @@
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('page:start', () => {
NProgress.start()
})
nuxtApp.hook('page:finish', () => {
NProgress.done()
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,219 @@
@font-face {
font-family: "cmsfont"; /* Project id 4718675 */
src: url('iconfont.woff2?t=1731151023913') format('woff2'),
url('iconfont.woff?t=1731151023913') format('woff'),
url('iconfont.ttf?t=1731151023913') format('truetype');
}
.cmsfont {
font-family: "cmsfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.cms-home1:before {
content: "\e637";
}
.cms-org1:before {
content: "\e6ae";
}
.cms-home2:before {
content: "\e627";
}
.cms-home3:before {
content: "\e638";
}
.cms-cooperation1:before {
content: "\e65a";
}
.cms-org2:before {
content: "\e602";
}
.cms-special-subject1:before {
content: "\e87a";
}
.cms-org3:before {
content: "\e620";
}
.cms-home4:before {
content: "\e610";
}
.cms-cooperation3:before {
content: "\e606";
}
.cms-org4:before {
content: "\e7ca";
}
.cms-special-subject2:before {
content: "\e671";
}
.cms-public1:before {
content: "\e63f";
}
.cms-dynamic2:before {
content: "\e612";
}
.cms-cooperation4:before {
content: "\e777";
}
.cms-dynamic3:before {
content: "\e672";
}
.cms-home5:before {
content: "\e628";
}
.cms-special-subject3:before {
content: "\e607";
}
.cms-cooperation5:before {
content: "\e61a";
}
.cms-special-subject4:before {
content: "\e61e";
}
.cms-home6:before {
content: "\e669";
}
.cms-policy1:before {
content: "\e61d";
}
.cms-policy2:before {
content: "\e65c";
}
.cms-home7:before {
content: "\e601";
}
.cms-policy3:before {
content: "\e667";
}
.cms-policy4:before {
content: "\e609";
}
.cms-policy5:before {
content: "\e60a";
}
.cms-home8:before {
content: "\e617";
}
.cms-gov1:before {
content: "\e753";
}
.cms-cooperation2-copy:before {
content: "\ebc9";
}
.cms-category:before {
content: "\e625";
}
.cms-category1:before {
content: "\e6a4";
}
.cms-category2:before {
content: "\e6a7";
}
.cms-category3:before {
content: "\e63b";
}
.cms-service:before {
content: "\e6d8";
}
.cms-article:before {
content: "\e624";
}
.cms-goods:before {
content: "\e67b";
}
.cms-template:before {
content: "\e60f";
}
.cms-material:before {
content: "\e66d";
}
.cms-template2:before {
content: "\e666";
}
.cms-content:before {
content: "\e890";
}
.cms-content2:before {
content: "\e6b2";
}
.cms-content3:before {
content: "\e681";
}
.cms-article2:before {
content: "\e7d3";
}
.cms-material2:before {
content: "\e665";
}
.cms-article3:before {
content: "\e615";
}
.cms-content4:before {
content: "\e616";
}
.cms-service2:before {
content: "\e694";
}
.cms-material3:before {
content: "\e7ef";
}
.cms-service3:before {
content: "\e636";
}
.cms-goods2:before {
content: "\e61f";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,366 @@
{
"id": "4718675",
"name": "eva-cms",
"font_family": "cmsfont",
"css_prefix_text": "cms-",
"description": "",
"glyphs": [
{
"icon_id": "166640",
"name": "首页",
"font_class": "home1",
"unicode": "e637",
"unicode_decimal": 58935
},
{
"icon_id": "974813",
"name": "机构",
"font_class": "org1",
"unicode": "e6ae",
"unicode_decimal": 59054
},
{
"icon_id": "1129603",
"name": "首页",
"font_class": "home2",
"unicode": "e627",
"unicode_decimal": 58919
},
{
"icon_id": "1244100",
"name": "首页",
"font_class": "home3",
"unicode": "e638",
"unicode_decimal": 58936
},
{
"icon_id": "2570131",
"name": "合作",
"font_class": "cooperation1",
"unicode": "e65a",
"unicode_decimal": 58970
},
{
"icon_id": "5744737",
"name": "组织机构",
"font_class": "org2",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "5838824",
"name": "专题",
"font_class": "special-subject1",
"unicode": "e87a",
"unicode_decimal": 59514
},
{
"icon_id": "5873042",
"name": "组织机构",
"font_class": "org3",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "6865351",
"name": "首页",
"font_class": "home4",
"unicode": "e610",
"unicode_decimal": 58896
},
{
"icon_id": "7814499",
"name": "合作/招商",
"font_class": "cooperation3",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "7876369",
"name": "组织机构",
"font_class": "org4",
"unicode": "e7ca",
"unicode_decimal": 59338
},
{
"icon_id": "9466859",
"name": "专题",
"font_class": "special-subject2",
"unicode": "e671",
"unicode_decimal": 58993
},
{
"icon_id": "10158782",
"name": "公开",
"font_class": "public1",
"unicode": "e63f",
"unicode_decimal": 58943
},
{
"icon_id": "11032202",
"name": "动态",
"font_class": "dynamic2",
"unicode": "e612",
"unicode_decimal": 58898
},
{
"icon_id": "11521307",
"name": "合作",
"font_class": "cooperation4",
"unicode": "e777",
"unicode_decimal": 59255
},
{
"icon_id": "12129940",
"name": "动态",
"font_class": "dynamic3",
"unicode": "e672",
"unicode_decimal": 58994
},
{
"icon_id": "16290378",
"name": "首页",
"font_class": "home5",
"unicode": "e628",
"unicode_decimal": 58920
},
{
"icon_id": "16994568",
"name": "专题",
"font_class": "special-subject3",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "18028238",
"name": "合作",
"font_class": "cooperation5",
"unicode": "e61a",
"unicode_decimal": 58906
},
{
"icon_id": "18272239",
"name": "专题",
"font_class": "special-subject4",
"unicode": "e61e",
"unicode_decimal": 58910
},
{
"icon_id": "18758196",
"name": "首页",
"font_class": "home6",
"unicode": "e669",
"unicode_decimal": 58985
},
{
"icon_id": "21800726",
"name": "政策",
"font_class": "policy1",
"unicode": "e61d",
"unicode_decimal": 58909
},
{
"icon_id": "22236044",
"name": "政策",
"font_class": "policy2",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "22771475",
"name": "首页",
"font_class": "home7",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "22843424",
"name": "政策",
"font_class": "policy3",
"unicode": "e667",
"unicode_decimal": 58983
},
{
"icon_id": "34547473",
"name": "政策",
"font_class": "policy4",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "34548382",
"name": "政策",
"font_class": "policy5",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "41012044",
"name": "首页",
"font_class": "home8",
"unicode": "e617",
"unicode_decimal": 58903
},
{
"icon_id": "42192435",
"name": "政府",
"font_class": "gov1",
"unicode": "e753",
"unicode_decimal": 59219
},
{
"icon_id": "42408386",
"name": "合作",
"font_class": "cooperation2-copy",
"unicode": "ebc9",
"unicode_decimal": 60361
},
{
"icon_id": "1757454",
"name": "所有栏目列表",
"font_class": "category",
"unicode": "e625",
"unicode_decimal": 58917
},
{
"icon_id": "20605189",
"name": "树",
"font_class": "category1",
"unicode": "e6a4",
"unicode_decimal": 59044
},
{
"icon_id": "20683172",
"name": "新建下级栏目",
"font_class": "category2",
"unicode": "e6a7",
"unicode_decimal": 59047
},
{
"icon_id": "39691573",
"name": "栏目管理",
"font_class": "category3",
"unicode": "e63b",
"unicode_decimal": 58939
},
{
"icon_id": "680102",
"name": "服务",
"font_class": "service",
"unicode": "e6d8",
"unicode_decimal": 59096
},
{
"icon_id": "924719",
"name": "文章",
"font_class": "article",
"unicode": "e624",
"unicode_decimal": 58916
},
{
"icon_id": "1137786",
"name": "商品",
"font_class": "goods",
"unicode": "e67b",
"unicode_decimal": 59003
},
{
"icon_id": "1367324",
"name": "模板",
"font_class": "template",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "1509751",
"name": "素材",
"font_class": "material",
"unicode": "e66d",
"unicode_decimal": 58989
},
{
"icon_id": "1680696",
"name": "模板",
"font_class": "template2",
"unicode": "e666",
"unicode_decimal": 58982
},
{
"icon_id": "2076263",
"name": "内容",
"font_class": "content",
"unicode": "e890",
"unicode_decimal": 59536
},
{
"icon_id": "2786517",
"name": "内容字段管理",
"font_class": "content2",
"unicode": "e6b2",
"unicode_decimal": 59058
},
{
"icon_id": "3005918",
"name": "内容",
"font_class": "content3",
"unicode": "e681",
"unicode_decimal": 59009
},
{
"icon_id": "8798005",
"name": "我的文章",
"font_class": "article2",
"unicode": "e7d3",
"unicode_decimal": 59347
},
{
"icon_id": "10940098",
"name": "素材",
"font_class": "material2",
"unicode": "e665",
"unicode_decimal": 58981
},
{
"icon_id": "11124968",
"name": "文章",
"font_class": "article3",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "14559362",
"name": "内容",
"font_class": "content4",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "15765349",
"name": "服务",
"font_class": "service2",
"unicode": "e694",
"unicode_decimal": 59028
},
{
"icon_id": "18494060",
"name": "素材资源",
"font_class": "material3",
"unicode": "e7ef",
"unicode_decimal": 59375
},
{
"icon_id": "19449190",
"name": "服务",
"font_class": "service3",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "31313020",
"name": "商品",
"font_class": "goods2",
"unicode": "e61f",
"unicode_decimal": 58911
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /_nuxt/

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@ -0,0 +1 @@
pm2 start server/index.mjs --name 'eva-cms-website-gov' --instances max

View File

@ -0,0 +1,14 @@
import { defineStore } from 'pinia'
import { fetchConfig } from '@/api/client'
export const useDefaultStore = defineStore('default', {
state: () => ({
// 客户端配置(包含系统配置和字典数据)
clientConfig: null
}),
actions: {
async fetchConfig () {
this.clientConfig = await fetchConfig()
}
}
})

View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -0,0 +1,81 @@
class CategoryUtil {
#tree = []
constructor (tree) {
this.#tree = tree
this.#fillParent(this.#tree)
}
/**
* 获取面包屑
*
* @param uid
* @returns {*[]}
*/
getBreadcrumb (uid) {
let target = this.#getNode(this.#tree, uid)
if (target == null) {
return []
}
const nodes = []
while (target != null) {
nodes.push(target)
target = target.parent
}
return nodes.map(node => {
return {
uid: node.uid,
title: node.title,
uri: node.uri
}
}).reverse()
}
/**
* 获取栏目
*
* @param uid
* @returns {*|null}
*/
getCategory (uid) {
return this.#getNode(this.#tree, uid)
}
/**
* 获取目标节点
*
* @param categoryList 栏目列表
* @param uid 栏目唯一标识
* @returns {*|null}
*/
#getNode (categoryList, uid) {
for (const node of categoryList) {
if (node.uid === uid) {
return node
}
if (node.children != null && node.children.length > 0) {
const result = this.#getNode(node.children, uid)
if (result != null) {
return result
}
}
}
return null
}
/**
* 填充父节点
*
* @param categoryList 栏目列表
* @param parent 父节点
*/
#fillParent (categoryList, parent) {
for (const node of categoryList) {
node.parent = parent
if (node.children != null && node.children.length > 0) {
this.#fillParent(node.children, node)
}
}
}
}
export default CategoryUtil

View File

@ -0,0 +1,79 @@
import axios from 'axios'
import { trim } from './util'
// 默认配置
axios.defaults.headers.common['Content-Type'] = 'application/json;charset=UTF-8'
const axiosInstance = axios.create({
baseURL: '/api',
// 请求超时时间
timeout: 60000
})
// 添加请求拦截器
axiosInstance.interceptors.request.use(function (config) {
// 服务端调用,使用全路径
if (window === undefined) {
config.baseURL = import.meta.env.VITE_API_URL
}
// 导出处理
if (config.download === true) {
config.responseType = 'blob'
}
// 参数去空格
if (config.trim === true) {
if (config.data != null) {
config.data = trim(config.data)
}
if (config.params != null) {
config.params = trim(config.params)
}
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axiosInstance.interceptors.response.use(function (response) {
// 下载接口处理
if (response.headers['x-opera-type'] === 'download') {
if (response.config.responseType !== 'blob') {
return Promise.reject(new Error('下载接口返回类型错误请检查接口定义是否缺少download标识'))
}
// Blob类型数据导出下载文件时如果接口未正确执行返回类型为Blob
return new Promise((resolve, reject) => {
if (response.data.type !== 'application/json') {
resolve(response)
return
}
const blob = new Blob([response.data])
const fileReader = new FileReader()
// 读取Blob内容
fileReader.readAsText(blob, 'utf-8')
fileReader.onload = function () {
const result = JSON.parse(fileReader.result)
// 业务失败
if (!result.success) {
reject(result)
return
}
resolve(result)
}
})
}
// 对响应数据做点什么
if (response.data.success) {
return response.data.data
}
console.error('接口错误', response.data.message)
return Promise.reject(response.data)
}, function (error) {
if (error.response.status === 500) {
return Promise.reject(new Error('网络繁忙,请稍后再试!'))
}
// 对响应错误做点什么
return Promise.reject(error)
})
export default axiosInstance

View File

@ -0,0 +1,30 @@
import axios from 'axios'
export default {
// 缓存时间为一天
cacheMaxAgeSeconds: 60 * 60 * 24,
// 自动检测最后更新时间
autoLastmod: true,
// 排除的链接
exclude: [],
// 发起请求获取链接
urls: async () => {
return await axios.post(`${import.meta.env.VITE_API_URL}/search`, {
page: 1,
capacity: 100,
model: ''
})
.then(res => {
if (res.data.code !== 200) {
throw new Error(res.data.message)
}
// 循环构建地址
return res.data.data.articlePage.records.map(article => {
return `/article/${article.uid}`
})
})
.catch(e => {
console.error(e)
})
}
}

View File

@ -0,0 +1,120 @@
import { useDefaultStore } from '../stores'
// 获取Store
function getStore () {
return useDefaultStore()
}
/**
* 为对象数组字符串等数据去空
*
* @param data 数据
* @returns {string|null|*}
*/
export function trim (data) {
if (data == null) {
return null
}
if (typeof data === 'string') {
return data.trim()
}
if (data instanceof Array) {
for (const item of data) {
trim(item)
}
}
if (typeof data === 'object') {
for (const key in data) {
data[key] = trim(data[key])
}
}
return data
}
/**
* 根据编码表达式获取字典或数据标签
*
* @param codeExpress 编码表达式
* 语法1字典编码.数据编码如GENDER.MALE
* 语法2字典编码如GENDER
*/
export function getDictLabel (codeExpress) {
const dictMap = getStore().clientConfig.dictMap
if (dictMap == null) {
return ''
}
const codes = codeExpress.split('.')
const dictCode = codes[0]
const dataValue = codes[1]
const dict = dictMap[dictCode]
if (dict == null) {
return codeExpress
}
// 如果不存在数据编码,则直接返回字典名称
if (dataValue == null) {
return dict.name
}
const data = dict.dataList.find(d => d.value === dataValue)
if (data == null) {
return codeExpress
}
return data.label
}
/**
* 获取图片路径
*
* @param fileKey 文件key
* @returns {*}
*/
export function getImageURL (fileKey) {
if (fileKey.startsWith('http://') || fileKey.startsWith('https://')) {
return fileKey
}
return `${import.meta.env.VITE_RESOURCE_PREFIX}/oss/image?f=${fileKey}`
}
/**
* 获取附件路径
*
* @param fileKey 文件key
* @param filename 文件名称
* @returns {*}
*/
export function getAttachURL (fileKey, filename) {
if (fileKey.startsWith('http://') || fileKey.startsWith('https://')) {
return fileKey
}
return `${import.meta.env.VITE_RESOURCE_PREFIX}/oss/attach?f=${fileKey}&fn=${filename}`
}
/**
* 严格包装模板数据
* - 返回代理对象获取不到模板数据时将抛出错误
*
* @param templateData 模板数据
* @param defaultValues 数据默认值
* @return Proxy
*/
export function strictPackage (templateData, defaultValues = {}) {
if (templateData == null) {
return null
}
if (templateData instanceof Array) {
return templateData
}
if (typeof templateData !== 'object') {
return templateData
}
return new Proxy(templateData, {
get (target, key) {
if (target[key] === undefined) {
if (defaultValues[key] != null) {
return defaultValues[key]
}
throw new Error(`缺少 ${key} 数据配置`)
}
return target[key]
}
})
}

5
.prettierignore Normal file
View File

@ -0,0 +1,5 @@
# 规范代码格式时的忽略文件
.*
*.json
*.md
node_modules/**

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 金镐开源组织
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
README.md Normal file
View File

@ -0,0 +1,55 @@
<div align="center">
<img src="https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/logo.png" width="120px" style="width:80px;height:80px;" />
<h1>伊娃CMS政企门户主题</h1>
</div>
## 在线演示 & 技术文档
- 演示地址/政企门户主题:[http://gov.eva-cms.goldpankit.com/](http://gov.eva-cms.goldpankit.com/)
- 演示地址/后台管理:[http://admin.eva-cms.goldpankit.com/](http://admin.eva-cms.goldpankit.com/)
- 官方文档:[https://www.yuque.com/u21334242/eva-cms](https://www.yuque.com/u21334242/eva-cms)
## 金镐开源组织生态项目
| 版本 | 说明 | 开源地址 | 构建地址 |
|---------------------|-------------------|--------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| eva-cms | 伊娃CMS后端 | [https://gitee.com/goldpankit/eva-cms](https://gitee.com/goldpankit/eva-cms) | [http://goldpankit.com/space/Eva/eva-cms](http://goldpankit.com/space/Eva/eva-cms) |
| eva-cms-admin-front | 伊娃CMS后台管理前端 | [https://gitee.com/goldpankit/eva-cms-admin-front](https://gitee.com/goldpankit/eva-cms-admin-front) | [http://goldpankit.com/space/Eva/eva-cms-admin-front](http://goldpankit.com/space/Eva/eva-cms-admin-front) |
| eva-cms-website-gov | 伊娃CMS政企门户主题站点前端 | [https://gitee.com/goldpankit/eva-cms-website-gov](https://gitee.com/goldpankit/eva-cms-website-gov) | [http://goldpankit.com/space/Eva/eva-cms-website-gov](http://goldpankit.com/space/Eva/eva-cms-website-gov) |
| eva-server | 伊娃权限系统单工程版本 | [https://gitee.com/goldpankit/eva-server](https://gitee.com/goldpankit/eva-server) | [http://goldpankit.com/space/Eva/eva-server](http://goldpankit.com/space/Eva/eva-server) |
| eva-server-modules | 伊娃权限系统Maven多模块版本 | [https://gitee.com/goldpankit/eva-server-modules](https://gitee.com/goldpankit/eva-server-modules) | [http://www.goldpankit.com/space/Eva/eva-server-modules](http://www.goldpankit.com/space/Eva/eva-server-modules) |
| eva-vue2 | 伊娃权限系统前端vue2版本 | [https://gitee.com/goldpankit/eva-vue/tree/vue2/](https://gitee.com/goldpankit/eva-vue/tree/vue2/) | [http://goldpankit.com/space/Eva/eva-vue2](http://goldpankit.com/space/Eva/eva-vue2) |
| eva-vue3-options | 伊娃权限系统前端vue3选项式版本 | [https://gitee.com/goldpankit/eva-vue/tree/vue3-options/](https://gitee.com/goldpankit/eva-vue/tree/vue3-options/) | [http://www.goldpankit.com/space/Eva/eva-vue3-options](http://www.goldpankit.com/space/Eva/eva-vue3-options) |
## 项目特点
1. 可扩展的功能模块默认情况下提供了文章管理、栏目管理、资源管理、模板管理等CMS系统的基础模块以及用户管理、角色管理、菜单管理等权限系统基础模块使用GoldPanKit可进一步进行源码级功能模块的扩展。
2. 不用担心存在BUG如果存在BUG使用GoldPanKit可实现一键升级。
3. 不用担心存在安全漏洞如果存在安全漏洞GoldPanKit会进行提醒并支持一键升级。
4. 规范化代码 + 详细的代码注释。
5. 基于Eva 4权限系统进行研发安全稳定
6. 丰富的插件市场可使用GoldPanKit进行单表、多表的页面生成。
## 绝对优势
结合GoldPanKit可实现代码直接生成到项目中安装更多的功能模块一键修复BUG等。
## 项目预览
**登录页**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/1.png)
**文章管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/2.png)
**栏目管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/3.png)
**资源管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/4.png)
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/5.png)
**模板管理**
![输入图片说明](https://adjustrd-public.oss-cn-shenzhen.aliyuncs.com/eva-cms/6.png)

6
api/client.js Normal file
View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取客户端配置
export function fetchConfig () {
return request.get('/client/config')
}

6
api/cms/article.js Normal file
View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取文章分页数据
export function fetchArticlePage (data) {
return request.post('/article/profile/page', data)
}

11
api/cms/category.js Normal file
View File

@ -0,0 +1,11 @@
import request from '@/utils/request.js'
// 获取栏目树
export function fetchCategoryTree () {
return request.post('/category/tree')
}
// 获取子栏目树
export function fetchSubCategoryTree (parentCategoryUid) {
return request.post(`/category/${parentCategoryUid}/tree`)
}

6
api/cms/resource.js Normal file
View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取资源列表
export function fetchResources (groupUid) {
return request.post(`/resource/${groupUid}/list`)
}

6
api/cms/search.js Normal file
View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 站内搜索
export function search (data) {
return request.post('/search', data)
}

6
api/cms/template.js Normal file
View File

@ -0,0 +1,6 @@
import request from '@/utils/request.js'
// 获取模板数据
export function fetchTemplateData (data) {
return request.post('/template/data', data)
}

5
app.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

84
assets/style/app.scss Normal file
View File

@ -0,0 +1,84 @@
@import "variables";
html body {
padding: 0;
margin: 0;
background: url("/public/images/background.jpg") repeat;
background-color: var(--page-background);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
min-width: var(--body-min-width);
.content-wrap {
max-width: var(--page-width);
margin: 0 auto;
box-sizing: border-box;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
a {
text-decoration: none;
&:link {
color: var(--font-color);
}
}
// 加载动画
#nprogress {
--loading-color: red;
// 改变进度条的颜色
.bar {
background: var(--loading-color) !important;
}
// 改变进度条的阴影颜色
.peg {
box-shadow: 0 0 10px var(--loading-color), 0 0 5px var(--loading-color) !important; /* 这里设置阴影颜色 */
}
// 改变进度条的旋转动画颜色
.spinner-icon {
border-top-color: var(--loading-color) !important;
border-left-color: var(--loading-color) !important;
}
}
}
// 菜单
.el-menu {
--el-menu-bg-color: var(--primary-color-dark);
--el-menu-text-color: var(--color-white);
--el-menu-hover-bg-color: var(--primary-color-dark);
--el-bg-color-overlay: var(--primary-color-dark);
--el-menu-item-font-size: var(--font-size-large);
--el-border-color-light: var(--primary-color-dark);
}
// 页面标题
.page-title {
--font-size-title: 25px;
color: var(--primary-color-dark);
font-size: var(--font-size-title);
margin: 0;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
font-weight: bold;
}
// 首页内容宽度
@media (max-width: $--mobile-max-width) {
html body {
--page-width: 100%;
--body-min-width: 100%;
}
}
// 移动端
@media (max-width: $--mobile-max-width) {
html body {
// 边距
--gap: 15px;
--gap-mini: 8px;
--gap-midele: 20px;
--gap-large: 25px;
--gap-huge: 30px;
}
}

40
assets/style/theme.scss Normal file
View File

@ -0,0 +1,40 @@
body {
// 页面
--page-width: 1280px;
--page-background: #f7f7f7;
// - body最小宽度避免屏幕过窄时页面溢出
--body-min-width: calc(var(--page-width) + 60px);
// 主题色
--primary-color: #224b9d;
--primary-color-light: #285dc4;
--primary-color-light-deep: #e5ebff;
--primary-color-dark: #224da2;
--primary-color-dark-deep: #16336b;
// 字体
--font-size-base: 16px;
--font-size-small: 14px;
--font-size-middle: 18px;
--font-size-large: 20px;
--font-family-base: "微软雅黑";
--font-color: #333;
// 背景色
--background-color: #f7f7f7;
// 颜色
--color-white: #ffffff;
--color-gray: #999;
--color-gray-light: #ccc;
--color-keyword: #ff0000;
// 边框
--border-color: #eee;
// 子栏目树
--sub-category-width: 275px;
// 覆盖element-plus的默认字体大小
--el-font-size-base: var(--font-size-base);
// 边距
--gap: 20px;
--gap-mini: 10px;
--gap-midele: 30px;
--gap-large: 40px;
--gap-huge: 50px;
}

View File

@ -0,0 +1,11 @@
// bootstrap默认断点
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1400px
);
// 移动端最大宽度小于此宽度视为手机访问
$--mobile-max-width: map-get($grid-breakpoints, sm);

View File

@ -0,0 +1,95 @@
<template>
<ul v-if="articles.length > 0" class="article-list">
<li v-for="article in articles" :key="article.uid">
<nuxt-link :to="`/article/${article.uid}`" target="_blank">
<span class="title" v-html="getTitle(article)"></span>
<span v-if="article.updatedAt" class="date">{{ article.updatedAt }}</span>
</nuxt-link>
</li>
</ul>
<EmptyTip v-else/>
</template>
<script>
import EmptyTip from '@/components/common/EmptyTip'
export default {
name: 'ArticleList',
components: { EmptyTip },
props: {
//
articles: {
type: Array,
required: true
},
//
keyword: {
type: String
}
},
methods: {
//
getTitle (article) {
// em
if (this.keyword != null && this.keyword.trim().length > 0) {
return article.title.replace(new RegExp(this.keyword, 'g'), `<em>${this.keyword}</em>`)
}
return article.title
}
}
}
</script>
<style scoped lang="scss">
ul.article-list {
padding: 10px 0 15px;
li {
border-bottom: 1px dashed var(--color-gray-light);
&:last-of-type {
border-bottom: 0;
}
}
li a {
position: relative;
display: flex;
justify-content: space-between;
padding: 15px 10px 15px 25px;
color: var(--font-color);
text-decoration: none;
gap: 20px;
&:hover {
color: var(--primary-color);
}
//
&::before {
content: '';
width: 5px;
height: 5px;
background-color: var(--color-gray);
border-radius: 50%;
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
}
//
.title {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
//
:deep(em) {
font-style: normal;
color: var(--color-keyword);
}
}
//
.date {
flex-shrink: 0;
color: var(--color-gray);
font-size: var(--font-size-small);
}
}
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div class="article-preview">
<!-- 标题 -->
<h2>{{ data.title }}</h2>
<!-- 信息 -->
<ul class="article-information">
<li>
<label>发布时间</label>
{{ data.updatedAt || data.createdAt }}
</li>
</ul>
<!-- 内容 -->
<article v-html="data.content"></article>
</div>
</template>
<script lang="ts">
export default {
name: 'ArticlePreview',
props: {
data: {
required: true
}
}
}
</script>
<style scoped lang="scss">
@import "@/assets/style/variables";
.article-preview {
--article-title: 30px;
--article-content-font-size: 16px;
--article-content-font-color: #555;
//
h2 {
text-align: center;
color: var(--primary-color);
font-size: var(--article-title);
line-height: 50px;
font-weight: bold;
padding: 0 var(--gap);
word-break: break-all;
}
//
.article-information {
color: var(--color-gray);
line-height: 36px;
border-bottom: 1px solid var(--border-color);
padding: 0 var(--gap);
}
//
article {
padding: 0 var(--gap-midele);
overflow: hidden;
font-size: var(--article-content-font-size);
color: var(--article-content-font-color);
line-height: 1.8;
:deep(img) {
max-width: 100%;
}
}
}
@media (max-width: $--mobile-max-width) {
.article-preview {
--article-title: 20px;
//
h2 {
font-size: var(--article-title);
line-height: 25px;
padding: 20px;
margin: 0;
}
//
.article-information {
line-height: 25px;
padding: 0 10px;
}
//
article {
padding: 10px;
font-size: var(--article-content-font-size);
line-height: 1.5;
:deep(iframe) {
width: 100%!important;
}
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<!-- 网络图标 -->
<img
v-if="isNetworkIcon"
class="category-icon image-icon"
:src="value"
alt="栏目图标"
:style="{ width: sizeWidth }"
/>
<!-- class图标 -->
<i
v-else-if="isClassIcon"
class="category-icon"
:class="value"
:style="{ 'font-size': size }"
></i>
<!-- 不正确的图标 -->
<span class="category-icon holder" v-else-if="withHolder"><em>x</em></span>
</template>
<script>
export default {
name: 'CategoryIcon',
props: {
//
value: {},
//
size: {
default: 'inherit'
},
//
withHolder: {
default: true
}
},
computed: {
// sizewidth
sizeWidth () {
if (this.size === 'inherit') {
return '16px'
}
return this.size
},
// class
isClassIcon () {
if (this.value == null || this.value === '') {
return false
}
return !this.value.startsWith('/') &&
!this.value.startsWith('http://') && !this.value.startsWith('https://')
},
//
isNetworkIcon () {
if (this.value == null || this.value === '') {
return false
}
return this.value.startsWith('/') ||
this.value.startsWith('http://') || this.value.startsWith('https://')
}
}
}
</script>
<style scoped lang="scss">
.holder {
width: 16px !important;
height: 16px !important;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 12px;
color: red !important;
background: #e0e0e0;
em {
position: relative;
top: -1px;
font-style: normal;
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="category-tree">
<el-tree
:data="data"
node-key="uid"
:props="{
children: 'children',
label: 'title'
}"
:current-node-key="selected"
:highlight-current="true"
:default-expand-all="true"
:expand-on-click-node="false"
>
<template #default="{ data }">
<nuxt-link :to="data.uri">{{ data.title }}</nuxt-link>
</template>
</el-tree>
</div>
</template>
<script>
export default {
name: 'CategoryTree',
props: {
//
data : {},
//
selected: {}
},
data() {
return {
}
},
methods: {
}
}
</script>
<style scoped lang="scss">
.category-tree {
height: 800px;
background-color: var(--background-color);
padding: 15px;
box-sizing: border-box;
:deep(.el-tree) {
--el-tree-node-content-height: auto;
background-color: var(--background-color);
.el-tree-node__content {
border-radius: 8px;
a {
height: 100%;
min-height: 50px;
flex-grow: 1;
display: flex;
align-items: center;
padding: 10px;
box-sizing: border-box;
text-wrap: initial;
}
.el-tree-node__expand-icon {
display: none;
}
}
//
.is-current {
& > .el-tree-node__content {
background-color: var(--primary-color-light);
color: var(--color-white);
a {
color: var(--color-white) !important;
text-decoration: none;
}
}
}
a {
color: var(--font-color) !important;
text-decoration: none;
}
}
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<div class="special-column">
<div class="title-wrap">
<label>专题专栏</label>
<!-- 箭头 -->
<span/>
</div>
<div class="swiper-wrap">
<swiper
class="swiper-container"
ref="swiper"
:speed="500"
:slidesPerView="numberColumns"
:loop="true"
:autoplay="{
delay: 1500,
disableOnInteraction: false //
}"
:modules="modules"
@mouseenter="$refs.swiper.$el.swiper.autoplay.stop()"
@mouseleave="$refs.swiper.$el.swiper.autoplay.start()"
>
<swiper-slide v-for="(item, index) in getIcons" :key="index">
<nuxt-link target="_blank" :to="JSON.parse(item.value).link">
<img
:src="getImageURL(JSON.parse(item.value).image)"
:alt="item.title"
/>
</nuxt-link>
</swiper-slide>
</swiper>
</div>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay } from 'swiper/modules'
import 'swiper/scss'
import 'swiper/scss/free-mode'
import { getImageURL } from '@/utils/util'
export default {
name: 'SpecialColumn',
components: { Swiper, SwiperSlide },
props: {
icons: {
default: () => {
return []
}
}
},
computed: {
//
getIcons () {
if (
this.icons.length < this.numberColumns + 1 ||
this.icons.length > this.numberColumns + 3
) {
return this.icons
}
return [...this.icons, ...this.icons]
}
},
data() {
return {
numberColumns: 4,
modules: []
}
},
methods: {
getImageURL
},
created () {
if (this.$device.isMobile) {
this.numberColumns = 2
}
this.modules = [Autoplay]
}
}
</script>
<style lang="scss" scoped>
@import "@/assets/style/variables";
.special-column {
--height: 100px;
--img-border-radius: 20px;
//
--title-width: 34px
}
@media (max-width: $--mobile-max-width) {
.special-column {
--height: 50px;
--img-border-radius: 10px;
//
--title-width: 40px
}
.title-wrap > label{
overflow: hidden;
font-size: 12px!important;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.special-column {
display: flex;
//
.title-wrap {
height: var(--height);
width: var(--title-width);
margin: auto 20px auto 0;
padding: 0 var(--gap-mini);
background: linear-gradient(90deg, var(--primary-color-light) 0%, var(--primary-color) 100%);
border-radius: 10px;
text-align: center;
display: flex;
align-items: center;
position: relative;
//
& > span {
position: absolute;
right: -12px;
top: calc(50% - 10px);
width: 0;
height: 0;
border-bottom:10px solid transparent;
border-top: 10px solid transparent;
border-left: 12px solid var(--primary-color);
}
//
label {
display: block;
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
}
//
.swiper-wrap {
width: calc(100% - 80px);
height: calc(50% - 10px);
flex-grow: 1;
//
.swiper-slide {
height: 100%;
overflow: hidden;
padding: 0 10px;
box-sizing: border-box;
&:first-of-type {
padding-left: 0;
}
&:last-of-type {
padding-right: 0;
}
& > a {
display: block;
cursor: pointer;
img {
width: 90%;
height: var(--height);
border-radius: var(--img-border-radius);
margin-left: var(--gap);
object-fit: cover;
cursor: pointer;
}
}
}
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div class="data-list-tabs">
<!-- 页签 -->
<div class="tabs-header">
<ul>
<li
v-for="(tab, index) in data"
:key="tab.label"
:class="{ hover: activeIndex === index }"
@mouseover="trigger === 'hover' ? changeTab(index) : null"
@click="trigger === 'click' ? changeTab(index) : null"
>
{{ tab.label }}
</li>
</ul>
<nuxt-link
v-if="withMore && currentTab.moreLink != null"
:to="currentTab.moreLink"
>
更多<el-icon><ElIconDArrowRight /></el-icon>
</nuxt-link>
</div>
<!-- 搜索表单 -->
<slot name="search"></slot>
<!-- 数据列表 -->
<slot v-if="items.length > 0" :items="items" :tab="data[activeIndex]">
<ArticleList :articles="items"/>
</slot>
<!-- 暂无数据 -->
<EmptyTip v-else/>
</div>
</template>
<script>
import EmptyTip from "~/components/common/EmptyTip.vue";
import ArticleList from "~/components/cms/ArticleList.vue";
export default {
name: 'DataListTabs',
components: {ArticleList, EmptyTip},
emits: ['changeTab'],
props: {
// [{ label: '', items: [ { title: '', updateTime: '' } ] }]
data: {
default: () => {
return []
}
},
//
withMore: {
default: true
},
//
trigger: {
default: 'hover'
}
},
computed: {
// tab
currentTab () {
return this.data[this.activeIndex]
},
//
items () {
if (this.currentTab == null) {
return
}
return this.currentTab.items
}
},
methods: {
/**
* 切换tab
* @param index 坐标
*/
changeTab (index) {
this.activeIndex = index
this.$emit('changeTab', index)
}
},
data() {
return {
// tab
activeIndex: 0
}
}
}
</script>
<style scoped lang="scss">
.data-list-tabs {
width: 100%;
min-height: 200px;
overflow: hidden;
//
.tabs-header {
position: relative;
border-bottom: 1px solid #eee;
color: var(--primary-color);
ul {
width: 100%;
display: flex;
padding-right: 100px;
overflow: hidden;
box-sizing: border-box;
li {
padding: 8px 15px;
cursor: default;
font-size: var(--font-size-middle);
flex-shrink: 0;
&.hover {
font-weight: bold;
border-bottom: 3px solid var(--primary-color);
}
}
}
//
a {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: var(--font-size-small);
display: flex;
align-items: center;
color: var(--primary-color) !important;
text-decoration: none;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More