初始化仓库

This commit is contained in:
tianshixing 2025-03-13 09:50:20 +08:00
parent 1a5feeda2e
commit a5d098ce71
196 changed files with 21060 additions and 1 deletions

32
.env Normal file
View File

@ -0,0 +1,32 @@
# 运行模式EVA基础工程中并没有使用但在实际开发中可以用于做环境级别的特殊处理取值如下
# testing=测试模式
# production=生产模式
VITE_APP_MODE = 'production'
# 是否开启DEBUG模式on开启off关闭开启后将详细的输出日志如接口请求缓存读取日志等
VITE_APP_DEBUG = 'off'
# 路由方式
VITE_APP_ROUTER_MODE = 'history'
# 项目上下文路径
VITE_APP_CONTEXT_PATH = '/'
# 接口前缀
VITE_APP_API_PREFIX = '/api'
# 接口地址
VITE_APP_API_URL = 'http://localhost:10010'
# OSS通用图片访问前缀
VITE_APP_COMMON_IMAGE_PREFIX = '/resource/oss/image?f='
# OSS通用文件访问前缀
VITE_APP_COMMON_ATTACH_PREFIX = '/resource/oss/attach?f='
# 加密请求配置
# 密钥
VITE_APP_ENCRYPT_REQUEST_KEY = 2B7E151628AED2A6
# 向量
VITE_APP_ENCRYPT_REQUEST_IV = 3D8A9F0BAC4E7D61

8
.env.development Normal file
View File

@ -0,0 +1,8 @@
# 开发环境配置
VITE_APP_NODE_ENV = 'development'
# 运行模式
VITE_APP_MODE = 'testing'
# 是否开启DEBUG模式
VITE_APP_DEBUG = 'on'

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
# 生产环境配置
VITE_APP_NODE_ENV = 'production'

2
.env.staging Normal file
View File

@ -0,0 +1,2 @@
# 测试环境配置
VITE_APP_NODE_ENV = 'production'

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
.DS_Store
node_modules
/dist
dist.zip
package-lock.json
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# GoldPanKit
.kit/translated

101
.kit/kit.json Normal file
View File

@ -0,0 +1,101 @@
{
"name": "eva-cms-admin-front",
"label": "eva-cms-admin-front",
"version": "1.0.0",
"private": false,
"receivable": false,
"compiler": "freemarker",
"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": "syjljese1mow",
"type": "variable",
"name": "loginTitle",
"label": "项目标题",
"inputType": "input",
"required": true,
"hidden": false,
"defaultValue": "伊娃CMS-轻量级CMS系统",
"compiler": "static",
"remark": ""
},
{
"id": "1ncl776trtwgg",
"type": "variable",
"name": "loginDescription",
"label": "登录页项目说明",
"inputType": "input",
"required": false,
"hidden": false,
"defaultValue": "金镐开源组织研发,免费、开源、轻量、高规范!",
"compiler": "static",
"remark": ""
},
{
"id": "1qm75h7t43dw0",
"type": "variable",
"name": "menuTitle",
"label": "菜单顶部项目名称",
"inputType": "input",
"required": true,
"hidden": false,
"defaultValue": "伊娃CMS",
"compiler": "static",
"remark": ""
}
],
"translator": {
"output": ".kit/translated",
"filepath": "",
"content": "// freemarker语法修复\nif (setting.compiler === 'freemarker') {\n content = content\n .replace(/\\$\\{/g, '<#noparse>${</#noparse>')\n .replace(/\\#\\{/g, '<#noparse>#{</#noparse>')\n}\n\n// login.vue内容替换\nif (filename === 'login.vue') {\n content = content\n .replace(/<h2>.*<\\/h2>/, '<h2>${loginTitle}</h2>')\n .replace(/<h3>.*<\\/h3>/, '<h3>${loginDescription}</h3>')\n}\n\n// index.html内容替换\nif (filename === 'index.html') {\n content = content\n .replace(/<title>.*<\\/title>/, '<title>${loginTitle}</title>')\n}\n\n// AppMenu替换菜单顶部标题\nif (filename === 'AppMenu.vue') {\n content = content\n .replace('伊娃CMS', '${menuTitle}')\n}\n\nreturn content"
},
"settings": [
{
"path": "src/views/login.vue",
"compiler": "freemarker",
"withoutIfNotExists": false,
"enableExpress": "",
"variables": []
},
{
"path": "index.html",
"compiler": "freemarker",
"withoutIfNotExists": false,
"enableExpress": "",
"variables": []
},
{
"path": "src/components/layout/AppMenu.vue",
"compiler": "freemarker",
"withoutIfNotExists": false,
"enableExpress": "",
"variables": []
}
],
"introduce": "🚩 eva-cms管理后台的前端工程。",
"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| 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"
}

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.

View File

@ -1,2 +1,54 @@
# nankai-cms-admin <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)

15
index.html Normal file
View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" data-href="/favicon.ico">
<link rel="stylesheet" data-href="/icons/system/iconfont.css">
<link rel="stylesheet" data-href="/icons/cms/iconfont.css">
<title>伊娃CMS-轻量级CMS系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

71
package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "eva-vue3-options",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"staging": "vite --mode staging",
"build": "vite build",
"build:staging": "vite build --mode staging",
"preview": "vite preview",
"lint": "eslint src",
"fix": "eslint src --fix"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"element-plus": "^2.6.2",
"js-base64": "^3.7.7",
"js-cookie": "^3.0.5",
"js-file-download": "^0.4.12",
"pinia": "^2.1.7",
"pinyin-pro": "^3.25.0",
"vue": "^3.4.21",
"vue-clipboard3": "^2.0.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"crypto-js": "^4.2.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.24.0",
"sass": "^1.72.0",
"vite": "^5.2.0",
"vite-plugin-eslint": "^1.8.1"
},
"optionalDependencies": {
"@rollup/rollup-win32-x64-msvc": "^4.22.1"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
],
"vue/multi-word-component-names": "off",
"vue/no-reserved-component-names": "off",
"generator-star-spacing": "off",
"no-debugger": "error"
}
},
"volta": {
"node": "18.15.0",
"npm": "8.19.4",
"yarn": "1.22.21"
}
}

BIN
public/avatar/man.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/avatar/woman.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

539
public/icons/cms/demo.css Normal file
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.

View File

@ -0,0 +1,394 @@
@font-face {
font-family: "iconfont"; /* Project id 4356808 */
src: url('iconfont.woff2?t=1707808486506') format('woff2'),
url('iconfont.woff?t=1707808486506') format('woff'),
url('iconfont.ttf?t=1707808486506') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-file:before {
content: "\e63d";
}
.icon-file1:before {
content: "\e63e";
}
.icon-word:before {
content: "\e603";
}
.icon-pdf:before {
content: "\e604";
}
.icon-word1:before {
content: "\e607";
}
.icon-excel:before {
content: "\e665";
}
.icon-pdf1:before {
content: "\e605";
}
.icon-excel1:before {
content: "\ea46";
}
.icon-exit-fullscreen2:before {
content: "\e768";
}
.icon-exit-fullscreen1:before {
content: "\e671";
}
.icon-exit-fullscreen:before {
content: "\eb98";
}
.icon-menu1:before {
content: "\e61a";
}
.icon-menu:before {
content: "\e634";
}
.icon-member:before {
content: "\e628";
}
.icon-member1:before {
content: "\e77d";
}
.icon-user:before {
content: "\e629";
}
.icon-user1:before {
content: "\e6ab";
}
.icon-role1:before {
content: "\e618";
}
.icon-log2:before {
content: "\e68d";
}
.icon-log1:before {
content: "\e663";
}
.icon-icon:before {
content: "\eb80";
}
.icon-maintenance:before {
content: "\e615";
}
.icon-maintenance1:before {
content: "\e6af";
}
.icon-maintenance2:before {
content: "\e616";
}
.icon-home5:before {
content: "\e601";
}
.icon-home4:before {
content: "\e610";
}
.icon-home3:before {
content: "\e602";
}
.icon-home:before {
content: "\e6cb";
}
.icon-home1:before {
content: "\e62e";
}
.icon-home2:before {
content: "\e8c6";
}
.icon-permission:before {
content: "\e8a3";
}
.icon-settings1:before {
content: "\e8b7";
}
.icon-monitor:before {
content: "\e613";
}
.icon-interface:before {
content: "\e74a";
}
.icon-emoji:before {
content: "\ec80";
}
.icon-interface2:before {
content: "\e60b";
}
.icon-maintenance4:before {
content: "\e6a5";
}
.icon-task:before {
content: "\e612";
}
.icon-teacher:before {
content: "\e633";
}
.icon-yuyue1:before {
content: "\e65f";
}
.icon-class:before {
content: "\e60d";
}
.icon-department:before {
content: "\e621";
}
.icon-contract:before {
content: "\e6c8";
}
.icon-customer1:before {
content: "\e648";
}
.icon-message:before {
content: "\e8bd";
}
.icon-goods:before {
content: "\e683";
}
.icon-score:before {
content: "\e61b";
}
.icon-course1:before {
content: "\e61f";
}
.icon-course2:before {
content: "\e630";
}
.icon-store:before {
content: "\e6f6";
}
.icon-address3:before {
content: "\e814";
}
.icon-contract1:before {
content: "\e614";
}
.icon-contract2:before {
content: "\e619";
}
.icon-role:before {
content: "\e60c";
}
.icon-teacher1:before {
content: "\e81f";
}
.icon-store2:before {
content: "\e60a";
}
.icon-position1:before {
content: "\e716";
}
.icon-login-log:before {
content: "\ea45";
}
.icon-position:before {
content: "\e611";
}
.icon-user-group:before {
content: "\e70b";
}
.icon-student2:before {
content: "\e73f";
}
.icon-student1:before {
content: "\e740";
}
.icon-goods1:before {
content: "\e600";
}
.icon-address2:before {
content: "\e652";
}
.icon-course3:before {
content: "\e6be";
}
.icon-contract4:before {
content: "\e632";
}
.icon-order:before {
content: "\e65c";
}
.icon-store1:before {
content: "\e606";
}
.icon-dict:before {
content: "\e666";
}
.icon-settings:before {
content: "\e8b8";
}
.icon-student:before {
content: "\e7c0";
}
.icon-notice:before {
content: "\e62f";
}
.icon-address1:before {
content: "\e63f";
}
.icon-log:before {
content: "\e644";
}
.icon-score2:before {
content: "\e67e";
}
.icon-service:before {
content: "\e6d8";
}
.icon-service2:before {
content: "\e704";
}
.icon-article:before {
content: "\e624";
}
.icon-goods:before {
content: "\e67b";
}
.icon-template:before {
content: "\e60f";
}
.icon-material:before {
content: "\e66d";
}
.icon-template2:before {
content: "\e666";
}
.icon-content:before {
content: "\e890";
}
.icon-article2:before {
content: "\e61e";
}
.icon-content2:before {
content: "\e6b2";
}
.icon-content3:before {
content: "\e681";
}
.icon-template3:before {
content: "\e605";
}
.icon-article3:before {
content: "\e7d3";
}
.icon-material2:before {
content: "\e665";
}
.icon-article4:before {
content: "\e615";
}
.icon-content4:before {
content: "\e616";
}
.icon-service3:before {
content: "\e694";
}
.icon-sucaiziyuan:before {
content: "\e7ef";
}
.icon-service4:before {
content: "\e636";
}
.icon-goods2:before {
content: "\e61f";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/images/login.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

234
src/App.vue Normal file
View File

@ -0,0 +1,234 @@
<template>
<router-view/>
<LoginFormWindow/>
</template>
<script>
import pkg from '../package.json'
import constants from '@/core/plugins/consts'
import router from '@/router'
import { useDefaultStore} from '@/core/store'
import { mapState, mapActions } from 'pinia'
import { fetchUserMenus } from '@/api/system/index'
import LoginFormWindow from '@/components/system/LoginFormWindow'
export default {
components: {LoginFormWindow},
data () {
return {
loading: false
}
},
computed: {
...mapState(useDefaultStore, ['userInfo', 'menuData', 'homePage'])
},
watch: {
//
async userInfo () {
if (this.userInfo == null) {
return
}
await this.initRoutes()
}
},
methods: {
...mapActions(useDefaultStore, ['switchCollapseMenu']),
/**
* 为静态引入添加版本号
*
* @param tag 标签
* @param prop url属性名称
* @param dataProp 预置的url属性名称
*/
handleVersion (tag, prop, dataProp) {
const tags = document.querySelectorAll(tag)
for (const tag of tags) {
const uri = tag.getAttribute(dataProp)
if (uri == null || uri === '') {
continue
}
const prefix = import.meta.env.BASE_URL === '/' ? '' : import.meta.env.BASE_URL
tag[prop] = `${prefix}${uri}?v=${pkg.version}`
}
},
/**
* 初始化本地配置
*/
initLocalConfig () {
//
const menuStatus = window.localStorage.getItem('MENU_STATUS')
if (menuStatus != null) {
this.switchCollapseMenu(menuStatus === 'true')
}
},
/**
* 初始化路由
*
* @returns {Promise<void>}
*/
async initRoutes () {
if (this.loading || this.userInfo == null) {
return
}
this.loading = true
//
this.$defaultStore.menuData.list = []
//
const storeMenus = this.menuData.list
await fetchUserMenus()
.then(menus => {
// 访
if (menus.length === 0) {
this.$router.push({
name: 'error',
params: {
type: 'not-allowed'
}
})
return
}
//
const homePage = this.__getHomePage(menus)
// - IFrame访
if (homePage == null) {
this.$router.push({
name: 'error',
params: {
type: 'not-allowed'
}
})
return
}
this.$defaultStore.homePage = this.__getHomePage(menus)
//
storeMenus.push.apply(storeMenus, menus)
//
const viewsComponents = import.meta.glob('@/views/**/**.vue')
this.__addRouters(storeMenus, [], viewsComponents)
// 404404
router.addRoute({
path: '/:catchAll(.*)',
redirect: '/error/not-found'
})
//
router.addRoute({
name: 'index',
path: '/',
redirect: this.__getHomeUri()
})
// 使
// - 访
if (this.$route.path === '/') {
this.$router.push(this.__getHomeUri())
}
// - push
else {
this.$router.push({
path: this.$route.path,
query: this.$route.query,
params: this.$route.params
})
}
})
.catch(e => {
throw e
})
.finally(() => {
this.loading = false
})
},
/**
* 新建路由
*
* @param routes 需添加的路由
* @param parents 需添加到的目标列表
* @param viewComponents 页面组件
* @private
*/
__addRouters (routes, parents = [], viewComponents) {
if (routes == null || routes.length === 0) {
return
}
const rs = router.getRoutes()
for (const route of routes) {
const parentsDump = JSON.parse(JSON.stringify(parents))
parentsDump.push(route)
if (route.type === 'DIR') {
this.__addRouters(route.children, parentsDump, viewComponents)
continue
}
//
if (route.type === 'EXTERNAL' || route.type === 'IFRAME') {
continue
}
if (rs.findIndex(r => r.path === route.path) > -1) {
this.__addRouters(route.children, parentsDump, viewComponents)
continue
}
if (this.homePage == null) {
this.defaultStore.homePage = route
}
const viewComponent = viewComponents[`/src/views${route.uri}.vue`]
if (viewComponent != null) {
router.addRoute('layout', {
path: route.uri,
name: route.name,
meta: {
title: route.name,
paths: [...parents.map(p => p.name), route.name]
},
component: viewComponent
})
} else {
//
route.__exists = false
console.error(`未找到路由组件:@/views${route.uri}.vue`)
}
this.__addRouters(route.children, parentsDump, viewComponents)
}
},
/**
* 获取首页
* @private
*/
__getHomePage (menus) {
for (const menu of menus) {
//
if (menu.type === constants.SYSTEM_MENU.TYPE_DIR) {
const homePage = this.__getHomePage(menu.children)
//
if (homePage == null) {
continue
}
return homePage
}
//
if (menu.type === constants.SYSTEM_MENU.TYPE_EXTERNAL) {
continue
}
return menu
}
return null
},
// uri
__getHomeUri () {
if (this.$defaultStore.homePage.type === 'IFRAME') {
return `/iframe?url=${this.$defaultStore.homePage.uri}`
}
return this.$defaultStore.homePage.uri
}
},
async created () {
this.handleVersion('link', 'href', 'data-href')
this.handleVersion('script', 'src', 'data-src')
//
if (this.userInfo != null) {
await this.initRoutes()
.catch(() => {})
}
},
mounted () {
this.initLocalConfig()
}
}
</script>

63
src/api/cms/article.js Normal file
View File

@ -0,0 +1,63 @@
import request from '@/core/utils/request'
// 保存草稿/更新文章
export function save (data) {
return request.post('/cms/article/save', data)
}
// 发布
export function publish (id) {
return request.get(`/cms/article/publish/${id}`)
}
// 上线
export function online (id) {
return request.get(`/cms/article/online/${id}`)
}
// 批量上线
export function onlineInBatch (ids) {
return request.get('/cms/article/online/batch', {
params: {
ids
}
})
}
// 下线
export function offline (id) {
return request.get(`/cms/article/offline/${id}`)
}
// 批量下线
export function offlineInBatch (ids) {
return request.get('/cms/article/offline/batch', {
params: {
ids
}
})
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/cms/article/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/cms/article/delete/batch', {
params: {
ids
}
})
}
// 分页查询
export function fetchPage (data) {
return request.post('/cms/article/page', data)
}
// 为文章下拉选择查询概要信息列表
export function fetchProfileList4Select (data) {
return request.post('/cms/article/select/list', data)
}

