LI WENHAO

add blog-admin part

add server\admin UI
server:
  blog manage\label manage\message manage\upload\user
admin UI:login mode\home\user\auth\label\article\
Showing 87 changed files with 6773 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 +}
1 +!(function(c) {
2 + var l,
3 + t,
4 + a,
5 + e,
6 + o,
7 + i =
8 + '<svg><symbol id="icon-password" viewBox="0 0 1024 1024"><path d="M781.994667 453.632 781.994667 293.546667c0-146.773333-119.125333-265.898667-265.898667-265.898667-1.706667 0-3.072 0-5.12 0-1.365333 0-3.413333 0-4.778667 0-146.773333 0-265.898667 119.125333-265.898667 265.898667l0 160.085333L146.090667 453.632 146.090667 1003.52l732.16 0L878.250667 453.632 781.994667 453.632 781.994667 453.632zM595.626667 902.826667l-165.888 0 40.618667-177.834667c-24.234667-14.336-40.618667-40.96-40.618667-71.338667 0-45.738667 37.205333-82.944 82.944-82.944 45.738667 0 82.944 37.205333 82.944 82.944 0 30.378667-16.384 57.002667-40.96 71.68L595.626667 902.826667 595.626667 902.826667zM648.874667 453.632l-275.456 0L373.418667 293.546667c0-73.386667 59.733333-132.778667 132.778667-132.778667 2.048 0 6.485333 0 6.485333 0s2.389333 0 3.413333 0c73.386667 0 132.778667 59.733333 132.778667 132.778667L648.874667 453.632 648.874667 453.632zM648.874667 453.632" ></path></symbol><symbol id="icon-label" viewBox="0 0 1024 1024"><path d="M115.7 615.7c-19.4-19.4-19.4-50.8 0-70.1L567.9 93.3l0.2-0.2c3.2-3.1 1.1-8.5-3.4-8.5h-53.3c-7.2 0.5-13.9 3.6-19 8.6L43.6 542.1c-21.3 21.3-21.3 55.8 0 77.1l320.7 320.7c11.6 11.6 30.5 11.6 42.1 0 9.2-9.2 9.2-24.2 0-33.5L115.7 615.7z" ></path><path d="M944.8 97.8l-313.5 19.1c-6.7 0.4-13.1 3.3-17.8 8L193.9 544.4c-18 18-18 47.1 0 65l300.8 300.8c10.8 10.8 28.3 10.8 39 0l432.6-432.6c4.8-4.8 7.6-11.1 8-17.8l19.2-313.3c1.7-27.6-21.2-50.4-48.7-48.7z m-124.7 232c-32.5 0-58.8-26.3-58.8-58.8s26.3-58.8 58.8-58.8 58.8 26.3 58.8 58.8-26.4 58.8-58.8 58.8z" ></path></symbol><symbol id="icon-authority" viewBox="0 0 1024 1024"><path d="M898.34 188.81L538.35 74.51l-359.87 114.3c-13.53 4.27-22.67 16.86-22.55 31.1v454.94c0.72 3.09 17.69 77.98 91.27 150.5 56.61 55.78 192.4 127.48 251.63 158.57 9.25 4.87 13.65 6.65 17.56 8.78l17.57 8.78c4.63 2.49 12.94 2.38 17.57 0l17.57-8.78c34.65-16.97 195-102.9 260.4-167.35 73.71-72.52 90.68-147.41 91.39-150.5V219.91c0.23-14.24-9.02-26.83-22.55-31.1zM542.86 930.98l-0.24-0.12h0.59c-0.23 0.12-0.35 0.12-0.35 0.12z m85.81-264.32h-58.16v81.9c0 18.99-15.43 34.42-34.42 34.42-18.99 0-34.42-15.43-34.42-34.42V537.29c0-0.71 0.12-1.3 0.12-1.9-6.29-1.9-12.58-4.16-18.87-7-21.24-9.73-39.4-27.18-51.51-47-12.58-20.65-19.23-46.53-17.21-70.74 2.37-27.65 12.34-51.75 30.03-73.23 30.62-37.15 86.88-52.22 131.98-35.73 25.52 9.38 46.29 25.05 61.96 47.12 13.77 19.35 20.77 43.44 21.37 67.06 0 0.71 0.12 1.3 0 2.02 0 0.83 0 1.66-0.12 2.49-1.66 52.46-37.74 101.95-89.02 115.61v60.41h58.16c18.99 0 34.42 15.43 34.42 34.42v1.42h0.12c-0.01 18.99-15.44 34.42-34.43 34.42z" ></path><path d="M577.99 449.1c0.35-0.59 0.71-1.07 0.83-1.19a97.38 97.38 0 0 0 5.58-9.74c1.54-4.39 2.73-8.78 3.68-13.41 0.12-2.25 0.24-4.63 0.36-6.89 0-2.26-0.12-4.63-0.36-6.88-0.83-4.51-2.14-9.02-3.68-13.41a108.73 108.73 0 0 0-4.51-8.19c-0.36-0.36-1.19-1.54-2.61-3.8-1.3-1.43-2.61-2.97-4.03-4.27-1.66-1.66-3.44-3.32-5.34-4.87-0.59-0.35-1.06-0.71-1.19-0.83a97.226 97.226 0 0 0-9.73-5.58c-4.39-1.54-8.78-2.73-13.29-3.68-4.63-0.35-9.26-0.35-13.89 0-4.51 0.83-9.02 2.14-13.29 3.68a108.73 108.73 0 0 0-8.19 4.51c-0.36 0.36-1.54 1.19-3.8 2.61-1.43 1.3-2.97 2.61-4.27 4.03-1.66 1.66-3.32 3.44-4.87 5.34-0.35 0.59-0.71 1.07-0.83 1.19a97.226 97.226 0 0 0-5.58 9.73c-1.54 4.39-2.73 8.78-3.68 13.29-0.35 4.63-0.35 9.26 0 13.89 0.83 4.51 2.14 9.02 3.68 13.29 1.42 2.85 2.85 5.46 4.51 8.19 0.36 0.36 1.19 1.54 2.61 3.8 1.3 1.43 2.61 2.97 4.03 4.27 1.66 1.66 3.44 3.32 5.34 4.87 0.59 0.35 1.07 0.71 1.19 0.83 3.08 2.02 6.41 3.92 9.73 5.58 4.39 1.54 8.78 2.73 13.29 3.68 4.63 0.36 9.26 0.36 13.89 0 4.51-0.83 9.02-2.14 13.29-3.68 2.85-1.42 5.46-2.85 8.19-4.51 0.36-0.36 1.54-1.19 3.8-2.61 1.54-1.19 2.97-2.49 4.27-3.92 1.67-1.64 3.33-3.42 4.87-5.32z" ></path></symbol><symbol id="icon-home" viewBox="0 0 1024 1024"><path d="M1024 590.432l-512-397.44-512 397.44 0-162.048 512-397.44 512 397.44zM896 576l0 384-256 0 0-256-256 0 0 256-256 0 0-384 384-288z" ></path></symbol><symbol id="icon-article" viewBox="0 0 1024 1024"><path d="M803.885176 571.286588c-67.026824 0-121.554824 54.528-121.554823 121.554824s54.528 121.554824 121.554823 121.554823c67.026824 0 121.554824-54.528 121.554824-121.554823s-54.512941-121.554824-121.554824-121.554824z m-1.731764 166.821647a45.989647 45.989647 0 1 1-0.015059-91.994353 45.989647 45.989647 0 0 1 0.015059 91.994353z" ></path><path d="M1019.286588 669.530353c0-7.228235-3.222588-13.778824-10.149647-15.811765l-28.672-7.198117c-4.502588-18.025412-10.420706-34.996706-19.802353-50.477177l12.965647-22.588235c3.478588-6.339765 2.439529-15.706353-2.68047-20.826353L941.778824 523.294118c-3.192471-3.192471-8.011294-4.909176-12.724706-4.909177-2.831059 0-5.632 0.617412-8.026353 1.92753l-22.994824 12.604235a182.407529 182.407529 0 0 0-49.724235-20.299294l-7.469177-20.61553c-2.032941-6.942118-9.396706-13.568-16.624941-13.568l-41.607529 0.692706c-7.228235 0-14.878118 5.948235-16.911059 12.890353l-7.664941 23.702588c-17.227294 4.472471-33.460706 10.164706-48.338824 19.124706l-24.259764-13.884235a17.694118 17.694118 0 0 0-8.417883-2.032941c-4.623059 0-9.291294 1.596235-12.40847 4.713412l-29.334589 29.168941c-5.104941 5.104941-6.460235 14.411294-2.981647 20.751059l13.507765 24.681411a182.482824 182.482824 0 0 0-19.636706 47.585883l-25.479529 7.936c-6.927059 2.032941-16.308706 9.592471-16.308706 16.820706l0.406588 41.472c0 7.228235 8.96 14.802824 15.902118 16.835764l23.009882 7.951059c4.306824 16.941176 12.227765 32.918588 20.87153 47.600941l-12.905412 24.71153c-3.463529 6.339765-1.822118 15.706353 3.297882 20.826353l29.485177 29.334588c3.192471 3.192471 8.071529 4.909176 12.830117 4.909176 2.861176 0 5.677176-0.617412 8.07153-1.927529l24.304941-13.266824a182.678588 182.678588 0 0 0 48.353882 20.344471l7.68 24.515765c2.032941 6.942118 9.035294 16.956235 16.26353 16.956235l42.405647 0.030118c7.228235 0 14.456471-10.059294 16.489411-16.986353l7.484236-21.443765c17.724235-4.336941 34.439529-12.468706 49.739294-21.534118l23.04 11.986824c2.288941 1.249882 4.954353 1.837176 7.68 1.837176 4.848941 0 9.878588-1.852235 13.146353-5.135058l29.334588-29.485177c5.104941-5.104941 6.460235-14.561882 2.981647-20.901647l-12.348235-22.633412a182.934588 182.934588 0 0 0 21.037176-50.492235l26.217412-7.213177c6.927059-2.032941 10.767059-9.622588 10.767059-16.850823l-0.632471-42.496zM803.885176 820.976941c-70.640941 0-128.135529-57.479529-128.135529-128.135529s57.479529-128.135529 128.135529-128.13553 128.135529 57.479529 128.13553 128.13553-57.479529 128.135529-128.13553 128.135529zM461.507765 94.328471a75.158588 75.158588 0 0 0-57.569883 7.243294l-0.150588 0.075294h75.14353c-5.436235 0-11.218824-5.616941-17.423059-7.318588z" ></path><path d="M592.941176 752.775529c-16.248471-5.074824-33.264941-20.886588-33.264941-40.493176l-0.421647-43.866353c0-2.544941 0.301176-9.592471 0.828236-9.592471H398.245647a7.544471 7.544471 0 1 1 0-15.058823h169.155765c6.656 0 16.429176-13.372235 25.825882-16.112941l12.619294-2.891294c3.117176-9.411765 6.881882-18.085647 11.324236-27.000471l-6.987295-12.423529a43.188706 43.188706 0 0 1 7.243295-50.492236l29.123764-29.078588a42.059294 42.059294 0 0 1 29.861647-12.032c7.288471 0 13.748706 1.776941 20.284236 5.360941l10.947764 6.957177c8.493176-4.080941 15.284706-7.695059 25.675294-10.917647l-4.336941-10.752c5.315765-17.438118 12.679529-30.674824 27.738353-30.674824v-0.286118l67.102118-5.406117h0.406588c17.618824 0 34.846118 15.781647 40.463059 33.355294l-2.138353 4.186353c1.822118-2.846118-0.421647-3.975529-0.421647-4.502588V206.320941c0-41.351529-26.608941-89.615059-69.285647-89.615059H504.470588a14.757647 14.757647 0 0 0-5.421176 1.099294c-0.346353-0.391529-0.752941-1.099294-1.084236-1.099294h-107.248941c-21.383529 0-40.568471 31.096471-40.56847 57.690353v605.364706c0 27.226353 27.151059 44.709647 56.214588 44.709647h200.282353c-3.343059 0-2.56-19.968 2.785882-29.741176l6.610824-11.294118c-5.270588-9.984-9.426824-18.642824-12.619294-27.738353l-10.480942-2.921412zM398.245647 282.352941h379.316706a7.544471 7.544471 0 1 1 0 15.058824H398.245647a7.544471 7.544471 0 1 1 0-15.058824z m0 135.529412H686.531765a7.544471 7.544471 0 1 1 0 15.058823H398.245647a7.544471 7.544471 0 1 1 0-15.058823z m0 105.411765h197.240471a7.544471 7.544471 0 1 1 0 15.058823H398.245647a7.544471 7.544471 0 1 1 0-15.058823z" ></path><path d="M335.058824 779.760941v-605.364706c0-13.733647 4.773647-28.069647 10.962823-40.357647L43.008 307.606588C6.686118 328.357647-5.662118 374.784 15.088941 411.105882l277.775059 485.707294a75.535059 75.535059 0 0 0 46.004706 35.493648c6.686118 1.822118 13.492706 2.710588 20.224 2.710588 13.010824 0 25.856-6.189176 37.421176-12.8l139.700706-82.672941h-129.882353c-37.933176-0.015059-71.273412-23.717647-71.273411-59.78353z m-25.705412-137.426823l-37.12 21.217882a7.559529 7.559529 0 0 1-10.360471-2.816 7.574588 7.574588 0 0 1 2.831059-10.345412l37.12-21.217882a7.574588 7.574588 0 1 1 7.529412 13.161412z m0.873412-131.192471l-94.313412 53.910588a7.649882 7.649882 0 0 1-10.360471-2.831059 7.604706 7.604706 0 0 1 2.831059-10.36047l94.313412-53.910588a7.589647 7.589647 0 1 1 7.529412 13.191529z m2.258823-155.768471L149.368471 448.617412a7.559529 7.559529 0 0 1-10.345412-2.816 7.559529 7.559529 0 0 1 2.816-10.345412l163.102117-93.244235a7.574588 7.574588 0 1 1 7.544471 13.161411z" ></path></symbol><symbol id="icon-avatar" viewBox="0 0 1024 1024"><path d="M410.156231 702.707207h212.242673c16.639103 0 30.116023-13.48194 30.116023-30.116023s-13.47692-30.116023-30.116023-30.116023H410.156231c-16.634083 0-30.116023 13.48194-30.116023 30.116023s13.48194 30.116023 30.116023 30.116023z" fill="#EB4548" ></path><path d="M999.597075 85.840705a20.042213 20.042213 0 0 0-28.258869-2.765655l-57.692262 47.397601C888.348485 54.781046 816.923317 0 732.824322 0c-80.339511 0-149.099411 49.992598-177.157506 120.464093H468.270117C440.22206 49.992598 371.46216 0 291.122649 0 207.028674 0 135.603506 54.781046 110.301027 130.472651L52.608765 83.07505a20.052252 20.052252 0 0 0-28.258868 2.760635 20.06731 20.06731 0 0 0 2.770674 28.258869L96.683565 171.239708c1.405414 1.154448 2.991525 1.862174 4.557558 2.579939a192.873051 192.873051 0 0 0-0.853287 16.910147c0 105.170172 85.564641 190.734813 190.734813 190.734813s190.734813-85.564641 190.734813-190.734813c0-10.274583-1.039003-20.278122-2.615074-30.116023h65.467215c-1.571053 9.837901-2.620094 19.84144-2.620094 30.116023 0 105.170172 85.564641 190.734813 190.734813 190.734813s190.734813-85.564641 190.734813-190.734813c0-5.706986-0.361392-11.333663-0.863326-16.915166 1.581091-0.712746 3.167202-1.415453 4.567597-2.57492l69.568014-57.155193a20.06731 20.06731 0 0 0 2.760635-28.24883zM291.122649 341.314929c-53.24011 0-100.010294-27.852302-126.793477-69.6684l-0.250967 0.190735a19.99202 19.99202 0 0 1-12.48811 4.366823 20.082368 20.082368 0 0 1-12.508189-35.787874l7.900437-6.294249A149.987834 149.987834 0 0 1 140.542533 190.734813c0-83.034895 67.545221-150.580116 150.580116-150.580115 27.174692 0 52.617712 7.328232 74.642564 19.976962l3.824735-3.036699a20.06731 20.06731 0 0 1 28.213694 3.212375c5.802354 7.308155 5.390768 17.206288 0.050193 24.333747C424.938179 111.881026 441.702765 149.385514 441.702765 190.734813c0 83.034895-67.545221 150.580116-150.580116 150.580116z m441.701673 0c-53.24011 0-100.010294-27.852302-126.793477-69.6684l-0.250967 0.190735a19.99202 19.99202 0 0 1-12.48811 4.366823 20.082368 20.082368 0 0 1-12.508189-35.787874l7.900437-6.294249A149.987834 149.987834 0 0 1 582.244206 190.734813c0-83.034895 67.545221-150.580116 150.580116-150.580115 27.174692 0 52.617712 7.328232 74.642564 19.976962l3.824734-3.036699a20.072329 20.072329 0 0 1 28.213695 3.212375c5.802354 7.308155 5.390768 17.206288 0.050193 24.333747 27.084343 27.234924 43.84893 64.74443 43.84893 106.09373 0 83.034895-67.545221 150.580116-150.580116 150.580116z" fill="#4F676D" ></path><path d="M394.586247 88.52605L164.329172 271.64151C191.112356 313.462627 237.87752 341.314929 291.122649 341.314929c83.034895 0 150.580116-67.545221 150.580116-150.580116 0-41.3493-16.764586-78.853787-43.84893-106.09373-1.008887 1.355221-1.882251 2.785732-3.262569 3.884967zM291.122649 40.154698c-83.034895 0-150.580116 67.545221-150.580116 150.580115 0 15.088128 2.293837 29.644205 6.43981 43.387151L365.770232 60.13166A149.41563 149.41563 0 0 0 291.132688 40.154698zM836.28792 88.52605l-230.257075 183.11546C632.814029 313.462627 679.579193 341.314929 732.824322 341.314929c83.034895 0 150.580116-67.545221 150.580116-150.580116 0-41.3493-16.764586-78.853787-43.84893-106.09373-1.008887 1.355221-1.882251 2.785732-3.262569 3.884967zM732.824322 40.154698c-83.034895 0-150.580116 67.545221-150.580116 150.580115 0 15.088128 2.293837 29.644205 6.43981 43.387151L807.471905 60.13166A149.41563 149.41563 0 0 0 732.834361 40.154698z" fill="#7BE5E4" ></path><path d="M839.555508 84.641083c5.340575-7.127459 5.75216-17.025592-0.050193-24.328727a20.072329 20.072329 0 0 0-28.213695-3.217395l-3.824734 3.036699-218.78287 173.990304-7.900437 6.28421a20.082368 20.082368 0 0 0 24.996299 31.43109l0.250967-0.195754 230.257075-183.11546c1.385337-1.099235 2.258702-2.529746 3.262569-3.884967zM397.853835 84.641083c5.340575-7.127459 5.75216-17.025592-0.050193-24.328727a20.06731 20.06731 0 0 0-28.213694-3.217395l-3.824735 3.036699-218.78287 173.990304-7.900437 6.28421a20.082368 20.082368 0 0 0 24.996299 31.43109l0.250967-0.195754L394.581227 88.52605c1.385337-1.099235 2.258702-2.529746 3.26257-3.884967z" fill="#B7F5F0" ></path><path d="M722.931209 441.701673H301.337c-88.661572 0-160.794467 72.057605-160.794467 160.61879v63.163339c0 5.541348 2.283798 10.83173 6.324365 14.626349a19.876575 19.876575 0 0 0 14.977702 5.410845c24.91097-1.606188 56.803839 4.657945 96.918382 14.405498a19.931788 19.931788 0 0 0 15.303959-2.444417 20.027155 20.027155 0 0 0 9.009711-12.618614c8.221674-36.189421 41.153546-62.465651 78.30166-62.465651h301.195367c37.158153 0 70.085005 26.261172 78.30166 62.455613a20.072329 20.072329 0 0 0 24.30865 15.06805c40.129601-9.732495 71.987334-15.986589 96.923401-14.400479 5.701967 0.411586 10.942155-1.616227 14.977702-5.410845a20.06731 20.06731 0 0 0 6.324365-14.626349V602.320463c0-88.561185-71.987334-160.61879-160.478248-160.61879zM792.96602 737.195073c-31.466225-24.604791-76.55995-13.40163-133.685026 0.783016-41.1937 10.22439-87.878556 21.814039-137.268834 21.81404s-96.070114-11.58965-137.263814-21.81404c-57.110019-14.184647-102.243899-25.377769-133.690046-0.783016-20.609399 16.117092-30.206371 44.034645-30.206372 87.858478 0 175.265216 152.236497 198.891236 301.160232 198.891236s301.160232-23.62602 301.160232-198.891236c0-43.818814-9.596973-71.741386-30.201352-87.858478z" fill="#89714D" ></path></symbol><symbol id="icon-user" viewBox="0 0 1024 1024"><path d="M984.615385 846.769231v43.323077c0 51.2-43.323077 94.523077-94.523077 94.523077H133.907692C82.707692 984.615385 39.384615 941.292308 39.384615 890.092308V846.769231c0-114.215385 133.907692-185.107692 259.938462-240.246154l11.815385-5.907692c9.846154-3.938462 19.692308-3.938462 29.538461 1.96923 51.2 33.476923 108.307692 51.2 169.353846 51.2s120.123077-19.692308 169.353846-51.2c9.846154-5.907692 19.692308-5.907692 29.538462-1.96923l11.815385 5.907692C850.707692 661.661538 984.615385 730.584615 984.615385 846.769231zM512 39.384615c129.969231 0 234.338462 116.184615 234.338462 259.938462S641.969231 559.261538 512 559.261538s-234.338462-116.184615-234.338462-259.938461S382.030769 39.384615 512 39.384615z" ></path></symbol><symbol id="icon-message" viewBox="0 0 1024 1024"><path d="M906.68 436.31v180.51c0 80.71-48.86 146.36-108.9 146.36H431.94v19.18c0 47.43 28.88 86.23 64.17 86.23h269.13l33.24 91.62 33.24-91.62h63.45c35.29 0 64.17-38.81 64.17-86.23V521c-0.01-42.17-22.83-77.33-52.66-84.69z" ></path><path d="M888.06 606.91V198.58c0-74.09-45.11-134.71-100.24-134.71H164.45c-55.13 0-100.24 60.62-100.24 134.71v408.33c0 74.09 45.11 134.71 100.24 134.71h99.11l51.92 143.13 51.92-143.13h420.42c55.18 0 100.24-60.62 100.24-134.71z m-632.5-169.46a53.35 53.35 0 1 1 53.35-53.35 53.35 53.35 0 0 1-53.35 53.35z m221.38 0a53.35 53.35 0 1 1 53.36-53.35 53.35 53.35 0 0 1-53.35 53.35z m221.39 0a53.35 53.35 0 1 1 53.35-53.35 53.35 53.35 0 0 1-53.35 53.35z" ></path></symbol></svg>',
9 + n = (n = document.getElementsByTagName("script"))[
10 + n.length - 1
11 + ].getAttribute("data-injectcss"),
12 + h = function(c, l) {
13 + l.parentNode.insertBefore(c, l);
14 + };
15 + if (n && !c.__iconfont__svg__cssinject__) {
16 + c.__iconfont__svg__cssinject__ = !0;
17 + try {
18 + document.write(
19 + "<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>"
20 + );
21 + } catch (c) {
22 + console && console.log(c);
23 + }
24 + }
25 + function s() {
26 + o || ((o = !0), a());
27 + }
28 + function d() {
29 + try {
30 + e.documentElement.doScroll("left");
31 + } catch (c) {
32 + return void setTimeout(d, 50);
33 + }
34 + s();
35 + }
36 + (l = function() {
37 + var c, l;
38 + ((l = document.createElement("div")).innerHTML = i),
39 + (i = null),
40 + (c = l.getElementsByTagName("svg")[0]) &&
41 + (c.setAttribute("aria-hidden", "true"),
42 + (c.style.position = "absolute"),
43 + (c.style.width = 0),
44 + (c.style.height = 0),
45 + (c.style.overflow = "hidden"),
46 + (l = c),
47 + (c = document.body).firstChild ? h(l, c.firstChild) : c.appendChild(l));
48 + }),
49 + document.addEventListener
50 + ? ~["complete", "loaded", "interactive"].indexOf(document.readyState)
51 + ? setTimeout(l, 0)
52 + : ((t = function() {
53 + document.removeEventListener("DOMContentLoaded", t, !1), l();
54 + }),
55 + document.addEventListener("DOMContentLoaded", t, !1))
56 + : document.attachEvent &&
57 + ((a = l),
58 + (e = c.document),
59 + (o = !1),
60 + d(),
61 + (e.onreadystatechange = function() {
62 + "complete" == e.readyState && ((e.onreadystatechange = null), s());
63 + }));
64 +})(window);
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>
1 +<template>
2 + <div class="reply-list">
3 + <zp-page-list>
4 + <template v-slot:header>
5 + <span>回复列表</span>
6 + </template>
7 +
8 + <!-- list start -->
9 + <div slot="list">
10 + <zp-table-list
11 + ref="sensitiveBehaviorRecordList"
12 + :loading="listLoading"
13 + :source="replyList"
14 + :count="pageInfo.total"
15 + :columns="columns"
16 + @size-change="handleSizeChange"
17 + @current-change="handleCurrentChange"
18 + >
19 + <template slot="_id" slot-scope="scope">
20 + <el-button
21 + icon="el-icon-document-copy"
22 + type="primary"
23 + size="mini"
24 + class="clipboardBtn"
25 + :data-clipboard-text="scope.row._id"
26 + @click="handleCopyId"
27 + >复制
28 + </el-button>
29 + </template>
30 + <template slot="replyContent" slot-scope="scope">
31 + <div v-html="scope.row.replyContent"></div>
32 + </template>
33 + <template slot="replyTime" slot-scope="scope">
34 + {{ scope.row.replyTime | formatTime("yyyy-MM-dd hh:mm:ss") }}
35 + </template>
36 + <template slot="operate" slot-scope="scope">
37 + <el-button
38 + size="mini"
39 + type="danger"
40 + @click="handleDelete(scope.row)"
41 + >删除</el-button
42 + >
43 + </template>
44 + </zp-table-list>
45 + </div>
46 + <!-- list end -->
47 + </zp-page-list>
48 + </div>
49 +</template>
50 +
51 +<script>
52 +import Clipboard from "clipboard";
53 +import { apiGetMessageList, apiDelReply } from "src/api/message";
54 +export default {
55 + name: "replyList",
56 + components: {},
57 + computed: {},
58 + data() {
59 + return {
60 + listLoading: false,
61 + searchForm: {
62 + keyword: "",
63 + },
64 + messageList: [],
65 + replyList: [],
66 + columns: [
67 + {
68 + label: "留言_id",
69 + prop: "_id",
70 + slot: "_id",
71 + },
72 + {
73 + label: "回复用户",
74 + prop: "replyUser",
75 + },
76 + {
77 + label: "被回复用户",
78 + prop: "byReplyUser",
79 + },
80 + {
81 + label: "头像颜色",
82 + prop: "replyHeaderColor",
83 + },
84 + {
85 + label: "回复内容",
86 + prop: "replyContent",
87 + slot: "replyContent",
88 + showTooltip: true,
89 + },
90 + {
91 + label: "回复时间",
92 + prop: "replyTime",
93 + slot: "replyTime",
94 + },
95 + {
96 + label: "操作",
97 + slot: "operate",
98 + width: "150",
99 + },
100 + ],
101 + };
102 + },
103 + created() {
104 + this.requestPageData = this.getMessageList;
105 + this.getMessageList();
106 + },
107 + mounted() { },
108 + methods: {
109 + getMessageList() {
110 + let params = {
111 + keyword: this.searchForm.keyword,
112 + pageindex: this.pageInfo.pageNum,
113 + pagesize: this.pageInfo.pageSize,
114 + };
115 + this.listLoading = true;
116 + return apiGetMessageList(params)
117 + .then((res) => {
118 + let { list } = res.data;
119 + this.messageList = list;
120 + this.replyList = this.messageList
121 + .map((item) => item.replyList)
122 + .flat();
123 + this.pageInfo.total = this.replyList.length;
124 + })
125 + .catch((err) => {
126 + this.listLoading = false;
127 + console.log(err);
128 + })
129 + .finally(() => {
130 + this.listLoading = false;
131 + this.hasLoad = true;
132 + });
133 + },
134 + handleCopyId() {
135 + let clipboard = new Clipboard(".clipboardBtn");
136 + clipboard.on("success", (e) => {
137 + this.$message.success("复制成功");
138 + clipboard.destroy();
139 + });
140 + clipboard.on("error", (e) => {
141 + this.$message.error("该浏览器不支持自动复制");
142 + clipboard.destroy();
143 + });
144 + },
145 + handleDeleteReply(_id) {
146 + return apiDelReply({ _id })
147 + .then((res) => {
148 + this.$message.success("删除成功");
149 + this.getMessageList(true);
150 + })
151 + .catch((err) => {
152 + console.log(err);
153 + this.$message.info("删除失败");
154 + })
155 + .finally(() => { });
156 + },
157 + handleDelete(row) {
158 + this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
159 + confirmButtonText: "确定",
160 + cancelButtonText: "取消",
161 + type: "warning",
162 + })
163 + .then(() => {
164 + this.handleDeleteReply(row._id);
165 + })
166 + .catch(() => {
167 + this.$message.info("已取消删除");
168 + });
169 + },
170 + },
171 +};
172 +</script>
173 +
174 +
175 +<style lang="less" scoped>
176 +</style>
1 +<template>
2 + <div class="permission-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="username">
15 + <el-input type="text" v-model="info.username"></el-input>
16 + </el-form-item>
17 + <el-form-item label="密码:" prop="pwd">
18 + <el-input type="password" v-model="info.pwd"></el-input>
19 + </el-form-item>
20 + <el-form-item label="权限:" prop="roles">
21 + <el-select
22 + v-model="info.roles"
23 + multiple
24 + placeholder="请选择"
25 + class="block"
26 + >
27 + <el-option
28 + v-for="item in roles"
29 + :key="item.value"
30 + :label="item.label"
31 + :value="item.value"
32 + >
33 + </el-option>
34 + </el-select>
35 + </el-form-item>
36 + <el-form-item>
37 + <el-button
38 + type="primary"
39 + @click="handleSubmit('form')"
40 + :loading="loading"
41 + >立即创建</el-button
42 + >
43 + </el-form-item>
44 + </el-form>
45 + </zp-page-edit>
46 + </div>
47 +</template>
48 +
49 +<script>
50 +export default {
51 + name: "permissionAdd",
52 + components: {},
53 + data() {
54 + return {
55 + header: "添加管理员",
56 + info: {
57 + username: "",
58 + pwd: "",
59 + avatar: "",
60 + roles: ["default"],
61 + },
62 + roles: [
63 + { label: "超级管理员", value: "admin" },
64 + { label: "普通管理员", value: "default" },
65 + ],
66 + loading: false,
67 + rules: {
68 + username: [
69 + { required: true, message: "请填写用户名", trigger: "blur" },
70 + ],
71 + pwd: [{ required: true, message: "请填写密码", trigger: "blur" }],
72 + roles: [
73 + {
74 + required: true,
75 + message: "请选择权限",
76 + trigger: "change",
77 + type: "array",
78 + },
79 + ],
80 + },
81 + };
82 + },
83 + methods: {
84 + handleSubmit(formName) {
85 + this.loading = true;
86 + this.$refs[formName].validate(async (valid) => {
87 + if (valid) {
88 + try {
89 + await this.$store.dispatch("addUser", this.info);
90 + this.loading = false;
91 + this.$router.push("/permission/list");
92 + this.$message.success("新增成功");
93 + } catch (e) {
94 + this.$message.error("新增失败");
95 + this.loading = false;
96 + }
97 + } else {
98 + console.log("error submit!!");
99 + this.loading = false;
100 + return false;
101 + }
102 + });
103 + },
104 + },
105 +};
106 +</script>
107 +
108 +
109 +<style lang="less" scoped>
110 +</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="username">
13 + <el-input type="text" v-model="info.username"></el-input>
14 + </el-form-item>
15 + <el-form-item label="原密码:" prop="old_pwd">
16 + <el-input type="password" v-model="info.old_pwd"></el-input>
17 + </el-form-item>
18 + <el-form-item label="新密码:" prop="pwd">
19 + <el-input type="password" v-model="info.pwd"></el-input>
20 + </el-form-item>
21 + <el-form-item label="权限:" prop="roles">
22 + <el-select
23 + v-model="info.roles"
24 + multiple
25 + placeholder="请选择"
26 + class="block"
27 + >
28 + <el-option
29 + v-for="item in roles"
30 + :key="item.value"
31 + :label="item.label"
32 + :value="item.value"
33 + >
34 + </el-option>
35 + </el-select>
36 + </el-form-item>
37 + </el-form>
38 + <div slot="footer" class="dialog-footer">
39 + <el-button @click="close">取消</el-button>
40 + <el-button
41 + type="primary"
42 + :loading="loading"
43 + @click="handleSubmit('form')"
44 + >
45 + 确定
46 + </el-button>
47 + </div>
48 + </zp-dialog>
49 +</template>
50 +
51 +<script>
52 +export default {
53 + props: ["info"],
54 + components: {},
55 + data() {
56 + return {
57 + roles: [
58 + { label: "超级管理员", value: "admin" },
59 + { label: "普通管理员", value: "default" },
60 + ],
61 + loading: false,
62 + dialogTableVisible: true,
63 + rules: {
64 + username: [
65 + { required: true, message: "请填写用户名", trigger: "blur" },
66 + ],
67 + old_pwd: [{ required: true, message: "请填写原密码", trigger: "blur" }],
68 + pwd: [{ required: true, message: "请填写密码", trigger: "blur" }],
69 + roles: [
70 + {
71 + required: true,
72 + message: "请选择权限",
73 + trigger: "change",
74 + type: "array",
75 + },
76 + ],
77 + },
78 + };
79 + },
80 + methods: {
81 + close() {
82 + this.$emit("close");
83 + },
84 + handleSubmit(formName) {
85 + this.loading = true;
86 + this.$refs[formName].validate(async (valid) => {
87 + if (valid) {
88 + try {
89 + delete this.info.createTime;
90 + delete this.info.releaseTime;
91 + await this.$store.dispatch("updateUser", this.info);
92 + this.loading = false;
93 + this.$message.success("编辑成功");
94 + this.close();
95 + } catch (e) {
96 + this.info.pwd = "";
97 + this.info.old_pwd = "";
98 + this.loading = false;
99 + this.$message.error("编辑失败");
100 + }
101 + } else {
102 + console.log("error submit!!");
103 + this.loading = false;
104 + return false;
105 + }
106 + });
107 + },
108 + },
109 +};
110 +</script>
111 +
112 +
113 +<style lang="less" scoped>
114 +.el-form {
115 + .el-form-item {
116 + display: flex;
117 + align-items: center;
118 + /deep/ .el-form-item__label {
119 + min-width: 80px;
120 + }
121 + /deep/.el-form-item__content {
122 + margin-left: 0 !important;
123 + width: 260px;
124 + .el-select {
125 + width: 260px;
126 + }
127 + }
128 + }
129 +}
130 +</style>
...\ No newline at end of file ...\ No newline at end of file
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="getUserList"
20 + ></el-input>
21 + </el-form-item>
22 + <el-button type="primary" @click="getUserList"> 查询 </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="userList"
31 + :count="userTotal"
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="roles" slot-scope="scope">
48 + <el-tag
49 + class="tag"
50 + type="primary"
51 + close-transition
52 + v-for="(tag, index) in scope.row.roles"
53 + :key="index"
54 + >{{ tag }}</el-tag
55 + >
56 + </template>
57 + <template slot="operate" slot-scope="scope">
58 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)"
59 + >编辑</el-button
60 + >
61 + <el-button
62 + size="mini"
63 + type="danger"
64 + @click="handleDelete(scope.row)"
65 + >删除</el-button
66 + >
67 + </template>
68 + </zp-table-list>
69 + </div>
70 + <!-- list end -->
71 + </zp-page-list>
72 + <editDialog
73 + v-if="editShow"
74 + :info="userInfo"
75 + @close="handleClose"
76 + ></editDialog>
77 + </div>
78 +</template>
79 +<script>
80 +import Clipboard from "clipboard";
81 +import editDialog from "./components/editDialog";
82 +import { mapGetters } from "vuex";
83 +export default {
84 + components: {
85 + editDialog,
86 + },
87 + computed: {
88 + ...mapGetters(["userList", "userTotal"]),
89 + },
90 + data() {
91 + return {
92 + listLoading: false,
93 + editShow: false,
94 + searchForm: {
95 + keyword: "",
96 + },
97 + userInfo: {},
98 + columns: [
99 + {
100 + label: "_id",
101 + prop: "_id",
102 + slot: "_id",
103 + },
104 + {
105 + label: "用户名",
106 + prop: "username",
107 + },
108 + {
109 + label: "权限",
110 + prop: "roles",
111 + slot: "roles",
112 + },
113 + {
114 + label: "操作",
115 + slot: "operate",
116 + width: "150",
117 + },
118 + ],
119 + };
120 + },
121 + mounted() {
122 + this.requestPageData = this.getUserList;
123 + this.getUserList();
124 + },
125 +
126 + methods: {
127 + async getUserList() {
128 + this.listLoading = true;
129 + try {
130 + await this.$store.dispatch("getUserList", {
131 + keyword: this.searchForm.keyword,
132 + pageindex: this.pageInfo.pageNum,
133 + pagesize: this.pageInfo.pageSize,
134 + });
135 + this.listLoading = false;
136 + } catch (e) {
137 + this.listLoading = false;
138 + }
139 + },
140 + handleCopyId() {
141 + let clipboard = new Clipboard(".clipboardBtn");
142 + clipboard.on("success", (e) => {
143 + this.$message.success("复制成功");
144 + clipboard.destroy();
145 + });
146 + clipboard.on("error", (e) => {
147 + this.$message.error("该浏览器不支持自动复制");
148 + clipboard.destroy();
149 + });
150 + },
151 + handleClose() {
152 + this.editShow = false;
153 + },
154 + handleDelete(row) {
155 + this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
156 + confirmButtonText: "确定",
157 + cancelButtonText: "取消",
158 + type: "warning",
159 + })
160 + .then(async () => {
161 + try {
162 + await this.$store.dispatch("delUser", row._id);
163 + this.$message.success("删除成功");
164 + this.getUserList();
165 + } catch (e) {
166 + this.$message.error("删除失败");
167 + console.log(e);
168 + }
169 + })
170 + .catch(() => {
171 + this.$message.info("已取消删除");
172 + });
173 + },
174 + handleEdit(row) {
175 + this.editShow = true;
176 + row.releaseTime = new Date(row.releaseTime);
177 + this.userInfo = row;
178 + },
179 + },
180 +};
181 +</script>
182 +
183 +<style lang="less" scoped>
184 +.tag {
185 + margin: 0 10px;
186 +}
187 +</style>
1 +import Koa from 'koa'
2 +import ip from 'ip'
3 +import conf from './config'
4 +import router from './router'
5 +import middleware from './middleware'
6 +import './mongodb'
7 +
8 +const app = new Koa()
9 +middleware(app)
10 +router(app)
11 +app.listen(conf.port, '0.0.0.0', () => {
12 + console.log(`server is running at http://${ip.address()}:${conf.port}`)
13 +})
...\ No newline at end of file ...\ No newline at end of file
1 +import path from "path";
2 +const auth = {
3 + admin_secret: "admin-token",
4 + tokenKey: "Token-Auth",
5 + whiteList: ["login", "client_api"],
6 + blackList: ["admin_api"],
7 +};
8 +
9 +const log = {
10 + logLevel: "debug", // 指定记录的日志级别
11 + dir: path.resolve(__dirname, "../../logs"), // 指定日志存放的目录名
12 + projectName: "blog", // 项目名,记录在日志中的项目信息
13 + ip: "0.0.0.0", // 默认情况下服务器 ip 地址
14 +};
15 +const port = process.env.NODE_ENV === "production" ? "80" : "3000";
16 +
17 +export default {
18 + env: process.env.NODE_ENV,
19 + port,
20 + auth,
21 + log,
22 + mongodb: {
23 + username: "rf",
24 + pwd: 123456,
25 + address: "localhost:27017",
26 + db: "rfBlog",
27 + },
28 +};
1 +import blogModel from "../../models/blog";
2 +import marked from "marked";
3 +
4 +marked.setOptions({
5 + renderer: new marked.Renderer(),
6 + gfm: true, //允许 Git Hub标准的markdown.
7 + tables: true, //允许支持表格语法。该选项要求 gfm 为true。
8 + breaks: true, //允许回车换行。该选项要求 gfm 为true。
9 + pedantic: false, //尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。
10 + sanitize: true, //对输出进行过滤(清理),将忽略任何已经输入的html代码(标签)
11 + smartLists: true, //使用比原生markdown更时髦的列表。 旧的列表将可能被作为pedantic的处理内容过滤掉.
12 + smartypants: false, //使用更为时髦的标点,比如在引用语法中加入破折号。
13 + highlight: function(code) {
14 + return require("highlight.js").highlightAuto(code).value;
15 + },
16 +});
17 +
18 +module.exports = {
19 + async list(ctx, next) {
20 + console.log(
21 + "----------------获取博客列表 blog/list-----------------------"
22 + );
23 + let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query;
24 + console.log("ctx.request =>", ctx.request);
25 + console.log(
26 + "keyword:" +
27 + keyword +
28 + "," +
29 + "pageindex:" +
30 + pageindex +
31 + "," +
32 + "pagesize:" +
33 + pagesize
34 + );
35 + try {
36 + let reg = new RegExp(keyword, "i");
37 + let data = await ctx.findPage(
38 + blogModel,
39 + {
40 + $or: [{ type: { $regex: reg } }, { title: { $regex: reg } }],
41 + },
42 + null,
43 + { limit: pagesize * 1, skip: (pageindex - 1) * pagesize }
44 + );
45 + ctx.send(data);
46 + } catch (e) {
47 + console.log(e);
48 + ctx.sendError(e);
49 + }
50 + },
51 +
52 + async add(ctx, next) {
53 + console.log("----------------添加博客 blog/add-----------------------");
54 + let paramsData = ctx.request.body;
55 + try {
56 + let data = await ctx.findOne(blogModel, { title: paramsData.title });
57 + if (data) {
58 + ctx.sendError("数据已经存在, 请重新添加!");
59 + } else {
60 + paramsData.html = marked(paramsData.html);
61 + let data = await ctx.add(blogModel, paramsData);
62 + ctx.send(paramsData);
63 + }
64 + } catch (e) {
65 + ctx.sendError(e);
66 + }
67 + },
68 +
69 + async update(ctx, next) {
70 + console.log("----------------更新博客 blog/update-----------------------");
71 + let paramsData = ctx.request.body;
72 + try {
73 + paramsData.html = marked(paramsData.html);
74 + let data = await ctx.update(
75 + blogModel,
76 + { _id: paramsData._id },
77 + paramsData
78 + );
79 + ctx.send();
80 + } catch (e) {
81 + if (e === "暂无数据") {
82 + ctx.sendError(e);
83 + }
84 + }
85 + },
86 +
87 + async del(ctx, next) {
88 + console.log("----------------删除博客 blog/del-----------------------");
89 + let id = ctx.request.query.id;
90 + try {
91 + ctx.remove(blogModel, { _id: id });
92 + ctx.send();
93 + } catch (e) {
94 + ctx.sendError(e);
95 + }
96 + },
97 +
98 + async info(ctx, next) {
99 + console.log(
100 + "----------------获取博客信息 blog/info-----------------------"
101 + );
102 + let _id = ctx.request.query._id;
103 + try {
104 + let data = await ctx.findOne(blogModel, { _id });
105 + return ctx.send(data);
106 + } catch (e) {
107 + return ctx.sendError(e);
108 + }
109 + },
110 +};
1 +import labelModel from "../../models/label";
2 +
3 +module.exports = {
4 + async list(ctx, next) {
5 + console.log(
6 + "----------------获取标签列表 label/list-----------------------"
7 + );
8 + let { keyword, pageindex = 1, pagesize = 50 } = ctx.request.query;
9 + try {
10 + let reg = new RegExp(keyword, "i");
11 + let data = await ctx.findPage(
12 + labelModel,
13 + {
14 + $or: [{ label: { $regex: reg } }, { bgColor: { $regex: reg } }],
15 + },
16 + null,
17 + { limit: pagesize * 1, skip: (pageindex - 1) * pagesize }
18 + );
19 + ctx.send(data);
20 + } catch (e) {
21 + console.log(e);
22 + ctx.sendError(e);
23 + }
24 + },
25 +
26 + async add(ctx, next) {
27 + console.log("----------------添加标签 label/add-----------------------");
28 + let paramsData = ctx.request.body;
29 + try {
30 + let data = await ctx.findOne(labelModel, { label: paramsData.label });
31 + if (data) {
32 + ctx.sendError("数据已经存在, 请重新添加!");
33 + } else {
34 + let result = await ctx.add(labelModel, paramsData);
35 + ctx.send(result);
36 + }
37 + } catch (e) {
38 + ctx.sendError(e);
39 + }
40 + },
41 +
42 + async update(ctx, next) {
43 + console.log("----------------更新标签 label/update-----------------------");
44 + let paramsData = ctx.request.body;
45 + try {
46 + let data = await ctx.update(
47 + labelModel,
48 + { _id: paramsData._id },
49 + paramsData
50 + );
51 + ctx.send(data);
52 + } catch (e) {
53 + if (e === "暂无数据") {
54 + ctx.sendError(e);
55 + }
56 + }
57 + },
58 +
59 + async del(ctx, next) {
60 + console.log("----------------删除标签 label/del-----------------------");
61 + let id = ctx.request.query.id;
62 + try {
63 + let data = await ctx.remove(labelModel, { _id: id });
64 + ctx.send(data);
65 + } catch (e) {
66 + ctx.sendError(e);
67 + }
68 + },
69 +};
1 +import messageModel from '../../models/message';
2 +
3 +module.exports = {
4 + async list(ctx, next) {
5 + console.log(
6 + '----------------获取留言列表 admin_api/message/list-----------------------'
7 + );
8 + let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query;
9 +
10 + let reg = new RegExp(keyword, 'i');
11 +
12 + let conditions = {
13 + $or: [{ nickname: { $regex: reg } }, { content: { $regex: reg } }],
14 + };
15 +
16 + // 排序参数
17 + let sortParams = {
18 + createTime: -1,
19 + };
20 +
21 + let options = {
22 + limit: pagesize * 1,
23 + skip: (pageindex - 1) * pagesize,
24 + sort: sortParams,
25 + };
26 +
27 + try {
28 + let data = await ctx.find(messageModel, conditions, null, options);
29 + return ctx.send(data);
30 + } catch (e) {
31 + console.log(e);
32 + return ctx.sendError(e);
33 + }
34 + },
35 +
36 + async del(ctx, next) {
37 + console.log(
38 + '----------------删除留言 admin_api/message/del-----------------------'
39 + );
40 + let id = ctx.request.query.id;
41 + try {
42 + ctx.remove(messageModel, { _id: id });
43 + ctx.send();
44 + } catch (e) {
45 + ctx.sendError(e);
46 + }
47 + },
48 +
49 + async delReply(ctx, next) {
50 + console.log(
51 + '----------------删除回复 admin_api/message/delReply-----------------------'
52 + );
53 + let { _id } = ctx.request.body;
54 + let options = {
55 + $pull: { replyList: { _id } },
56 + };
57 + try {
58 + let data = await ctx.update(messageModel, { _id }, options);
59 + ctx.send();
60 + } catch (e) {
61 + ctx.sendError(e);
62 + }
63 + },
64 +};
1 +const path = require("path");
2 +
3 +module.exports = {
4 + async uploadImage(ctx, next) {
5 + console.log("----------------添加图片 uploadImage-----------------------");
6 + try {
7 + let opts = {
8 + path: path.resolve(__dirname, "../../../../public"),
9 + };
10 + let result = await ctx.uploadFile(ctx, opts);
11 + ctx.send(result);
12 + } catch (e) {
13 + ctx.sendError(e);
14 + }
15 + },
16 + async delUploadImage(ctx, next) {
17 + console.log(
18 + "----------------删除图片 delUploadImage-----------------------"
19 + );
20 + let fileName = ctx.request.body.fileName;
21 + let fileCoverImgUrl = `public/images/${fileName}`;
22 + try {
23 + ctx.removeFile(fileCoverImgUrl);
24 + ctx.send();
25 + } catch (e) {
26 + ctx.sendError(e);
27 + }
28 + },
29 +};
1 +import jwt from "jsonwebtoken";
2 +import conf from "../../config";
3 +import userModel from "../../models/user";
4 +module.exports = {
5 + async login(ctx, next) {
6 + console.log("----------------登录接口 user/login-----------------------");
7 + let { username, pwd } = ctx.request.body;
8 + try {
9 + let data = await ctx.findOne(userModel, { username: username });
10 + console.log(data);
11 + if (!data) {
12 + return ctx.sendError("用户名不存在!");
13 + }
14 + if (pwd !== data.pwd) {
15 + return ctx.sendError("密码错误,请重新输入!");
16 + }
17 + await ctx.update(
18 + userModel,
19 + { _id: data._id },
20 + { $set: { loginTime: new Date() } }
21 + ); //更新登陆时间
22 +
23 + let payload = {
24 + _id: data._id,
25 + username: data.username,
26 + roles: data.roles,
27 + };
28 + // token签名 有效期为24小时
29 + let token = jwt.sign(payload, conf.auth.admin_secret, {
30 + expiresIn: "24h",
31 + });
32 + // 是否只用于http请求中获取
33 + ctx.cookies.set(conf.auth.tokenKey, token, {
34 + httpOnly: false,
35 + });
36 + ctx.send({ message: "登录成功" });
37 + } catch (e) {
38 + if (e === "暂无数据") {
39 + console.log("用户名不存在");
40 + return ctx.sendError("用户名不存在");
41 + }
42 + ctx.throw(e);
43 + ctx.sendError(e);
44 + }
45 + },
46 + async info(ctx, next) {
47 + console.log(
48 + "----------------获取用户信息接口 user/getUserInfo-----------------------"
49 + );
50 + let token = ctx.request.query.token;
51 + try {
52 + let tokenInfo = jwt.verify(token, conf.auth.admin_secret);
53 + console.log("log tokenInfo =>", tokenInfo);
54 + ctx.send({
55 + username: tokenInfo.username,
56 + _id: tokenInfo._id,
57 + roles: tokenInfo.roles,
58 + });
59 + } catch (e) {
60 + if ("TokenExpiredError" === e.name) {
61 + ctx.sendError("鉴权失败, 请重新登录!");
62 + ctx.throw(401, "token验证失败, 请重新登录!");
63 + }
64 + ctx.throw(401, "invalid token");
65 + ctx.sendError("系统异常!");
66 + }
67 + },
68 +
69 + async list(ctx, next) {
70 + console.log(
71 + "----------------获取用户信息列表接口 user/getUserList-----------------------"
72 + );
73 + let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query;
74 + console.log(
75 + "keyword:" +
76 + keyword +
77 + "," +
78 + "pageindex:" +
79 + pageindex +
80 + "," +
81 + "pagesize:" +
82 + pagesize
83 + );
84 +
85 + try {
86 + let reg = new RegExp(keyword, "i");
87 + let data = await ctx.findPage(
88 + userModel,
89 + {
90 + $or: [{ username: { $regex: reg } }],
91 + },
92 + { pwd: 0 },
93 + { limit: pagesize * 1, skip: (pageindex - 1) * pagesize }
94 + );
95 +
96 + ctx.send(data);
97 + } catch (e) {
98 + console.log(e);
99 + ctx.sendError(e);
100 + }
101 + },
102 +
103 + async add(ctx, next) {
104 + console.log("----------------添加管理员 user/add-----------------------");
105 + let paramsData = ctx.request.body;
106 + try {
107 + let data = await ctx.findOne(userModel, {
108 + username: paramsData.username,
109 + });
110 + if (data) {
111 + ctx.sendError("数据已经存在, 请重新添加!");
112 + } else {
113 + await ctx.add(userModel, paramsData);
114 + ctx.send(paramsData);
115 + }
116 + } catch (e) {
117 + ctx.sendError(e);
118 + }
119 + },
120 +
121 + async update(ctx, next) {
122 + console.log(
123 + "----------------更新管理员 user/update-----------------------"
124 + );
125 + let paramsData = ctx.request.body;
126 + console.log(paramsData);
127 + try {
128 + let data = await ctx.findOne(userModel, {
129 + username: paramsData.username,
130 + });
131 + if (paramsData.old_pwd !== data.pwd) {
132 + return ctx.sendError("密码不匹配!");
133 + }
134 + delete paramsData.old_pwd;
135 + await ctx.update(userModel, { _id: paramsData._id }, paramsData);
136 + ctx.send();
137 + } catch (e) {
138 + if (e === "暂无数据") {
139 + ctx.sendError(e);
140 + }
141 + }
142 + },
143 +
144 + async del(ctx, next) {
145 + console.log("----------------删除管理员 user/del-----------------------");
146 + let id = ctx.request.query.id;
147 + try {
148 + ctx.remove(userModel, { _id: id });
149 + ctx.send();
150 + } catch (e) {
151 + ctx.sendError(e);
152 + }
153 + },
154 +};
1 +require('babel-core/register') // babel编译
2 +module.exports = require('./app.js')
...\ No newline at end of file ...\ No newline at end of file
1 +import jwt from 'jsonwebtoken';
2 +import conf from '../../config';
3 +
4 +export default () => {
5 + return async (ctx, next) => {
6 + // 白名单就不需要走 jwt 鉴权
7 + if (!conf.auth.whiteList.some((v) => ctx.path.includes(v))) {
8 + let token = ctx.cookies.get(conf.auth.tokenKey);
9 + try {
10 + jwt.verify(token, conf.auth.admin_secret);
11 + } catch (e) {
12 + if ('TokenExpiredError' === e.name) {
13 + ctx.sendError('token已过期, 请重新登录!');
14 + ctx.throw(401, 'token已过期, 请重新登录!');
15 + }
16 + ctx.sendError('token验证失败, 请重新登录!');
17 + ctx.throw(401, 'token验证失败, 请重新登录!');
18 + }
19 + console.log('鉴权成功');
20 + }
21 + await next();
22 + };
23 +};
1 +/*
2 + * 公共Add方法
3 + * @param model 要操作数据库的模型
4 + * @param conditions 增加的条件,如{id:xxx}
5 + */
6 +export const add = (model, conditions) => {
7 + return new Promise((resolve, reject) => {
8 + model.create(conditions, (err, res) => {
9 + if (err) {
10 + console.error("Error: " + JSON.stringify(err));
11 + reject(err);
12 + return false;
13 + }
14 + console.log("save success!");
15 + resolve(res);
16 + });
17 + });
18 +};
19 +
20 +/*
21 + * 公共update方法
22 + * @param model 要操作数据库的模型
23 + * @param conditions 增加的条件,如{id:xxx}
24 + * @param update 更新条件{set{id:xxx}}
25 + * @param options
26 + */
27 +export const update = (model, conditions, update, options) => {
28 + return new Promise((resolve, reject) => {
29 + model.update(conditions, update, options, (err, res) => {
30 + if (err) {
31 + console.error("Error: " + JSON.stringify(err));
32 + reject(err);
33 + return false;
34 + }
35 + if (res.n !== 0) {
36 + console.log("update success!");
37 + } else {
38 + console.log("update fail:no this data!");
39 + return reject("update fail:no this data!");
40 + }
41 + resolve(res);
42 + });
43 + });
44 +};
45 +
46 +/**
47 + * 公共remove方法
48 + * @param model
49 + * @param conditions
50 + */
51 +
52 +export const remove = (model, conditions) => {
53 + return new Promise((resolve, reject) => {
54 + model.remove(conditions, function(err, res) {
55 + if (err) {
56 + console.error("Error: " + JSON.stringify(err));
57 + reject(err);
58 + return false;
59 + } else {
60 + if (res.result.n !== 0) {
61 + console.log("remove success!");
62 + } else {
63 + console.log("remove fail:no this data!");
64 + }
65 + resolve(res);
66 + }
67 + });
68 + });
69 +};
70 +
71 +/**
72 + * 公共find方法 非关联查找
73 + * @param model
74 + * @param conditions
75 + * @param fields 查找时限定的条件,如顺序,某些字段不查找等
76 + * @param options
77 + * @param callback
78 + */
79 +export const find = async (model, conditions, fields, options = {}) => {
80 + let { sort } = options;
81 + delete options.sort;
82 +
83 + const getCount = () => {
84 + return new Promise((resolve, reject) => {
85 + model.find(conditions, fields).count({}, (err, res) => {
86 + if (err) {
87 + console.log("查询长度错误");
88 + return reject(err);
89 + }
90 +
91 + resolve(res);
92 + });
93 + });
94 + };
95 +
96 + const count = await getCount();
97 +
98 + return new Promise((resolve, reject) => {
99 + model
100 + .find(conditions, fields, options, function(err, res) {
101 + if (err) {
102 + console.error("Error: " + JSON.stringify(err));
103 + reject(err);
104 + return false;
105 + } else {
106 + if (res.length !== 0) {
107 + resolve({
108 + list: res,
109 + total: count,
110 + });
111 + console.log("find success!");
112 + } else {
113 + console.log("find fail:no this data!");
114 + }
115 + // resolve(res);
116 + resolve({
117 + list: res,
118 + total: count,
119 + });
120 + }
121 + })
122 + .sort(sort);
123 + });
124 +};
125 +
126 +/**
127 + * 公共findOne方法 非关联查找
128 + * @param model
129 + * @param conditions
130 + * @param fields 查找时限定的条件,如顺序,某些字段不查找等
131 + * @param options
132 + * @param callback
133 + */
134 +export const findOne = (model, conditions, fields, options = {}) => {
135 + let { sort } = options;
136 + delete options.sort;
137 + return new Promise((resolve, reject) => {
138 + model
139 + .findOne(conditions, fields, options, function(err, res) {
140 + if (err) {
141 + console.error("Error: " + JSON.stringify(err));
142 + reject(err);
143 + return false;
144 + } else {
145 + if (res) {
146 + console.log("find success!");
147 + } else {
148 + console.log("find fail:no this data!");
149 + }
150 + resolve(res);
151 + }
152 + })
153 + .sort(sort);
154 + });
155 +};
156 +
157 +export const findPage = async (model, conditions, fields, options = {}) => {
158 + let { sort } = options;
159 + delete options.sort;
160 +
161 + const getCount = () => {
162 + return new Promise((resolve, reject) => {
163 + model.find(conditions, fields).count({}, (err, res) => {
164 + if (err) {
165 + console.log("查询长度错误");
166 + return reject(err);
167 + }
168 + resolve(res);
169 + });
170 + });
171 + };
172 +
173 + const count = await getCount();
174 +
175 + return new Promise((resolve, reject) => {
176 + model.find(conditions, fields, options, function(err, res) {
177 + if (err) {
178 + console.error("Error: " + JSON.stringify(err));
179 + reject(err);
180 + return false;
181 + } else {
182 + if (res.length !== 0) {
183 + console.log("find success!");
184 + resolve({
185 + list: res,
186 + total: count,
187 + });
188 + } else {
189 + console.log("find fail:no this data!");
190 + resolve({
191 + list: res,
192 + total: count,
193 + });
194 + }
195 + }
196 + });
197 + });
198 +};
199 +
200 +/*
201 + * 公共aggregate方法
202 + * @param model 要操作数据库的模型
203 + * @param conditions 增加的条件,如{id:xxx}
204 + */
205 +export const aggregate = (model, conditions) => {
206 + return new Promise((resolve, reject) => {
207 + model.aggregate(conditions, (err, res) => {
208 + if (err) {
209 + console.error("Error: " + JSON.stringify(err));
210 + reject(err);
211 + return false;
212 + }
213 + console.log("aggregate success!");
214 + resolve(res);
215 + });
216 + });
217 +};
1 +import Busboy from "busboy";
2 +import fs from "fs";
3 +import path from "path";
4 +
5 +//检测文件并创建文件
6 +const mkdirSync = (dirname) => {
7 + if (fs.existsSync(dirname)) {
8 + return true;
9 + } else {
10 + if (mkdirSync(path.dirname(dirname))) {
11 + fs.mkdirSync(dirname);
12 + return true;
13 + }
14 + }
15 +};
16 +// 删除本地图片
17 +export const removeFile = (filePath) => {
18 + fs.unlink(filePath, function(err) {
19 + if (err) {
20 + throw err;
21 + }
22 + console.log("文件:" + filePath + "删除成功!");
23 + });
24 +};
25 +
26 +export const uploadFile = (ctx, opts) => {
27 + //重命名
28 + function rename(fileName) {
29 + return (
30 + Math.random()
31 + .toString(16)
32 + .substr(2) +
33 + "." +
34 + fileName.split(".").pop()
35 + );
36 + }
37 + let busboy = new Busboy({ headers: ctx.req.headers });
38 + console.log("start uploading...");
39 + /*
40 + filename: 字段名,
41 + file: 文件流,
42 + filename: 文件名
43 + */
44 + return new Promise((resolve, reject) => {
45 + var fileObj = {};
46 + busboy.on("file", async (fieldname, file, filename, encoding, mimetype) => {
47 + let filePath = "",
48 + imgPrefix = "";
49 +
50 + filePath = path.join(opts.path, mimetype.split("/")[0] + "s");
51 + // 现网图片路径不一样
52 + imgPrefix = `${ctx.protocol}://${ctx.host}/${mimetype.split("/")[0]}s`;
53 +
54 + if (!mkdirSync(filePath)) {
55 + throw new Error("没找到目录");
56 + }
57 + let fName = rename(filename),
58 + fPath = path.join(path.join(filePath, fName));
59 + file.pipe(fs.createWriteStream(fPath));
60 +
61 + console.log("fName =>", fName);
62 + console.log("fPath =>", fPath);
63 +
64 + file.on("end", () => {
65 + fileObj[fieldname] = `${imgPrefix}/${fName}`;
66 + });
67 + });
68 +
69 + busboy.on(
70 + "field",
71 + (
72 + fieldname,
73 + val,
74 + fieldnameTruncated,
75 + valTruncated,
76 + encoding,
77 + mimetype
78 + ) => {
79 + fileObj[fieldname] = val;
80 + }
81 + );
82 +
83 + busboy.on("finish", async () => {
84 + resolve(fileObj);
85 + console.log("finished...", fileObj);
86 + });
87 + busboy.on("error", function(err) {
88 + console.log("err:" + err);
89 + reject(err);
90 + });
91 +
92 + ctx.req.pipe(busboy);
93 + });
94 +};
1 +export const get_client_ip = (ctx) => {
2 + return (
3 + ctx.request.headers["x-forwarded-for"] ||
4 + (ctx.request.connection && ctx.request.connection.remoteAddress) ||
5 + ctx.request.socket.remoteAddress ||
6 + (ctx.request.connection.socket &&
7 + ctx.request.connection.socket.remoteAddress) ||
8 + null
9 + );
10 +};
1 +import * as get_Info_func from "./get_info";
2 +import * as db_func from "./db";
3 +import * as file_func from "./file";
4 +
5 +export default () => {
6 + const func = Object.assign({}, get_Info_func, db_func, file_func);
7 + return async (ctx, next) => {
8 + for (let v in func) {
9 + if (func.hasOwnProperty(v)) ctx[v] = func[v];
10 + }
11 + await next();
12 + };
13 +};
1 +import path from "path";
2 +import bodyParser from "koa-bodyparser";
3 +import staticFiles from "koa-static";
4 +import Rule from "./rule";
5 +import Send from "./send";
6 +import Auth from "./auth";
7 +import Log from "./log";
8 +import Func from "./func";
9 +
10 +export default (app) => {
11 + //缓存拦截器
12 + app.use(async (ctx, next) => {
13 + if (ctx.url == "/favicon.ico") return;
14 +
15 + await next();
16 + ctx.status = 200;
17 + ctx.set("Cache-Control", "must-revalidation");
18 + if (ctx.fresh) {
19 + ctx.status = 304;
20 + return;
21 + }
22 + });
23 +
24 + // 日志中间件
25 + app.use(Log());
26 +
27 + // 数据返回的封装
28 + app.use(Send());
29 +
30 + // 方法封装
31 + app.use(Func());
32 +
33 + //权限中间件
34 + app.use(Auth());
35 +
36 + //post请求中间件
37 + app.use(bodyParser());
38 +
39 + //静态文件中间件
40 + app.use(staticFiles(path.resolve(__dirname, "../../../public")));
41 +
42 + // 规则中间件
43 + Rule({
44 + app,
45 + rules: [
46 + {
47 + path: path.join(__dirname, "../controller/admin"),
48 + name: "admin",
49 + },
50 + {
51 + path: path.join(__dirname, "../controller/client"),
52 + name: "client",
53 + },
54 + ],
55 + });
56 +
57 + // 增加错误的监听处理
58 + app.on("error", (err, ctx) => {
59 + if (ctx && !ctx.headerSent && ctx.status < 500) {
60 + ctx.status = 500;
61 + }
62 + if (ctx && ctx.log && ctx.log.error) {
63 + if (!ctx.state.logged) {
64 + ctx.log.error(err.stack);
65 + }
66 + }
67 + });
68 +};
1 +export default (ctx, msg, commonInfo) => {
2 + const {
3 + method, // 请求方法 get post或其他
4 + url, // 请求链接
5 + host, // 发送请求的客户端的host
6 + headers // 请求中的headers
7 + } = ctx.request;
8 + const client = {
9 + method,
10 + url,
11 + host,
12 + msg,
13 + ip: ctx.get_client_ip(ctx),
14 + referer: headers['referer'], // 请求的源地址
15 + userAgent: headers['user-agent'] // 客户端信息 设备及浏览器信息
16 + }
17 + return JSON.stringify(Object.assign(commonInfo, client));
18 +}
...\ No newline at end of file ...\ No newline at end of file
1 +import logger from './log'
2 +
3 +export default opts => {
4 + let loggerMiddleware = logger(opts);
5 + return async (ctx, next) => {
6 + return loggerMiddleware(ctx, next)
7 + .catch( e => {
8 + if (ctx.status < 500) {
9 + ctx.status = 500;
10 + }
11 + ctx.log.error(e.stack);
12 + ctx.state.logged = true;
13 + ctx.throw(e);
14 + })
15 + }
16 +}
...\ No newline at end of file ...\ No newline at end of file
1 +import log4js from 'log4js'
2 +import access from './access' // 引入日志输出信息的封装文件
3 +import config from '../../config'
4 +const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"];
5 +
6 +// 提取默认公用参数对象
7 +const baseInfo = config.log
8 +export default (options = {}) => {
9 + let contextLogger = {}, //错误日志等级对象,最后会赋值给ctx上,用于打印各种日志
10 + appenders = {}, //日志配置
11 + opts = Object.assign({}, baseInfo, options), //系统配置
12 + {
13 + logLevel,
14 + dir,
15 + ip,
16 + projectName
17 + } = opts,
18 + commonInfo = {
19 + projectName,
20 + ip
21 + }; //存储公用的日志信息
22 +
23 + //指定要记录的日志分类
24 + appenders.all = {
25 + type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符
26 + filename: `${dir}/all/`, //日志文件名,可以设置相对路径或绝对路径
27 + pattern: 'task-yyyy-MM-dd.log', //占位符,紧跟在filename后面
28 + alwaysIncludePattern: true //是否总是有后缀名
29 + }
30 +
31 + // 环境变量为dev local development 认为是开发环境
32 + if (config.env === "dev" || config.env === "local" || config.env === "development") {
33 + appenders.out = {
34 + type: "console"
35 + }
36 + }
37 +
38 + let logConfig = {
39 + appenders,
40 +
41 + /**
42 + * 指定日志的默认配置项
43 + * 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项
44 + */
45 + categories: {
46 + default: {
47 + appenders: Object.keys(appenders),
48 + level: logLevel
49 + }
50 + }
51 + }
52 +
53 + let logger = log4js.getLogger('cheese');
54 + return async (ctx, next) => {
55 + const start = Date.now() // 记录请求开始的时间
56 +
57 + // 循环methods将所有方法挂载到ctx 上
58 + methods.forEach((method, i) => {
59 + contextLogger[method] = message => {
60 + logConfig.appenders.cheese = {
61 + type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符
62 + filename: `${dir}/${method}/`,
63 + pattern: `${method}-yyyy-MM-dd.log`,
64 + alwaysIncludePattern: true //是否总是有后缀名
65 + }
66 + log4js.configure(logConfig)
67 + logger[method](access(ctx, message, commonInfo))
68 + }
69 + })
70 + ctx.log = contextLogger
71 + await next()
72 + // 记录完成的时间 作差 计算响应时间
73 + const responseTime = Date.now() - start
74 +
75 + ctx.log.info(access(ctx, {
76 + responseTime: `响应时间为${responseTime/1000}s`
77 + }, commonInfo))
78 +
79 + }
80 +
81 +}
...\ No newline at end of file ...\ No newline at end of file
1 +import Path from "path";
2 +import fs from "fs";
3 +
4 +export default (opts) => {
5 + let { app, rules = [] } = opts;
6 + if (!app) {
7 + throw new Error("the app params is necessary!");
8 + }
9 +
10 + app.router = {};
11 + const appKeys = Object.keys(app);
12 + rules.forEach((item) => {
13 + let { path, name } = item;
14 + if (appKeys.includes(name)) {
15 + throw new Error(`the name of ${name} already exists!`);
16 + }
17 + let content = {};
18 + //readdirSync: 方法将返回一个包含“指定目录下所有文件名称”的数组对象。
19 + //extname: 返回path路径文件扩展名,如果path以 ‘.' 为结尾,将返回 ‘.',如果无扩展名 又 不以'.'结尾,将返回空值。
20 + //basename: path.basename(p, [ext]) p->要处理的path ext->要过滤的字符
21 + fs.readdirSync(path).forEach((filename) => {
22 + let extname = Path.extname(filename);
23 + if (extname === ".js") {
24 + let name = Path.basename(filename, extname);
25 + content[name] = require(Path.join(path, filename));
26 + content[name].filename = name;
27 + }
28 + });
29 + app[name] = content;
30 + });
31 +};
1 +export default () => {
2 + let render = (ctx) => {
3 + return (json, msg) => {
4 + ctx.set("Content-Type", "application/json");
5 + ctx.body = JSON.stringify({
6 + code: 1,
7 + data: json || {},
8 + msg: msg || "success",
9 + });
10 + };
11 + };
12 + let renderError = (ctx) => {
13 + return (msg) => {
14 + ctx.set("Content-Type", "application/json");
15 + ctx.body = JSON.stringify({
16 + code: 0,
17 + data: {},
18 + msg: msg.toString(),
19 + });
20 + };
21 + };
22 + return async (ctx, next) => {
23 + ctx.send = render(ctx);
24 + ctx.sendError = renderError(ctx);
25 + await next();
26 + };
27 +};
1 +import db from "../mongodb";
2 +let blogSchema = db.Schema({
3 + type: Array,
4 + title: String,
5 + desc: String,
6 + fileCoverImgUrl: String,
7 + html: String,
8 + markdown: String,
9 + level: Number,
10 + github: String,
11 + auth: String,
12 + source: Number,
13 + isVisible: Boolean,
14 + releaseTime: String,
15 + pv: { type: Number, default: 0 },
16 + likes: { type: Number, default: 0 },
17 + comments: { type: Number, default: 0 },
18 +});
19 +export default db.model("blog", blogSchema);
1 +import db from "../mongodb";
2 +let labelSchema = db.Schema({
3 + label: String,
4 + bgColor: String,
5 + createTime: { type: Date, default: Date.now },
6 +});
7 +export default db.model("label", labelSchema);
1 +import db from "../mongodb";
2 +let messageSchema = db.Schema({
3 + content: String,
4 + headerColor: { type: String, default: "#ff6c1a" },
5 + nickname: { type: String, default: "匿名网友" },
6 + createTime: String,
7 + likes: { type: Number, default: 0 },
8 + comments: { type: Number, default: 0 },
9 + replyList: [
10 + {
11 + replyHeaderColor: { type: String, default: "#009688" },
12 + replyContent: String,
13 + replyUser: { type: String, default: "匿名网友" },
14 + byReplyUser: String,
15 + replyTime: String,
16 + },
17 + ],
18 +});
19 +export default db.model("message", messageSchema);
1 +import db from "../mongodb";
2 +let userSchema = db.Schema({
3 + username: String,
4 + pwd: String,
5 + avatar: String,
6 + roles: Array,
7 + createTime: { type: Date, default: Date.now },
8 + loginTime: Date,
9 +});
10 +export default db.model("user", userSchema);
1 +import mongoose from "mongoose";
2 +import conf from "./config";
3 +const DB_URL = `mongodb://${conf.mongodb.username}:${conf.mongodb.pwd}@${conf.mongodb.address}/${conf.mongodb.db}`; // 账号登陆
4 +mongoose.Promise = global.Promise;
5 +mongoose.connect(DB_URL, { useMongoClient: true }, (err) => {
6 + if (err) {
7 + console.log("数据库连接失败!");
8 + } else {
9 + console.log("数据库连接成功!");
10 + }
11 +});
12 +export default mongoose;
1 +import koaRouter from "koa-router";
2 +const router = koaRouter();
3 +
4 +export default (app) => {
5 + /*----------------------admin-------------------------------*/
6 + // 用户请求
7 + router.post("/admin_api/user/login", app.admin.user.login);
8 + router.get("/admin_api/user/info", app.admin.user.info);
9 + router.get("/admin_api/user/list", app.admin.user.list);
10 + router.post("/admin_api/user/add", app.admin.user.add);
11 + router.post("/admin_api/user/update", app.admin.user.update);
12 + router.get("/admin_api/user/del", app.admin.user.del);
13 +
14 + // 文章请求
15 + router.get("/admin_api/blog/list", app.admin.blog.list);
16 + router.post("/admin_api/blog/add", app.admin.blog.add);
17 + router.post("/admin_api/blog/update", app.admin.blog.update);
18 + router.get("/admin_api/blog/del", app.admin.blog.del);
19 + router.get("/admin_api/blog/info", app.admin.blog.info);
20 +
21 + // 标签请求
22 + router.get("/admin_api/label/list", app.admin.label.list);
23 + router.post("/admin_api/label/add", app.admin.label.add);
24 + router.post("/admin_api/label/update", app.admin.label.update);
25 + router.get("/admin_api/label/del", app.admin.label.del);
26 +
27 + // 留言请求
28 + router.get("/admin_api/message/list", app.admin.message.list);
29 + router.get("/admin_api/message/del", app.admin.message.del);
30 + router.post("/admin_api/message/delReply", app.admin.message.delReply);
31 +
32 + // 图片请求
33 + router.post("/admin_api/uploadImage", app.admin.upload.uploadImage);
34 + router.post("/admin_api/delUploadImage", app.admin.upload.delUploadImage);
35 +
36 + /*----------------------client-------------------------------*/
37 + // 文章请求
38 + router.get("/client_api/blog/list", app.client.blog.list);
39 + router.get("/client_api/blog/info", app.client.blog.info);
40 + router.post("/client_api/blog/updateLikes", app.client.blog.updateLikes);
41 + router.post("/client_api/blog/updatePV", app.client.blog.updatePV);
42 +
43 + // 标签请求
44 + router.get("/client_api/label/list", app.client.label.list);
45 +
46 + // 留言请求
47 + router.post("/client_api/message/add", app.client.message.add);
48 + router.get("/client_api/message/list", app.client.message.list);
49 + router.get("/client_api/message/replyCount", app.client.message.replyCount);
50 + router.post(
51 + "/client_api/message/updateLikes",
52 + app.client.message.updateLikes
53 + );
54 + router.post(
55 + "/client_api/message/updateReplys",
56 + app.client.message.updateReplys
57 + );
58 +
59 + app.use(router.routes()).use(router.allowedMethods());
60 +};