HU XUYANG

Merge remote-tracking branch 'remotes/origin/dev-admin'

Showing 87 changed files with 4912 additions and 0 deletions
1 +<!DOCTYPE html>
2 +<html>
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0, user-scalable=0" />
6 + <title>Document</title>
7 +</head>
8 +<body>
9 + <div id="app"></div>
10 +</body>
11 +</html>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div id="app">
3 + <router-view></router-view>
4 + </div>
5 +</template>
6 +<script>
7 +export default {
8 + name: "app",
9 +};
10 +</script>
...\ No newline at end of file ...\ No newline at end of file
1 +import axios from "utils/request";
2 +
3 +/**
4 + * 获取博客列表
5 + * @param data
6 + * @returns {AxiosPromise}
7 + */
8 +export function apiGetBlogList(params) {
9 + return axios.get("/blog/list", params);
10 +}
11 +
12 +/**
13 + * 获取博客详情
14 + * @param data
15 + * @returns {AxiosPromise}
16 + */
17 +export function apiGetBlogDetail(params) {
18 + return axios.get("/blog/info", params);
19 +}
20 +
21 +/**
22 + * 新增博客
23 + * @param data
24 + * @returns {AxiosPromise}
25 + */
26 +export function apiAddBlog(params) {
27 + return axios.postFile("/blog/add", params);
28 +}
29 +/**
30 + * 修改博客
31 + * @param data
32 + * @returns {AxiosPromise}
33 + */
34 +export function apiUpdateBlog(params) {
35 + return axios.postFile("/blog/update", params);
36 +}
37 +/**
38 + * 删除博客
39 + * @param data
40 + * @returns {AxiosPromise}
41 + */
42 +export function apiDelBlog(params) {
43 + return axios.get("/blog/del", params);
44 +}
1 +import axios from "utils/request";
2 +
3 +/**
4 + * 获取标签列表
5 + * @param data
6 + * @returns {AxiosPromise}
7 + */
8 +export function apiGetLabelList(params) {
9 + return axios.get("/label/list", params);
10 +}
11 +/**
12 + * 新增标签
13 + * @param data
14 + * @returns {AxiosPromise}
15 + */
16 +export function apiAddLabel(params) {
17 + return axios.postFile("/label/add", params);
18 +}
19 +/**
20 + * 修改标签
21 + * @param data
22 + * @returns {AxiosPromise}
23 + */
24 +export function apiUpdateLabel(params) {
25 + return axios.post("/label/update", params);
26 +}
27 +/**
28 + * 删除标签
29 + * @param data
30 + * @returns {AxiosPromise}
31 + */
32 +export function apiDelLabel(params) {
33 + return axios.get("/label/del", params);
34 +}
1 +import axios from "utils/request";
2 +
3 +/**
4 + * 获取留言列表
5 + * @param data
6 + * @returns {AxiosPromise}
7 + */
8 +export function apiGetMessageList(params) {
9 + return axios.get("/message/list", params);
10 +}
11 +
12 +/**
13 + * 删除留言
14 + * @param data
15 + * @returns {AxiosPromise}
16 + */
17 +export function apiDelMessage(params) {
18 + return axios.get("/message/del", params);
19 +}
20 +
21 +/**
22 + * 删除回复
23 + * @param data
24 + * @returns {AxiosPromise}
25 + */
26 +export function apiDelReply(params) {
27 + return axios.postFile("/message/delReply", params);
28 +}
1 +import axios from "utils/request";
2 +
3 +/**
4 + * 上传图片
5 + * @param data
6 + * @returns {AxiosPromise}
7 + */
8 +export function apiUploadImg(params) {
9 + return axios.postFile("/uploadImage", params);
10 +}
11 +/**
12 + * 删除图片
13 + * @param data
14 + * @returns {AxiosPromise}
15 + */
16 +export function apiDelUploadImg(params) {
17 + return axios.post("/delUploadImage", params);
18 +}
1 +<template>
2 + <div id="markdowm">
3 + <div class="md-title">
4 + <ul class="cf">
5 + <li>
6 + <span>图片</span>
7 + <input type="file" class="uploadFile" @change="insertImg" />
8 + </li>
9 + <li @click="insertCode">
10 + <span>代码块</span>
11 + </li>
12 + <li @click="setCursorPosition($refs.text, '***')">
13 + <span>分割线</span>
14 + </li>
15 + <li @click="setCursorPosition($refs.text, '****', 2)">
16 + <span>粗体</span>
17 + </li>
18 + <li @click="setCursorPosition($refs.text, '**', 1)">
19 + <span>斜体</span>
20 + </li>
21 + <li @click="setCursorPosition($refs.text, '> ', 2)">
22 + <span>引用</span>
23 + </li>
24 + </ul>
25 + </div>
26 + <textarea v-model="val" ref="text" @keydown.tab="tabMarkdown"></textarea>
27 + <div class="render fmt" v-html="renderHtml"></div>
28 + </div>
29 +</template>
30 +
31 +<script>
32 +import marked from "marked";
33 +import highlightJs from "highlight.js";
34 +import { apiUploadImg } from "src/api/upload";
35 +export default {
36 + props: ["value"],
37 + computed: {
38 + renderHtml() {
39 + marked.setOptions({
40 + renderer: new marked.Renderer(),
41 + gfm: true, //允许 Git Hub标准的markdown.
42 + tables: true, //允许支持表格语法。该选项要求 gfm 为true。
43 + breaks: true, //允许回车换行。该选项要求 gfm 为true。
44 + pedantic: false, //尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。
45 + sanitize: true, //对输出进行过滤(清理),将忽略任何已经输入的html代码(标签)
46 + smartLists: true, //使用比原生markdown更时髦的列表。 旧的列表将可能被作为pedantic的处理内容过滤掉.
47 + smartypants: false, //使用更为时髦的标点,比如在引用语法中加入破折号。
48 + highlight: function (code) {
49 + return highlightJs.highlightAuto(code).value;
50 + },
51 + });
52 + return marked(this.val);
53 + },
54 + },
55 + watch: {
56 + val(newVal) {
57 + this.handleModelInput(newVal);
58 + },
59 + },
60 + data() {
61 + return {
62 + val: this.value,
63 + link: "",
64 + textarea: null,
65 + };
66 + },
67 + mounted() {
68 + this.textarea = this.$refs.text;
69 + },
70 + methods: {
71 + handleModelInput(newVal) {
72 + this.$emit("input", newVal);
73 + },
74 + tabMarkdown(e) {
75 + // tab键
76 + e.preventDefault();
77 + let indent = " ";
78 + let start = this.textarea.selectionStart;
79 + let end = this.textarea.selectionEnd;
80 + let selected = window.getSelection().toString();
81 + selected = indent + selected.replace(/\n/g, "\n" + indent);
82 + this.textarea.value =
83 + this.textarea.value.substring(0, start) +
84 + selected +
85 + this.textarea.value.substring(end);
86 + this.textarea.setSelectionRange(
87 + start + indent.length,
88 + start + selected.length
89 + );
90 + },
91 + insertImg(e) {
92 + // 插入图片
93 + let formData = new FormData(),
94 + img = "";
95 + formData.append("markdown_img", e.target.files[0]);
96 + return apiUploadImg(formData)
97 + .then((res) => {
98 + img = res.data.markdown_img;
99 + let val = `![图片描述](${img})`;
100 + this.setCursorPosition(this.$refs.text, val, 6);
101 + })
102 + .catch((err) => {
103 + console.log(err);
104 + })
105 + .finally(() => {});
106 + },
107 + insertCode() {
108 + let val = `
109 +\`\`\`
110 +
111 +\`\`\``;
112 + this.setCursorPosition(this.$refs.text, val, val.length - 8);
113 + },
114 + setCursorPosition(dom, val, posLen) {
115 + // 设置光标位置
116 + var cursorPosition = 0;
117 + if (dom.selectionStart) {
118 + cursorPosition = dom.selectionStart;
119 + }
120 + this.insertAtCursor(dom, val);
121 + dom.focus();
122 + dom.setSelectionRange(
123 + dom.value.length,
124 + cursorPosition + (posLen || val.length)
125 + );
126 + this.val = dom.value;
127 + },
128 + insertAtCursor(dom, val) {
129 + // 光标所在位置插入字符
130 + if (document.selection) {
131 + dom.focus();
132 + sel = document.selection.createRange();
133 + sel.text = val;
134 + sel.select();
135 + } else if (dom.selectionStart || dom.selectionStart == "0") {
136 + let startPos = dom.selectionStart;
137 + let endPos = dom.selectionEnd;
138 + let restoreTop = dom.scrollTop;
139 + dom.value =
140 + dom.value.substring(0, startPos) +
141 + val +
142 + dom.value.substring(endPos, dom.value.length);
143 + if (restoreTop > 0) {
144 + dom.scrollTop = restoreTop;
145 + }
146 + dom.focus();
147 + dom.selectionStart = startPos + val.length;
148 + dom.selectionEnd = startPos + val.length;
149 + } else {
150 + dom.value += val;
151 + dom.focus();
152 + }
153 + },
154 + },
155 +};
156 +</script>
157 +
158 +<style lang="less" scoped>
159 +@import "./markdown.less";
160 +@import "../../../../../node_modules/highlight.js/styles/tomorrow-night-eighties.css";
161 +@md-bd-color: #dcdfe6;
162 +@md-title-color: rgb(233, 234, 237);
163 +@md-bg-color: #fff;
164 +@btn-hover: #3b7cff;
165 +#markdowm {
166 + width: 100%;
167 + height: 500px;
168 + text-align: left;
169 + overflow: hidden;
170 + border: 1px solid @md-bd-color;
171 + position: relative;
172 + .md-title {
173 + width: 100%;
174 + height: 40px;
175 + border-bottom: 1px solid #dcdfe6;
176 + background: @md-title-color;
177 + position: absolute;
178 + left: 0;
179 + top: 0;
180 + z-index: 99;
181 + li {
182 + width: 100px;
183 + height: 100%;
184 + text-align: center;
185 + position: relative;
186 + float: left;
187 + cursor: pointer;
188 + color: #606266;
189 + &:hover {
190 + color: @btn-hover;
191 + }
192 + &:after {
193 + content: "";
194 + position: absolute;
195 + left: 100%;
196 + top: 50%;
197 + transform: translateY(-50%);
198 + width: 1px;
199 + height: 20px;
200 + background: @borderBoldColor;
201 + }
202 + &:last-child::after {
203 + content: none;
204 + }
205 + .uploadFile {
206 + position: absolute;
207 + top: 0;
208 + left: 0;
209 + width: 100%;
210 + height: 100%;
211 + opacity: 0;
212 + cursor: pointer;
213 + }
214 + }
215 + }
216 + textarea,
217 + .render {
218 + float: left;
219 + width: 50%;
220 + height: 100%;
221 + vertical-align: top;
222 + box-sizing: border-box;
223 + line-height: 22px;
224 + padding: 0 20px;
225 + }
226 + textarea {
227 + border: none;
228 + border-right: 1px solid @md-bd-color;
229 + resize: none;
230 + outline: none;
231 + background-color: @md-bg-color;
232 + color: @mainColor;
233 + font-size: 14px;
234 + line-height: 22px;
235 + padding: 20px;
236 + padding-top: 50px;
237 + }
238 + .render {
239 + background-color: @md-title-color;
240 + overflow-y: scroll;
241 + padding-top: 50px;
242 + }
243 +
244 + .mask {
245 + position: fixed;
246 + left: 0;
247 + top: 0;
248 + width: 100%;
249 + height: 100%;
250 + background: rgba(0, 0, 0, 0.5);
251 + z-index: 10;
252 + }
253 + .link-text {
254 + width: 500px;
255 + text-align: center;
256 + position: absolute;
257 + left: 50%;
258 + top: 50%;
259 + transform: translate(-50%, -50%);
260 + .link-input {
261 + width: 400px;
262 + }
263 + }
264 +}
265 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +.fmt {
2 + line-height: 1.6;
3 + word-wrap: break-word;
4 + color: @mainColor;
5 +}
6 +.fmt a {
7 + color: #009a61;
8 + text-decoration: none;
9 +}
10 +.fmt h1 {
11 + font-size: 2.25em;
12 +}
13 +
14 +.fmt h2 {
15 + font-size: 1.75em;
16 +}
17 +
18 +.fmt h3 {
19 + font-size: 1.5em;
20 +}
21 +
22 +.fmt h4 {
23 + font-size: 1.25em;
24 +}
25 +
26 +.fmt h5 {
27 + font-size: 1em;
28 +}
29 +
30 +.fmt h6 {
31 + font-size: 0.86em;
32 +}
33 +.fmt p {
34 + margin-top: 0.86em;
35 + line-height: 1.8em;
36 +}
37 +
38 +.fmt h1,
39 +.fmt h2,
40 +.fmt h3,
41 +.fmt h4,
42 +.fmt h5,
43 +.fmt h6 {
44 + margin-top: 1.2em;
45 +}
46 +
47 +.fmt h1 + .widget-codetool + pre,
48 +.fmt h2 + .widget-codetool + pre,
49 +.fmt h3 + .widget-codetool + pre {
50 + margin-top: 1.2em !important;
51 +}
52 +
53 +.fmt h1,
54 +.fmt h2 {
55 + border-bottom: 1px solid #eee;
56 + padding-bottom: 10px;
57 +}
58 +
59 +.fmt > h1:first-child,
60 +.fmt h2:first-child,
61 +.fmt h3:first-child,
62 +.fmt h4:first-child,
63 +.fmt p:first-child,
64 +.fmt ul:first-child,
65 +.fmt ol:first-child,
66 +.fmt blockquote:first-child {
67 + margin-top: 0;
68 +}
69 +
70 +.fmt ul,
71 +.fmt ol {
72 + margin-left: 2em;
73 + margin-top: 0.86em;
74 + padding-left: 0;
75 +}
76 +
77 +.fmt ul li,
78 +.fmt ol li {
79 + margin: 0.3em 0;
80 + list-style: unset;
81 +}
82 +
83 +.fmt ul ul,
84 +.fmt ul ol,
85 +.fmt ol ul,
86 +.fmt ol ol {
87 + margin-top: 0;
88 + margin-bottom: 0;
89 +}
90 +
91 +.fmt ul p,
92 +.fmt ol p {
93 + margin: 0;
94 +}
95 +
96 +.fmt p:last-child {
97 + margin-bottom: 0;
98 +}
99 +
100 +.fmt p > p:empty,
101 +.fmt div > p:empty,
102 +.fmt p > div:empty,
103 +.fmt div > div:empty,
104 +.fmt div > br:only-child,
105 +.fmt p + br,
106 +.fmt img + br {
107 + display: none;
108 +}
109 +
110 +.fmt img,
111 +.fmt video,
112 +.fmt audio {
113 + position: static !important;
114 + max-width: 100%;
115 +}
116 +
117 +.fmt img {
118 + padding: 3px;
119 + border: 1px solid #ddd;
120 +}
121 +
122 +.fmt img.emoji {
123 + padding: 0;
124 + border: none;
125 +}
126 +
127 +.fmt blockquote {
128 + border-left: 2px solid #009a61;
129 + background: @thinBgColor;
130 + color: @thinColor;
131 + font-size: 1em;
132 +}
133 +
134 +.fmt pre,
135 +.fmt code {
136 + font-size: 0.93em;
137 + margin-top: 0.86em;
138 +}
139 +
140 +.fmt pre {
141 + font-family: "Source Code Pro", Consolas, Menlo, Monaco, "Courier New",
142 + monospace;
143 + padding: 1em;
144 + margin-top: 0.86em;
145 + border: none;
146 + overflow: auto;
147 + line-height: 1.45;
148 + max-height: 35em;
149 + position: relative;
150 + background: url("./blueprint.png") @thinBgColor;
151 + background-size: 30px, 30px;
152 + font-size: 12px;
153 + -webkit-overflow-scrolling: touch;
154 + border-radius: 5px;
155 +}
156 +
157 +.fmt pre code {
158 + background: none;
159 + font-size: 1em;
160 + overflow-wrap: normal;
161 + white-space: inherit;
162 +}
163 +
164 +.fmt hr {
165 + margin: 1.5em auto;
166 + border-top: 2px dotted #eee;
167 +}
168 +
169 +.fmt kbd {
170 + margin: 0 4px;
171 + padding: 3px 4px;
172 + background: #eee;
173 + color: @thinColor;
174 +}
175 +
176 +.fmt .x-scroll {
177 + overflow-x: auto;
178 +}
179 +
180 +.fmt table {
181 + width: 100%;
182 +}
183 +
184 +.fmt table th,
185 +.fmt table td {
186 + border: 1px solid #e6e6e6;
187 + padding: 5px 8px;
188 + word-break: normal;
189 +}
190 +
191 +.fmt table th {
192 + background: #f3f3f3;
193 +}
194 +
195 +.fmt a:not(.btn) {
196 + border-bottom: 1px solid rgba(0, 154, 97, 0.25);
197 + padding-bottom: 1px;
198 +}
199 +
200 +.fmt a:not(.btn):hover {
201 + border-bottom: 1px solid #009a61;
202 + text-decoration: none;
203 +}
204 +
205 +.hljs {
206 + display: block;
207 + overflow-x: auto;
208 + padding: 0.5em;
209 + color: @mainColor;
210 + background: #f8f8f8;
211 +}
212 +
213 +.hljs-comment,
214 +.hljs-quote {
215 + color: #998;
216 + font-style: italic;
217 +}
218 +
219 +.hljs-keyword,
220 +.hljs-selector-tag,
221 +.hljs-subst {
222 + color: @mainColor;
223 + font-weight: bold;
224 +}
225 +
226 +.hljs-number,
227 +.hljs-literal,
228 +.hljs-variable,
229 +.hljs-template-variable,
230 +.hljs-tag .hljs-attr {
231 + color: #008080;
232 +}
233 +
234 +.hljs-string,
235 +.hljs-doctag {
236 + color: #d14;
237 +}
238 +
239 +.hljs-title,
240 +.hljs-section,
241 +.hljs-selector-id {
242 + color: #900;
243 + font-weight: bold;
244 +}
245 +
246 +.hljs-subst {
247 + font-weight: normal;
248 +}
249 +
250 +.hljs-type,
251 +.hljs-class .hljs-title {
252 + color: #458;
253 + font-weight: bold;
254 +}
255 +
256 +.hljs-tag,
257 +.hljs-name,
258 +.hljs-attribute {
259 + color: #000080;
260 + font-weight: normal;
261 +}
262 +
263 +.hljs-regexp,
264 +.hljs-link {
265 + color: #009926;
266 +}
267 +
268 +.hljs-symbol,
269 +.hljs-bullet {
270 + color: #990073;
271 +}
272 +
273 +.hljs-built_in,
274 +.hljs-builtin-name {
275 + color: #0086b3;
276 +}
277 +
278 +.hljs-meta {
279 + color: @assistColor;
280 + font-weight: bold;
281 +}
282 +
283 +.hljs-deletion {
284 + background: #fdd;
285 +}
286 +
287 +.hljs-addition {
288 + background: #dfd;
289 +}
290 +
291 +.hljs-emphasis {
292 + font-style: italic;
293 +}
294 +
295 +.hljs-strong {
296 + font-weight: bold;
297 +}
1 +/**
2 + * 注入公共组件
3 + */
4 +import Vue from "vue";
5 +// 检索当前目录的vue文件,便检索子文件夹
6 +const componentsContext = require.context("./", true, /.vue$/);
7 +componentsContext.keys().forEach((component) => {
8 + // 获取文件中的 default 模块
9 + const componentConfig = componentsContext(component).default;
10 + componentConfig.name && Vue.component(componentConfig.name, componentConfig);
11 +});
1 +<template>
2 + <zp-page>
3 + <!-- 页面标题 -->
4 + <div slot="header">
5 + <zp-page-header :back="back" @back="$emit('back')">
6 + <slot name="header">{{ header }}</slot>
7 + </zp-page-header>
8 + </div>
9 +
10 + <!-- 主体body -->
11 + <div class="zp-page-edit">
12 + <slot></slot>
13 +
14 + <!-- 保存等操作按钮 -->
15 + <div class="zp-page-edit-button" v-if="this.$slots.button">
16 + <slot name="button"></slot>
17 + </div>
18 + </div>
19 + </zp-page>
20 +</template>
21 +<script>
22 +import zpPage from "./zp-page";
23 +import zpPageHeader from "./zp-page-header";
24 +
25 +export default {
26 + name: "zpPageEdit",
27 + components: {
28 + zpPage,
29 + zpPageHeader,
30 + },
31 + props: {
32 + header: {
33 + type: String,
34 + default: "",
35 + },
36 + back: {
37 + type: Boolean,
38 + default: true,
39 + },
40 + },
41 + created() {},
42 + methods: {},
43 +};
44 +</script>
45 +<style lang="less" scope>
46 +.zp-page-edit {
47 + .el-form {
48 + margin-top: 20px;
49 + }
50 + .el-input {
51 + width: 220px;
52 + }
53 + .el-textarea {
54 + width: 440px;
55 + .el-textarea__inner {
56 + min-height: 100px !important;
57 + }
58 + }
59 + .zp-notice-subtitle {
60 + width: 540px;
61 + box-sizing: border-box;
62 + }
63 +}
64 +.zp-page-edit-button {
65 + padding: 20px 20px 20px 120px;
66 +}
67 +</style>
1 +<template>
2 + <div class="zp-page-filter">
3 + <el-form ref="pageFilter" :inline="true" :label-width="labelWidth + 'px'">
4 + <el-form-item
5 + v-for="(searchItem, searchIndex) in searchForm"
6 + :key="searchIndex"
7 + :label="searchItem.label"
8 + >
9 + <!-- 输入框 -->
10 + <el-input
11 + v-if="searchItem.type == 'text'"
12 + v-model="searchItem.value"
13 + :placeholder="searchItem.placeholder || '请输入' + searchItem.label"
14 + />
15 + </el-form-item>
16 + </el-form>
17 + </div>
18 +</template>
19 +<script>
20 +export default {
21 + name: "zpPageFilter",
22 + components: {},
23 + props: {
24 + labelWidth: {
25 + type: Number,
26 + default: 100,
27 + },
28 + searchForm: {
29 + type: Array,
30 + default: () => [],
31 + },
32 + },
33 + data() {
34 + return {};
35 + },
36 + watch: {},
37 + methods: {},
38 +};
39 +</script>
40 +<style lang="less" scope></style>
1 +<!--
2 + * @Descripttion:
3 + * @Author: givon.chen
4 + * @Date: 2020-06-02 14:48:49
5 + * @LastEditTime: 2020-06-30 15:41:59
6 +-->
7 +<template>
8 + <div class="zp-page-header">
9 + <!-- 带返回 -->
10 + <el-page-header
11 + :class="{ hideBack: !back }"
12 + @back="$emit('back')"
13 + :title="title"
14 + >
15 + <template v-slot:content>
16 + <slot>
17 + {{ header }}
18 + </slot>
19 + </template>
20 + </el-page-header>
21 + </div>
22 +</template>
23 +
24 +<script>
25 +export default {
26 + name: "zpPageHeader",
27 + props: {
28 + header: {
29 + type: String,
30 + default: "",
31 + },
32 + back: {
33 + type: Boolean,
34 + default: false,
35 + },
36 + title: {
37 + type: String,
38 + default: "返回",
39 + },
40 + },
41 +};
42 +</script>
43 +
44 +<style lang="less" scope>
45 +.zp-page-header {
46 + display: flex;
47 + align-items: center;
48 + height: 54px;
49 + font-weight: bold;
50 +
51 + .el-page-header {
52 + flex-grow: 1;
53 + padding-left: 0;
54 + box-shadow: none;
55 + line-height: 28px !important;
56 +
57 + .el-page-header__content {
58 + // header 里 tabs的情形
59 + .el-tabs {
60 + .el-tabs__header {
61 + margin: 0;
62 + .el-tabs__nav-wrap {
63 + padding-left: 0;
64 + }
65 + }
66 + }
67 + }
68 + }
69 + .hideBack {
70 + .el-page-header__left {
71 + display: none;
72 + }
73 + }
74 +}
75 +</style>
1 +<!--
2 + * @Descripttion:
3 + * @Author: givon.chen
4 + * @Date: 2020-05-07 21:05:06
5 + * @LastEditTime: 2020-06-29 12:09:31
6 +-->
7 +<template>
8 + <zp-page>
9 + <!-- 页面标题 -->
10 + <template v-slot:header v-if="!hideHeader">
11 + <zp-page-header :back="back" @back="back && $emit('back')">
12 + <slot name="header">{{ header }}</slot>
13 + </zp-page-header>
14 + </template>
15 +
16 + <!-- 搜索框 -->
17 + <div class="zp-page-filter" v-if="this.$slots['filter']">
18 + <slot name="filter"></slot>
19 + </div>
20 +
21 + <!-- 搜索框按钮组 -->
22 + <div class="zp-search-button" v-if="this.$slots['button']">
23 + <slot name="button"></slot>
24 + </div>
25 +
26 + <!-- 列表、分页 -->
27 + <div class="zp-page-list">
28 + <slot name="list"></slot>
29 + </div>
30 + </zp-page>
31 +</template>
32 +<script>
33 +import zpPage from "./zp-page";
34 +import zpPageHeader from "./zp-page-header";
35 +
36 +export default {
37 + name: "zpPageList",
38 + components: {
39 + zpPage,
40 + zpPageHeader,
41 + },
42 + props: {
43 + back: {
44 + type: Boolean,
45 + default: false,
46 + },
47 + header: {
48 + type: String,
49 + default: "",
50 + },
51 + hideHeader: {
52 + type: Boolean,
53 + default: false,
54 + },
55 + },
56 + created() {},
57 + methods: {},
58 +};
59 +</script>
60 +<style lang="less" scope>
61 +.zp-search-button {
62 + padding-bottom: 20px;
63 + padding-top: 0;
64 + .el-button {
65 + border-radius: 1px;
66 + width: 100px !important;
67 + }
68 +}
69 +.el-table {
70 + th {
71 + font-size: 14px;
72 + }
73 + td {
74 + font-size: 13px;
75 + }
76 + .el-button--text {
77 + padding-top: 0;
78 + padding-bottom: 0;
79 + height: 18px;
80 + line-height: 18px;
81 + font-size: 13px;
82 + border: 0 none;
83 + span {
84 + vertical-align: top;
85 + display: inline-block;
86 + line-height: 18px;
87 + }
88 + }
89 +}
90 +.el-pagination {
91 + padding-bottom: 0 !important;
92 + margin-top: 24px;
93 +}
94 +.el-pagination__sizes {
95 + .el-select {
96 + .el-input {
97 + .el-input__inner {
98 + padding-left: 0 !important;
99 + }
100 + }
101 + }
102 +}
103 +</style>
1 +<!--
2 + * @Descripttion:
3 + * @Author: givon.chen
4 + * @Date: 2020-05-15 12:48:29
5 + * @LastEditTime: 2020-06-30 15:43:16
6 +-->
7 +<template>
8 + <div class="zp-page-container">
9 + <el-card class="zp-page-el-card" shadow="never">
10 + <template v-slot:header class="clearfix" v-if="this.$slots.header">
11 + <slot name="header"></slot>
12 + </template>
13 +
14 + <div class="zp-page-body">
15 + <slot></slot>
16 + </div>
17 + </el-card>
18 + </div>
19 +</template>
20 +<script>
21 +export default {
22 + name: "zpPage",
23 + created() {},
24 + methods: {},
25 +};
26 +</script>
27 +<style lang="less" scope>
28 +.zp-page-container {
29 + background-color: #fff;
30 + .zp-page-el-card {
31 + border-radius: 0;
32 + border: 0 none;
33 +
34 + .el-card__header {
35 + font-size: 16px;
36 + font-weight: bold;
37 + color: $strongFontColor;
38 + background: #ffffff;
39 + padding: 0px 20px !important;
40 + line-height: 28px;
41 + }
42 + }
43 + .el-icon-edit {
44 + position: relative;
45 + width: 16px;
46 + height: 16px;
47 + display: inline-block !important;
48 + vertical-align: middle !important;
49 + padding: 10px;
50 + cursor: pointer;
51 + &:before {
52 + position: absolute;
53 + left: 10px;
54 + top: 10px;
55 + content: "" !important;
56 + width: 16px;
57 + height: 16px;
58 + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAByUExURUxpcTx//z1//zt9/zx8/zt9/zt8/z9//zt8/2aZ/zt8/z1//z59/z19/zx9/zx8/z9//z1//zt9/zx//zt//zt8/zt8/zx9/zt8/zt8/zt8/zt8/zt8/zt8/zt8/zt8/z9//z9//zyH/zx8/zt8/zt8/4mnP5MAAAAldFJOUwAuQoqu0usU0QW9MjVPO0wwPkVIOPf4rfXw8/bt8vnvCAwRx4muFNzGAAAAtklEQVRIx+3Q2Q6DIBCF4SloUbvv+955/1dsK6kOpoTDlUnjuSTfj0Gibv+362YU5W8FnycR/v5g5vUCD5782WoOB3lSFgcFaaPzb7GdIT7lpCr2Y8SzKE5LxMtiN0S8LC4DxMviOA34TPXdogh4Qz2neB8EPDkF4mWhIS8KwjyRKt+rvT5reHuQmpY8/fbe/0kUeb8NIu63QcT9boB4GUBeBJivA+C9VVAv7CnWN77grK2gm38vXdQ1SLUwEaMAAAAASUVORK5CYII=")
59 + no-repeat;
60 + background-size: 16px;
61 + }
62 + }
63 +}
64 +</style>
1 +<!--
2 +table 配置项:
3 +1. loading
4 +2. source: 数据list
5 +3. count: 数据总数
6 +4. columns: 列配置
7 + 同el-table-column 属性
8 + 补充:
9 + 1. zpMark: 特殊几个字段固定宽度,具体查看 @zpAdmin/utils/config/styleConfig
10 + 2. slot: 自定义列名
11 +5. leftFix: 左侧固定列栏数
12 +6. rightFix: 右侧固定列栏数
13 +7. pageable: 是否显示分页
14 +8. pageSize: 默认每页条数
15 +9. layout: 分页配置
16 +
17 +事件:
18 +1. sizeChange: 分页切换 | 参数: { pageNo, pageSize }
19 +2. selectChange: 多选切换 | 参数: 同element table
20 + -->
21 +<template>
22 + <div class="zp-table-list">
23 + <el-table
24 + ref="table"
25 + v-loading="loading"
26 + :stripe="stripe"
27 + :data="source"
28 + :row-key="rowKey"
29 + :show-header="showHeader"
30 + :empty-text="loading ? ' ' : '暂无数据'"
31 + @selection-change="handleSelectionChange"
32 + @sort-change="handleSortChange"
33 + >
34 + <template v-for="(column, columnKey) in columns">
35 + <template v-if="column.type">
36 + <template v-if="column.type === 'selection'">
37 + <!-- 选择栏固定宽度 -->
38 + <el-table-column
39 + type="selection"
40 + width="55"
41 + :show-overflow-tooltip="column.showTooltip"
42 + :align="column.align"
43 + :key="columnKey"
44 + :reserve-selection="column.reserveSelection"
45 + :selectable="
46 + column.selectable
47 + ? column.selectable
48 + : () => {
49 + return true;
50 + }
51 + "
52 + ></el-table-column>
53 + </template>
54 + </template>
55 +
56 + <template v-else>
57 + <el-table-column
58 + :type="column.type"
59 + :key="column.prop"
60 + :prop="column.prop"
61 + :label="column.label"
62 + :width="getWidth(column)"
63 + :fixed="getFixed(columnKey)"
64 + :class-name="column.className"
65 + :min-width="column.minWidth"
66 + :render-header="column.renderHeader"
67 + :show-overflow-tooltip="column.showTooltip"
68 + :align="column.align"
69 + :header-align="column.headerAlign"
70 + :label-class-name="column.labelClassName"
71 + :formatter="column.formatter"
72 + :sortable="column.sortable || false"
73 + >
74 + <template v-if="column.slotHeader" slot="header">
75 + <slot :name="column.slotHeader"></slot>
76 + </template>
77 + <template slot-scope="scope">
78 + <!-- 自定义列: 比如操作列 start -->
79 + <slot
80 + v-if="column.slot"
81 + :name="column.slot"
82 + :$index="scope.$index"
83 + :column="scope.column"
84 + :row="scope.row"
85 + ></slot>
86 + <!-- 自定义列: 比如操作列 end -->
87 + <template v-else>
88 + <template v-if="column.formatter">
89 + {{ column.formatter(scope.row, column) }}
90 + </template>
91 +
92 + <template v-else>
93 + {{ scope.row[column.prop] }}
94 + </template>
95 + </template>
96 + </template>
97 + </el-table-column>
98 + </template>
99 + </template>
100 + </el-table>
101 + <el-pagination
102 + v-if="pageable && count > 0"
103 + @size-change="(value) => handleSizeChange(value)"
104 + @current-change="(value) => handleCurrentChange(value)"
105 + :current-page="page"
106 + :page-sizes="pageSizes"
107 + :page-size="currentPageSize"
108 + :layout="pageLayout"
109 + :total="count"
110 + ></el-pagination>
111 + </div>
112 +</template>
113 +<script>
114 +import { tableFixWidths } from "src/utils/config/styleConfig";
115 +
116 +export default {
117 + name: "zpTableList",
118 + data() {
119 + return {
120 + page: 1,
121 + currentPageSize: 20,
122 + pageSizes: [10, 20, 30, 50],
123 + pageLayout: "",
124 + };
125 + },
126 + props: {
127 + // 是否显示表头
128 + showHeader: {
129 + type: Boolean,
130 + default: true,
131 + },
132 + // 是否斑马纹
133 + stripe: {
134 + type: Boolean,
135 + default: true,
136 + },
137 + // 加载状态
138 + loading: {
139 + type: Boolean,
140 + default: false,
141 + },
142 + // 数据资源
143 + source: {
144 + type: Array,
145 + default: () => [],
146 + },
147 + // 列配置
148 + columns: {
149 + type: Array,
150 + default: () => [],
151 + },
152 + // 列表总数量
153 + count: {
154 + type: Number,
155 + default: 0,
156 + },
157 + // 左侧固定列栏数
158 + leftFix: {
159 + type: Number,
160 + default: 1,
161 + },
162 + // 右侧固定列栏数
163 + rightFix: {
164 + type: Number,
165 + default: 1,
166 + },
167 + // 是否分页
168 + pageable: {
169 + type: Boolean,
170 + default: true,
171 + },
172 + // 每页条数
173 + pageSize: {
174 + type: Number,
175 + default: 10,
176 + },
177 + // 分页组件布局
178 + layout: {
179 + type: String,
180 + default: "total, sizes, prev, pager, next, jumper",
181 + },
182 + rowKey: [String, Function],
183 + },
184 + watch: {
185 + count: {
186 + handler(value) {
187 + // 解决查询列表,分页器页码没同步更新问题
188 + if (value === 0) {
189 + this.page = 1;
190 + }
191 + },
192 + },
193 + pageSize: {
194 + handler(value) {
195 + value && (this.currentPageSize = value);
196 + },
197 + immediate: true,
198 + },
199 + layout: {
200 + handler(value) {
201 + value && (this.pageLayout = value);
202 + },
203 + immediate: true,
204 + },
205 + },
206 + created() {},
207 + methods: {
208 + doLayout() {
209 + this.$refs.table.doLayout();
210 + },
211 + handleSortChange(value) {
212 + console.log(value);
213 + this.$emit("sort-change", value);
214 + },
215 + /**
216 + * 选择操作
217 + * @param value
218 + */
219 + handleSelectionChange(value) {
220 + this.$emit("selection-change", value);
221 + },
222 + /**
223 + * 当前页码变化
224 + * @param val
225 + */
226 + handleCurrentChange(val) {
227 + this.page = val;
228 + this.$emit("current-change", val);
229 + },
230 + /**
231 + * 每页数量变化
232 + * @param val
233 + */
234 + handleSizeChange(val) {
235 + this.currentPageSize = val;
236 + this.$emit("size-change", val);
237 + },
238 + // 是否固定
239 + getFixed(columnKey) {
240 + if (columnKey < this.leftFix) {
241 + return "left";
242 + } else if (columnKey > this.columns.length - this.rightFix - 1) {
243 + return "right";
244 + }
245 + return false;
246 + },
247 + // 宽度
248 + getWidth(column) {
249 + // 1. 优先 zpMark 固定宽度项
250 + // 2. width属性
251 + // 3. 不设宽度
252 + if (column.zpMark) {
253 + if (Object.keys(tableFixWidths).includes(column.zpMark)) {
254 + return tableFixWidths[column.zpMark];
255 + }
256 + }
257 + if (column.width) {
258 + return column.width;
259 + }
260 + return "";
261 + },
262 + },
263 +};
264 +</script>
265 +<style lang="less" scope>
266 +.zp-table-list {
267 + .el-table {
268 + .el-button--text {
269 + padding: 0 2px;
270 + }
271 +
272 + .el-tag {
273 + height: 24px;
274 + line-height: 22px;
275 + border-radius: 12px;
276 + }
277 + }
278 +
279 + .el-table__fixed,
280 + .el-table__fixed-right {
281 + &:before {
282 + background-color: transparent;
283 + }
284 + }
285 +}
286 +</style>
287 +<style lang="less" scoped>
288 +.zp-table-list {
289 + height: 100%;
290 + display: flex;
291 + flex-direction: column;
292 +
293 + .el-pagination {
294 + text-align: right;
295 +
296 + .el-pagination__total {
297 + float: left;
298 + }
299 + }
300 +}
301 +</style>
1 +<template>
2 + <svg class="icon" aria-hidden="true">
3 + <use :xlink:href="iconName"></use>
4 + </svg>
5 +</template>
6 +
7 +<script>
8 +export default {
9 + name: "Icon",
10 + props: {
11 + name: {
12 + type: String,
13 + required: true,
14 + },
15 + },
16 + computed: {
17 + iconName() {
18 + return `#icon-${this.name}`;
19 + },
20 + },
21 +};
22 +</script>
1 +<template>
2 + <el-dialog
3 + :title="title"
4 + class="zp-dialog__wrapper"
5 + v-bind="$attrs"
6 + v-on="$listeners"
7 + >
8 + <template v-slot:title v-if="this.$slots.title">
9 + <slot name="title"></slot>
10 + </template>
11 + <template v-slot:footer>
12 + <slot name="footer"></slot>
13 + </template>
14 + <slot></slot>
15 + </el-dialog>
16 +</template>
17 +
18 +<script>
19 +export default {
20 + name: "ZpDialog",
21 + components: {},
22 + props: {
23 + title: {
24 + type: String,
25 + default: "",
26 + },
27 + },
28 + computed: {},
29 + data() {
30 + return {};
31 + },
32 + watch: {},
33 + created() {},
34 + mounted() {},
35 + beforeDestroy() {},
36 + methods: {},
37 +};
38 +</script>
39 +
40 +<style lang="less" scoped>
41 +.zp-dialog__wrapper {
42 + /deep/ .el-dialog {
43 + .el-dialog__body {
44 + box-sizing: border-box;
45 + max-height: calc(~"85vh - 112px");
46 + overflow-y: auto;
47 + }
48 + }
49 +}
50 +</style>
1 +<template>
2 + <div class="zp-single-img-upload__container">
3 + <el-upload
4 + ref="upload"
5 + :action="action"
6 + :accept="accept"
7 + :file-list="fileList"
8 + :headers="headers"
9 + :data="data"
10 + list-type="picture-card"
11 + :auto-upload="true"
12 + :on-change="onChange"
13 + :on-success="onSuccess"
14 + :before-upload="beforeUpload"
15 + >
16 + <div ref="triggerWrapper" class="zp-single-img-upload__trigger-wrapper">
17 + <slot v-if="this.$slots['trigger']" name="trigger"></slot>
18 + <i v-else slot="default" class="el-icon-plus"></i>
19 + </div>
20 +
21 + <div
22 + class="zp-single-img-upload__img-wrapper"
23 + slot="file"
24 + slot-scope="{ file }"
25 + >
26 + <el-image
27 + ref="zpSingleImg"
28 + class="zp-single-img-upload__img"
29 + v-if="imgUrl"
30 + :src="imgUrl"
31 + :preview-src-list="[imgUrl]"
32 + fit="contain"
33 + ></el-image>
34 + <span class="el-upload-list__item-actions" v-if="preview">
35 + <span
36 + class="el-upload-list__item-preview"
37 + @click="handlePictureCardPreview(file)"
38 + >
39 + <i class="el-icon-zoom-in"></i>
40 + </span>
41 + <span class="el-upload-list__item-delete" @click="handleDelete()">
42 + <i class="el-icon-delete"></i>
43 + </span>
44 + </span>
45 + <div class="zp-single-img-upload__reload" @click="handleReload" v-else>
46 + 更换图片
47 + </div>
48 + </div>
49 + <div class="el-upload__tip" slot="tip">
50 + <slot v-if="this.$slots['tip']" name="tip"></slot>
51 + <div v-else>支持图片格式:jpg、png</div>
52 + </div>
53 + </el-upload>
54 + </div>
55 +</template>
56 +
57 +<script>
58 +import { getToken } from "utils/auth";
59 +
60 +export default {
61 + name: "zpSingleImgUpload",
62 + props: {
63 + //图片url
64 + imgUrl: {
65 + type: String,
66 + default: "",
67 + },
68 + //图片上传接口地址
69 + action: {
70 + type: String,
71 + default: "/admin_api/uploadImage",
72 + },
73 + //支持的图片格式
74 + accept: {
75 + type: String,
76 + default: "image/*",
77 + },
78 + //文件最大大小(M)
79 + maxSize: {
80 + type: Number,
81 + default: 6,
82 + },
83 + //文件存储是否公有(公有存储读取的时候不需要签名,敏感文件私有,否则公有)
84 + public: {
85 + type: Boolean,
86 + default: false,
87 + },
88 + //是否可以预览图片
89 + preview: {
90 + type: Boolean,
91 + default: false,
92 + },
93 + },
94 + model: {
95 + prop: "imgUrl",
96 + event: "input",
97 + },
98 + data() {
99 + return {
100 + headers: {}, //请求头
101 + data: {}, //请求额外参数
102 + fileList: [], //文件列表
103 + limitCount: 1, //最多支持个数
104 + };
105 + },
106 + watch: {
107 + imgUrl: {
108 + immediate: true,
109 + handler(val) {
110 + console.log("imgUrl:", val);
111 + if (val) {
112 + this.fileList = [{ url: val }];
113 + } else {
114 + this.fileList = [];
115 + }
116 + },
117 + },
118 + },
119 + computed: {},
120 + created() {
121 + this.headers["Token-Auth"] = getToken();
122 + if (this.public) {
123 + this.data["acl"] = "public-read";
124 + }
125 + },
126 + methods: {
127 + /**
128 + * 上传图片校验
129 + */
130 + beforeUpload(file) {
131 + let isIMG;
132 + const regList = this.accept.split(",");
133 + if (this.accept === "image/*") {
134 + isIMG = /image/i.test(file.type);
135 + } else {
136 + isIMG = regList.includes(file.type); // 是否符合类型
137 + }
138 + const isLt6M = file.size / 1024 / 1024 < this.maxSize;
139 + if (!isIMG) {
140 + this.fileList = [];
141 + this.$message.error("图片格式仅支持png、jpg,请更换图片!");
142 + }
143 + if (!isLt6M) {
144 + this.$message.error(`上传图片大小不能超过${this.maxSize}MB!`);
145 + }
146 + console.log("beforeUpload", file, isIMG && isLt6M);
147 + return isIMG && isLt6M;
148 + },
149 + /**
150 + * 文件修改
151 + */
152 + onChange(file, fileList) {
153 + console.log("onChange file:", file);
154 + console.log("onChange fileList:", fileList);
155 + const overMaxSize = file.size / 1024 / 1024 < this.maxSize;
156 + if (overMaxSize && fileList.length > 0) {
157 + this.fileList = [fileList[fileList.length - 1]];
158 + } else {
159 + this.fileList = [];
160 + }
161 + },
162 + /**
163 + * 上传成功
164 + */
165 + onSuccess(res, file, fileList) {
166 + console.log("onSuccess res:", res);
167 + console.log("onSuccess file:", file);
168 + console.log("onSuccess fileList:", fileList);
169 + const { data } = res;
170 + this.$emit("input", data.file);
171 + },
172 + /**
173 + * 删除
174 + */
175 + handleDelete() {
176 + console.log("handleDelete");
177 + this.$emit("input", "");
178 + },
179 + /**
180 + * 预览
181 + * @param file
182 + */
183 + handlePictureCardPreview(file) {
184 + this.$refs.zpSingleImg.clickHandler();
185 + },
186 + handleReload() {
187 + this.$refs.triggerWrapper.click();
188 + },
189 + },
190 +};
191 +</script>
192 +
193 +<style lang="less" scoped>
194 +.zp-single-img-upload__container {
195 + display: inline-block;
196 + width: 148px;
197 + margin-right: 16px;
198 + /deep/ .el-upload {
199 + border: 1px dashed #d8dce5;
200 + }
201 + /deep/ .el-upload-list__item {
202 + border: 1px solid #d8dce5;
203 + }
204 + .zp-single-img-upload__img-wrapper {
205 + height: 100%;
206 + .zp-single-img-upload__img {
207 + height: 100%;
208 + width: 100%;
209 + }
210 + }
211 +
212 + .zp-single-img-upload__reload {
213 + position: absolute;
214 + left: 0;
215 + bottom: 0;
216 + width: 100%;
217 + text-align: center;
218 + background: rgba(51, 51, 51, 0.8);
219 + color: #fff;
220 + font-size: 12px;
221 + cursor: pointer;
222 + }
223 +
224 + /deep/ .el-upload-list {
225 + position: absolute;
226 + }
227 +}
228 +</style>
1 +<template>
2 + <a
3 + :href="githubLink"
4 + target="_blank"
5 + class="github-corner"
6 + aria-label="View source on Github"
7 + >
8 + <svg
9 + width="80"
10 + height="80"
11 + viewBox="0 0 250 250"
12 + style=""
13 + aria-hidden="true"
14 + >
15 + <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
16 + <path
17 + d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
18 + fill="currentColor"
19 + style="transform-origin: 130px 106px"
20 + class="octo-arm"
21 + ></path>
22 + <path
23 + d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
24 + fill="currentColor"
25 + class="octo-body"
26 + ></path>
27 + </svg>
28 + </a>
29 +</template>
30 +<script>
31 +export default {
32 + name: "Github",
33 + props: {
34 + githubLink: {
35 + type: String,
36 + default: null,
37 + },
38 + },
39 +};
40 +</script>
41 +
42 +<style lang="less" scoped>
43 +.github-corner {
44 + fill: #4ab7bd;
45 + color: #fff;
46 + position: absolute;
47 + top: 50px;
48 + border: 0;
49 + right: 0;
50 +
51 + &:hover .octo-arm {
52 + animation: octocat-wave 560ms ease-in-out;
53 + }
54 +}
55 +
56 +@keyframes octocat-wave {
57 + 0%,
58 + 100% {
59 + transform: rotate(0);
60 + }
61 + 20%,
62 + 60% {
63 + transform: rotate(-25deg);
64 + }
65 + 40%,
66 + 80% {
67 + transform: rotate(10deg);
68 + }
69 +}
70 +</style>
71 +
1 +<template>
2 + <div>
3 + <svg
4 + t="1492500959545"
5 + @click="toggleClick"
6 + class="wscn-icon hamburger"
7 + :class="{ 'is-active': isActive }"
8 + style=""
9 + viewBox="0 0 1024 1024"
10 + version="1.1"
11 + xmlns="http://www.w3.org/2000/svg"
12 + p-id="1691"
13 + xmlns:xlink="http://www.w3.org/1999/xlink"
14 + width="64"
15 + height="64"
16 + >
17 + <path
18 + d="M966.8023 568.849776 57.196677 568.849776c-31.397081 0-56.850799-25.452695-56.850799-56.850799l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 543.397081 998.200404 568.849776 966.8023 568.849776z"
19 + p-id="1692"
20 + ></path>
21 + <path
22 + d="M966.8023 881.527125 57.196677 881.527125c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.849776 56.850799-56.849776l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.849776l0 0C1023.653099 856.07443 998.200404 881.527125 966.8023 881.527125z"
23 + p-id="1693"
24 + ></path>
25 + <path
26 + d="M966.8023 256.17345 57.196677 256.17345c-31.397081 0-56.850799-25.452695-56.850799-56.849776l0 0c0-31.397081 25.452695-56.850799 56.850799-56.850799l909.605623 0c31.397081 0 56.849776 25.452695 56.849776 56.850799l0 0C1023.653099 230.720755 998.200404 256.17345 966.8023 256.17345z"
27 + p-id="1694"
28 + ></path>
29 + </svg>
30 + </div>
31 +</template>
32 +
33 +<script>
34 +export default {
35 + name: "hamburger",
36 + props: {
37 + isActive: {
38 + type: Boolean,
39 + default: false,
40 + },
41 + toggleClick: {
42 + type: Function,
43 + default: null,
44 + },
45 + },
46 +};
47 +</script>
48 +
49 +<style scoped>
50 +.hamburger {
51 + display: inline-block;
52 + cursor: pointer;
53 + width: 20px;
54 + height: 20px;
55 + transform: rotate(0deg);
56 + transition: 0.38s;
57 + transform-origin: 50% 50%;
58 +}
59 +
60 +.hamburger.is-active {
61 + transform: rotate(90deg);
62 +}
63 +</style>
1 +// 来源
2 +export const sources = [
3 + { name: "原创", id: 1 },
4 + { name: "转载", id: 2 },
5 + { name: "翻译", id: 3 },
6 +];
1 +/**
2 + * 时间日期格式化
3 + * 用法 formatTime(new Date(), 'yyyy-MM-dd hh:mm:ss')
4 + * @param time
5 + * @param fmt
6 + */
7 +export function formatTime(time, fmt) {
8 + time = parseInt(time);
9 + if (!time) {
10 + return "";
11 + }
12 + const date = new Date(time);
13 + let o = {
14 + "M+": date.getMonth() + 1, // 月份
15 + "d+": date.getDate(), // 日
16 + "h+": date.getHours(), // 小时
17 + "m+": date.getMinutes(), // 分
18 + "s+": date.getSeconds(), // 秒
19 + "q+": Math.floor((date.getMonth() + 3) / 3), // 季度
20 + S: date.getMilliseconds(), // 毫秒
21 + };
22 + if (/(y+)/.test(fmt)) {
23 + fmt = fmt.replace(
24 + RegExp.$1,
25 + (date.getFullYear() + "").substr(4 - RegExp.$1.length)
26 + );
27 + }
28 + for (let k in o) {
29 + if (new RegExp("(" + k + ")").test(fmt)) {
30 + fmt = fmt.replace(
31 + RegExp.$1,
32 + RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
33 + );
34 + }
35 + }
36 + return fmt;
37 +}
1 +import Vue from "vue";
2 +import App from "./App.vue";
3 +import router from "./router/permission";
4 +import store from "./store";
5 +import ElementUI from "element-ui";
6 +import "element-ui/lib/theme-chalk/index.css";
7 +import "./styles/index.less";
8 +import pageInfoMixin from "src/mixins/pageInfo";
9 +
10 +import "./utils/config/iconfont";
11 +import "src/components/common/index.js";
12 +
13 +import * as filters from "./filters";
14 +Object.keys(filters).forEach((key) => {
15 + Vue.filter(key, filters[key]);
16 +});
17 +
18 +Vue.mixin(pageInfoMixin);
19 +Vue.use(ElementUI);
20 +
21 +new Vue({
22 + el: "#app",
23 + router,
24 + store,
25 + template: "<App/>",
26 + components: { App },
27 +});
1 +/**
2 + * 1、先引入 pageInfo 的mixin文件,注入mixin
3 + * 2、给 requestPageData 方法赋值页面翻页查询方法
4 + */
5 +
6 +export default {
7 + data() {
8 + return {
9 + requestPageData: null, // 每个table组建的数据源获取方法
10 + pageInfo: {
11 + total: 0,
12 + pageNum: 1,
13 + pageSize: 10,
14 + },
15 + };
16 + },
17 + watch: {
18 + "pageInfo.pageNum"() {
19 + this.requestPageData && this.requestPageData();
20 + },
21 + "pageInfo.pageSize"() {
22 + this.requestPageData && this.requestPageData();
23 + },
24 + },
25 + methods: {
26 + handleCurrentChange(page) {
27 + this.pageInfo.pageNum = page;
28 + },
29 + handleSizeChange(val) {
30 + this.pageInfo.pageSize = val;
31 + },
32 + },
33 +};
1 +import Vue from "vue";
2 +import Router from "vue-router";
3 +Vue.use(Router);
4 +
5 +// 解决同一页面,参数不同的路由报错
6 +const VueRouterPush = Router.prototype.push;
7 +Router.prototype.push = function push(to) {
8 + return VueRouterPush.call(this, to).catch((err) => err);
9 +};
10 +
11 +/**
12 + * 加载模块
13 + * @param {string | Component} component 路径或组件
14 + * @param {boolean} lazy 是否懒加载
15 + * @returns {Function | object} 懒加载方法或组件对象
16 + */
17 +const loadComponent = (component, lazy = true) =>
18 + lazy ? () => import(`views/${component}.vue`) : component;
19 +
20 +export const constantRouterMap = [
21 + {
22 + path: "/login",
23 + name: "登录",
24 + component: loadComponent("Login/index"),
25 + hidden: true,
26 + },
27 + {
28 + path: "/",
29 + name: "首页",
30 + component: loadComponent("Layout/index"),
31 + redirect: "/home",
32 + icon: "home",
33 + children: [
34 + { path: "home", component: loadComponent("Home/index"), name: "首页" },
35 + ],
36 + },
37 +];
38 +export const asyncRouterMap = [
39 + {
40 + path: "/permission",
41 + name: "权限管理",
42 + meta: { role: ["admin"] },
43 + component: loadComponent("Layout/index"),
44 + redirect: "/permission/list",
45 + requireAuth: true, // 是否需要登录
46 + dropdown: true,
47 + icon: "authority",
48 + children: [
49 + {
50 + path: "list",
51 + component: loadComponent("Permission/list"),
52 + name: "管理员列表",
53 + },
54 + {
55 + path: "add",
56 + component: loadComponent("Permission/add"),
57 + name: "添加管理员",
58 + },
59 + ],
60 + },
61 + {
62 + path: "/article",
63 + name: "文章管理",
64 + component: loadComponent("Layout/index"),
65 + redirect: "/article/list",
66 + dropdown: true,
67 + icon: "article",
68 + children: [
69 + {
70 + path: "list",
71 + component: loadComponent("Article/list"),
72 + name: "文章列表",
73 + },
74 + {
75 + path: "edit",
76 + component: loadComponent("Article/edit"),
77 + name: "文章编辑",
78 + hidden: true,
79 + },
80 + {
81 + path: "add",
82 + component: loadComponent("Article/add"),
83 + name: "添加文章",
84 + },
85 + ],
86 + },
87 + {
88 + path: "/label",
89 + name: "标签管理",
90 + component: loadComponent("Layout/index"),
91 + redirect: "/label/list",
92 + dropdown: true,
93 + icon: "label",
94 + children: [
95 + {
96 + path: "list",
97 + component: loadComponent("Label/list"),
98 + name: "标签列表",
99 + },
100 + {
101 + path: "add",
102 + component: loadComponent("Label/add"),
103 + name: "添加标签",
104 + },
105 + ],
106 + },
107 + {
108 + path: "/message",
109 + name: "留言管理",
110 + component: loadComponent("Layout/index"),
111 + redirect: "/message/list",
112 + dropdown: true,
113 + icon: "message",
114 + children: [
115 + {
116 + path: "list",
117 + component: loadComponent("Message/list"),
118 + name: "留言列表",
119 + },
120 + {
121 + path: "reply",
122 + component: loadComponent("Message/replyList"),
123 + name: "回复列表",
124 + },
125 + ],
126 + },
127 +];
128 +
129 +export const router = new Router({
130 + // mode: 'history',
131 + routes: constantRouterMap,
132 +});
1 +import store from "../store";
2 +import { getToken } from "src/utils/auth";
3 +import { router } from "./index";
4 +import NProgress from "nprogress"; // Progress 进度条
5 +import "nprogress/nprogress.css"; // Progress 进度条样式
6 +
7 +router.beforeEach((to, from, next) => {
8 + NProgress.start();
9 + if (getToken()) {
10 + if (!store.state.user.roles) {
11 + // 重新拉取用户信息
12 + store.dispatch("getUserInfo").then((res) => {
13 + // 如果token过期,则需重新登录
14 + if (res.code === 401) {
15 + next("/login");
16 + } else {
17 + let roles = res.data.roles;
18 + store.dispatch("setRoutes", { roles }).then(() => {
19 + // 根据权限动态添加路由
20 + router.addRoutes(store.state.permission.addRouters);
21 + next({ ...to }); // hash模式 确保路由加载完成
22 + });
23 + }
24 + });
25 + } else {
26 + next();
27 + }
28 + } else {
29 + to.path === "/login" ? next() : next("/login");
30 + }
31 +});
32 +router.afterEach((to, from) => {
33 + document.title = to.name;
34 + NProgress.done();
35 +});
36 +
37 +export default router;
1 +const getters = {
2 + userName: (state) => state.user.username,
3 + userList: (state) => state.user.list,
4 + userTotal: (state) => state.user.total,
5 +};
6 +export default getters;
1 +import Vue from "vue";
2 +import Vuex from "vuex";
3 +import getters from "./getters";
4 +import app from "./modules/app";
5 +import user from "./modules/user";
6 +import permission from "./modules/permission";
7 +
8 +Vue.use(Vuex);
9 +
10 +const store = new Vuex.Store({
11 + modules: {
12 + app,
13 + user,
14 + permission,
15 + },
16 + getters,
17 +});
18 +
19 +export default store;
1 +import { Local } from "src/utils/storage";
2 +const app = {
3 + state: {
4 + slideBar: {
5 + opened: Local.get("slideBarStatus"),
6 + },
7 + tagViews: JSON.parse(Local.get("tagViews")) || [],
8 + is_add_router: false,
9 + },
10 + mutations: {
11 + TOGGLE_SIDEBAR(state) {
12 + if (state.slideBar.opened) {
13 + Local.set("slideBarStatus", false);
14 + } else {
15 + Local.set("slideBarStatus", true);
16 + }
17 + state.slideBar.opened = !state.slideBar.opened;
18 + },
19 + ADD_TAGVIEW(state, tag) {
20 + if (state.tagViews.some((v) => v.name === tag.name)) return;
21 + state.tagViews.push({ name: tag.name, path: tag.path });
22 + Local.set("tagViews", JSON.stringify(state.tagViews));
23 + },
24 + DEL_TAGVIEW(state, tag) {
25 + let index;
26 + for (let [i, v] of state.tagViews.entries()) {
27 + if (v.name === tag.name) index = i;
28 + }
29 + state.tagViews.splice(index, 1);
30 + Local.set("tagViews", JSON.stringify(state.tagViews));
31 + },
32 + },
33 + actions: {
34 + toggleSideBar({ commit }) {
35 + commit("TOGGLE_SIDEBAR");
36 + },
37 + addTagView({ commit }, tag) {
38 + commit("ADD_TAGVIEW", tag);
39 + },
40 + delTagView({ commit }, tag) {
41 + commit("DEL_TAGVIEW", tag);
42 + },
43 + },
44 +};
45 +export default app;
1 +import { constantRouterMap, asyncRouterMap } from "src/router";
2 +
3 +/**
4 + * 通过meta.role判断是否与当前用户权限匹配
5 + * @param role
6 + * @param route
7 + */
8 +const hasPermission = (roles, route) => {
9 + if (route.meta && route.meta.role) {
10 + return roles.some((role) => route.meta.role.indexOf(role) >= 0);
11 + } else {
12 + return true;
13 + }
14 +};
15 +
16 +/**
17 + * 递归过滤异步路由表,返回符合用户角色权限的路由表
18 + * @param asyncRouterMap
19 + * @param role
20 + */
21 +const filterAsyncRouter = (asyncRouterMap, roles) => {
22 + const accessedRouters = asyncRouterMap.filter((route) => {
23 + if (hasPermission(roles, route)) {
24 + if (route.children && route.children.length) {
25 + route.children = filterAsyncRouter(route.children, roles);
26 + }
27 + return true;
28 + }
29 + return false;
30 + });
31 + return accessedRouters;
32 +};
33 +
34 +const permission = {
35 + state: {
36 + routes: constantRouterMap.concat(asyncRouterMap),
37 + addRouters: [],
38 + },
39 + mutations: {
40 + SETROUTES(state, routers) {
41 + state.addRouters = routers;
42 + state.routes = constantRouterMap.concat(routers);
43 + },
44 + },
45 + actions: {
46 + setRoutes({ commit }, info) {
47 + return new Promise((resolve, reject) => {
48 + let { roles } = info;
49 + let accessedRouters = [];
50 + if (roles.indexOf("admin") >= 0) {
51 + accessedRouters = asyncRouterMap;
52 + } else {
53 + accessedRouters = filterAsyncRouter(asyncRouterMap, roles);
54 + }
55 +
56 + commit("SETROUTES", accessedRouters);
57 + resolve();
58 + });
59 + },
60 + },
61 +};
62 +export default permission;
1 +import axios from "src/utils/request";
2 +import { getToken } from "src/utils/auth";
3 +import md5 from "js-md5";
4 +
5 +const user = {
6 + state: {
7 + list: [],
8 + total: 0,
9 + username: "",
10 + roles: null,
11 + token: getToken(),
12 + otherList: [],
13 + },
14 + mutations: {
15 + SET_TOKEN(state, token) {
16 + state.token = token;
17 + },
18 + SET_USERINFO(state, info) {
19 + state.username = info.username;
20 + state.roles = info.roles;
21 + },
22 + USERLIST(state, data) {
23 + state.list = data.list;
24 + state.total = data.list.length || 0;
25 + },
26 + GET_INFOLIST(state, data) {
27 + state.otherList = data;
28 + },
29 + CLEARINFO(state) {
30 + state.username = "";
31 + state.roles = null;
32 + },
33 + },
34 + actions: {
35 + clearInfo({ commit }) {
36 + commit("CLEARINFO");
37 + },
38 + userLogin({ state, commit }, info) {
39 + let { username, pwd } = info;
40 + return new Promise((resolve, reject) => {
41 + axios
42 + .post("/user/login", {
43 + username: username,
44 + pwd: md5(pwd),
45 + })
46 + .then((res) => {
47 + state.token = getToken();
48 + resolve(res);
49 + })
50 + .catch((err) => {
51 + reject(err);
52 + });
53 + });
54 + },
55 + getUserInfo({ state, commit }) {
56 + return new Promise((resolve, reject) => {
57 + axios
58 + .get("/user/info", {
59 + token: state.token,
60 + })
61 + .then((res) => {
62 + commit("SET_USERINFO", res.data);
63 + resolve(res);
64 + })
65 + .catch((err) => {
66 + // console.log(err)
67 + reject(err);
68 + });
69 + });
70 + },
71 + getUserList({ commit }, params) {
72 + return new Promise((resolve, reject) => {
73 + axios
74 + .get("/user/list", params)
75 + .then((res) => {
76 + commit("USERLIST", res.data);
77 + resolve(res);
78 + })
79 + .catch((err) => {
80 + // console.log(err)
81 + reject(err);
82 + });
83 + });
84 + },
85 + addUser({ commit }, info) {
86 + info.pwd = md5(info.pwd);
87 + return new Promise((resolve, reject) => {
88 + axios
89 + .post("/user/add", info)
90 + .then((res) => {
91 + resolve(res);
92 + })
93 + .catch((err) => {
94 + reject(err);
95 + });
96 + });
97 + },
98 + delUser({ commit }, id) {
99 + return new Promise((resolve, reject) => {
100 + axios
101 + .get("/user/del", { id: id })
102 + .then((res) => {
103 + resolve(res);
104 + })
105 + .catch((err) => {
106 + reject(err);
107 + });
108 + });
109 + },
110 + updateUser({ commit }, info) {
111 + info.pwd = md5(info.pwd);
112 + info.old_pwd = md5(info.old_pwd);
113 + return new Promise((resolve, reject) => {
114 + axios
115 + .post("/user/update", info)
116 + .then((res) => {
117 + resolve(res);
118 + })
119 + .catch((err) => {
120 + reject(err);
121 + });
122 + });
123 + },
124 + },
125 +};
126 +
127 +export default user;
1 +@import "./less/reset.less";
2 +@import "./less/init.less";
3 +@import "./less/element-ui.less";
1 +@bg-color: #324157;
2 +@font-color: #bfcbd9;
3 +@font-hover-color: #48576a;
4 +@active-font-color: #409EFF;
5 +@submenu-color: #1f2d3d;
6 +.el-menu-vertical {
7 + background-color: @bg-color;
8 + .el-submenu__title {
9 + color: @font-color;
10 + background-color: @bg-color;
11 + }
12 + .el-submenu .el-menu {
13 + background-color: @submenu-color;
14 + .el-menu-item {
15 + background: transparent;
16 + }
17 + }
18 + .el-menu-item {
19 + color: @font-color;
20 + background: @bg-color;
21 + }
22 + .el-menu-item:hover,
23 + .el-submenu__title:hover {
24 + background-color: @font-hover-color;
25 + }
26 + .el-menu-item.is-active {
27 + color: @active-font-color;
28 + background-color: transparent;
29 + }
30 +}
31 +
32 +.music-cover-uploader {
33 + .el-upload {
34 + border: 1px dashed #d9d9d9;
35 + border-radius: 6px;
36 + cursor: pointer;
37 + position: relative;
38 + overflow: hidden;
39 + }
40 + .el-upload:hover {
41 + border-color: #409EFF;
42 + }
43 + .avatar-uploader-icon {
44 + font-size: 28px;
45 + color: #8c939d;
46 + width: 178px;
47 + height: 178px;
48 + line-height: 178px;
49 + text-align: center;
50 + }
51 + .avatar {
52 + width: 178px;
53 + height: 178px;
54 + display: block;
55 + }
56 +}
57 +.el-breadcrumb__inner, .el-breadcrumb__inner a {
58 + color: #48576a;
59 + font-weight: 400;
60 +}
61 +
62 +.el-breadcrumb__item:last-child .el-breadcrumb__inner, .el-breadcrumb__item:last-child .el-breadcrumb__inner a, .el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover, .el-breadcrumb__item:last-child .el-breadcrumb__inner:hover {
63 + color: #97a8be;
64 + cursor: text;
65 + font-weight: 400;
66 +}
...\ No newline at end of file ...\ No newline at end of file
1 +.icon {
2 + width: 1em;
3 + height: 1em;
4 + vertical-align: -0.15em;
5 + fill: currentColor;
6 + overflow: hidden;
7 +}
8 +
9 +::-webkit-scrollbar {
10 + width: 4px;
11 + height: 4px;
12 +}
13 +
14 +::-webkit-scrollbar-thumb {
15 + opacity: 0.8;
16 + background: #ddd;
17 + border-radius: 4px;
18 + transition: all 0.5s;
19 +}
1 +/* normalize.css */
2 +html {
3 + line-height: 1.15; /* 1 */
4 + -ms-text-size-adjust: 100%; /* 2 */
5 + -webkit-text-size-adjust: 100%; /* 2 */
6 +}
7 +
8 +body {
9 + margin: 0;
10 +}
11 +
12 +article,
13 +aside,
14 +footer,
15 +header,
16 +nav,
17 +section {
18 + display: block;
19 +}
20 +
21 +h1 {
22 + font-size: 2em;
23 + margin: 0.67em 0;
24 +}
25 +
26 +figcaption,
27 +figure,
28 +main {
29 + /* 1 */
30 + display: block;
31 +}
32 +
33 +figure {
34 + margin: 1em 40px;
35 +}
36 +
37 +hr {
38 + box-sizing: content-box; /* 1 */
39 + height: 0; /* 1 */
40 + overflow: visible; /* 2 */
41 +}
42 +
43 +pre {
44 + font-family: monospace, monospace; /* 1 */
45 + font-size: 1em; /* 2 */
46 +}
47 +
48 +a {
49 + background-color: transparent; /* 1 */
50 + -webkit-text-decoration-skip: objects; /* 2 */
51 +}
52 +
53 +abbr[title] {
54 + border-bottom: none; /* 1 */
55 + text-decoration: underline; /* 2 */
56 + text-decoration: underline dotted; /* 2 */
57 +}
58 +
59 +b,
60 +strong {
61 + font-weight: inherit;
62 +}
63 +
64 +b,
65 +strong {
66 + font-weight: bolder;
67 +}
68 +
69 +code,
70 +kbd,
71 +samp {
72 + font-family: monospace, monospace; /* 1 */
73 + font-size: 1em; /* 2 */
74 +}
75 +
76 +dfn {
77 + font-style: italic;
78 +}
79 +
80 +mark {
81 + background-color: #ff0;
82 + color: @mainColor;
83 +}
84 +
85 +small {
86 + font-size: 80%;
87 +}
88 +
89 +sub,
90 +sup {
91 + font-size: 75%;
92 + line-height: 0;
93 + position: relative;
94 + vertical-align: baseline;
95 +}
96 +
97 +sub {
98 + bottom: -0.25em;
99 +}
100 +
101 +sup {
102 + top: -0.5em;
103 +}
104 +
105 +audio,
106 +video {
107 + display: inline-block;
108 +}
109 +audio:not([controls]) {
110 + display: none;
111 + height: 0;
112 +}
113 +
114 +img {
115 + border-style: none;
116 +}
117 +
118 +svg:not(:root) {
119 + overflow: hidden;
120 +}
121 +
122 +button,
123 +input,
124 +optgroup,
125 +select,
126 +textarea {
127 + font-family: sans-serif; /* 1 */
128 + font-size: 100%; /* 1 */
129 + line-height: 1.15; /* 1 */
130 + margin: 0; /* 2 */
131 +}
132 +button,
133 +input {
134 + /* 1 */
135 + overflow: visible;
136 +}
137 +button,
138 +select {
139 + /* 1 */
140 + text-transform: none;
141 +}
142 +
143 +button,
144 + html [type="button"], /* 1 */
145 + [type="reset"],
146 + [type="submit"] {
147 + -webkit-appearance: button; /* 2 */
148 +}
149 +
150 +button::-moz-focus-inner,
151 +[type="button"]::-moz-focus-inner,
152 +[type="reset"]::-moz-focus-inner,
153 +[type="submit"]::-moz-focus-inner {
154 + border-style: none;
155 + padding: 0;
156 +}
157 +
158 +button:-moz-focusring,
159 +[type="button"]:-moz-focusring,
160 +[type="reset"]:-moz-focusring,
161 +[type="submit"]:-moz-focusring {
162 + outline: 1px dotted ButtonText;
163 +}
164 +
165 +fieldset {
166 + padding: 0.35em 0.75em 0.625em;
167 +}
168 +
169 +legend {
170 + box-sizing: border-box; /* 1 */
171 + color: inherit; /* 2 */
172 + display: table; /* 1 */
173 + max-width: 100%; /* 1 */
174 + padding: 0; /* 3 */
175 + white-space: normal; /* 1 */
176 +}
177 +
178 +progress {
179 + display: inline-block; /* 1 */
180 + vertical-align: baseline; /* 2 */
181 +}
182 +
183 +textarea {
184 + overflow: auto;
185 +}
186 +
187 +[type="checkbox"],
188 +[type="radio"] {
189 + box-sizing: border-box; /* 1 */
190 + padding: 0; /* 2 */
191 +}
192 +
193 +[type="number"]::-webkit-inner-spin-button,
194 +[type="number"]::-webkit-outer-spin-button {
195 + height: auto;
196 +}
197 +
198 +[type="search"] {
199 + -webkit-appearance: textfield; /* 1 */
200 + outline-offset: -2px; /* 2 */
201 +}
202 +
203 +[type="search"]::-webkit-search-cancel-button,
204 +[type="search"]::-webkit-search-decoration {
205 + -webkit-appearance: none;
206 +}
207 +
208 +::-webkit-file-upload-button {
209 + -webkit-appearance: button; /* 1 */
210 + font: inherit; /* 2 */
211 +}
212 +
213 +details, /* 1 */
214 + menu {
215 + display: block;
216 +}
217 +
218 +summary {
219 + display: list-item;
220 +}
221 +
222 +canvas {
223 + display: inline-block;
224 +}
225 +
226 +template {
227 + display: none;
228 +}
229 +
230 +[hidden] {
231 + display: none;
232 +}
233 +
234 +/* reset */
235 +* {
236 + box-sizing: border-box;
237 +}
238 +html,
239 +body {
240 + font: Oswald, "Open Sans", Helvetica, Arial, sans-serif;
241 +}
242 +/* 禁止长按链接与图片弹出菜单 */
243 +a,
244 +img {
245 + -webkit-touch-callout: none;
246 +}
247 +
248 +/*ios android去除自带阴影的样式*/
249 +a,
250 +input {
251 + -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
252 +}
253 +
254 +input[type="text"] {
255 + -webkit-appearance: none;
256 +}
257 +
258 +html,
259 +body,
260 +h1,
261 +h2,
262 +h3,
263 +h4,
264 +h5,
265 +h6,
266 +div,
267 +dl,
268 +dt,
269 +dd,
270 +ul,
271 +ol,
272 +li,
273 +p,
274 +blockquote,
275 +pre,
276 +hr,
277 +figure,
278 +table,
279 +caption,
280 +th,
281 +td,
282 +form,
283 +fieldset,
284 +legend,
285 +input,
286 +button,
287 +textarea,
288 +menu {
289 + margin: 0;
290 + padding: 0;
291 +}
292 +header,
293 +footer,
294 +section,
295 +article,
296 +aside,
297 +nav,
298 +hgroup,
299 +address,
300 +figure,
301 +figcaption,
302 +menu,
303 +details {
304 + display: block;
305 +}
306 +table {
307 + border-collapse: collapse;
308 + border-spacing: 0;
309 +}
310 +caption,
311 +th {
312 + text-align: left;
313 + font-weight: normal;
314 +}
315 +html,
316 +body,
317 +fieldset,
318 +img,
319 +iframe,
320 +abbr {
321 + border: 0;
322 +}
323 +i,
324 +cite,
325 +em,
326 +var,
327 +address,
328 +dfn {
329 + font-style: normal;
330 +}
331 +[hidefocus],
332 +summary {
333 + outline: 0;
334 +}
335 +li {
336 + list-style: none;
337 +}
338 +h1,
339 +h2,
340 +h3,
341 +h4,
342 +h5,
343 +h6,
344 +small {
345 + font-size: 100%;
346 +}
347 +sup,
348 +sub {
349 + font-size: 83%;
350 +}
351 +pre,
352 +code,
353 +kbd,
354 +samp {
355 + font-family: inherit;
356 +}
357 +q:before,
358 +q:after {
359 + content: none;
360 +}
361 +textarea {
362 + overflow: auto;
363 + resize: none;
364 +}
365 +label,
366 +summary {
367 + cursor: default;
368 +}
369 +a,
370 +button {
371 + cursor: pointer;
372 +}
373 +h1,
374 +h2,
375 +h3,
376 +h4,
377 +h5,
378 +h6,
379 +em,
380 +strong,
381 +b {
382 + font-weight: bold;
383 +}
384 +del,
385 +ins,
386 +u,
387 +s,
388 +a,
389 +a:hover {
390 + text-decoration: none;
391 +}
1 +import { Cookie } from "./storage";
2 +const TokenKey = "Token-Auth";
3 +
4 +export function getToken() {
5 + return Cookie.get(TokenKey);
6 +}
7 +
8 +export function setToken(token) {
9 + return Cookie.set(TokenKey, token);
10 +}
11 +
12 +export function removeToken() {
13 + return Cookie.remove(TokenKey);
14 +}
This diff is collapsed. Click to expand it.
1 +/*
2 +列表表格里 固定宽度 栏
3 + */
4 +const TABLE_FIX_WIDTH_PHONE = 'phone'; // 手机号
5 +const TABLE_FIX_WIDTH_USERNAME = 'username'; // 名字
6 +const TABLE_FIX_WIDTH_ID_CARD = 'idCard'; // 身份证
7 +const TABLE_FIX_WIDTH_BANK_CARD = 'bankCard'; // 银行卡号码
8 +const TABLE_FIX_WIDTH_LABEL = 'label'; // 标签
9 +const TABLE_FIX_WIDTH_TIME = 'datetime'; // 时间
10 +const TABLE_FIX_WIDTH_DATE = 'date'; // 日期
11 +
12 +const tableFixWidths = {
13 + [TABLE_FIX_WIDTH_PHONE]: 120,
14 + [TABLE_FIX_WIDTH_USERNAME]: 88,
15 + [TABLE_FIX_WIDTH_LABEL]: 88,
16 + [TABLE_FIX_WIDTH_TIME]: 168,
17 + [TABLE_FIX_WIDTH_DATE]: 120,
18 + [TABLE_FIX_WIDTH_ID_CARD]: 180,
19 + [TABLE_FIX_WIDTH_BANK_CARD]: 190,
20 +};
21 +
22 +export {
23 + TABLE_FIX_WIDTH_PHONE,
24 + TABLE_FIX_WIDTH_USERNAME,
25 + TABLE_FIX_WIDTH_LABEL,
26 + TABLE_FIX_WIDTH_TIME,
27 + TABLE_FIX_WIDTH_DATE,
28 + TABLE_FIX_WIDTH_ID_CARD,
29 + TABLE_FIX_WIDTH_BANK_CARD,
30 + tableFixWidths,
31 +};
1 +import axios from 'axios';
2 +import qs from 'qs';
3 +import { Message } from 'element-ui';
4 +import { removeToken } from './auth';
5 +
6 +axios.defaults.withCredentials = true;
7 +
8 +// 发送时
9 +axios.interceptors.request.use(
10 + (config) => config,
11 + (err) => Promise.reject(err)
12 +);
13 +
14 +// 响应时
15 +axios.interceptors.response.use(
16 + (response) => response,
17 + (err) => Promise.resolve(err.response)
18 +);
19 +
20 +// 检查状态码
21 +function checkStatus(res) {
22 + if (res.status === 200 || res.status === 304) {
23 + return res.data;
24 + }
25 + // token过期清掉
26 + if (res.status === 401) {
27 + removeToken();
28 + Message({
29 + message: res.data,
30 + type: 'error',
31 + duration: 2 * 1000,
32 + });
33 + return {
34 + code: 401,
35 + msg: res.data || res.statusText,
36 + data: res.data,
37 + };
38 + }
39 + return {
40 + code: 0,
41 + msg: res.data.msg || res.statusText,
42 + data: res.statusText,
43 + };
44 +}
45 +
46 +// 检查CODE值
47 +function checkCode(res) {
48 + if (res.code === 0) {
49 + Message({
50 + message: res.msg,
51 + type: 'error',
52 + duration: 2 * 1000,
53 + });
54 + throw new Error(res.msg);
55 + }
56 + return res;
57 +}
58 +
59 +const prefix = '/admin_api';
60 +export default {
61 + get(url, params) {
62 + if (!url) return;
63 + return axios({
64 + method: 'get',
65 + url: prefix + url,
66 + params,
67 + timeout: 30000,
68 + })
69 + .then(checkStatus)
70 + .then(checkCode);
71 + },
72 + post(url, data) {
73 + if (!url) return;
74 + return axios({
75 + method: 'post',
76 + url: prefix + url,
77 + data: qs.stringify(data),
78 + timeout: 30000,
79 + })
80 + .then(checkStatus)
81 + .then(checkCode);
82 + },
83 + postFile(url, data) {
84 + if (!url) return;
85 + return axios({
86 + method: 'post',
87 + url: prefix + url,
88 + data,
89 + })
90 + .then(checkStatus)
91 + .then(checkCode);
92 + },
93 +};
1 +const ls = window.localStorage;
2 +const ss = window.sessionStorage;
3 +
4 +export const Cookie = {
5 + get(key) {
6 + let arr = document.cookie.split('; ');
7 + for (let i = 0; i < arr.length; i++) {
8 + let arr2 = arr[i].trim().split('=');
9 + if (arr2[0] == key) {
10 + return arr2[1];
11 + }
12 + }
13 + return '';
14 + },
15 + set(key, value, day) {
16 + let setting = arguments[0];
17 + if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') {
18 + for (let i in setting) {
19 + let oDate = new Date();
20 + oDate.setDate(oDate.getDate() + day);
21 + document.cookie = i + '=' + setting[i] + ';expires=' + oDate;
22 + }
23 + } else {
24 + let oDate = new Date();
25 + oDate.setDate(oDate.getDate() + day);
26 + document.cookie = key + '=' + value + ';expires=' + oDate;
27 + }
28 + },
29 + remove(key) {
30 + this.set(key, 1, -1);
31 + },
32 +};
33 +
34 +export const Local = {
35 + get(key) {
36 + if (key) return JSON.parse(ls.getItem(key));
37 + return null;
38 + },
39 + set(key, val) {
40 + const setting = arguments[0];
41 + if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') {
42 + for (const i in setting) {
43 + ls.setItem(i, JSON.stringify(setting[i]));
44 + }
45 + } else {
46 + ls.setItem(key, JSON.stringify(val));
47 + }
48 + },
49 + remove(key) {
50 + ls.removeItem(key);
51 + },
52 + clear() {
53 + ls.clear();
54 + },
55 +};
56 +
57 +export const Session = {
58 + get(key) {
59 + if (key) return JSON.parse(ss.getItem(key));
60 + return null;
61 + },
62 + set(key, val) {
63 + const setting = arguments[0];
64 + if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object') {
65 + for (const i in setting) {
66 + ss.setItem(i, JSON.stringify(setting[i]));
67 + }
68 + } else {
69 + ss.setItem(key, JSON.stringify(val));
70 + }
71 + },
72 + remove(key) {
73 + ss.removeItem(key);
74 + },
75 + clear() {
76 + ss.clear();
77 + },
78 +};
1 +<template>
2 + <div class="article-add">
3 + <zp-page-edit :back="true" @back="$router.back()">
4 + <div slot="header">
5 + {{ header }}
6 + </div>
7 +
8 + <el-form
9 + :model="info"
10 + :rules="rules"
11 + ref="form"
12 + label-width="100px"
13 + class="form"
14 + >
15 + <el-form-item label="博客类型:" prop="type">
16 + <el-select
17 + v-model="info.type"
18 + multiple
19 + clearable
20 + placeholder="请选择博客类型"
21 + class="block"
22 + >
23 + <el-option
24 + v-for="item in labelList"
25 + :key="item.label"
26 + :label="item.label"
27 + :value="item.label"
28 + >
29 + </el-option>
30 + </el-select>
31 + </el-form-item>
32 + <el-form-item label="文章标题:" prop="title">
33 + <el-input type="text" v-model="info.title"></el-input>
34 + </el-form-item>
35 + <el-form-item label="文章描述:" prop="desc">
36 + <el-input type="textarea" v-model="info.desc"></el-input>
37 + </el-form-item>
38 + <el-form-item label="文章封面:" prop="fileCoverImgUrl">
39 + <zp-single-img-upload v-model="info.fileCoverImgUrl" :public="true">
40 + </zp-single-img-upload>
41 + </el-form-item>
42 + <el-form-item label="文章内容:" prop="markdown" class="markdown">
43 + <Markdown v-model="info.markdown"></Markdown>
44 + </el-form-item>
45 + <el-form-item label="级别:" prop="album">
46 + <el-select
47 + v-model="info.level"
48 + placeholder="请选择级别"
49 + class="block"
50 + >
51 + <el-option
52 + v-for="item in [1, 2, 3, 4, 5, 6]"
53 + :key="item"
54 + :label="item"
55 + :value="item"
56 + ></el-option>
57 + </el-select>
58 + </el-form-item>
59 + <el-form-item label="来源:" prop="source">
60 + <el-select
61 + v-model="info.source"
62 + placeholder="请选择来源"
63 + class="block"
64 + >
65 + <el-option
66 + v-for="item in sources"
67 + :key="item.id"
68 + :label="item.name"
69 + :value="item.id"
70 + ></el-option>
71 + </el-select>
72 + </el-form-item>
73 + <el-form-item label="Github:" prop="github">
74 + <el-input type="text" v-model="info.github"></el-input>
75 + </el-form-item>
76 + <el-form-item label="Auth:" prop="auth">
77 + <el-input type="text" v-model="info.auth"></el-input>
78 + </el-form-item>
79 + <el-form-item label="是否可见:" prop="isVisible" class="left-item">
80 + <el-switch v-model="info.isVisible"></el-switch>
81 + </el-form-item>
82 + <el-form-item>
83 + <el-button
84 + type="primary"
85 + @click="handleSubmit('form')"
86 + :loading="loading"
87 + >立即创建</el-button
88 + >
89 + </el-form-item>
90 + </el-form>
91 + </zp-page-edit>
92 + </div>
93 +</template>
94 +
95 +<script>
96 +import { sources } from "src/constant/classify";
97 +import { apiGetLabelList } from "src/api/label";
98 +import { apiAddBlog } from "src/api/blog";
99 +import Markdown from "components/markdown";
100 +export default {
101 + name: "articleAdd",
102 + components: { Markdown },
103 + computed: {},
104 + data() {
105 + return {
106 + sources,
107 + header: "添加文章",
108 + labelList: [],
109 + info: {
110 + type: ["JavaScript"],
111 + title: "",
112 + desc: "",
113 + fileCoverImgUrl: "",
114 + html: "",
115 + markdown: "",
116 + level: 1,
117 + source: 1,
118 + github: "",
119 + auth: "",
120 + isVisible: true,
121 + releaseTime: new Date().getTime() + "",
122 + },
123 + loading: false,
124 + rules: {
125 + type: [
126 + {
127 + required: true,
128 + message: "请选择至少选择一个文章类型",
129 + trigger: "change",
130 + type: "array",
131 + },
132 + ],
133 + title: [{ required: true, message: "请填写文章标题", trigger: "blur" }],
134 + desc: [{ required: true, message: "请填写文章描述", trigger: "blur" }],
135 + isVisible: [
136 + {
137 + required: true,
138 + message: "请选择",
139 + trigger: "change",
140 + type: "boolean",
141 + },
142 + ],
143 + fileCoverImgUrl: [
144 + { required: true, message: "请上传文章封面", trigger: "change" },
145 + ],
146 + },
147 + };
148 + },
149 + created() {
150 + this.getLabelList();
151 + },
152 + methods: {
153 + handleSubmit(formName) {
154 + this.loading = true;
155 + if (!this.info.markdown) {
156 + this.$message.warning("请填写文章内容");
157 + this.loading = false;
158 + return;
159 + }
160 + this.$refs[formName].validate((valid) => {
161 + if (valid) {
162 + this.info.html = this.info.markdown;
163 + this.handleAddBlog();
164 + } else {
165 + this.loading = false;
166 + return false;
167 + }
168 + });
169 + },
170 + handleAddBlog() {
171 + return apiAddBlog(this.info)
172 + .then((res) => {
173 + this.$message.success("新增文章成功");
174 + this.$router.push("/article/list");
175 + })
176 + .catch((err) => {
177 + console.log(err);
178 + this.$message.info("新增文章失败");
179 + })
180 + .finally(() => {
181 + this.loading = false;
182 + });
183 + },
184 + getLabelList() {
185 + let params = {
186 + pageindex: 1,
187 + pagesize: 50,
188 + };
189 + return apiGetLabelList(params)
190 + .then((res) => {
191 + let { list } = res.data;
192 + this.labelList = list;
193 + })
194 + .catch((err) => {
195 + console.log(err);
196 + })
197 + .finally(() => {
198 + });
199 + },
200 + },
201 +};
202 +</script>
203 +
204 +
205 +<style lang="less" scoped>
206 +/deep/ .markdown {
207 + .el-form-item__content {
208 + width: 1400px;
209 + }
210 +}
211 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="article-edit" v-show="hasLoad">
3 + <zp-page-edit :back="true" @back="$router.back()">
4 + <div slot="header">
5 + {{ header }}
6 + </div>
7 +
8 + <el-form
9 + :model="info"
10 + :rules="rules"
11 + ref="form"
12 + label-width="100px"
13 + class="form"
14 + >
15 + <el-form-item label="文章类型:" prop="type">
16 + <el-select
17 + v-model="info.type"
18 + multiple
19 + clearable
20 + placeholder="请选择文章类型"
21 + class="block"
22 + >
23 + <el-option
24 + v-for="item in labelList"
25 + :key="item.label"
26 + :label="item.label"
27 + :value="item.label"
28 + >
29 + </el-option>
30 + </el-select>
31 + </el-form-item>
32 + <el-form-item label="文章标题:" prop="title">
33 + <el-input type="text" v-model="info.title"></el-input>
34 + </el-form-item>
35 + <el-form-item label="文章描述:" prop="desc">
36 + <el-input type="textarea" v-model="info.desc"></el-input>
37 + </el-form-item>
38 + <el-form-item label="文章封面:" prop="fileCoverImgUrl">
39 + <zp-single-img-upload v-model="info.fileCoverImgUrl" :public="true">
40 + </zp-single-img-upload>
41 + </el-form-item>
42 + <el-form-item
43 + label="文章内容:"
44 + prop="markdown"
45 + v-if="info.markdown"
46 + class="markdown"
47 + >
48 + <Markdown v-model="info.markdown"></Markdown>
49 + </el-form-item>
50 + <el-form-item label="级别:" prop="album">
51 + <el-select
52 + v-model="info.level"
53 + placeholder="请选择级别"
54 + class="block"
55 + >
56 + <el-option
57 + v-for="item in [1, 2, 3, 4, 5, 6]"
58 + :key="item"
59 + :label="item"
60 + :value="item"
61 + >
62 + </el-option>
63 + </el-select>
64 + </el-form-item>
65 + <el-form-item label="来源:" prop="source">
66 + <el-select
67 + v-model="info.source"
68 + placeholder="请选择来源"
69 + class="block"
70 + >
71 + <el-option
72 + v-for="item in sources"
73 + :key="item.id"
74 + :label="item.name"
75 + :value="item.id"
76 + ></el-option>
77 + </el-select>
78 + </el-form-item>
79 + <el-form-item label="Github:" prop="github">
80 + <el-input type="text" v-model="info.github"></el-input>
81 + </el-form-item>
82 + <el-form-item label="Auth:" prop="auth">
83 + <el-input type="text" v-model="info.auth"></el-input>
84 + </el-form-item>
85 + <el-form-item label="是否可见:" prop="isVisible" class="left-item">
86 + <el-switch v-model="info.isVisible"></el-switch>
87 + </el-form-item>
88 + <el-form-item>
89 + <el-button
90 + type="primary"
91 + @click="handleSubmit('form')"
92 + :loading="loading"
93 + >更新</el-button
94 + >
95 + </el-form-item>
96 + </el-form>
97 + </zp-page-edit>
98 + </div>
99 +</template>
100 +
101 +<script>
102 +import { apiUpdateBlog, apiGetBlogDetail } from "src/api/blog";
103 +import { apiGetLabelList } from "src/api/label";
104 +import { sources } from "src/constant/classify";
105 +import Markdown from "components/markdown";
106 +export default {
107 + name: "articleEdit",
108 + components: { Markdown },
109 + props: {},
110 + computed: {},
111 + data() {
112 + return {
113 + sources,
114 + header: "文章编辑",
115 + loading: false,
116 + hasLoad: false,
117 + labelList: [],
118 + info: {},
119 + id: "", // 文章id
120 + rules: {
121 + type: [
122 + {
123 + required: true,
124 + message: "请选择至少选择一个文章类型",
125 + trigger: "change",
126 + type: "array",
127 + },
128 + ],
129 + title: [{ required: true, message: "请填写文章标题", trigger: "blur" }],
130 + desc: [{ required: true, message: "请填写文章描述", trigger: "blur" }],
131 + markdown: [
132 + { required: true, message: "请填写文章内容", trigger: "blur" },
133 + ],
134 + fileCoverImgUrl: [
135 + { required: true, message: "请上传文章封面", trigger: "change" },
136 + ],
137 + },
138 + };
139 + },
140 + watch: {},
141 + async created() {
142 + this.id = this.$route.query["id"];
143 + await this.getLabelList();
144 + await this.getBlogDetail();
145 +
146 + },
147 + mounted() { },
148 + beforeDestroy() { },
149 + methods: {
150 + handleSubmit(formName) {
151 + this.loading = true;
152 + this.$refs[formName].validate((valid) => {
153 + if (valid) {
154 + this.info.releaseTime = new Date().getTime() + "";
155 + this.info.html = this.info.markdown;
156 + this.handleUpdateBlog();
157 + } else {
158 + console.log("error submit!!");
159 + return false;
160 + }
161 + });
162 + },
163 + getBlogDetail() {
164 + return apiGetBlogDetail({ _id: this.id })
165 + .then((res) => {
166 + this.info = res.data;
167 + })
168 + .catch((err) => {
169 + console.log(err);
170 + this.$message.info("获取文章详情失败");
171 + })
172 + .finally(() => {
173 + this.hasLoad = true;
174 + });
175 + },
176 + handleUpdateBlog() {
177 + return apiUpdateBlog(this.info)
178 + .then((res) => {
179 + this.$message.success("修改文章成功");
180 + this.$router.push("/article/list");
181 + })
182 + .catch((err) => {
183 + console.log(err);
184 + this.$message.info("修改文章失败");
185 + })
186 + .finally(() => {
187 + this.loading = false;
188 + });
189 + },
190 + getLabelList() {
191 + let params = {
192 + pageindex: 1,
193 + pagesize: 50,
194 + };
195 + return apiGetLabelList(params)
196 + .then((res) => {
197 + let { list } = res.data;
198 + this.labelList = list;
199 + })
200 + .catch((err) => {
201 + console.log(err);
202 + })
203 + .finally(() => {
204 + });
205 + },
206 + },
207 +};
208 +</script>
209 +
210 +<style lang="less" scoped>
211 +/deep/ .markdown {
212 + .el-form-item__content {
213 + width: 1400px;
214 + }
215 +}
216 +</style>
1 +<template>
2 + <div class="article-list">
3 + <zp-page-list>
4 + <template v-slot:header>
5 + <span>文章列表</span>
6 + </template>
7 + <!-- filter start -->
8 + <div slot="filter">
9 + <el-form
10 + ref="searchForm"
11 + class="zp-search-form"
12 + :inline="true"
13 + :model="searchForm"
14 + >
15 + <el-form-item label="关键词:" prop="keyword">
16 + <el-input
17 + placeholder="请输入类型、标题"
18 + v-model="searchForm.keyword"
19 + @keydown.enter.native="getBlogList(true)"
20 + ></el-input>
21 + </el-form-item>
22 + <el-button type="primary" @click="getBlogList(true)">
23 + 查询
24 + </el-button>
25 + </el-form>
26 + </div>
27 + <!-- filter end -->
28 + <!-- list start -->
29 + <div slot="list">
30 + <zp-table-list
31 + ref="sensitiveBehaviorRecordList"
32 + :loading="listLoading"
33 + :source="blogList"
34 + :count="pageInfo.total"
35 + :columns="columns"
36 + @size-change="handleSizeChange"
37 + @current-change="handleCurrentChange"
38 + >
39 + <template slot="_id" slot-scope="scope">
40 + <el-button
41 + icon="el-icon-document-copy"
42 + type="primary"
43 + size="mini"
44 + class="clipboardBtn"
45 + :data-clipboard-text="scope.row._id"
46 + @click="handleCopyId"
47 + >复制
48 + </el-button>
49 + </template>
50 + <template slot="isVisible" slot-scope="scope">
51 + {{ scope.row.isVisible ? "是" : "否" }}
52 + </template>
53 + <template slot="fileCoverImgUrl" slot-scope="scope">
54 + <img
55 + :src="scope.row.fileCoverImgUrl"
56 + style="width: 60px; object-fit: contain"
57 + />
58 + </template>
59 + <template slot="source" slot-scope="scope">
60 + {{
61 + scope.row.source === 1
62 + ? "原创"
63 + : scope.row.source === 2
64 + ? "转载"
65 + : "翻译"
66 + }}
67 + </template>
68 + <template slot="releaseTime" slot-scope="scope">
69 + {{ scope.row.releaseTime | formatTime("yyyy-MM-dd hh:mm:ss") }}
70 + </template>
71 + <template slot="type" slot-scope="scope">
72 + <el-tag
73 + class="tag"
74 + type="primary"
75 + close-transition
76 + v-for="(tag, index) in scope.row.type"
77 + :key="index"
78 + >{{ tag }}</el-tag
79 + >
80 + </template>
81 + <template slot="operate" slot-scope="scope">
82 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)"
83 + >编辑</el-button
84 + >
85 + <el-button
86 + size="mini"
87 + type="danger"
88 + @click="handleDelete(scope.row)"
89 + >删除</el-button
90 + >
91 + </template>
92 + </zp-table-list>
93 + </div>
94 + <!-- list end -->
95 + </zp-page-list>
96 + </div>
97 +</template>
98 +
99 +<script>
100 +import Clipboard from "clipboard";
101 +import { apiGetBlogList, apiDelBlog } from "src/api/blog";
102 +import { apiDelUploadImg } from "src/api/upload";
103 +export default {
104 + name: "articleList",
105 + components: {},
106 + computed: {},
107 + data() {
108 + return {
109 + listLoading: false,
110 + searchForm: {
111 + keyword: "",
112 + },
113 + blogList: [],
114 + columns: [
115 + {
116 + label: "_id",
117 + prop: "_id",
118 + slot: "_id",
119 + },
120 + {
121 + label: "类型",
122 + prop: "type",
123 + slot: "type",
124 + showTooltip: true,
125 + width: "120",
126 + },
127 + {
128 + label: "标题",
129 + prop: "title",
130 + width: "160",
131 + },
132 + {
133 + label: "描述",
134 + prop: "desc",
135 + width: "160",
136 + showTooltip: true,
137 + },
138 + {
139 + label: "封面",
140 + slot: "fileCoverImgUrl",
141 + prop: "fileCoverImgUrl",
142 + },
143 + {
144 + label: "来源",
145 + prop: "source",
146 + slot: "source",
147 + },
148 + {
149 + label: "级别",
150 + prop: "level",
151 + },
152 + {
153 + label: "发布时间",
154 + prop: "releaseTime",
155 + slot: "releaseTime",
156 + width: "160",
157 + },
158 + {
159 + label: "是否可见",
160 + prop: "isVisible",
161 + slot: "isVisible",
162 + },
163 + {
164 + label: "作者",
165 + prop: "auth",
166 + },
167 + {
168 + label: "浏览量",
169 + prop: "pv",
170 + },
171 + {
172 + label: "点赞数",
173 + prop: "likes",
174 + },
175 + {
176 + label: "评论数",
177 + prop: "comments",
178 + },
179 + {
180 + label: "操作",
181 + slot: "operate",
182 + width: "150",
183 + },
184 + ],
185 + };
186 + },
187 + created() {
188 + this.requestPageData = this.getBlogList;
189 + this.getBlogList();
190 + },
191 + mounted() { },
192 + methods: {
193 + handleCopyId() {
194 + let clipboard = new Clipboard(".clipboardBtn");
195 + clipboard.on("success", (e) => {
196 + this.$message.success("复制成功");
197 + clipboard.destroy();
198 + });
199 + clipboard.on("error", (e) => {
200 + this.$message.error("该浏览器不支持自动复制");
201 + clipboard.destroy();
202 + });
203 + },
204 + getBlogList() {
205 + let params = {
206 + keyword: this.searchForm.keyword,
207 + pageindex: this.pageInfo.pageNum,
208 + pagesize: this.pageInfo.pageSize,
209 + };
210 + this.listLoading = true;
211 + return apiGetBlogList(params)
212 + .then((res) => {
213 + let { list, total } = res.data;
214 + this.blogList = list;
215 + this.pageInfo.total = total;
216 + })
217 + .catch((err) => {
218 + this.listLoading = false;
219 + console.log(err);
220 + })
221 + .finally(() => {
222 + this.listLoading = false;
223 + });
224 + },
225 + handleDeleteBlog(id) {
226 + return apiDelBlog({ id })
227 + .then((res) => {
228 + this.$message.success("删除成功");
229 + this.getBlogList(true);
230 + })
231 + .catch((err) => {
232 + console.log(err);
233 + this.$message.info("删除失败");
234 + })
235 + .finally(() => { });
236 + },
237 + // 删除本地图片
238 + handleDeleteImg(fileName) {
239 + return apiDelUploadImg({ fileName })
240 + .then((res) => { })
241 + .catch((err) => {
242 + console.log(err);
243 + });
244 + },
245 + handleDelete(row) {
246 + this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
247 + confirmButtonText: "确定",
248 + cancelButtonText: "取消",
249 + type: "warning",
250 + })
251 + .then(async () => {
252 + await this.handleDeleteBlog(row._id);
253 + let index = row.fileCoverImgUrl.lastIndexOf('/');
254 + let fileName = row.fileCoverImgUrl.substring(index + 1);
255 + this.handleDeleteImg(fileName);
256 + })
257 + .catch(() => {
258 + this.$message.info("已取消删除");
259 + });
260 + },
261 + handleEdit(row) {
262 + this.$router.push({ path: "edit", query: { id: row._id } });
263 + },
264 + },
265 +};
266 +</script>
267 +
268 +
269 +<style lang="less" scoped>
270 +.tag {
271 + margin: 0 10px;
272 +}
273 +</style>
1 +<template>
2 + <div class="home-container">home</div>
3 +</template>
4 +
5 +<script>
6 +export default {
7 + name: "home",
8 + components: {},
9 + computed: {},
10 + data() {
11 + return {};
12 + },
13 + created() {},
14 + mounted() {},
15 + methods: {},
16 +};
17 +</script>
18 +
19 +
20 +<style lang="less" scoped>
21 +</style>
1 +<template>
2 + <div class="label-add">
3 + <zp-page-edit :back="true" @back="$router.back()">
4 + <div slot="header">
5 + {{ header }}
6 + </div>
7 + <el-form
8 + :model="info"
9 + :rules="rules"
10 + ref="form"
11 + label-width="100px"
12 + class="form"
13 + >
14 + <el-form-item label="标签:" prop="label">
15 + <el-input type="text" v-model="info.label"></el-input>
16 + </el-form-item>
17 + <el-form-item label="背景色:" prop="bgColor">
18 + <el-input readonly :value="currentColor"></el-input>
19 + </el-form-item>
20 + <el-form-item>
21 + <sketch-picker
22 + v-model="currentColor"
23 + @input="colorValueChange"
24 + ></sketch-picker>
25 + </el-form-item>
26 + <el-form-item label="预览:" v-if="info.label">
27 + <div class="label-box" :style="{ backgroundColor: currentColor }">
28 + {{ info.label }}
29 + </div>
30 + </el-form-item>
31 + <el-form-item>
32 + <el-button
33 + type="primary"
34 + @click="handleSubmit('form')"
35 + :loading="loading"
36 + >立即创建</el-button
37 + >
38 + </el-form-item>
39 + </el-form>
40 + </zp-page-edit>
41 + </div>
42 +</template>
43 +
44 +<script>
45 +import { Sketch } from 'vue-color'
46 +import { apiAddLabel } from "src/api/label";
47 +export default {
48 + name: "permissionAdd",
49 + components: {
50 + 'sketch-picker': Sketch
51 + },
52 + data() {
53 + return {
54 + header: "添加标签",
55 + info: {
56 + label: "",
57 + bgColor: "rgba(70, 70, 70, 0.9)",
58 + },
59 + currentColor: 'rgba(70, 70, 70, 0.9)',
60 + loading: false,
61 + rules: {
62 + label: [
63 + { required: true, message: "请填写标签", trigger: "blur" },
64 + ],
65 + bgColor: [{ required: true, message: "请填写背景色", trigger: "blur" }],
66 + },
67 + };
68 + },
69 + methods: {
70 + handleSubmit(formName) {
71 + this.loading = true;
72 + this.$refs[formName].validate(async (valid) => {
73 + if (valid) {
74 + return apiAddLabel(this.info)
75 + .then((res) => {
76 + this.$message.success("新增成功");
77 + this.$router.push("/label/list");
78 + })
79 + .catch((err) => {
80 + console.log(err);
81 + this.$message.info("新增失败");
82 + })
83 + .finally(() => {
84 + this.loading = false;
85 + });
86 + } else {
87 + console.log("error submit!!");
88 + this.loading = false;
89 + return false;
90 + }
91 + });
92 + },
93 + // 颜色值改变事件处理
94 + colorValueChange(fmtObj) {
95 + // 取颜色对象的 rgba 值
96 + const { r, g, b, a } = fmtObj.rgba;
97 + this.currentColor = `rgba(${r}, ${g}, ${b}, ${a})`;
98 + this.info.bgColor = this.currentColor;
99 + }
100 + },
101 +};
102 +</script>
103 +<style lang="less" scoped>
104 +.label-box {
105 + color: #fff;
106 + border-radius: 12px;
107 + font-size: 14px;
108 + text-align: center;
109 + max-width: 150px;
110 +}
111 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <zp-dialog
3 + title="标签编辑"
4 + :visible.sync="dialogTableVisible"
5 + @close="close"
6 + width="480px"
7 + :destroy-on-close="true"
8 + v-bind="$attrs"
9 + v-on="$listeners"
10 + >
11 + <el-form :model="info" :rules="rules" ref="form">
12 + <el-form-item label="标签:" prop="label">
13 + <el-input type="text" v-model="info.label"></el-input>
14 + </el-form-item>
15 + <el-form-item label="背景色:" prop="bgColor">
16 + <el-input readonly :value="currentColor"></el-input>
17 + </el-form-item>
18 + <el-form-item style="margin-left: 80px">
19 + <sketch-picker
20 + v-model="currentColor"
21 + @input="colorValueChange"
22 + ></sketch-picker>
23 + </el-form-item>
24 + <el-form-item label="预览:" v-if="info.label">
25 + <div class="label-box" :style="{ backgroundColor: currentColor }">
26 + {{ info.label }}
27 + </div>
28 + </el-form-item>
29 + </el-form>
30 + <div slot="footer" class="dialog-footer">
31 + <el-button @click="close">取消</el-button>
32 + <el-button
33 + type="primary"
34 + :loading="loading"
35 + @click="handleSubmit('form')"
36 + >
37 + 确定
38 + </el-button>
39 + </div>
40 + </zp-dialog>
41 +</template>
42 +
43 +<script>
44 +import { Sketch } from 'vue-color'
45 +import { apiUpdateLabel } from "src/api/label";
46 +export default {
47 + props: ["info"],
48 + components: {
49 + 'sketch-picker': Sketch
50 + },
51 + data() {
52 + return {
53 + loading: false,
54 + dialogTableVisible: true,
55 + currentColor: '',
56 + rules: {
57 + label: [
58 + { required: true, message: "请填写标签名", trigger: "blur" },
59 + ],
60 + bgColor: [{ required: true, message: "请填写背景色", trigger: "blur" }],
61 + },
62 + };
63 + },
64 + created() {
65 + this.currentColor = this.info.bgColor;
66 + },
67 + methods: {
68 + // 颜色值改变事件处理
69 + colorValueChange(fmtObj) {
70 + // 取颜色对象的 rgba 值
71 + const { r, g, b, a } = fmtObj.rgba;
72 + this.currentColor = `rgba(${r}, ${g}, ${b}, ${a})`;
73 + this.info.bgColor = this.currentColor;
74 + },
75 + close() {
76 + this.$emit("close");
77 + },
78 + handleSubmit(formName) {
79 + this.loading = true;
80 + this.$refs[formName].validate(async (valid) => {
81 + if (valid) {
82 + return apiUpdateLabel(this.info)
83 + .then((res) => {
84 + this.$message.success("编辑成功");
85 + this.$emit("close");
86 + })
87 + .catch((err) => {
88 + console.log(err);
89 + this.$message.info("编辑失败");
90 + })
91 + .finally(() => {
92 + this.loading = false;
93 + });
94 +
95 + } else {
96 + console.log("error submit!!");
97 + this.loading = false;
98 + return false;
99 + }
100 + });
101 + },
102 + },
103 +};
104 +</script>
105 +
106 +
107 +<style lang="less" scoped>
108 +.el-form {
109 + .el-form-item {
110 + display: flex;
111 + align-items: center;
112 + /deep/ .el-form-item__label {
113 + min-width: 80px;
114 + }
115 + /deep/.el-form-item__content {
116 + margin-left: 0 !important;
117 + width: 260px;
118 + .el-select {
119 + width: 260px;
120 + }
121 + }
122 + }
123 +}
124 +.label-box {
125 + color: #fff;
126 + border-radius: 12px;
127 + font-size: 14px;
128 + text-align: center;
129 + max-width: 150px;
130 +}
131 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="label-list">
3 + <zp-page-list>
4 + <template v-slot:header>
5 + <span>标签列表</span>
6 + </template>
7 + <!-- filter start -->
8 + <div slot="filter">
9 + <el-form
10 + ref="searchForm"
11 + class="zp-search-form"
12 + :inline="true"
13 + :model="searchForm"
14 + >
15 + <el-form-item label="关键词:" prop="keyword">
16 + <el-input
17 + placeholder="请输入标签、背景色"
18 + v-model="searchForm.keyword"
19 + @keydown.enter.native="getLabelList"
20 + ></el-input>
21 + </el-form-item>
22 + <el-button type="primary" @click="getLabelList"> 查询 </el-button>
23 + </el-form>
24 + </div>
25 + <!-- filter end -->
26 + <!-- list start -->
27 + <div slot="list">
28 + <zp-table-list
29 + :loading="listLoading"
30 + :source="labelList"
31 + :count="pageInfo.total"
32 + :columns="columns"
33 + @size-change="handleSizeChange"
34 + @current-change="handleCurrentChange"
35 + >
36 + <template slot="_id" slot-scope="scope">
37 + <el-button
38 + icon="el-icon-document-copy"
39 + type="primary"
40 + size="mini"
41 + class="clipboardBtn"
42 + :data-clipboard-text="scope.row._id"
43 + @click="handleCopyId"
44 + >复制
45 + </el-button>
46 + </template>
47 + <template slot="label" slot-scope="scope">
48 + <div
49 + class="label-box"
50 + :style="{ backgroundColor: scope.row.bgColor }"
51 + >
52 + {{ scope.row.label }}
53 + </div>
54 + </template>
55 + <template slot="operate" slot-scope="scope">
56 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)"
57 + >编辑</el-button
58 + >
59 + <el-button
60 + size="mini"
61 + type="danger"
62 + @click="handleDelete(scope.row)"
63 + >删除</el-button
64 + >
65 + </template>
66 + </zp-table-list>
67 + </div>
68 + <!-- list end -->
69 + </zp-page-list>
70 + <editDialog
71 + v-if="editShow"
72 + :info="labelInfo"
73 + @close="handleClose"
74 + ></editDialog>
75 + </div>
76 +</template>
77 +<script>
78 +import Clipboard from "clipboard";
79 +import editDialog from "./components/editDialog";
80 +import { apiGetLabelList, apiDelLabel } from "src/api/label";
81 +export default {
82 + components: {
83 + editDialog,
84 + },
85 + computed: {
86 + },
87 + data() {
88 + return {
89 + listLoading: false,
90 + editShow: false,
91 + searchForm: {
92 + keyword: "",
93 + },
94 + labelList: [],
95 + columns: [
96 + {
97 + label: "_id",
98 + prop: "_id",
99 + slot: "_id",
100 + },
101 + {
102 + label: "标签",
103 + prop: "label",
104 + slot: "label",
105 + },
106 + {
107 + label: "背景色",
108 + prop: "bgColor",
109 + },
110 + {
111 + label: "操作",
112 + slot: "operate",
113 + width: "150",
114 + },
115 + ],
116 + };
117 + },
118 + mounted() {
119 + this.requestPageData = this.getLabelList;
120 + this.getLabelList();
121 + },
122 +
123 + methods: {
124 + getLabelList() {
125 + let params = {
126 + keyword: this.searchForm.keyword,
127 + pageindex: this.pageInfo.pageNum,
128 + pagesize: this.pageInfo.pageSize,
129 + };
130 + this.listLoading = true;
131 + return apiGetLabelList(params)
132 + .then((res) => {
133 + let { list, total } = res.data;
134 + this.labelList = list;
135 + this.pageInfo.total = total;
136 + })
137 + .catch((err) => {
138 + this.listLoading = false;
139 + console.log(err);
140 + })
141 + .finally(() => {
142 + this.listLoading = false;
143 + });
144 + },
145 + handleCopyId() {
146 + let clipboard = new Clipboard(".clipboardBtn");
147 + clipboard.on("success", (e) => {
148 + this.$message.success("复制成功");
149 + clipboard.destroy();
150 + });
151 + clipboard.on("error", (e) => {
152 + this.$message.error("该浏览器不支持自动复制");
153 + clipboard.destroy();
154 + });
155 + },
156 + handleClose() {
157 + this.editShow = false;
158 + },
159 + handleDelete(row) {
160 + this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
161 + confirmButtonText: "确定",
162 + cancelButtonText: "取消",
163 + type: "warning",
164 + })
165 + .then(async () => {
166 + try {
167 + await this.handleDeleteLabel(row._id);;
168 + } catch (e) {
169 + this.$message.error("删除失败");
170 + console.log(e);
171 + }
172 + })
173 + .catch(() => {
174 + this.$message.info("已取消删除");
175 + });
176 + },
177 + handleDeleteLabel(id) {
178 + return apiDelLabel({ id })
179 + .then((res) => {
180 + this.$message.success("删除成功");
181 + this.getLabelList(true);
182 + })
183 + .catch((err) => {
184 + console.log(err);
185 + this.$message.info("删除失败");
186 + })
187 + .finally(() => { });
188 + },
189 + handleEdit(row) {
190 + this.editShow = true;
191 + this.labelInfo = row;
192 + },
193 + },
194 +};
195 +</script>
196 +
197 +<style lang="less" scoped>
198 +.label-box {
199 + color: #fff;
200 + padding: 4px 0;
201 + border-radius: 12px;
202 + font-size: 14px;
203 + text-align: center;
204 + max-width: 150px;
205 +}
206 +</style>
1 +<template>
2 + <div class="sign_out">
3 + <el-dropdown>
4 + <div class="el-dropdown-link">
5 + <Icon name="avatar" class="avatar"></Icon>
6 + </div>
7 + <el-dropdown-menu slot="dropdown">
8 + <el-dropdown-item>{{ userName }}</el-dropdown-item>
9 + <el-dropdown-item @click.native="removeCookie">退出</el-dropdown-item>
10 + </el-dropdown-menu>
11 + </el-dropdown>
12 + </div>
13 +</template>
14 +<script>
15 +import { removeToken } from "../../utils/auth";
16 +import { mapGetters } from "vuex";
17 +export default {
18 + methods: {
19 + removeCookie() {
20 + removeToken();
21 + this.$store.dispatch("clearInfo");
22 + this.$router.push("/login");
23 + },
24 + },
25 + computed: {
26 + ...mapGetters(["userName"]),
27 + },
28 +};
29 +</script>
30 +<style lang="less" scoped>
31 +.sign_out {
32 + float: right;
33 + margin-right: 20px;
34 + > * {
35 + display: inline-block;
36 + vertical-align: middle;
37 + }
38 + .avatar {
39 + font-size: 40px;
40 + margin-top: 5px;
41 + color: @highlightColor;
42 + }
43 + img {
44 + width: 40px;
45 + height: 40px;
46 + border-radius: 10px;
47 + margin-top: 5px;
48 + }
49 + .sign_out_icon {
50 + color: #5a5e66;
51 + font-size: 20px;
52 + cursor: pointer;
53 + &:hover {
54 + color: #aaa;
55 + }
56 + }
57 +}
58 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="content-wrapper">
3 + <transition>
4 + <router-view></router-view>
5 + </transition>
6 + </div>
7 +</template>
1 +export { default as NavBar } from './navBar'
2 +export { default as SlideBar } from './slideBar'
3 +export { default as ContentView } from './content'
1 +<template>
2 + <div class="app-wrapper" :class="{hideSidebar: $store.state.app.slideBar.opened}">
3 + <SlideBar class="slidebar-container"></SlideBar>
4 + <div class="main-container">
5 + <NavBar></NavBar>
6 + <ContentView></ContentView>
7 + </div>
8 + </div>
9 +</template>
10 +
11 +<script>
12 +import { SlideBar, NavBar, ContentView } from './index'
13 +export default {
14 + name: 'layout',
15 + components: {
16 + SlideBar,
17 + NavBar,
18 + ContentView
19 + }
20 +
21 +}
22 +</script>
23 +
24 +
25 +<style lang="less" scoped>
26 + .app-wrapper {
27 + &.hideSidebar {
28 + .slidebar-container{
29 + width: 64px;
30 + overflow: inherit;
31 + }
32 + .main-container {
33 + margin-left: 64px;
34 + }
35 + }
36 + .slidebar-container {
37 + height: 100%;
38 + position: fixed;
39 + top: 0;
40 + bottom: 0;
41 + left: 0;
42 + z-index: 1001;
43 + overflow-y: auto;
44 + transition: width 0.28s ease-out;
45 + }
46 + .main-container {
47 + min-height: 100%;
48 + margin-left: 200px;
49 + transition: margin-left 0.28s ease-out;
50 + }
51 + }
52 +</style>
53 +
54 +
1 +<template>
2 + <el-breadcrumb class="levelbar-wrapper" separator="/">
3 + <el-breadcrumb-item v-for="(item, index) in levelList" :key="index">
4 + <router-link :to="item.path">{{ item.name }}</router-link>
5 + </el-breadcrumb-item>
6 + </el-breadcrumb>
7 +</template>
8 +
9 +<script>
10 +export default {
11 + created() {
12 + this.getBreadcrumb();
13 + },
14 + data() {
15 + return {
16 + levelList: []
17 + };
18 + },
19 + methods: {
20 + getBreadcrumb() {
21 + let matched = this.$route.matched.filter(item => item.name);
22 + let first = matched[0],
23 + second = matched[1];
24 + if (first && first.name !== '首页' && first.name !== '') {
25 + matched = [{ name: '首页', path: '/' }].concat(matched);
26 + }
27 + if (second && second.name === '首页') {
28 + this.levelList = [second];
29 + } else {
30 + this.levelList = matched;
31 + }
32 +
33 + }
34 + },
35 + watch: {
36 + $route() {
37 + this.getBreadcrumb();
38 + }
39 + }
40 +};
41 +</script>
42 +
43 +<style lang="less" scoped>
44 +.levelbar-wrapper.el-breadcrumb {
45 + display: inline-block;
46 + font-size: 14px;
47 + line-height: 50px;
48 + margin-left: 10px;
49 + .no-redirect {
50 + color: #97a8be;
51 + cursor: text;
52 + }
53 +}
54 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="nav-bar-wrapper" separator="/">
3 + <Hamburger
4 + class="hamburger"
5 + :toggleClick="toggleSlideBar"
6 + :isActive="app.slideBar.opened"
7 + ></Hamburger>
8 + <Levelbar class="levelbar"></Levelbar>
9 + <TabsView class="tabsview"></TabsView>
10 + <Account></Account>
11 + </div>
12 +</template>
13 +
14 +<script>
15 +import { mapState } from "vuex";
16 +import Hamburger from "components/hamburger";
17 +import Levelbar from "./levelbar";
18 +import TabsView from "./tabsView";
19 +import Account from "./account";
20 +
21 +export default {
22 + components: {
23 + Hamburger,
24 + Levelbar,
25 + TabsView,
26 + Account,
27 + },
28 + methods: {
29 + toggleSlideBar() {
30 + this.$store.dispatch("toggleSideBar");
31 + },
32 + },
33 + computed: {
34 + ...mapState(["app"]),
35 + },
36 +};
37 +</script>
38 +
39 +<style lang="less" scoped>
40 +.nav-bar-wrapper {
41 + height: 50px;
42 + // line-height: 50px;
43 + background: #eef1f6;
44 + > * {
45 + display: inline-block;
46 + vertical-align: middle;
47 + }
48 + .hamburger {
49 + line-height: 58px;
50 + width: 40px;
51 + height: 50px;
52 + padding: 0 10px;
53 + }
54 + .levelbar {
55 + font-size: 14px;
56 + line-height: 50px;
57 + margin-left: 10px;
58 + }
59 + .tabsview {
60 + margin-left: 10px;
61 + }
62 +}
63 +</style>
64 +
65 +
1 +<template>
2 + <div class="silde-bar-wrapper">
3 + <el-menu
4 + class="el-menu-vertical"
5 + :default-active="$route.path"
6 + unique-opened
7 + router
8 + :collapse="$store.state.app.slideBar.opened"
9 + >
10 + <template v-for="item in $store.state.permission.routes">
11 + <el-menu-item
12 + v-if="!item.hidden && !item.dropdown"
13 + :index="
14 + (item.path === '/' ? item.path : item.path + '/') +
15 + item.children[0].path
16 + "
17 + :key="item.path"
18 + >
19 + <Icon :name="item.icon" class="slide-icon"></Icon>
20 + <span slot="title">{{ item.name }}</span>
21 + </el-menu-item>
22 + <el-submenu
23 + v-if="!item.hidden && item.dropdown"
24 + :index="item.path"
25 + :key="item.path"
26 + >
27 + <template slot="title">
28 + <Icon :name="item.icon" class="slide-icon"></Icon>
29 + <span>{{ item.name }}</span>
30 + </template>
31 + <template v-for="child in getSubMenuHiddleList(item.children)">
32 + <el-menu-item
33 + :index="item.path + '/' + child.path"
34 + :key="child.path"
35 + >{{ child.name }}</el-menu-item
36 + >
37 + </template>
38 + </el-submenu>
39 + </template>
40 + </el-menu>
41 + </div>
42 +</template>
43 +
44 +<script>
45 +export default {
46 + computed: {
47 + getSubMenuHiddleList() {
48 + return (list) =>
49 + list.filter(item => !item.hidden)
50 + },
51 + },
52 + data() {
53 + return {
54 + }
55 + },
56 + methods: {
57 + }
58 +};
59 +</script>
60 +
61 +<style lang="less" scoped>
62 +.silde-bar-wrapper {
63 + .el-menu-vertical:not(.el-menu--collapse) {
64 + width: 200px;
65 + height: 100%;
66 + }
67 + .el-menu-vertical {
68 + height: 100%;
69 + }
70 + .el-menu {
71 + border-right: none;
72 + }
73 + .slide-icon {
74 + width: 24px;
75 + font-size: 20px;
76 + text-align: center;
77 + vertical-align: middle;
78 + }
79 +}
80 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="tabs-wwrapper">
3 + <router-link
4 + class="tab-view"
5 + :to="tag.path"
6 + v-for="tag in getTags"
7 + :key="tag.name"
8 + >
9 + <el-tag
10 + size="small"
11 + closable
12 + :type="getIsActive(tag) ? '' : 'info'"
13 + @close="closeTagView(tag, $event)"
14 + >
15 + {{ tag.name }}
16 + </el-tag>
17 + </router-link>
18 + </div>
19 +</template>
20 +
21 +<script>
22 +export default {
23 + computed: {
24 + getTags() {
25 + let tagArr = this.$store.state.app.tagViews;
26 + return tagArr.filter((item) => item.path !== "/article/edit").slice(-6);
27 + },
28 + },
29 + watch: {
30 + $route() {
31 + this.addTagView();
32 + },
33 + },
34 + mounted() {
35 + this.getIsActive();
36 + },
37 + methods: {
38 + getIsActive(tag) {
39 + if (!tag) return;
40 + return tag.path === this.$route.path;
41 + },
42 + closeTagView(tag, e) {
43 + this.$store.dispatch("delTagView", tag);
44 + e.preventDefault();
45 + },
46 + generateRoute() {
47 + if (this.$route.matched[this.$route.matched.length - 1].name) {
48 + return this.$route.matched[this.$route.matched.length - 1];
49 + }
50 + return this.$route.matched[0];
51 + },
52 + addTagView() {
53 + this.$store.dispatch("addTagView", this.generateRoute());
54 + },
55 + },
56 +};
57 +</script>
58 +
59 +
60 +<style lang="less" scoped>
61 +.tab-view {
62 + margin-left: 10px;
63 +}
64 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="login-wrapper">
3 + <el-form class="login-form">
4 + <h3>博客系统登录</h3>
5 + <el-form-item prop="username">
6 + <Icon name="user" class="icon-user"></Icon>
7 + <el-input
8 + type="text"
9 + placeholder="请输入用户名"
10 + class="username"
11 + v-model="loginInfo.username"
12 + @keydown.enter.native="login"
13 + />
14 + </el-form-item>
15 + <el-form-item prop="password">
16 + <Icon name="password" class="icon-pwd"></Icon>
17 + <el-input
18 + type="password"
19 + placeholder="请输入密码"
20 + class="pwd"
21 + v-model="loginInfo.pwd"
22 + @keydown.enter.native="login"
23 + />
24 + </el-form-item>
25 + <el-button type="primary" class="submit" @click="login" :loading="loading"
26 + >登录</el-button
27 + >
28 + </el-form>
29 + </div>
30 +</template>
31 +
32 +<script>
33 +export default {
34 + name: "login",
35 + data() {
36 + return {
37 + loading: false,
38 + msg: "",
39 + loginInfo: {
40 + username: "",
41 + pwd: "",
42 + },
43 + };
44 + },
45 + methods: {
46 + async login() {
47 + this.loading = true;
48 + if (!this.loginInfo.username) {
49 + this.msg = "请输入用户名";
50 + } else if (!this.loginInfo.pwd) {
51 + this.msg = "请输入密码";
52 + }
53 + if (this.msg) {
54 + this.$message({
55 + message: this.msg,
56 + type: "warning",
57 + });
58 + this.msg = "";
59 + this.loading = false;
60 + return;
61 + }
62 + try {
63 + await this.$store.dispatch("userLogin", this.loginInfo);
64 + this.$router.push("/article/list");
65 + } catch (e) {
66 + console.log(e);
67 + }
68 + this.loading = false;
69 + },
70 + },
71 +};
72 +</script>
73 +
74 +<style lang="less">
75 +.login-wrapper {
76 + width: 100%;
77 + height: 100%;
78 + position: fixed;
79 + background: #2d3a4b;
80 + .login-form {
81 + width: 400px;
82 + padding: 35px;
83 + position: absolute;
84 + left: 0%;
85 + right: 0%;
86 + top: 20%;
87 + margin: auto;
88 + }
89 + .el-form-item {
90 + border: 1px solid rgba(255, 255, 255, 0.1);
91 + background: rgba(0, 0, 0, 0.1);
92 + border-radius: 5px;
93 + color: #454545;
94 + }
95 + h3 {
96 + font-size: 26px;
97 + color: #fff;
98 + margin-bottom: 50px;
99 + text-align: center;
100 + }
101 + .icon {
102 + color: #889aa4;
103 + vertical-align: middle;
104 + width: 1em;
105 + height: 1em;
106 + display: inline-block;
107 + margin-left: 10px;
108 + }
109 + .icon-user,
110 + .icon-pwd {
111 + width: 1.5em;
112 + height: 1.5em;
113 + margin-left: 8px;
114 + }
115 + input {
116 + background: transparent;
117 + border: 0px;
118 + -webkit-appearance: none;
119 + border-radius: 0px;
120 + padding: 12px 5px 12px 15px;
121 + color: #889aa4;
122 + height: 47px;
123 + vertical-align: middle;
124 + color: #eee;
125 + }
126 + .username input {
127 + padding: 12px 5px 12px 10px;
128 + }
129 + .el-input {
130 + display: inline-block;
131 + height: 47px;
132 + width: 85%;
133 + }
134 + .submit {
135 + width: 100%;
136 + }
137 +}
138 +</style>
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <div class="message-list">
3 + <zp-page-list>
4 + <template v-slot:header>
5 + <span>留言列表</span>
6 + </template>
7 + <!-- filter start -->
8 + <div slot="filter">
9 + <el-form
10 + ref="searchForm"
11 + class="zp-search-form"
12 + :inline="true"
13 + :model="searchForm"
14 + >
15 + <el-form-item label="关键词:" prop="keyword">
16 + <el-input
17 + placeholder="请输入内容、昵称"
18 + v-model="searchForm.keyword"
19 + @keydown.enter.native="getMessageList(true)"
20 + ></el-input>
21 + </el-form-item>
22 + <el-button type="primary" @click="getMessageList(true)">
23 + 查询
24 + </el-button>
25 + </el-form>
26 + </div>
27 + <!-- filter end -->
28 + <!-- list start -->
29 + <div slot="list">
30 + <zp-table-list
31 + ref="sensitiveBehaviorRecordList"
32 + :loading="listLoading"
33 + :source="messageList"
34 + :count="pageInfo.total"
35 + :columns="columns"
36 + @size-change="handleSizeChange"
37 + @current-change="handleCurrentChange"
38 + >
39 + <template slot="_id" slot-scope="scope">
40 + <el-button
41 + icon="el-icon-document-copy"
42 + type="primary"
43 + size="mini"
44 + class="clipboardBtn"
45 + :data-clipboard-text="scope.row._id"
46 + @click="handleCopyId"
47 + >复制
48 + </el-button>
49 + </template>
50 + <template slot="content" slot-scope="scope">
51 + <div v-html="scope.row.content"></div>
52 + </template>
53 + <template slot="createTime" slot-scope="scope">
54 + {{ scope.row.createTime | formatTime("yyyy-MM-dd hh:mm:ss") }}
55 + </template>
56 + <template slot="operate" slot-scope="scope">
57 + <el-button
58 + size="mini"
59 + type="danger"
60 + @click="handleDelete(scope.row)"
61 + >删除</el-button
62 + >
63 + </template>
64 + </zp-table-list>
65 + </div>
66 + <!-- list end -->
67 + </zp-page-list>
68 + </div>
69 +</template>
70 +
71 +<script>
72 +import Clipboard from "clipboard";
73 +import { apiGetMessageList, apiDelMessage } from "src/api/message";
74 +export default {
75 + name: "messageList",
76 + components: {},
77 + computed: {},
78 + data() {
79 + return {
80 + listLoading: false,
81 + searchForm: {
82 + keyword: "",
83 + },
84 + messageList: [],
85 + columns: [
86 + {
87 + label: "_id",
88 + prop: "_id",
89 + slot: "_id",
90 + },
91 + {
92 + label: "昵称",
93 + prop: "nickname",
94 + },
95 + {
96 + label: "头像颜色",
97 + prop: "headerColor",
98 + },
99 + {
100 + label: "评论内容",
101 + prop: "content",
102 + slot: "content",
103 + showTooltip: true,
104 + },
105 + {
106 + label: "时间",
107 + prop: "createTime",
108 + slot: "createTime",
109 + },
110 + {
111 + label: "点赞数",
112 + prop: "likes",
113 + },
114 + {
115 + label: "操作",
116 + slot: "operate",
117 + width: "150",
118 + },
119 + ],
120 + };
121 + },
122 + created() {
123 + this.requestPageData = this.getMessageList;
124 + this.getMessageList();
125 + },
126 + mounted() { },
127 + methods: {
128 + getMessageList() {
129 + let params = {
130 + keyword: this.searchForm.keyword,
131 + pageindex: this.pageInfo.pageNum,
132 + pagesize: this.pageInfo.pageSize,
133 + };
134 + this.listLoading = true;
135 + return apiGetMessageList(params)
136 + .then((res) => {
137 + let { list, total } = res.data;
138 + this.messageList = list;
139 + this.pageInfo.total = total;
140 + })
141 + .catch((err) => {
142 + this.listLoading = false;
143 + console.log(err);
144 + })
145 + .finally(() => {
146 + this.listLoading = false;
147 + this.hasLoad = true;
148 + });
149 + },
150 + handleCopyId() {
151 + let clipboard = new Clipboard(".clipboardBtn");
152 + clipboard.on("success", (e) => {
153 + this.$message.success("复制成功");
154 + clipboard.destroy();
155 + });
156 + clipboard.on("error", (e) => {
157 + this.$message.error("该浏览器不支持自动复制");
158 + clipboard.destroy();
159 + });
160 + },
161 + handleDeleteMessage(id) {
162 + return apiDelMessage({ id })
163 + .then((res) => {
164 + this.$message.success("删除成功");
165 + this.getMessageList(true);
166 + })
167 + .catch((err) => {
168 + console.log(err);
169 + this.$message.info("删除失败");
170 + })
171 + .finally(() => { });
172 + },
173 + handleDelete(row) {
174 + this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
175 + confirmButtonText: "确定",
176 + cancelButtonText: "取消",
177 + type: "warning",
178 + })
179 + .then(() => {
180 + this.handleDeleteMessage(row._id);
181 + })
182 + .catch(() => {
183 + this.$message.info("已取消删除");
184 + });
185 + },
186 + },
187 +};
188 +</script>
189 +
190 +
191 +<style lang="less" scoped>
192 +</style>
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.