View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/cms/article/tag/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/cms/article/tag/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/cms/article/tag/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/cms/article/tag/updateById', data)
}
// 分页查询
export function fetchPage (data) {
return request.post('/cms/article/tag/page', data)
}
// 查询所有
export function fetchAll (data) {
return request.get('/cms/article/tag/all', data)
}

35
src/api/cms/category.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/cms/category/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/cms/category/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/cms/category/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/cms/category/updateById', data)
}
// 修改状态
export function updateStatus (data) {
return request.post('/cms/category/updateStatus', data)
}
// 查询所有
export function fetchAll (data) {
return request.get('/cms/category/tree', data)
}

View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/cms/res/group/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/cms/res/group/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/cms/res/group/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/cms/res/group/updateById', data)
}
// 分页查询
export function fetchPage (data) {
return request.post('/cms/res/group/page', data)
}
// 查询所有
export function fetchAll (data) {
return request.get('/cms/res/group/all', data)
}

35
src/api/cms/resource.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/cms/res/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/cms/res/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/cms/res/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/cms/res/updateById', data)
}
// 修改状态
export function updateStatus (data) {
return request.post('/cms/res/updateStatus', data)
}
// 分页查询
export function fetchPage (data) {
return request.post('/cms/res/page', data)
}

35
src/api/cms/template.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/cms/template/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/cms/template/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/cms/template/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/cms/template/updateById', data)
}
// 分页查询
export function fetchPage (data) {
return request.post('/cms/template/page', data)
}
// 分页查询
export function fetchAll (data) {
return request.get('/cms/template/all', data)
}

35
src/api/system/config.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/system/config/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/system/config/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/config/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/system/config/updateById', data)
}
// 分页查询
export function fetchPage (data) {
return request.post('/system/config/page', data)
}
// 刷新缓存
export function refreshCache (data) {
return request.get('/system/config/cache/refresh', data)
}

View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 新建
export function create (data) {
return request.post('/system/dictData/create', data)
}
// 删除
export function deleteById (id) {
return request.get(`/system/dictData/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/dictData/delete/batch', {
params: {
ids
}
})
}
// 修改
export function updateById (data) {
return request.post('/system/dictData/updateById', data)
}
// 排序
export function sort (data) {
return request.post('/system/dictData/sort', data)
}
// 查询
export function fetchPage (data) {
return request.post('/system/dictData/page', data)
}

35
src/api/system/dict.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 新建
export function create (data) {
return request.post('/system/dict/create', data)
}
// 删除
export function deleteById (id) {
return request.get(`/system/dict/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/dict/delete/batch', {
params: {
ids
}
})
}
// 修改
export function updateById (data) {
return request.post('/system/dict/updateById', data)
}
// 查询
export function fetchPage (data) {
return request.post('/system/dict/page', data)
}
// 刷新缓存
export function refreshCache (data) {
return request.get('/system/dict/cache/refresh', data)
}

35
src/api/system/icon.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/core/utils/request'
// 创建
export function create (data) {
return request.post('/system/icon/create', data)
}
// 根据ID删除
export function deleteById (id) {
return request.get(`/system/icon/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/icon/delete/batch', {
params: {
ids
}
})
}
// 根据ID修改
export function updateById (data) {
return request.post('/system/icon/updateById', data)
}
// 分页查询
export function fetchPage (data) {
return request.post('/system/icon/page', data)
}
// 查询所有图标
export function fetchAll (data) {
return request.get('/system/icon/all', data)
}

54
src/api/system/index.js Normal file
View File

@ -0,0 +1,54 @@
import request from '@/core/utils/request'
// 获取图片验证码
export function getCaptcha () {
return request.get('/common/captcha/image')
}
// 下载本地文件
export function downloadLocalFile (params) {
return request.get('/resource/local/download', {
params,
download: true
})
}
// 下载OSS文件
export function downloadOSSFile (params) {
return request.get('/resource/oss/attach', {
params,
download: true
})
}
// 根据密码登录
export function loginByPassword (data) {
return request.secure().post('/system/login', data)
}
// 登出
export function logout (data) {
return request.post('/system/logout', data)
}
// 修改密码
export function updatePwd (data) {
return request.post('/system/updatePwd', data)
}
// 获取已登录的用户信息
export function getUserInfo () {
return request.get('/system/getUserInfo', {
autoLogin: false
})
}
// 获取用户菜单
export function fetchUserMenus () {
return request.get('/system/menus')
}
// 查询客户端配置
export function fetchConfig (data) {
return request.get('/system/client/config', data)
}

View File

@ -0,0 +1,16 @@
import request from '../../core/utils/request'
// 查询
export function fetchPage (data) {
return request.post('/system/loginLog/page', data, {
trim: true
})
}
// 导出Excel
export function exportExcel (data) {
return request.post('/system/loginLog/exportExcel', data, {
download: true,
trim: true
})
}

View File

@ -0,0 +1,30 @@
import request from '@/core/utils/request'
// 新建
export function create (data) {
return request.post('/system/menu/func/create', data)
}
// 删除
export function deleteById (id) {
return request.get(`/system/menu/func/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/menu/func/delete/batch', {
params: {
ids
}
})
}
// 修改
export function updateById (data) {
return request.post('/system/menu/func/updateById', data)
}
// 查询
export function fetchPage (data) {
return request.post('/system/menu/func/page', data)
}

40
src/api/system/menu.js Normal file
View File

@ -0,0 +1,40 @@
import request from '@/core/utils/request'
// 新建
export function create (data) {
return request.post('/system/menu/create', data)
}
// 删除
export function deleteById (id) {
return request.get(`/system/menu/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/menu/delete/batch', {
params: {
ids
}
})
}
// 修改
export function updateById (data) {
return request.post('/system/menu/updateById', data)
}
// 修改状态
export function updateStatus (data) {
return request.post('/system/menu/updateStatus', data)
}
// 排序
export function sort (data) {
return request.post('/system/menu/updateSort', data)
}
// 查询
export function fetchAll (data) {
return request.post('/system/menu/all', data)
}

View File

@ -0,0 +1,11 @@
import request from '@/core/utils/request'
// 获取权限数据
export function fetchPermissions () {
return request.get('/system/permission/data')
}
// 配置权限
export function configPermissions (data) {
return request.post('/system/permission/config', data)
}

39
src/api/system/role.js Normal file
View File

@ -0,0 +1,39 @@
import request from '@/core/utils/request'
// 新建
export function create (data) {
return request.post('/system/role/create', data, {
trim: true
})
}
// 删除
export function deleteById (id) {
return request.get(`/system/role/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/role/delete/batch', {
params: {
ids
}
})
}
// 修改
export function updateById (data) {
return request.post('/system/role/updateById', data, {
trim: true
})
}
// 分页查询
export function fetchPage (data) {
return request.post('/system/role/page', data)
}
// 查询所有
export function fetchAll () {
return request.get('/system/role/all')
}

View File

@ -0,0 +1,16 @@
import request from '../../core/utils/request'
// 查询
export function fetchPage (data) {
return request.post('/system/traceLog/page', data, {
trim: true
})
}
// 导出Excel
export function exportExcel (data) {
return request.post('/system/traceLog/exportExcel', data, {
download: true,
trim: true
})
}

44
src/api/system/user.js Normal file
View File

@ -0,0 +1,44 @@
import request from '@/core/utils/request'
// 新建
export function create (data) {
return request.post('/system/user/create', data, {
trim: true
})
}
// 删除
export function deleteById (id) {
return request.get(`/system/user/delete/${id}`)
}
// 批量删除
export function deleteByIdInBatch (ids) {
return request.get('/system/user/delete/batch', {
params: {
ids
}
})
}
// 修改
export function updateById (data) {
return request.post('/system/user/updateById', data, {
trim: true
})
}
// 配置用户角色
export function createUserRole (data) {
return request.post('/system/user/createUserRole', data)
}
// 重置密码
export function resetPwd (data) {
return request.post('/system/user/resetPwd', data)
}
// 查询
export function fetchPage (data) {
return request.post('/system/user/page', data)
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,20 @@
/**
* @link https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
*/
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
// - 主色调
'primary': (
'base': #2d8cf0,
),
// - 危险色
'danger': (
'base': #F56C6C,
)
),
$button-font-size: (
// - 小型按钮字体大小
'small': 14px,
)
);
@use "element-plus/theme-chalk/src/index.scss" as *;

View File

@ -0,0 +1,2 @@
@use 'theme.scss';
@use 'style.scss';

101
src/assets/style/style.scss Normal file
View File

@ -0,0 +1,101 @@
// 样式重置
html {
height: 100%;
body {
height: 100%;
padding: 0;
margin: 0;
color: var(--font-color);
font-size: var(--font-size);
font-family: var(--font-family);
}
h1,h2,h3,h4,h5,h6,ul {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
#app {
height: 100%;
min-width: var(--page-min-width);
}
}
// 穿梭框的按钮
.el-transfer__buttons {
padding: 0 16px !important;
}
// dialog
.eva-dialog {
.el-form-item {
display: flex !important;
.el-form-item__content {
flex-grow: 1;
}
}
}
// 删除按钮
.el-button--delete {
background: transparent !important;
border: 0 !important;
color: var(--color-danger) !important;
&:hover {
color: var(--color-danger-hover) !important;
}
}
// 删除窗口
.delete-message-box {
.el-message-box__message p {
word-break: break-all !important;
}
}
// 按钮图标
.el-button.iconfont {
display: inline-block;
line-height: 1;
vertical-align: baseline;
margin-right: 5px;
font-size: 14px;
span {
padding-left: 6px;
}
}
// el-alert
.el-alert--info {
background: var(--alert-background-color) !important;
border: 1px solid var(--alert-border-color) !important;
color: var(--alert-text-color) !important;
padding: 5px 16px !important;
}
// 主要文字
.text-primary {
color: var(--color-primary);
}
// 危险文字
.text-danger {
color: var(--color-danger);
}
// 去掉表单自动填充的背景色
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: black;
background-color: #fff !important;
}
// 修改表格树的前方占位宽度原值为20px会导致树节点未对齐
.el-table__placeholder {
width: 12px;
}

View File

@ -0,0 +1,93 @@
// - 主色调如需调整同时也许要调整assets/style/element/index.scss中的主题色
$--color-primary: #2d8cf0;
// - 危险色如需调整同时也许要调整assets/style/element/index.scss中的危险色
$--color-danger: #F56C6C;
// - 字体路径
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@use 'element/index' as *;
// 自定义主题
// 主色调
$--color-primary-light: mix($color-white, $--color-primary, 20%);
$--color-primary-dark: mix($color-black, $--color-primary, 10%);
// 头部高度
$--header-height: 60px;
// 菜单宽度
$--menu-width: 255px;
// 页面最小宽度
$--page-min-width: 1000px;
// 字体
$--font-color: #282828;
$--font-color-gray: #999;
$--font-color-gray-deep: #555;
$--font-size: 13px;
$--font-family: 'Avenir', Helvetica, Arial, sans-serif;
// 背景色
$--background-color: #f7f7f7;
// 登录按钮背景色
$--login-button-background-color: linear-gradient(130deg, var(--color-primary-light) 0%, var(--color-primary-dark) 100%);
// 菜单色调
$--menu-background-color: #2f3044;
$--menu-hover-background-color: mix($color-white, $--menu-background-color, 20%);
$--menu-text-color: #9f9f9f;
$--menu-text-hover-color: #f7f7f7;
$--menu-active-background-color: $--menu-text-hover-color;
$--menu-active-text-color: #333;
$--menu-active-text-hover-color: $--menu-active-text-color;
$--submenu-background-color: mix($color-black, $--menu-background-color, 20%);
// 菜单选中后的圆弧大小
$--menu-active-radius-size: 10px;
// 危险色
$--color-danger-hover: mix($color-white, $--color-danger, 20%);
// alert提示色
$--alert-background-color: #f0faff;
$--alert-text-color: #555;
$--alert-border-color: #abdcff;
// 标签
$--tag-background-color: #eef5fd;
$--tag-text-color: #555;
$--tag-border-color: #abdcff;
:root {
// 头部高度
--header-height: #{$--header-height};
// 菜单宽度
--menu-width: #{$--menu-width};
// 页面最小宽度
--page-min-width: #{$--page-min-width};
// 字体
--font-color: #{$--font-color};
--font-color-gray: #{$--font-color-gray};
--font-color-gray-deep: #{$--font-color-gray-deep};
--font-size: #{$--font-size};
--font-family: #{$--font-family};
// 主色调注意受到Element-UI的实现影响修改该变量并不会影响Element-UI的主题色如需调整组件主色调需调整$--color-primary变量
--color-primary: #{$--color-primary};
--color-primary-light: #{$--color-primary-light};
--color-primary-dark: #{$--color-primary-dark};
// 背景色
--background-color: #{$--background-color};
// 登录按钮背景
--login-button-background-color: #{$--login-button-background-color};
// 菜单色调
--menu-background-color: #{$--menu-background-color};
--menu-hover-background-color: #{$--menu-hover-background-color};
--menu-text-color: #{$--menu-text-color};
--menu-text-hover-color: #{$--menu-text-hover-color};
--menu-active-background-color: #{$--menu-active-background-color};
--menu-active-text-color: #{$--menu-active-text-color};
--menu-active-text-hover-color: #{$--menu-active-text-hover-color};
--submenu-background-color: #{$--submenu-background-color};
// 菜单圆弧大小
--menu-active-radius-size: #{$--menu-active-radius-size};
// 危险色
--color-danger: #{$--color-danger};
--color-danger-hover: #{$--color-danger-hover};
// alert提示色
--alert-background-color: #{$--alert-background-color};
--alert-text-color: #{$--alert-text-color};
--alert-border-color: #{$--alert-border-color};
// 标签
--tag-background-color: #{$--tag-background-color};
--tag-text-color: #{$--tag-text-color};
--tag-border-color: #{$--tag-border-color};
}

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -0,0 +1,14 @@
<script>
export default {
name: 'BaseComponent',
methods: {
//
computeProp (propName) {
if (typeof this[propName] === 'function') {
return this[propName]()
}
return this[propName]
}
}
}
</script>

View File

@ -0,0 +1,35 @@
<script>
export default {
props: {
//
value: {},
//
code: {
type: String,
required: true
},
//
disabled: {
type: Boolean,
default: false
}
},
computed: {
// code
dataList () {
const clientConfig = this.$defaultStore.clientConfig
if (
clientConfig == null ||
clientConfig.dictMap == null
) {
return []
}
const dict = clientConfig.dictMap[this.code]
if (dict == null) {
return []
}
return dict.dataList.filter(data => !data.disabled)
}
}
}
</script>

View File

@ -0,0 +1,140 @@
<script>
export default {
name: 'BaseOpera',
data () {
return {
title: '',
visible: false,
isWorking: false,
//
api: null,
//
configData: {
'field.id': 'id'
}
}
},
methods: {
/**
* 配置
*
* @param extParams 配置参数
*/
config (extParams = {}) {
if (extParams == null) {
throw new Error('Parameter can not be null of method \'config\' .')
}
if (extParams.api == null) {
throw new Error('Missing config option \'api\'.')
}
this.api = extParams.api
extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
},
/**
* 打开窗口
*
* @param title 窗口标题
* @param target 行对象仅编辑需该参数
*/
open (title, target) {
this.title = title
this.visible = true
//
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
//
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
})
},
/**
* 确认点击确认按钮后触发
*/
confirm () {
if (this.form[this.configData['field.id']] == null || this.form[this.configData['field.id']] === '') {
this.__confirmCreate()
return
}
this.__confirmEdit()
},
/**
* 确认新建
*
* @private
*/
__confirmCreate () {
this.__validateForm((valid) => {
if (!valid) {
return
}
//
this.isWorking = true
this.api.create(this.__getForm())
.then(() => {
this.visible = false
this.$tip.apiSuccess('新建成功')
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
},
/**
* 确认修改
*
* @private
*/
__confirmEdit () {
this.__validateForm((valid) => {
if (!valid) {
return
}
//
this.isWorking = true
this.api.updateById(this.__getForm())
.then(() => {
this.visible = false
this.$tip.apiSuccess('修改成功')
this.$emit('success')
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking = false
})
})
},
/**
* 验证表单
*
* @param callback 验证回调
* @private
*/
__validateForm (callback) {
this.$refs.form.validate((valid) => {
callback && callback(valid)
})
},
/**
* 获取新增编辑表单对象
* @returns {*}
* @private
*/
__getForm () {
return this.form
}
}
}
</script>

View File

@ -0,0 +1,314 @@
<script>
export default {
name: 'BaseTable',
data () {
return {
//
api: null,
//
module: '数据',
//
configData: {
// id
'field.id': 'id',
//
'field.main': null
},
//
fullscreen: false,
//
isWorking: {
//
search: false,
//
delete: false,
//
export: false
},
//
tableData: {
//
selectedRows: [],
//
sorts: [],
//
list: [],
//
pagination: {
page: 1,
capacity: 10,
total: 0
}
}
}
},
methods: {
/**
* 配置
*
* @param extParams 配置参数
*/
config (extParams) {
if (extParams == null) {
throw new Error('Parameter can not be null of method \'config\' .')
}
if (extParams.api == null) {
throw new Error('Missing config option \'api\'.')
}
this.api = extParams.api
extParams.module && (this.module = extParams.module)
extParams['field.id'] && (this.configData['field.id'] = extParams['field.id'])
extParams['field.main'] && (this.configData['field.main'] = extParams['field.main'])
this.tableData.sorts = extParams.sorts
},
/**
* 搜索点击搜索按钮时触发
*/
search () {
this.handlePageChange(1)
},
/**
* 导出Excel点击导出按钮时触发
*/
exportExcel () {
this.__checkApi()
this.$dialog.exportConfirm('确认导出吗?')
.then(() => {
this.isWorking.export = true
return this.api.exportExcel({
page: this.tableData.pagination.page,
capacity: 1000000,
model: this.__getSearchForm(),
sorts: this.tableData.sorts
})
})
.then(response => {
this.$download(response)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.export = false
})
},
/**
* 重置搜索条件点击重置按钮时触发
*/
reset () {
this.$refs.searchForm.resetFields()
this.search()
},
/**
* 页容量变更处理切换页容量时触发
*
* @param capacity 页容量
*/
handleSizeChange (capacity) {
this.tableData.pagination.capacity = capacity
this.search()
},
/**
* 行选中处理点击选中列时触发
*
* @param selectedRows 已选中的行数组
*/
handleSelectionChange (selectedRows) {
this.tableData.selectedRows = selectedRows
},
/**
* 排序点击列头排序时触发
*
* @param sortData 排序参数
*/
handleSortChange (sortData) {
this.tableData.sorts = this.tableData.sorts.filter(item => item.fixed === true)
if (sortData.order != null) {
this.tableData.sorts.push({
property: sortData.column.sortBy,
direction: sortData.order === 'descending' ? 'DESC' : 'ASC'
})
}
this.handlePageChange()
},
/**
* 页码变更处理分页时触发
*
* @param page 新页码
*/
handlePageChange (page) {
this.__checkApi()
this.tableData.pagination.page = page || this.tableData.pagination.page
this.isWorking.search = true
// 使使
const api = this.api.fetchPage || this.api.fetchAll
if (api == null) {
throw new Error('未找到fetchPage或fetchAll的接口定义')
}
let params = this.__getSearchForm()
//
if (api === this.api.fetchPage) {
params = {
page: this.tableData.pagination.page,
capacity: this.tableData.pagination.capacity,
model: this.__getSearchForm(),
sorts: this.tableData.sorts
}
}
//
api(params)
.then(data => {
//
if (api === this.api.fetchPage) {
this.tableData.list = this.__handleTableData(data.records)
this.tableData.pagination.total = data.total
return
}
//
this.tableData.list = this.__handleTableData(data)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.search = false
})
},
/**
* 刷新
*/
refresh () {
this.handlePageChange(this.tableData.pagination.page)
},
/**
* 删除点击行操作/删除时触发
*
* @param row 行对象
* @param childConfirm 删除子节点时是否进行二次确认
*/
deleteById (row, childConfirm = true) {
this.__checkApi()
let message = `确认删除${this.module}${row[this.configData['field.main']]}】吗?`
if (row[this.configData['field.main']] == null) {
message = `确认删除该${this.module}吗?`
}
if (childConfirm && row.children != null && row.children.length > 0) {
message = `确认删除${this.module}${row[this.configData['field.main']]}】及其子${this.module}吗?`
if (row[this.configData['field.main']] == null) {
message = `确认删除该${this.module}及其子${this.module}吗?`
}
}
this.$dialog.deleteConfirm(message)
.then(() => {
this.isWorking.delete = true
return this.api.deleteById(row[this.configData['field.id']])
})
.then(() => {
this.__afterDelete()
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
},
/**
* 批量删除点击批量删除时触发
*
* @param childConfirm 删除子节点时是否进行二次确认
*/
deleteByIdInBatch (childConfirm = true) {
this.__checkApi()
if (this.tableData.selectedRows.length === 0) {
this.$tip.warning('请至少选择一条数据')
return
}
let message = `确认删除已选中的 ${this.tableData.selectedRows.length}${this.module}记录吗?`
if (childConfirm) {
const containChildrenRows = []
for (const row of this.tableData.selectedRows) {
if (row.children != null && row.children.length > 0) {
containChildrenRows.push(row[this.configData['field.main']])
}
}
if (containChildrenRows.length > 0) {
message = `本次将删除${this.module}${containChildrenRows.join('、')}】及其子${this.module}记录,确认删除吗?`
}
}
this.$dialog.deleteConfirm(message)
.then(() => {
this.isWorking.delete = true
return this.api.deleteByIdInBatch(this.tableData.selectedRows.map(row => row.id).join(','))
})
.then(() => {
this.__afterDelete(this.tableData.selectedRows.length)
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.delete = false
})
},
/**
* 获取资源类型显示标签
*
* @param dictValues 字典值字符串 ,MALE,FEMALE,
* @param dictCode 字典编码
* @returns {(string|*)[]|*[]}
*/
getDictLabels (dictValues, dictCode) {
if (dictValues == null || dictValues.length === 0) {
return []
}
return dictValues.split(',')
.filter(item => item.trim() !== '')
.map(item => this.$d(`${dictCode}.${item}`))
},
/**
* 获取搜索表单对象
*
* @returns {*}
* @private
*/
__getSearchForm () {
return this.searchForm
},
/**
* 删除后处理在单行删除或多行删除后调用
*
* @param deleteCount 删除数量
* @private
*/
__afterDelete (deleteCount = 1) {
this.$tip.apiSuccess('删除成功')
//
if (this.tableData.list.length - deleteCount === 0) {
this.handlePageChange(this.tableData.pagination.page - 1 === 0 ? 1 : this.tableData.pagination.page - 1)
} else {
this.handlePageChange(this.tableData.pagination.page)
}
},
/**
* 检查接口是否配置在调用接口时调用
*
* @private
*/
__checkApi () {
if (this.api == null) {
throw new Error('页面没有使用config方法进行初始化无法使用BaseTable提供的属性和方法。')
}
},
/**
* 处理列表数据
*
* @param data 列表数据
* @private
*/
__handleTableData (data) {
return data
}
}
}
</script>

View File

@ -0,0 +1,58 @@
<template>
<el-upload
class="attach-uploader"
action="/api/cms/article/upload/attach"
ref="upload"
:multiple="true"
:show-file-list="false"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
>
<slot></slot>
</el-upload>
</template>
<script>
export default {
name: 'ArticleAttachmentUploader',
emits: ['add-file', 'success', 'error'],
methods: {
handleBeforeUpload (file) {
this.$emit('add-file', file)
return true
},
//
handleUploadSuccess (res, file) {
if (!res.success) {
this.$emit('error', {
error: res.message,
file
})
return
}
this.$emit('success', {
file,
data: res.data
})
},
//
handleUploadError (error, file) {
this.$emit('error', {
error: error.message,
file
})
}
}
}
</script>
<style scoped lang="scss">
.attach-uploader {
display: flex;
:deep(.el-upload-list) {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div class="article-attachments">
<ul v-if="modelValue.length > 0">
<li v-for="(attach, index) of modelValue" :key="attach.uid">
<label>{{ attach.name }}</label>
<div>
<span v-if="attach.__uploading">
<el-icon class="is-loading" size="16">
<Loading/>
</el-icon>
<i>上传中...</i>
</span>
<Icon v-else value="Close" @click="deleteFile(index)"/>
</div>
</li>
</ul>
<EmptyTip v-else description="无附件"/>
</div>
</template>
<script>
export default {
name: 'ArticleAttachments',
props: {
modelValue: {
type: Array,
required: true
}
},
methods: {
deleteFile (index) {
this.$dialog.deleteConfirm(`确认删除 ${this.modelValue[index].name} 附件吗?`)
.then(() => {
const newModelValue = [...this.modelValue]
newModelValue.splice(index, 1)
this.$emit('update:modelValue', newModelValue)
})
.catch(e => {
this.$tip.apiFailed(e)
})
}
}
}
</script>
<style scoped lang="scss">
.article-attachments {
background-color: #fff;
border-radius: 5px;
ul li {
display: flex;
align-items: center;
padding: 3px 12px;
border-bottom: 1px dashed #eee;
&:hover {
color: var(--color-primary);
}
&:last-of-type {
border-bottom: 0;
}
& > label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
& > div {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
margin-left: 12px;
span {
display: flex;
align-items: center;
:deep(.el-icon) {
margin-right: 3px;
}
}
i {
font-style: normal;
font-size: 12px;
color: var(--font-color-gray);
}
.icon {
cursor: pointer;
&:hover {
color: var(--color-danger);
}
}
}
}
.el-empty {
padding: 30px 0;
}
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<el-dialog
title="选取文章封面"
v-model="visible"
:append-to-body="true"
class="article-cover-select-dialog"
>
<ul v-if="images.length > 0">
<li
v-for="url in images"
:key="url"
:class="{ selected: selected === url }"
@click="selected = url"
>
<img :src="url" alt="文章封面选取"/>
</li>
<!-- 补充差缺项实现规整的3列 -->
<li v-for="i in images.length % 3 === 0 ? 0 : (3 - images.length % 3)" :key="i"></li>
</ul>
<EmptyTip v-else description="文中暂无图片可选择"/>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
<el-button type="primary" :disabled="selected == null" @click="confirm">确认选择</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'ArticleCoverSelect',
data () {
return {
visible: false,
images: [],
selected: null
}
},
methods: {
/**
* 打开窗口
*
* @param images 图片URL集
*/
open (images) {
this.images = images
this.selected = null
this.visible = true
},
//
confirm () {
if (this.selected == null) {
this.$tip.warning('请先选择封面!')
return
}
this.visible = false
this.$emit('confirm-select', this.selected.split('=')[1])
}
}
}
</script>
<style lang="scss">
.article-cover-select-dialog {
width: 980px !important;
ul {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
li {
flex-shrink: 0;
width: 285px;
height: 200px;
background-color: var(--background-color);
padding: 10px;
margin-bottom: 10px;
border: 2px solid transparent;
transition: all ease .15s;
border-radius: 10px;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: contain;
transition: all ease .3s;
}
&.selected {
border-color: var(--color-primary);
&:hover {
border-color: var(--color-primary);
}
}
&:hover {
border-color: #eee;
img {
transform: scale(1.1);
}
}
&:empty {
cursor: default;
background-color: transparent;
&:hover {
border-color: transparent;
}
}
}
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<el-select
class="article-tag-select"
:class="{select__block: !inline}"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:multiple="multiple"
:model-value="modelValue"
filterable
remote
:remote-method="fetchList"
@update:modelValue="$emit('update:modelValue', $event)"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option
v-for="data in computedList"
:key="data.id"
:value="data[valueKey]"
:label="data.title"
/>
</el-select>
</template>
<script>
import { fetchProfileList4Select } from '@/api/cms/article'
export default {
name: 'ArticleSelect',
props: {
modelValue: {},
valueKey: {
default: 'id'
},
disabled: {
default: false
},
placeholder: {
default: '请选择文章'
},
inline: {
default: true
},
multiple: {
default: false
},
clearable: {
default: true
},
//
firstOptions: {
type: Array,
default () {
return []
}
}
},
data () {
return {
loading: false,
list: []
}
},
computed: {
computedList () {
return [
...this.firstOptions,
...this.list
]
}
},
methods: {
//
fetchList (kwd = '') {
if (this.loading) {
return
}
this.loading = true
fetchProfileList4Select({
uids: this.modelValue == null ? [] : [this.modelValue],
kwd
})
.then(data => {
this.list = data
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.loading = false
})
}
},
created () {
this.fetchList()
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<GlobalWindow
title="文章标签管理"
width="1000px"
v-model:visible="visible"
:with-footer="false"
:wrapper-closable="true"
:close-on-press-escape="true"
>
<TableLayout
:authorized="$hasPermissions([`cms:article:tag:query`])"
:fullscreen="fullscreen"
>
<!-- 搜索表单 -->
<template #search-form>
<SearchForm ref="searchForm" :model="searchForm" :collapse="false">
<el-form-item label="标签名" prop="name">
<el-input v-model="searchForm.name" v-trim placeholder="请输入标签名" @keypress.enter="search"/>
</el-form-item>
<template #buttons>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</template>
</SearchForm>
</template>
<!-- 表格和分页 -->
<template #table-wrap>
<SearchTable
v-loading="isWorking.search"
:data="tableData.list"
:with-fullscreen="false"
@selection-change="handleSelectionChange"
@refresh="refresh"
>
<template
v-if="$hasAnyPermissions([
'cms:article:tag:create',
'cms:article:tag:delete'
])"
#toolbar
>
<el-button
v-permissions="['cms:article:tag:create']"
type="primary"
icon="Plus"
@click="$refs.operaArticleTagWindow.open('新建标签')"
>新建</el-button>
<el-button
v-permissions="['cms:article:tag:delete']"
icon="Delete"
@click="deleteByIdInBatch"
>删除</el-button>
</template>
<el-table-column prop="name" label="标签名" min-width="100px"/>
<el-table-column prop="creatorRealName" label="创建人" min-width="100px"/>
<el-table-column prop="createdAt" label="创建时间" min-width="185px"/>
<el-table-column prop="updaterRealName" label="更新人" min-width="100px"/>
<el-table-column prop="updatedAt" label="更新时间" min-width="185px"/>
<template
v-if="$hasAnyPermissions([
'cms:article:tag:update',
'cms:article:tag:delete'
])"
#buttons="{ row }"
>
<el-button
v-permissions="['cms:article:tag:update']"
type="primary"
icon="EditPen"
link
@click="$refs.operaArticleTagWindow.open(`编辑${row.name}`, row)"
>编辑</el-button>
<el-button
v-permissions="['cms:article:tag:delete']"
type="danger"
icon="Delete"
link
@click="deleteById(row)"
>删除</el-button>
</template>
</SearchTable>
<pagination
:pagination="tableData.pagination"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</template>
<!-- 新建编辑窗口 -->
<OperaArticleTagWindow
ref="operaArticleTagWindow"
@success="refresh"
/>
</TableLayout>
</GlobalWindow>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import OperaArticleTagWindow from '@/components/cms/article/OperaArticleTagWindow'
export default {
name: 'ArticleTagManageWindow',
extends: BaseTable,
components: { OperaArticleTagWindow },
data () {
return {
visible: false,
//
searchForm: {
name: ''
}
}
},
methods: {
/**
* 打开窗口
*
* @param dictId 字典ID
* @param dictName 字典名称
*/
open (dictId, dictName) {
this.searchForm.dictId = dictId
this.dictName = dictName
this.visible = true
this.search()
},
},
async created () {
this.config({
module: '标签',
api: await import('@/api/cms/article.tag')
})
}
}
</script>

View File

@ -0,0 +1,84 @@
<template>
<el-select
class="article-tag-select"
:class="{select__block: !inline}"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:multiple="multiple"
:filterable="true"
:filter-method="filterMethod"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option
v-for="data in computedList"
:key="data.id"
:value="data.id"
:label="data.name"
/>
</el-select>
</template>
<script>
import { fetchAll } from '@/api/cms/article.tag'
export default {
name: 'ArticleTagSelect',
props: {
disabled: {
default: false
},
placeholder: {
default: '请选择文章标签'
},
inline: {
default: true
},
multiple: {
default: true
},
clearable: {
default: true
}
},
data () {
return {
filterWord: '',
list: []
}
},
computed: {
computedList () {
if (this.filterWord.trim() === '') {
return this.list
}
return this.list.filter(item => item.name.indexOf(this.filterWord) !== -1)
}
},
methods: {
//
filterMethod (kwd) {
this.filterWord = kwd
},
//
fetchList () {
fetchAll()
.then(data => {
this.list = data
})
.catch(e => {
this.$tip.apiFailed(e)
})
}
},
created () {
this.fetchList()
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>

View File

@ -0,0 +1,55 @@
<!-- 获取验证字段 -->
<template>
<GlobalWindow
:title="title"
v-model:visible="visible"
:confirm-working="isWorking"
width="750px"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules" @submit.prevent>
<el-form-item label="标签名" prop="name" required>
<el-input
v-model="form.name"
placeholder="请输入标签名"
v-trim
maxlength="50"
/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaArticleTagWindow',
extends: BaseOpera,
components: {
GlobalWindow
},
data () {
return {
//
form: {
id: null,
name: ''
},
//
rules: {
name: [
{ required: true, message: '请输入标签名' }
]
}
}
},
async created () {
this.config({
api: await import('@/api/cms/article.tag'),
'field.id': 'id'
})
}
}
</script>

View File

@ -0,0 +1,255 @@
<!-- 获取验证字段 -->
<template>
<el-dialog
ref="dialog"
:title="title"
v-model="visible"
:confirm-working="isWorking"
:fullscreen="true"
:append-to-body="true"
:close-on-press-escape="false"
:show-close="false"
:close-on-click-modal="false"
class="article-dialog"
@close="$emit('exit')"
>
<template #header>
<h3>{{ title }}</h3>
<div class="opera">
<el-button
v-if="form.status === $const.ARTICLE_STATUS.ONLINE"
type="primary"
:disabled="unsavable"
:loading="isWorking.save"
@click="save"
>更新</el-button>
<template v-else>
<el-button
:disabled="unsavable"
:loading="isWorking.save"
@click="save"
>保存</el-button>
<el-button
type="primary"
:disabled="isWorking.publish"
:loading="isWorking.publish"
@click="publish"
>发布</el-button>
</template>
<el-button
type="danger"
@click="exit"
>退出</el-button>
</div>
</template>
<div class="content-wrap">
<div class="settings-wrap">
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="所属栏目" prop="categoryIds" required>
<CategorySelect v-model="form.categoryIds"/>
</el-form-item>
<el-form-item label="文章摘要" prop="contentDigest" class="form-item-with-opera" required>
<template #label>
<span>文章摘要</span>
<el-button size="small" @click="extractDigest">自动提取</el-button>
</template>
<el-input
v-model="form.contentDigest"
type="textarea"
:rows="5"
placeholder="请输入文章摘要"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="form.keywords"
type="textarea"
:rows="2"
placeholder="请输入文章关键字"
v-trim
maxlength="200"
/>
<FormItemTip>文章关键字用于SEO优化</FormItemTip>
</el-form-item>
<el-form-item label="文章标签" prop="tagIds">
<ArticleTagSelect v-model="form.tagIds"/>
</el-form-item>
<el-form-item label="文章封面图" prop="cover" class="form-item-with-opera">
<template #label>
<span>文章封面图</span>
<el-button size="small" @click="openCoverSelect">从正文中选取</el-button>
</template>
<ImageUploader v-model="form.cover"/>
</el-form-item>
<el-form-item label="附件上传" prop="attachments" class="form-item-with-opera form-item-attachments">
<template #label>
<span>附件上传</span>
<ArticleAttachmentUploader
@add-file="handleAddAttachment"
@success="handleAttachmentUploadSuccess"
@error="handleAttachmentUploadError"
>
<el-button size="small" type="primary" icon="Upload">上传附件</el-button>
</ArticleAttachmentUploader>
</template>
<!-- 附件列表展示 -->
<ArticleAttachments v-model="form.attachments"/>
</el-form-item>
</el-form>
</div>
<div class="editor-wrap">
<el-input
v-model="form.title"
placeholder="请输入文章标题"
v-trim
maxlength="200"
/>
<RichEditor
ref="richEditor"
v-model="form.content"
placeholder="请输入文章内容"
@created="handleEditorCreated"
/>
</div>
</div>
<!-- 正文图片选择 -->
<ArticleCoverSelect ref="articleCoverSelect" @confirm-select="form.cover = $event"/>
<!-- 退出窗口 -->
<el-dialog
class="article-exit-dialog"
v-model="exitDialog.visible"
align-center
width="420px"
title="确认退出"
>
<Icon value="WarningFilled" size="24px"/>{{ exitDialog.message }}
<template #footer>
<el-button @click="exitDialog.visible = false">取消</el-button>
<el-button type="danger" @click="forceClose">直接退出</el-button>
<el-button type="primary" @click="saveAndClose">{{ exitDialog.confirmButtonText }}</el-button>
</template>
</el-dialog>
</el-dialog>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import RichEditor from '@/components/common/RichEditor'
import ImageUploader from '@/components/common/ImageUploader'
import CategorySelect from '@/components/cms/category/CategorySelect'
import ArticleTagSelect from '@/components/cms/article/ArticleTagSelect'
import ArticleCoverSelect from '@/components/cms/article/ArticleCoverSelect'
import ArticleAttachments from '@/components/cms/article/ArticleAttachments'
import ArticleAttachmentUploader from '@/components/cms/article/ArticleAttachmentUploader'
import BaseMixin from '@/components/cms/article/editor/BaseMixin'
import AttachmentsMixin from '@/components/cms/article/editor/AttachmentsMixin'
export default {
name: 'ArticleEditorWindow',
extends: BaseOpera,
mixins: [BaseMixin, AttachmentsMixin],
components: {
ArticleAttachmentUploader,
ArticleAttachments,
ArticleCoverSelect,
ArticleTagSelect,
CategorySelect,
ImageUploader,
RichEditor
},
data () {
return {
visible: false,
isWorking: {
save: false,
publish: false
},
//
form: {
id: null,
uid: '',
title: '',
status: '',
categoryIds: [],
tagIds: [],
cover: '',
attachments: [],
content: '',
contentDigest: '',
keywords: ''
},
//
savedForm: null,
//
rules: {
categoryIds: [
{ required: true, message: '请选择所属栏目' }
],
contentDigest: [
{ required: true, message: '请输入文章摘要' }
]
},
// 退
exitDialog: {
visible: false,
message: '',
confirmButtonText: ''
}
}
},
computed: {
//
unsavable () {
return this.isWorking.save || JSON.stringify(this.form) === JSON.stringify(this.savedForm)
}
},
methods: {
/**
* 打开窗口
*
* @param title 标题
* @param target 文章对象
*/
open (title, target) {
this.visible = true
this.title = title
this.$nextTick(() => {
//
this.$refs.form.resetFields()
// -
this.form.id = null
this.form.uid = ''
this.form.title = ''
this.form.content = ''
this.form.status = ''
//
if (target != null) {
for (const key in this.form) {
this.form[key] = target[key]
}
//
this.form.categoryIds = target.categoryIds == null || target.categoryIds.length === 0 ?
[] : target.categoryIds.split(',')
.filter(item => item.trim() !== '')
.map(item => Number(item))
//
this.form.tagIds = target.tagIds == null || target.tagIds.length === 0 ?
[] : target.tagIds.split(',')
.filter(item => item.trim() !== '')
.map(item => Number(item))
//
this.form.attachments = target.attachments == null ?
[] : JSON.parse(target.attachments)
}
//
this.savedForm = JSON.parse(JSON.stringify(this.form))
})
}
}
}
</script>
<style lang="scss">
@import "./editor";
</style>

View File

@ -0,0 +1,34 @@
<script>
export default {
name: 'AttachmentsMixin',
methods: {
//
handleAddAttachment (file) {
this.form.attachments.push({
uid: file.uid,
name: file.name,
lastModified: file.lastModified,
size: file.size,
//
__uploading: true
})
},
//
handleAttachmentUploadSuccess ({ data, file }) {
const targetFile = this.form.attachments.find(item => item.uid === file.uid)
if (targetFile != null) {
targetFile.fileKey = data.fileKey
delete targetFile.__uploading
}
},
//
handleAttachmentUploadError ({ error, file }) {
const targetFileIndex = this.form.attachments.findIndex(item => item.uid === file.uid)
if (targetFileIndex !== -1) {
this.form.attachments.splice(targetFileIndex, 1)
this.$tip.apiFailed(`附件 ${file.name} 上传失败,原因:${error}`)
}
}
}
}
</script>

View File

@ -0,0 +1,145 @@
<script>
import { publish, save } from '@/api/cms/article'
export default {
name: 'BaseMixin',
methods: {
//
openCoverSelect () {
this.$refs.articleCoverSelect.open(this.$refs.richEditor.getImages())
},
// 稿/
save () {
if (this.isWorking.save) {
return Promise.resolve()
}
if (this.form.title.trim() === '') {
this.$tip.warning('请填写文章标题!')
return
}
for (const attach of this.form.attachments) {
if (attach.__uploading) {
this.$tip.warning('存在正在上传中的附件,请等待上传完成!')
return
}
}
return new Promise((resolve, reject) => {
this.isWorking.save = true
save ({
...this.form,
// ,便
categoryIds: ',' + this.form.categoryIds.join(',') + ',',
// ,"便
tagIds: ',' + this.form.tagIds.join(',') + ',',
//
images: JSON.stringify(this.$refs.richEditor.getImages()),
//
attachments: JSON.stringify(this.form.attachments)
})
.then(data => {
this.form.id = data.id
//
this.savedForm = JSON.parse(JSON.stringify(this.form))
resolve(data)
})
.catch(e => {
this.$tip.apiFailed(e)
reject(e)
})
.finally(() => {
this.isWorking.save = false
})
})
},
//
saveAndClose () {
this.save()
.then(() => {
this.forceClose()
})
},
//
forceClose () {
this.visible = false
this.exitDialog.visible = false
},
// 退
exit () {
//
// - <p><br></p>
const copyForm = JSON.parse(JSON.stringify(this.form))
copyForm.content = copyForm.content === '<p><br></p>' ? '' : copyForm.content
if (JSON.stringify(copyForm) === JSON.stringify(this.savedForm)) {
this.visible = false
return
}
//
let operaName = '保存'
if (this.form.status === this.$const.ARTICLE_STATUS.ONLINE) {
operaName = '更新'
}
this.exitDialog.message = `当前存在数据还未${operaName},是否继续退出?`
this.exitDialog.confirmButtonText = `${operaName}并退出`
this.exitDialog.visible = true
},
//
publish () {
if (this.isWorking.publish) {
return
}
if (this.form.title.trim() === '') {
this.$tip.warning('请填写文章标题!')
return
}
if (this.$refs.richEditor.getText().trim() === '' &&
this.$refs.richEditor.getImages().length === 0 &&
this.form.attachments.length === 0) {
this.$tip.warning('请输入文章内容!')
return
}
for (const attach of this.form.attachments) {
if (attach.__uploading) {
this.$tip.warning('存在正在上传中的附件,请等待上传完成!')
return
}
}
this.$refs.form.validate(valid => {
if (!valid) {
return
}
this.isWorking.publish = true
//
this.save()
.then(() => {
//
publish(this.form.id)
.then(() => {
//
this.visible = false
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.isWorking.publish = false
})
})
.catch(() => {
this.isWorking.publish = false
})
})
},
//
extractDigest () {
this.form.contentDigest = this.$refs.richEditor.getDigest()
},
//
handleEditorCreated () {
//
this.$refs.richEditor.enableImageUpload({
url: '/cms/article/upload/image',
maxFileSize: 5
})
}
}
}
</script>

View File

@ -0,0 +1,105 @@
.article-dialog {
display: flex;
flex-direction: column;
padding: 0;
border-radius: 0;
& > .el-dialog__header {
flex-shrink: 0;
margin: 0;
padding: 10px 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
justify-content: space-between;
h3 {
font-weight: normal;
font-size: 16px;
}
// 关闭按钮
.el-dialog__headerbtn {
top: 3px;
font-size: 25px;
}
}
& > .el-dialog__body {
flex-grow: 1;
overflow: hidden;
}
.content-wrap {
height: 100%;
overflow: hidden;
display: flex;
& > * {
height: 100%;
}
// 设置
.settings-wrap {
overflow-y: auto;
padding: 20px;
box-sizing: border-box;
width: 15%;
min-width: 255px;
max-width: 350px;
flex-shrink: 0;
background-color: #f7f7f7;
border-right: 1px solid #eee;
// 表单项
.el-form-item {
display: block;
}
// 标签
.el-form-item__label {
float: none;
}
// 元素宽度为100%
.el-form-item__content{
& > * {
width: 100%;
}
}
// 带操作的 form-item
.form-item-with-opera {
.el-form-item__label {
width: 100%;
align-items: center;
justify-content: flex-start;
padding-right: 0;
position: relative;
.el-button {
position: absolute;
right: 0;
}
}
}
}
// 编辑器
.editor-wrap {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
& > .el-input {
height: 60px;
font-size: 25px;
flex-shrink: 0;
.el-input__wrapper {
box-shadow: none;
}
}
& > .rich-editor {
flex-grow: 1;
}
}
}
}
// 退出
.article-exit-dialog {
.el-dialog__body {
display: flex;
align-items: center;
.icon {
color: var(--el-color-warning);
margin-right: 10px;
}
}
}

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,105 @@
<template>
<TreeSelect
:placeholder="placeholder"
:data="data"
:append-to-body="appendToBody"
:clearable="clearable"
:inline="inline"
node-key="value"
:multiple="multiple"
:default-expand-all="true"
/>
</template>
<script>
import TreeSelect from '@/components/common/TreeSelect'
import { fetchAll } from '@/api/cms/category'
export default {
name: 'CategorySelect',
components: { TreeSelect },
props: {
valueKey: {
default: 'id'
},
multiple: {
default: true
},
inline: {
default: true
},
placeholder: {
default: '请选择栏目'
},
//
clearable: {
type: Boolean,
default: false
},
appendToBody: {
default: false
},
// ID
excludeValue: {},
//
firstOptions: {
default () {
return []
}
}
},
data () {
return {
data: []
}
},
watch: {
excludeValue () {
this.fetchData()
}
},
methods: {
/**
* 获取所有菜单
*/
fetchData () {
fetchAll()
.then(records => {
this.data = [
...this.firstOptions
]
this.__fillData(this.data, records)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
//
__fillData (list, pool) {
for (const category of pool) {
//
if (category[this.valueKey] === this.excludeValue) {
continue
}
const categoryNode = {
value: category[this.valueKey],
label: category.title,
//
disabled: category.type !== this.$const.CATEGORY_TYPE.DEFAULT
}
list.push(categoryNode)
if (category.children != null && category.children.length > 0) {
categoryNode.children = []
this.__fillData(categoryNode.children, category.children)
if (categoryNode.children.length === 0) {
delete categoryNode.children
}
}
}
}
},
created () {
this.fetchData()
}
}
</script>

View File

@ -0,0 +1,310 @@
<!-- 获取验证字段 -->
<template>
<GlobalWindow
:title="title"
v-model:visible="visible"
:confirm-working="isWorking"
width="750px"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="上级栏目" prop="parentId">
<CategorySelect v-model="form.parentId" :multiple="false" :clearable="true"/>
</el-form-item>
<el-form-item label="栏目标题" prop="title" required>
<el-input
v-model="form.title"
placeholder="请输入栏目标题"
v-trim
maxlength="200"
@input="paddingUid"
/>
</el-form-item>
<el-form-item label="栏目图标" prop="icon">
<IconSelect v-if="visible" v-model="form.icon"/>
</el-form-item>
<el-form-item label="唯一标识" prop="uid" required>
<el-input
v-model="form.uid"
placeholder="请输入唯一标识"
v-trim
maxlength="50"
/>
</el-form-item>
<el-form-item label="类型" prop="type" required>
<DictRadioGroup code="CATEGORY_TYPES" v-model="form.type" @change="handleTypeChange"/>
</el-form-item>
<el-form-item
v-if="form.type === $const.CATEGORY_TYPE.IN_LINK || form.type === $const.CATEGORY_TYPE.OUT_LINK"
label="链接地址"
prop="uri"
required
>
<el-input
v-model="form.uri"
placeholder="请输入链接地址"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item
v-if="visible && form.type === $const.CATEGORY_TYPE.DEFAULT"
label="模板"
prop="templateId"
>
<TemplateSelect
ref="templateSelect"
v-model="form.templateId"
v-model:parameters="form.templateParameters"
/>
</el-form-item>
<el-form-item label="状态" prop="hidden" required>
<el-switch
v-model="form.hidden"
active-text="显示"
:active-value="false"
inactive-text="隐藏"
:inactive-value="true"
/>
</el-form-item>
<el-form-item label="排序" prop="sort" required>
<el-input-number
v-model="form.sort"
placeholder="请输入排序"
:controls="false"
:min="1"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item v-if="form.type === $const.CATEGORY_TYPE.DEFAULT" label="权限码" prop="permission">
<el-input
v-model="form.permission"
placeholder="请输入权限码"
v-trim
maxlength="50"
/>
</el-form-item>
<el-form-item label="栏目配置" prop="config">
<el-input
type="textarea"
:rows="10"
v-model="form.config"
placeholder="请输入栏目配置"
v-trim
show-word-limit
maxlength="2000"
/>
<FormItemTip>栏目配置用于对栏目做进一步自定义设置无需配置可不填写</FormItemTip>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import CategorySelect from '@/components/cms/category/CategorySelect'
import TemplateSelect from '@/components/cms/template/TemplateSelect'
import IconSelect from '@/components/system/icon/IconSelect'
import { pinyin } from 'pinyin-pro'
import { checkUrl, checkRelativeUrl } from '@/core/utils/form'
export default {
name: 'OperaCategoryWindow',
extends: BaseOpera,
components: {
IconSelect,
TemplateSelect,
CategorySelect,
GlobalWindow
},
data () {
const _this = this
return {
//
parent: null,
//
originUid: null,
//
form: {
id: null,
parentId: null,
uid: '',
title: '',
type: 'DEFAULT',
uri: '',
icon: '',
permission: '',
templateId: null,
templateParameters: null,
hidden: false,
sort: 1,
config: ''
},
//
rules: {
title: [
{ required: true, message: '请输入栏目标题' }
],
uid: [
{ required: true, message: '请输入唯一标识' }
],
type: [
{ required: true, message: '请输入类型' }
],
uri: [
{ required: true, message: '请输入链接地址' },
{ validator: _this.checkLink }
],
sort: [
{ required: true, message: '请输入排序' }
]
}
}
},
methods: {
/**
* 打开窗口
*
* @param title 窗口标题
* @param target 行对象仅编辑需该参数
* @param parent 父栏目
*/
open (title, target, parent) {
this.title = title
this.parent = parent
this.visible = true
//
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
this.form.parentId = parent == null ? null : parent.id
this.form.templateId = null
this.form.templateParameters = null
})
return
}
//
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
//
this.originUid = this.form.uid
//
this.form.templateParameters = target.templateParameters == null || target.templateParameters === '' ?
[] : JSON.parse(this.form.templateParameters)
})
},
/**
* 确认点击确认按钮后触发
*/
confirm () {
if (this.form[this.configData['field.id']] == null || this.form[this.configData['field.id']] === '') {
this.__confirmCreate()
return
}
if (this.form.uid === this.originUid) {
this.__confirmEdit()
return
}
//
this.$dialog.confirm('唯一标识可能会在源码中使用,修改后需及时修改源码,确认修改吗?如不确定,请咨询超级管理员!', '提示', {
confirmButtonText: '确认修改',
confirmButtonClass: 'el-button--danger',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.__confirmEdit()
}).catch(() => {})
},
//
handleTypeChange () {
if (this.form.type === this.$const.CATEGORY_TYPE.DEFAULT) {
this.form.uri = ''
}
if (this.form.type === this.$const.CATEGORY_TYPE.IN_LINK || this.form.type === this.$const.CATEGORY_TYPE.OUT_LINK) {
this.form.templateId = null
this.form.templateParameters = null
}
},
/**
* 补充uid
*
* @param name 栏目名称
*/
paddingUid (name) {
//
if (this.form.id != null) {
return
}
this.form.uid = pinyin(name, {
toneType: 'none',
type: 'array'
}).map(item => item.substring(0,1)).join('')
},
//
checkLink (rule, value, callback) {
if (this.form.type === this.$const.CATEGORY_TYPE.IN_LINK) {
checkRelativeUrl(rule, value, callback)
} else {
checkUrl(rule, value, callback)
}
},
/**
* 获取新增编辑表单对象
*
* @returns {*}
* @private
*/
__getForm () {
const form = JSON.parse(JSON.stringify(this.form))
// JSON
if (form.templateParameters != null) {
form.templateParameters = JSON.stringify(form.templateParameters)
}
return form
},
/**
* 验证表单
*
* @param callback 验证回调
* @private
*/
__validateForm (callback) {
this.$refs.form.validate((valid) => {
if (!valid) {
callback(valid)
return
}
if (!this.$refs.templateSelect) {
callback(valid)
return
}
this.$refs.templateSelect.validateForm()
.then(() => {
callback(true)
})
.catch(() => {
callback(false)
})
})
}
},
async created () {
this.config({
api: await import('@/api/cms/category'),
'field.id': 'id'
})
}
}
</script>
<style scoped lang="scss">
:deep(.el-input-number) {
.el-input__inner {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,150 @@
<!-- 获取验证字段 -->
<template>
<GlobalWindow
:title="title"
v-model:visible="visible"
:confirm-working="isWorking"
width="750px"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="唯一标识" prop="uid" required>
<el-input
v-model="form.uid"
placeholder="请输入唯一标识"
v-trim
maxlength="50"
/>
</el-form-item>
<el-form-item label="组名" prop="name" required>
<el-input
v-model="form.name"
placeholder="请输入组名"
v-trim
maxlength="50"
/>
</el-form-item>
<el-form-item label="支持的资源类型" prop="supportedTypes">
<DictSelect
v-model="form.supportedTypes"
code="RESOURCE_TYPES"
placeholder="请选择支持的资源类型"
:multiple="true"
/>
<FormItemTip>选择支持的资源类型可避免创建资源时指定不支持的类型</FormItemTip>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
v-trim
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaResourceGroupWindow',
extends: BaseOpera,
components: {
GlobalWindow
},
data () {
return {
//
originUid: null,
//
form: {
id: null,
uid: '',
name: '',
supportedTypes: [],
remark: ''
},
//
rules: {
uid: [
{ required: true, message: '请输入唯一标识' }
],
name: [
{ required: true, message: '请输入组名' }
]
}
}
},
methods: {
/**
* 打开窗口
*
* @param title 窗口标题
* @param target 行对象仅编辑需该参数
*/
open (title, target) {
this.title = title
this.visible = true
//
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
//
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
//
this.originUid = this.form.uid
// -
this.form.supportedTypes = target.supportedTypes == null ? [] : target.supportedTypes.split(',')
.filter(item => item.trim() !== '')
})
},
/**
* 确认点击确认按钮后触发
*/
confirm () {
if (this.form[this.configData['field.id']] == null || this.form[this.configData['field.id']] === '') {
this.__confirmCreate()
return
}
if (this.form.uid === this.originUid) {
this.__confirmEdit()
return
}
//
this.$dialog.confirm('唯一标识可能会在源码中使用,修改后需及时修改源码,确认修改吗?如不确定,请咨询超级管理员!', '提示', {
confirmButtonText: '确认修改',
confirmButtonClass: 'el-button--danger',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.__confirmEdit()
}).catch(() => {})
},
//
__getForm () {
const form = JSON.parse(JSON.stringify(this.form))
form.supportedTypes = form.supportedTypes.join(',')
return form
}
},
async created () {
this.config({
api: await import('@/api/cms/resource.group'),
'field.id': 'id'
})
}
}
</script>

View File

@ -0,0 +1,213 @@
<!-- 获取验证字段 -->
<template>
<GlobalWindow
:title="title"
v-model:visible="visible"
:confirm-working="isWorking"
width="750px"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="资源标题" prop="title" required>
<el-input
v-model="form.title"
placeholder="请输入资源标题"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item label="资源副标题" prop="subTitle">
<el-input
v-model="form.subTitle"
placeholder="请输入资源副标题"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item label="资源值类型" prop="valueType" required>
<DictSelect
v-model="form.valueType"
code="RESOURCE_TYPES"
placeholder="请选择资源值类型"
:only-values="supportedTypes"
@change="form.value = ''"
/>
</el-form-item>
<el-form-item v-if="form.valueType != null && form.valueType !== ''" label="资源值" prop="value" required>
<ResourceInput
ref="resourceInput"
:type="form.valueType"
v-model="form.value"
:image-tip="imageTip"
/>
</el-form-item>
<el-form-item label="状态" prop="disabled" required class="form-item-status">
<el-switch
v-model="form.disabled"
:active-value="false"
active-text="启用"
:inactive-value="true"
inactive-text="禁用"
/>
</el-form-item>
<el-form-item label="排序" prop="sort" required>
<el-input-number
v-model="form.sort"
placeholder="请输入排序"
:controls="false"
:min="1"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item label="资源描述" prop="description">
<el-input
type="textarea"
:rows="3"
v-model="form.description"
placeholder="请输入资源描述"
v-trim
show-word-limit
maxlength="200"
/>
</el-form-item>
<el-form-item label="资源配置" prop="config">
<el-input
type="textarea"
:rows="5"
v-model="form.config"
placeholder="请输入资源配置"
v-trim
show-word-limit
maxlength="2000"
/>
<FormItemTip>资源配置用于通过JSON或其它形式的内容进一步对资源做设定</FormItemTip>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
import ResourceInput from '@/components/cms/resource/input/ResourceInput'
export default {
name: 'OperaResourceWindow',
extends: BaseOpera,
components: {
ResourceInput,
GlobalWindow
},
data () {
return {
//
group: null,
//
form: {
id: null,
groupId: '',
title: '',
subTitle: '',
sort: 1,
description: '',
value: '',
valueType: null,
config: '',
disabled: false
},
//
rules: {
title: [
{ required: true, message: '请输入资源标题' }
],
value: [
{ required: true, message: '请补充资源值' }
]
}
}
},
computed: {
supportedTypes () {
if (this.group == null) {
return []
}
if (this.group.supportedTypes == null || this.group.supportedTypes.length === 0) {
return []
}
return this.group.supportedTypes.split(',').filter(type => type.trim() !== '')
}
},
methods: {
/**
* 打开窗口
*
* @param title 窗口标题
* @param group 所属资源组
* @param target 行对象仅编辑需该参数
*/
open (title, group, target) {
this.group = group
this.title = title
this.visible = true
//
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form.id = null
this.form.groupId = group.id
})
return
}
//
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
})
},
/**
* 验证表单
*
* @param callback 验证回调
* @private
*/
__validateForm (callback) {
Promise.all([this.$refs.form.validate(), this.$refs.resourceInput.validate()])
.then(result => {
callback(result[0], result[1])
})
.catch(() => {
callback(false)
})
},
},
async created () {
this.config({
api: await import('@/api/cms/resource'),
'field.id': 'id'
})
}
}
</script>
<style scoped lang="scss">
:deep(.el-input-number) {
.el-input__inner {
text-align: left;
}
}
:deep(.form-item-status) {
.el-form-item__content{
display: flex;
& > * {
width: auto !important;
}
}
.status-text {
color: #999;
margin-left: 6px;
font-size: 13px;
vertical-align: middle;
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<el-select
class="resource-select"
:class="{select__block: !inline}"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:multiple="multiple"
@change="$emit('change', $event)"
@input="$emit('input', $event)"
>
<el-option
v-for="data in list"
:key="data.id"
:value="data[valueKey]"
:label="data.name"
/>
</el-select>
</template>
<script>
import { fetchAll } from '@/api/cms/resource.group'
export default {
name: 'ResourceGroupSelect',
props: {
disabled: {
default: false
},
valueKey: {
default: 'id'
},
placeholder: {
default: '请选择资源组'
},
inline: {
default: true
},
multiple: {
default: false
},
clearable: {
default: true
}
},
data () {
return {
loading: false,
list: []
}
},
methods: {
//
fetchList () {
if (this.loading) {
return
}
this.loading = true
fetchAll()
.then(data => {
this.list = data
})
.catch(e => {
this.$tip.apiFailed(e)
})
.finally(() => {
this.loading = false
})
}
},
created () {
this.fetchList()
}
}
</script>
<style lang="scss" scoped>
.select__block {
display: block;
}
</style>

View File

@ -0,0 +1,216 @@
<template>
<GlobalWindow
v-if="group != null"
:title="group.name + '资源管理'"
width="1200px"
v-model:visible="visible"
:with-footer="false"
:wrapper-closable="true"
:close-on-press-escape="true"
>
<TableLayout
:authorized="$hasPermissions([`cms:resource:query`])"
:fullscreen="fullscreen"
>
<!-- 表格和分页 -->
<template #table-wrap>
<SearchTable
v-loading="isWorking.search"
:data="tableData.list"
:with-fullscreen="false"
@selection-change="handleSelectionChange"
@refresh="refresh"
>
<template
v-if="$hasAnyPermissions([
'cms:resource:create',
'cms:resource:delete'
])"
#toolbar
>
<el-button
v-permissions="['cms:resource:create']"
type="primary"
icon="Plus"
@click="$refs.operaResourceWindow.open('新建资源', group)"
>新建</el-button>
<el-button
v-permissions="['cms:resource:delete']"
type="danger"
icon="Delete"
@click="deleteByIdInBatch"
>删除</el-button>
</template>
<el-table-column type="selection" fixed="left" width="55"/>
<el-table-column prop="title" label="资源标题" min-width="150px"/>
<el-table-column prop="sort" label="排序" min-width="80px"/>
<el-table-column prop="subTitle" label="资源副标题" min-width="150px"/>
<el-table-column prop="description" label="资源描述" min-width="150px">
<template #default="{ row }">
<PopoverCellValue :content="row.description"/>
</template>
</el-table-column>
<el-table-column prop="value" label="资源值" min-width="200px">
<template #default="{ row }">
<ResourcePreview :value="row.value" :type="row.valueType" />
</template>
</el-table-column>
<el-table-column prop="valueType" label="资源值类型" min-width="100px">
<template #default="{ row }">
{{ $d(`RESOURCE_TYPES.${row.valueType}`) }}
</template>
</el-table-column>
<el-table-column
v-if="$hasPermissions(['cms:resource:update'])"
prop="disabled"
label="是否启用"
min-width="80px"
>
<template #default="{ row }">
<el-switch
v-model="row.disabled"
:active-value="false"
:inactive-value="true"
@change="switchDisabled(row)"
/>
</template>
</el-table-column>
<el-table-column
v-else
prop="disabled"
label="状态"
min-width="80px"
>
<template #default="{ row }">
{{$filters.disabledText(row.disabled)}}
</template>
</el-table-column>
<el-table-column prop="config" label="资源配置" min-width="200px">
<template #default="{ row }">
<PopoverCellValue :content="row.description"/>
</template>
</el-table-column>
<el-table-column prop="creatorRealName" label="创建人" min-width="100px"/>
<el-table-column prop="createdAt" label="创建时间" min-width="185px"/>
<el-table-column prop="updaterRealName" label="更新人" min-width="100px"/>
<el-table-column prop="updatedAt" label="更新时间" min-width="185px"/>
<template
v-if="$hasAnyPermissions([
'cms:resource:update',
'cms:resource:delete'
])"
#buttons="{ row }"
>
<el-button
type="primary"
icon="EditPen"
v-permissions="['cms:resource:update']"
@click="$refs.operaResourceWindow.open(`编辑${row.title}`, group, row)"
link
>编辑</el-button>
<el-button
type="danger"
icon="Delete"
v-permissions="['cms:resource:delete']"
@click="deleteById(row)"
link
>删除</el-button>
</template>
</SearchTable>
<pagination
:pagination="tableData.pagination"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</template>
<!-- 新建编辑窗口 -->
<OperaResourceWindow
ref="operaResourceWindow"
@success="refresh"
/>
</TableLayout>
</GlobalWindow>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import OperaResourceWindow from '@/components/cms/resource/OperaResourceWindow'
import ResourcePreview from '@/components/cms/resource/preview/ResourcePreview'
import { updateStatus } from '@/api/cms/resource'
export default {
name: 'ResourceManageWindow',
extends: BaseTable,
components: { ResourcePreview, OperaResourceWindow },
data () {
return {
visible: false,
group: null,
//
searchForm: {
groupId: null
}
}
},
methods: {
/**
* 打开窗口
*
* @param group 资源组对象
*/
open (group) {
this.group = group
this.searchForm.groupId = group.id
this.visible = true
this.search()
},
/**
* 启用/禁用
*
* @param row 行对象
*/
switchDisabled (row) {
const newValue = row.disabled
row.disabled = !row.disabled
//
if (!newValue) {
this.__updateStatus(row, newValue)
return
}
//
this.$dialog.disableConfirm(`确认禁用 ${row.title} 资源吗?`)
.then(() => {
this.__updateStatus(row, newValue)
})
.catch(() => {
})
},
/**
* 修改状态
*
* @param row 行对象
* @param newValue 新值
* @private
*/
__updateStatus (row, newValue) {
updateStatus({
id: row.id,
disabled: newValue
})
.then(() => {
row.disabled = newValue
this.$tip.apiSuccess('修改成功')
})
.catch(e => {
this.$tip.apiFailed(e)
})
}
},
async created () {
this.config({
module: '资源',
api: await import('@/api/cms/resource')
})
}
}
</script>

View File

@ -0,0 +1,15 @@
<template>
<ImageUploader preview-height="450px"/>
</template>
<script>
import ImageUploader from '@/components/common/ImageUploader'
export default {
name: 'ImageInput',
components: {
ImageUploader
}
}
</script>

View File

@ -0,0 +1,84 @@
<template>
<el-form ref="form" class="image-link-input-form" :model="modelValueObj" :rules="rules">
<el-form-item label="资源链接" prop="link" required>
<LinkInput v-model="modelValueObj.link" @change="handleChange"/>
<FormItemTip>资源链接可为相对路径也可以为绝对路径</FormItemTip>
</el-form-item>
<el-form-item label="资源图片" prop="image" required>
<ImageInput v-model="modelValueObj.image" @change="handleChange"/>
<FormItemTip v-if="imageTip != null && imageTip !== ''">{{ imageTip }}</FormItemTip>
</el-form-item>
</el-form>
</template>
<script>
import LinkInput from './LinkInput'
import ImageInput from './ImageInput'
export default {
name: 'ImageLinkInput',
components: { ImageInput, LinkInput },
props: {
// { image: '', link: '' }
modelValue: {
type: String
},
//
imageTip: {
required: false
}
},
data () {
return {
modelValueObj: {
image: '',
link: ''
},
rules: {
image: [
{ required: true, message: '请上传图片', trigger: 'blur' }
],
link: [
{ required: true, message: '请输入链接地址', trigger: 'blur' }
]
}
}
},
watch: {
modelValue: {
immediate: true,
handler () {
this.initModelValueObj()
}
}
},
methods: {
//
initModelValueObj () {
this.modelValueObj.image = null
this.modelValueObj.link = null
if (this.modelValue != null && this.modelValue.length > 0) {
const modelValueObj = JSON.parse(this.modelValue)
this.modelValueObj.image = modelValueObj.image
this.modelValueObj.link = modelValueObj.link
}
},
handleChange () {
this.$emit('update:modelValue', JSON.stringify(this.modelValueObj))
},
validate () {
return this.$refs.form.validate()
}
}
}
</script>
<style scoped lang="scss">
.image-link-input-form {
padding: 0 20px;
background-color: var(--background-color);
.el-form-item {
margin-bottom: 16px;
}
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<el-input placeholder="请输入资源链接"/>
</template>
<script>
export default {
name: 'LinkInput'
}
</script>

View File

@ -0,0 +1,46 @@
<template>
<component ref="component" :is="component" />
</template>
<script>
const map = {
IMAGE: 'ImageInput',
LINK: 'LinkInput',
IMAGE_LINK: 'ImageLinkInput'
}
import ImageInput from './ImageInput'
import LinkInput from './LinkInput'
import ImageLinkInput from './ImageLinkInput'
export default {
name: 'ResourceInput',
components: {
ImageInput,
LinkInput,
ImageLinkInput
},
props: {
//
type: {
required: true
}
},
computed: {
component () {
let targetComponent = this.$options.components[map[this.type]]
if (targetComponent == null) {
throw new Error(`未能匹配资源类型 ${this.type}`)
}
return targetComponent
}
},
methods: {
//
validate () {
if (this.$refs.component.validate) {
return this.$refs.component.validate()
}
return Promise.resolve()
}
}
}
</script>

View File

@ -0,0 +1,59 @@
<template>
<div class="image-link-preview">
<el-image
:src="$getImageURL(valueObj.image)"
:zoom-rate="1.2"
:max-scale="3"
:min-scale="0.5"
:preview-src-list="[$getImageURL(valueObj.image)]"
:preview-teleported="true"
:hide-on-click-modal="true"
:initial-index="0"
fit="cover"
/>
<p>{{ valueObj.link }}</p>
</div>
</template>
<script>
export default {
name: 'ImageLinkPreview',
props: {
value: {
required: true
}
},
computed: {
valueObj () {
return JSON.parse(this.value)
}
}
}
</script>
<style scoped lang="scss">
.image-link-preview {
width: 150px;
height: 120px;
position: relative;
.el-image {
width: 100%;
height: 100%;
}
p {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 3px 5px;
margin: 0;
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<el-image
class="resource-image-preview"
:src="$getImageURL(value)"
:zoom-rate="1.2"
:max-scale="3"
:min-scale="0.5"
:preview-src-list="[$getImageURL(value)]"
:preview-teleported="true"
:hide-on-click-modal="true"
:initial-index="0"
fit="cover"
/>
</template>
<script>
export default {
name: 'ImagePreview',
props: {
value: {
required: true
}
}
}
</script>
<style scoped lang="scss">
.resource-image-preview {
width: 150px;
height: 100px;
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<a :href="value" target="_blank">{{ value }}</a>
</template>
<script>
export default {
name: 'LinkPreview',
props: {
value: {
required: true
}
}
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,38 @@
<template>
<template v-if="component == null">错误类型数据</template>
<component v-else ref="component" :is="component" :value="value"/>
</template>
<script>
const map = {
IMAGE: 'ImagePreview',
LINK: 'LinkPreview',
IMAGE_LINK: 'ImageLinkPreview'
}
import ImagePreview from './ImagePreview'
import LinkPreview from './LinkPreview'
import ImageLinkPreview from './ImageLinkPreview'
export default {
name: 'ResourcePreview',
components: {
ImagePreview,
LinkPreview,
ImageLinkPreview
},
props: {
//
type: {
required: true
},
//
value: {
required: true
}
},
computed: {
component () {
return this.$options.components[map[this.type]]
}
},
}
</script>

View File

@ -0,0 +1,150 @@
<!-- 获取验证字段 -->
<template>
<GlobalWindow
:title="title"
v-model:visible="visible"
:confirm-working="isWorking"
width="750px"
@confirm="confirm"
>
<el-form :model="form" ref="form" :rules="rules">
<el-form-item label="模板名称" prop="name" required>
<el-input
v-model="form.name"
placeholder="请输入模板名称"
v-trim
maxlength="200"
/>
</el-form-item>
<el-form-item label="模板文件路径" prop="path" required>
<el-input
v-model="form.path"
placeholder="请输入模板路径"
v-trim
maxlength="200"
/>
<FormItemTip>
模板文件路径也是模板的唯一标识用于查询模板的相关配置
<a href="https://www.yuque.com/u21334242/eva-cms/wz7s8t6cmd9t4pg1" target="_blank">查看更详细的说明</a>
</FormItemTip>
</el-form-item>
<el-form-item label="访问路径" prop="accessUri" required>
<el-input
v-model="form.accessUri"
placeholder="如:/article/${articleUid}"
v-trim
maxlength="200"
/>
<FormItemTip>
访问路径将使用freemarker解析作为挂载该模板数据的访问地址
<a href="https://www.yuque.com/u21334242/eva-cms/wz7s8t6cmd9t4pg1" target="_blank">查看更详细的说明</a>
</FormItemTip>
</el-form-item>
<el-form-item v-if="visible" label="模板数据" prop="presetDataCodes">
<DictSelect
code="TEMPLATE_DATA_LIST"
v-model="form.presetDataCodes"
placeholder="请选择模板数据"
:multiple="true"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
:rows="3"
v-model="form.remark"
placeholder="请输入备注"
v-trim
show-word-limit
maxlength="200"
/>
</el-form-item>
</el-form>
</GlobalWindow>
</template>
<script>
import BaseOpera from '@/components/base/BaseOpera'
import GlobalWindow from '@/components/common/GlobalWindow'
export default {
name: 'OperaCmsTemplateWindow',
extends: BaseOpera,
components: {
GlobalWindow
},
data () {
return {
//
form: {
id: null,
name: '',
path: '',
accessUri: '',
presetDataCodes: [],
remark: ''
},
//
rules: {
name: [
{ required: true, message: '请输入模板名称' }
],
path: [
{ required: true, message: '请输入模板文件路径' }
],
accessUri: [
{ required: true, message: '请输入访问路径' }
]
}
}
},
methods: {
/**
* 打开窗口
*
* @param title 窗口标题
* @param target 行对象仅编辑需该参数
*/
open (title, target) {
this.title = title
this.visible = true
//
if (target == null) {
this.$nextTick(() => {
this.$refs.form.resetFields()
this.form[this.configData['field.id']] = null
})
return
}
//
this.$nextTick(() => {
for (const key in this.form) {
this.form[key] = target[key]
}
//
this.form.presetDataCodes = target.presetDataCodes == null || target.presetDataCodes === '' ?
[] : target.presetDataCodes.split(',')
.filter(item => item.trim() !== '')
})
},
/**
* 获取新增编辑表单对象
*
* @returns {*}
* @private
*/
__getForm () {
const form = JSON.parse(JSON.stringify(this.form))
// ,便
form.presetDataCodes = ',' + form.presetDataCodes.join(',') + ','
return form
}
},
async created () {
this.config({
api: await import('@/api/cms/template'),
'field.id': 'id'
})
}
}
</script>

View File

@ -0,0 +1,251 @@
<template>
<div class="template-select">
<el-select
:class="{select__block: !inline}"
:model-value="modelValue"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:filterable="true"
:filter-method="filterMethod"
@change="handleChange"
@update:modelValue="$emit('update:modelValue', $event)"
>
<el-option
v-for="data in computedList"
:key="data.id"
:value="data.id"
:label="data.name"
/>
</el-select>
<div v-if="settingTableData.length > 0" class="setting-wrap">
<h3>设置模板参数</h3>
<el-table :data="settingTableData" row-key="name">
<el-table-column label="数据项" prop="label" width="100px"/>
<el-table-column label="数据所需参数设置">
<template #default="{ row }">
<el-form :ref="`${row.name}ParameterForm`" :model="row.form" :rules="row.rules">
<el-form-item
v-for="parameter in row.parameters"
:key="parameter.name"
:label="parameter.label"
:prop="parameter.name"
:required="parameter.required"
>
<TemplateParameterInput
:type="parameter.component"
v-model="row.form[parameter.name]"
@change="emitUpdateParameters"
/>
<FormItemTip v-if="!parameter.required">如不填写需在获取模板数据时传入该参数</FormItemTip>
</el-form-item>
</el-form>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import TemplateParameterInput from '@/components/cms/template/parameter-input'
import { fetchAll } from '@/api/cms/template'
export default {
name: 'TemplateSelect',
components: { TemplateParameterInput },
props: {
// ID
modelValue: {},
//
parameters: {},
disabled: {
default: false
},
placeholder: {
default: '请选择模板'
},
inline: {
default: true
},
clearable: {
default: true
}
},
data () {
return {
filterWord: '',
list: [],
//
settingTableData: [],
//
settingForm: {},
//
settingRules: {}
}
},
computed: {
computedList () {
if (this.filterWord.trim() === '') {
return this.list
}
return this.list.filter(item => item.name.indexOf(this.filterWord) !== -1)
}
},
methods: {
//
filterMethod (kwd) {
this.filterWord = kwd
},
//
fetchList () {
fetchAll()
.then(data => {
this.list = data
//
this.buildSettingTable(this.modelValue)
//
this.initSettingValues(this.parameters)
})
.catch(e => {
this.$tip.apiFailed(e)
})
},
// change
handleChange (evt) {
this.buildSettingTable(evt)
this.$emit('change', evt)
this.emitUpdateParameters()
},
/**
* 构建参数设置表
* 1. 获取模板所需的数据项编码集
* 2. 循环数据项编码集根据编码从字典中取出各个数据项的参数配置
* 3. 将编码和参数配置构建成对象写入settingTableData中
*
* @param templateId 模板唯一标识
* @returns {null}
*/
buildSettingTable (templateId) {
if (templateId == null) {
this.settingTableData = []
return
}
const template = this.list.find(item => item.id === templateId)
// 1.
const dataCodes = template.presetDataCodes == null || template.presetDataCodes.trim() === '' ?
[] : template.presetDataCodes
.split(',')
.filter(item => item.trim() !== '')
// -
if (dataCodes == null || dataCodes.length === 0) {
this.settingTableData = []
return
}
// 2.
this.settingTableData = dataCodes
.map(dataCode => {
// 2.1
let dictDataConfig = this.$dc(`TEMPLATE_DATA_LIST.${dataCode}`)
if (dictDataConfig == null || dictDataConfig === '') {
return null
}
dictDataConfig = JSON.parse(dictDataConfig)
const dataItemParameters = dictDataConfig.parameters
// 2.2
const parameters = []
const form = {}
const rules = {}
// - dataItemParameter: { name: , label: , component: , required: }
for (const dataItemParameter of dataItemParameters) {
parameters.push(dataItemParameter)
//
form[dataItemParameter.name] = null
//
if (dataItemParameter.required) {
rules[dataItemParameter.name] = [
{ required: true, message: `${dataItemParameter.label}不能为空` }
]
}
}
if (parameters.length === 0) {
return null
}
// 2.3
return {
name: dataCode,
label: this.$d(`TEMPLATE_DATA_LIST.${dataCode}`),
parameters,
form,
rules
}
})
.filter(item => item != null)
},
/**
* 初始化设置值
*
* @param parameters 预设数据的参数
*/
initSettingValues (parameters) {
for (const dataItem in parameters) {
const targetSettingRow = this.settingTableData.find(item => item.name === dataItem)
if (targetSettingRow == null) {
continue
}
const dataItemParameters = parameters[dataItem]
for (const p of dataItemParameters) {
targetSettingRow.form[p.name] = p.value
}
}
},
//
emitUpdateParameters () {
if (this.settingTableData.length === 0) {
this.$emit('update:parameters', null)
return
}
const parameters = {}
for (const dataItem of this.settingTableData) {
parameters[dataItem.name] = dataItem.parameters.map(parameter => {
return {
name: parameter.name,
value: dataItem.form[parameter.name]
}
})
}
this.$emit('update:parameters', parameters)
},
//
validateForm () {
const validatePromises = []
for (const dateItem of this.settingTableData) {
validatePromises.push(this.$refs[`${dateItem.name}ParameterForm`].validate())
}
return Promise.all(validatePromises)
}
},
created () {
this.fetchList()
}
}
</script>
<style lang="scss" scoped>
.template-select {
& > .el-select {
display: block;
}
.setting-wrap {
padding: 10px 20px;
background-color: var(--background-color);
h3 {
font-weight: normal;
font-size: var(--font-size);
}
.el-form-item {
margin-bottom: 18px;
}
}
}
</style>

View File

@ -0,0 +1,14 @@
<template>
<ArticleSelect
:multiple="false"
value-key="uid"
/>
</template>
<script>
import ArticleSelect from '@/components/cms/article/ArticleSelect'
export default {
name: 'ArticleInput',
components: { ArticleSelect }
}
</script>

View File

@ -0,0 +1,17 @@
<template>
<CategorySelect
:multiple="false"
value-key="uid"
:clearable="true"
:check-on-click-node="false"
/>
</template>
<script>
import CategorySelect from '@/components/cms/category/CategorySelect'
export default {
name: 'CategoryInput',
components: {CategorySelect}
}
</script>

View File

@ -0,0 +1,17 @@
<template>
<CategorySelect
:multiple="true"
:clearable="true"
:check-on-click-node="false"
value-key="uid"
/>
</template>
<script>
import CategorySelect from '@/components/cms/category/CategorySelect'
export default {
name: 'MultipleCategoryInput',
components: { CategorySelect }
}
</script>

View File

@ -0,0 +1,12 @@
<template>
<ResourceGroupSelect value-key="uid" :multiple="true"/>
</template>
<script>
import ResourceGroupSelect from '@/components/cms/resource/ResourceGroupSelect'
export default {
name: 'MultipleResourceGroupInput',
components: {ResourceGroupSelect}
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<el-input-number placeholder="请输入值" :step="1" :controls="false"/>
</template>
<script>
export default {
name: 'NumberInput'
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,10 @@
<template>
<el-input placeholder="请输入值"/>
</template>
<script>
export default {
name: 'TextInput'
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<component ref="component" :is="component"/>
</template>
<script>
import ArticleInput from './ArticleInput'
import MultipleResourceGroupInput from './MultipleResourceGroupInput'
import CategoryInput from './CategoryInput'
import MultipleCategoryInput from './MultipleCategoryInput'
import TextInput from './TextInput'
import NumberInput from './NumberInput'
export default {
name: 'TemplateParameterInput',
components: {
ArticleInput,
MultipleResourceGroupInput,
CategoryInput,
MultipleCategoryInput,
TextInput,
NumberInput,
},
props: {
//
type: {
required: true
}
},
computed: {
component () {
let targetComponent = this.$options.components[this.type]
if (targetComponent == null) {
throw new Error(`未能匹配参数输入类型 ${this.type}`)
}
return targetComponent
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<el-upload
class="attach-uploader"
action="/api/oss/upload/attach"
:accept="accept"
:show-file-list="false"
:on-success="handleUploadSuccess"
>
<div v-if="modelValue" class="upload-info">
<Icon :value="iconClass"/>
<p>{{filename}}</p>
</div>
<el-icon v-else class="attach-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</template>
<script>
export default {
name: 'AttachUploader',
props: {
// el-uploader accept
accept: {},
//
iconClass: {
default: 'iconfont icon-file1'
},
// fileKey
modelValue: {},
//
filename: {
default: '文件已上传'
}
},
methods: {
//
handleUploadSuccess (res) {
if (!res.success) {
this.$tip.apiFailed(res)
return
}
this.$emit('update:modelValue', res.data.fileKey)
this.$emit('success', res.data)
}
}
}
</script>
<style scoped lang="scss">
.attach-uploader {
background-color: var(--background-color);
:deep(.el-upload) {
border: 1px dashed #eee;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
transition: all ease .15s;
&:hover {
border-color: #409EFF;
}
}
.attach-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.upload-info {
height: 178px;
width: 178px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.icon {
font-size: 45px !important;
color: var(--font-color-gray-deep);
}
p {
margin: 10px 0 0 0;
color: var(--font-color-gray);
}
}
.attach {
width: 178px;
height: 178px;
display: block;
border-radius: 50%;
object-fit: cover;
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<el-upload
class="avatar-uploader"
action="/api/oss/upload/image"
accept=".jpg,.jpeg,.png"
:show-file-list="false"
:on-success="handleUploadSuccess"
>
<img v-if="modelValue" :src="$getImageURL(modelValue)" class="avatar" alt="头像">
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</template>
<script>
export default {
name: 'AvatarUploader',
props: {
// fileKey
modelValue: {}
},
computed: {},
methods: {
//
handleUploadSuccess (res) {
if (!res.success) {
this.$tip.apiFailed(res)
return
}
this.$emit('update:modelValue', res.data.fileKey)
this.$emit('success', res.data)
}
}
}
</script>
<style scoped lang="scss">
.avatar-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
&:hover {
border-color: #409EFF;
}
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
border-radius: 50%;
object-fit: cover;
}
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<ul class="colors-marking">
<li
v-for="item in computedData"
:key="item.label"
>
<span
:style="`border: 1px solid ${item.borderColor}; background-color: ${item.backgroundColor}`"
></span>
<label>{{ item.label }}</label>
</li>
</ul>
</template>
<script>
import BaseComponent from '@/components/BaseComponent'
export default {
name: 'ColorsMarking',
extends: BaseComponent,
props: {
data: {
required: true
}
},
computed: {
computedData () {
return this.computeProp('data')
}
}
}
</script>
<style scoped lang="scss">
.colors-marking {
padding: 10px 16px;
display: flex;
& > li {
font-size: 13px;
margin-right: 15px;
display: flex;
align-items: center;
& > span {
display: inline-block;
content: '';
width: 36px;
height: 14px;
margin-right: 6px;
}
}
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<div class="loading">
<el-icon class="is-loading" :size="iconSize">
<Loading/>
</el-icon>
<div class="loading-text__wrap"><slot></slot></div>
</div>
</template>
<script>
export default {
name: 'DataLoading',
props: {
//
iconSize: {
default: '20px'
}
}
}
</script>
<style scoped lang="scss">
.loading {
padding: 40px 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.loading-text__wrap {
color: #999;
margin: 10px 0 0 0;
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<el-checkbox-group
:model-value="value"
:disabled="disabled"
@input="$emit('input', $event)"
>
<el-checkbox
v-for="data in dataList"
:key="data.value"
:value="data.value"
>{{ data.label }}</el-checkbox>
</el-checkbox-group>
</template>
<script>
import BaseDict from '@/components/base/BaseDict'
export default {
name: 'DictCheckboxGroup',
extends: BaseDict,
props: {
value: {
type: Array,
default: () => []
}
}
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,27 @@
<template>
<el-radio-group
class="dict-radio-group"
:value="value"
:disabled="disabled"
@input="$emit('input', $event)"
>
<el-radio
v-for="data of dataList"
:key="data.value"
:value="data.value"
>{{ data.label }}</el-radio>
</el-radio-group>
</template>
<script>
import BaseDict from '@/components/base/BaseDict'
export default {
name: 'DictRadioGroup',
extends: BaseDict,
props: {}
}
</script>
<style scoped lang="scss">
</style>

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